225 lines
7.8 KiB
PHP
225 lines
7.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Kategori;
|
|
use App\Models\Wisata;
|
|
use Database\Seeders\DestinationSeeder;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\View\View;
|
|
|
|
class WisataController extends Controller
|
|
{
|
|
public function index(Request $request): View
|
|
{
|
|
$this->seedDefaultDestinationsIfEmpty();
|
|
|
|
$wisatas = Wisata::query()
|
|
->with('kategori')
|
|
->when($request->search, function ($query, string $search): void {
|
|
$query->where(function ($query) use ($search): void {
|
|
$query->where('title', 'like', "%{$search}%")
|
|
->orWhere('location', 'like', "%{$search}%");
|
|
});
|
|
})
|
|
->latest()
|
|
->paginate(10)
|
|
->withQueryString();
|
|
|
|
return view('admin.wisata.index', compact('wisatas'));
|
|
}
|
|
|
|
private function seedDefaultDestinationsIfEmpty(): void
|
|
{
|
|
if (Wisata::query()->exists()) {
|
|
return;
|
|
}
|
|
|
|
Artisan::call('db:seed', [
|
|
'--class' => DestinationSeeder::class,
|
|
'--force' => true,
|
|
]);
|
|
}
|
|
|
|
public function create(): View
|
|
{
|
|
return view('admin.wisata.form', [
|
|
'wisata' => new Wisata(),
|
|
'kategoris' => Kategori::query()->orderBy('nama')->get(),
|
|
]);
|
|
}
|
|
|
|
public function show(Wisata $wisata): View
|
|
{
|
|
$wisata->load('kategori');
|
|
|
|
return view('admin.wisata.show', compact('wisata'));
|
|
}
|
|
|
|
public function store(Request $request): RedirectResponse
|
|
{
|
|
$validated = $request->validate($this->rules(), $this->messages());
|
|
$validated['slug'] = $this->uniqueSlug($validated['title']);
|
|
$validated['is_video'] = $request->boolean('is_video');
|
|
$validated = $this->storeUploads($request, $validated);
|
|
|
|
Wisata::query()->create($validated);
|
|
|
|
return redirect()->route('admin.wisata.index')->with('success', 'Data wisata berhasil ditambahkan.');
|
|
}
|
|
|
|
public function edit(Wisata $wisata): View
|
|
{
|
|
return view('admin.wisata.form', [
|
|
'wisata' => $wisata,
|
|
'kategoris' => Kategori::query()->orderBy('nama')->get(),
|
|
]);
|
|
}
|
|
|
|
public function update(Request $request, Wisata $wisata): RedirectResponse
|
|
{
|
|
$validated = $request->validate($this->rules($wisata), $this->messages());
|
|
$validated['slug'] = $this->uniqueSlug($validated['title'], $wisata->id);
|
|
$validated['is_video'] = $request->boolean('is_video');
|
|
$validated = $this->storeUploads($request, $validated, $wisata);
|
|
|
|
$wisata->update($validated);
|
|
|
|
return redirect()->route('admin.wisata.index')->with('success', 'Data wisata berhasil diperbarui.');
|
|
}
|
|
|
|
public function destroy(Wisata $wisata): RedirectResponse
|
|
{
|
|
$this->deleteFiles($wisata);
|
|
$wisata->delete();
|
|
|
|
return back()->with('success', 'Data wisata berhasil dihapus.');
|
|
}
|
|
|
|
private function rules(?Wisata $wisata = null): array
|
|
{
|
|
return [
|
|
'kategori_id' => ['required', 'exists:kategoris,id'],
|
|
'title' => ['required', 'string', 'max:180', Rule::unique('wisatas', 'title')->ignore($wisata)],
|
|
'location' => ['required', 'string', 'max:255'],
|
|
'rating' => ['required', 'numeric', 'min:0', 'max:5'],
|
|
'distance' => ['nullable', 'string', 'max:80'],
|
|
'elevation' => ['nullable', 'string', 'max:80'],
|
|
'tiket_parkir' => ['nullable', 'string', 'max:120'],
|
|
'jam_operasional' => ['nullable', 'string', 'max:160'],
|
|
'short_description' => ['nullable', 'string', 'max:500'],
|
|
'overview' => ['nullable', 'string'],
|
|
'image' => [$wisata ? 'nullable' : 'required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
|
|
'model_path' => ['nullable', 'file', 'max:51200', $this->modelFileRule()],
|
|
'video_path' => ['nullable', 'file', 'mimes:mp4,webm,mov', 'max:102400'],
|
|
'is_video' => ['nullable', 'boolean'],
|
|
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
|
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
|
];
|
|
}
|
|
|
|
private function messages(): array
|
|
{
|
|
return [
|
|
'model_path.file' => 'File model 3D tidak valid. Silakan pilih file .glb atau .gltf.',
|
|
'model_path.max' => 'Ukuran file model 3D maksimal 50MB.',
|
|
'image.required' => 'Gambar wisata wajib diunggah.',
|
|
'image.image' => 'File gambar wisata harus berupa gambar yang valid.',
|
|
'image.mimes' => 'Gambar wisata harus berformat JPG, JPEG, PNG, atau WEBP.',
|
|
'image.max' => 'Ukuran gambar wisata maksimal 4MB.',
|
|
'video_path.mimes' => 'Video wisata harus berformat MP4, WEBM, atau MOV.',
|
|
'video_path.max' => 'Ukuran video wisata maksimal 100MB.',
|
|
];
|
|
}
|
|
|
|
private function modelFileRule(): \Closure
|
|
{
|
|
return function (string $attribute, mixed $value, \Closure $fail): void {
|
|
unset($attribute);
|
|
|
|
if (! $value) {
|
|
return;
|
|
}
|
|
|
|
$extension = Str::lower($value->getClientOriginalExtension());
|
|
|
|
if (! in_array($extension, ['glb', 'gltf'], true)) {
|
|
$fail('File model 3D harus berformat .glb atau .gltf.');
|
|
|
|
return;
|
|
}
|
|
|
|
$mimeType = Str::lower($value->getMimeType() ?: $value->getClientMimeType() ?: '');
|
|
$allowedMimeTypes = [
|
|
'model/gltf-binary',
|
|
'model/gltf+json',
|
|
'application/octet-stream',
|
|
'application/json',
|
|
'text/plain',
|
|
'text/json',
|
|
];
|
|
|
|
if ($mimeType && ! in_array($mimeType, $allowedMimeTypes, true)) {
|
|
$fail('File model 3D tidak dikenali. Gunakan file .glb atau .gltf yang valid.');
|
|
}
|
|
};
|
|
}
|
|
|
|
private function storeUploads(Request $request, array $data, ?Wisata $wisata = null): array
|
|
{
|
|
foreach (['image' => 'wisata', 'model_path' => 'models', 'video_path' => 'videos'] as $field => $folder) {
|
|
if ($request->hasFile($field)) {
|
|
if ($wisata?->{$field}) {
|
|
Storage::disk('public')->delete($wisata->publicStoragePath($wisata->{$field}));
|
|
}
|
|
|
|
$data[$field] = $field === 'image'
|
|
? Storage::disk('public')->putFile('wisata', $request->file('image'))
|
|
: $request->file($field)->storeAs(
|
|
$folder,
|
|
$this->uniqueUploadName($request->file($field)),
|
|
'public'
|
|
);
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function uniqueUploadName(\Illuminate\Http\UploadedFile $file): string
|
|
{
|
|
$extension = Str::lower($file->getClientOriginalExtension() ?: $file->extension());
|
|
|
|
return Str::uuid().($extension ? ".{$extension}" : '');
|
|
}
|
|
|
|
private function deleteFiles(Wisata $wisata): void
|
|
{
|
|
Storage::disk('public')->delete(array_filter([
|
|
$wisata->publicStoragePath($wisata->image),
|
|
$wisata->publicStoragePath($wisata->model_path),
|
|
$wisata->publicStoragePath($wisata->video_path),
|
|
]));
|
|
}
|
|
|
|
private function uniqueSlug(string $name, ?int $ignoreId = null): string
|
|
{
|
|
$slug = Str::slug($name);
|
|
$original = $slug;
|
|
$counter = 1;
|
|
|
|
while (Wisata::query()->where('slug', $slug)->when($ignoreId, fn ($query) => $query->whereKeyNot($ignoreId))->exists()) {
|
|
$slug = "{$original}-{$counter}";
|
|
$counter++;
|
|
}
|
|
|
|
return $slug;
|
|
}
|
|
}
|