mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 10:11:11 -05:00
Add photo management features, update database configuration, and enhance park model seeding
This commit is contained in:
212
app/Http/Controllers/PhotoController.php
Normal file
212
app/Http/Controllers/PhotoController.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\Photo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
use Intervention\Image\Encoders\JpegEncoder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PhotoController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the photos for a park.
|
||||
*/
|
||||
public function index(Park $park)
|
||||
{
|
||||
$photos = $park->photos()->ordered()->get();
|
||||
|
||||
return response()->json([
|
||||
'photos' => $photos,
|
||||
'featured_photo' => $park->featuredPhoto(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly uploaded photo.
|
||||
*/
|
||||
public function store(Request $request, Park $park)
|
||||
{
|
||||
$request->validate([
|
||||
'photo' => 'required|image|max:10240', // 10MB max
|
||||
'title' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'alt_text' => 'nullable|string|max:255',
|
||||
'credit' => 'nullable|string|max:255',
|
||||
'source_url' => 'nullable|url|max:255',
|
||||
'is_featured' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
// Handle file upload
|
||||
if ($request->hasFile('photo')) {
|
||||
$file = $request->file('photo');
|
||||
$originalFilename = $file->getClientOriginalName();
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
|
||||
// Generate a unique filename
|
||||
$filename = time() . '_' . Str::slug(pathinfo($originalFilename, PATHINFO_FILENAME)) . '.' . $extension;
|
||||
|
||||
// Define the storage path
|
||||
$storagePath = 'photos/parks/' . $park->id;
|
||||
$fullPath = $storagePath . '/' . $filename;
|
||||
|
||||
// Store the original file
|
||||
$file->storeAs('public/' . $storagePath, $filename);
|
||||
|
||||
// Create image manager instance
|
||||
$manager = new ImageManager(new Driver());
|
||||
|
||||
// Get image dimensions
|
||||
$image = $manager->read($file);
|
||||
$width = $image->width();
|
||||
$height = $image->height();
|
||||
|
||||
// Generate thumbnail
|
||||
$thumbnailFilename = pathinfo($filename, PATHINFO_FILENAME) . '_thumb.' . $extension;
|
||||
$thumbnail = $manager->read($file)->scaleDown(width: 300, height: 300);
|
||||
|
||||
// Save thumbnail
|
||||
$encodedThumbnail = $thumbnail->encode(
|
||||
new JpegEncoder(80)
|
||||
);
|
||||
Storage::put(
|
||||
'public/' . $storagePath . '/' . $thumbnailFilename,
|
||||
$encodedThumbnail->toString()
|
||||
);
|
||||
|
||||
// Create photo record
|
||||
$photo = $park->addPhoto([
|
||||
'title' => $request->input('title'),
|
||||
'description' => $request->input('description'),
|
||||
'file_path' => $fullPath,
|
||||
'file_name' => $originalFilename,
|
||||
'file_size' => $file->getSize(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'alt_text' => $request->input('alt_text'),
|
||||
'credit' => $request->input('credit'),
|
||||
'source_url' => $request->input('source_url'),
|
||||
'is_featured' => $request->input('is_featured', false),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Photo uploaded successfully',
|
||||
'photo' => $photo,
|
||||
], 201);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'No photo file provided',
|
||||
], 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified photo.
|
||||
*/
|
||||
public function show(Photo $photo)
|
||||
{
|
||||
return response()->json($photo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified photo.
|
||||
*/
|
||||
public function update(Request $request, Photo $photo)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'alt_text' => 'nullable|string|max:255',
|
||||
'credit' => 'nullable|string|max:255',
|
||||
'source_url' => 'nullable|url|max:255',
|
||||
'is_featured' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$photo->update($request->only([
|
||||
'title', 'description', 'alt_text', 'credit', 'source_url'
|
||||
]));
|
||||
|
||||
// Handle featured status
|
||||
if ($request->has('is_featured') && $request->input('is_featured')) {
|
||||
$photo->setAsFeatured();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Photo updated successfully',
|
||||
'photo' => $photo,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified photo.
|
||||
*/
|
||||
public function destroy(Photo $photo)
|
||||
{
|
||||
// Get the file paths
|
||||
$filePath = 'public/' . $photo->file_path;
|
||||
$pathInfo = pathinfo($photo->file_path);
|
||||
$thumbnailPath = 'public/' . $pathInfo['dirname'] . '/' . $pathInfo['filename'] . '_thumb.' . $pathInfo['extension'];
|
||||
|
||||
// Delete the files
|
||||
Storage::delete([$filePath, $thumbnailPath]);
|
||||
|
||||
// Delete the record
|
||||
$photo->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Photo deleted successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder photos for a park.
|
||||
*/
|
||||
public function reorder(Request $request, Park $park)
|
||||
{
|
||||
$request->validate([
|
||||
'photo_ids' => 'required|array',
|
||||
'photo_ids.*' => 'required|integer|exists:photos,id',
|
||||
]);
|
||||
|
||||
$success = $park->reorderPhotos($request->input('photo_ids'));
|
||||
|
||||
if ($success) {
|
||||
return response()->json([
|
||||
'message' => 'Photos reordered successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Failed to reorder photos',
|
||||
], 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a photo as featured.
|
||||
*/
|
||||
public function setFeatured(Request $request, Park $park, Photo $photo)
|
||||
{
|
||||
if ($photo->photoable_id !== $park->id || $photo->photoable_type !== Park::class) {
|
||||
return response()->json([
|
||||
'message' => 'Photo does not belong to this park',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$success = $park->setFeaturedPhoto($photo);
|
||||
|
||||
if ($success) {
|
||||
return response()->json([
|
||||
'message' => 'Featured photo set successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Failed to set featured photo',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
89
app/Livewire/FeaturedPhotoSelectorComponent.php
Normal file
89
app/Livewire/FeaturedPhotoSelectorComponent.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\Photo;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FeaturedPhotoSelectorComponent extends Component
|
||||
{
|
||||
public Park $park;
|
||||
public $photos = [];
|
||||
public $featuredPhotoId = null;
|
||||
public $isLoading = true;
|
||||
public $error = null;
|
||||
public $success = false;
|
||||
|
||||
protected $listeners = ['photoUploaded' => 'loadPhotos'];
|
||||
|
||||
public function mount(Park $park)
|
||||
{
|
||||
$this->park = $park;
|
||||
$this->loadPhotos();
|
||||
}
|
||||
|
||||
public function loadPhotos()
|
||||
{
|
||||
$this->isLoading = true;
|
||||
$this->error = null;
|
||||
$this->success = false;
|
||||
|
||||
try {
|
||||
$this->photos = $this->park->photos()->ordered()->get();
|
||||
$featuredPhoto = $this->park->featuredPhoto();
|
||||
$this->featuredPhotoId = $featuredPhoto ? $featuredPhoto->id : null;
|
||||
$this->isLoading = false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error loading photos: ' . $e->getMessage());
|
||||
$this->error = 'Failed to load photos: ' . $e->getMessage();
|
||||
$this->isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function setFeatured($photoId)
|
||||
{
|
||||
$this->isLoading = true;
|
||||
$this->error = null;
|
||||
$this->success = false;
|
||||
|
||||
try {
|
||||
$photo = Photo::findOrFail($photoId);
|
||||
|
||||
if ($photo->photoable_id !== $this->park->id || $photo->photoable_type !== Park::class) {
|
||||
throw new \Exception('Photo does not belong to this park');
|
||||
}
|
||||
|
||||
$success = $this->park->setFeaturedPhoto($photo);
|
||||
|
||||
if ($success) {
|
||||
$this->featuredPhotoId = $photoId;
|
||||
$this->success = true;
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'success',
|
||||
'message' => 'Featured photo updated successfully'
|
||||
]);
|
||||
|
||||
// Emit event to refresh other components
|
||||
$this->dispatch('featuredPhotoChanged');
|
||||
} else {
|
||||
throw new \Exception('Failed to set featured photo');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error setting featured photo: ' . $e->getMessage());
|
||||
$this->error = 'Failed to set featured photo: ' . $e->getMessage();
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'error',
|
||||
'message' => 'Failed to set featured photo'
|
||||
]);
|
||||
} finally {
|
||||
$this->isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.featured-photo-selector-component');
|
||||
}
|
||||
}
|
||||
108
app/Livewire/PhotoGalleryComponent.php
Normal file
108
app/Livewire/PhotoGalleryComponent.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\Photo;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PhotoGalleryComponent extends Component
|
||||
{
|
||||
public Park $park;
|
||||
public $photos = [];
|
||||
public $featuredPhoto = null;
|
||||
public $selectedPhoto = null;
|
||||
public $isLoading = true;
|
||||
public $error = null;
|
||||
public $viewMode = 'grid'; // grid or carousel
|
||||
|
||||
protected $listeners = ['photoUploaded' => 'loadPhotos'];
|
||||
|
||||
public function mount(Park $park)
|
||||
{
|
||||
$this->park = $park;
|
||||
$this->loadPhotos();
|
||||
}
|
||||
|
||||
public function loadPhotos()
|
||||
{
|
||||
$this->isLoading = true;
|
||||
$this->error = null;
|
||||
|
||||
try {
|
||||
$this->photos = $this->park->photos()->ordered()->get();
|
||||
$this->featuredPhoto = $this->park->featuredPhoto();
|
||||
$this->isLoading = false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error loading photos: ' . $e->getMessage());
|
||||
$this->error = 'Failed to load photos: ' . $e->getMessage();
|
||||
$this->isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function selectPhoto($photoId)
|
||||
{
|
||||
$this->selectedPhoto = collect($this->photos)->firstWhere('id', $photoId);
|
||||
}
|
||||
|
||||
public function closePhotoDetail()
|
||||
{
|
||||
$this->selectedPhoto = null;
|
||||
}
|
||||
|
||||
public function setFeatured($photoId)
|
||||
{
|
||||
try {
|
||||
$photo = Photo::findOrFail($photoId);
|
||||
$this->park->setFeaturedPhoto($photo);
|
||||
$this->loadPhotos();
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'success',
|
||||
'message' => 'Featured photo updated successfully'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error setting featured photo: ' . $e->getMessage());
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'error',
|
||||
'message' => 'Failed to set featured photo'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function deletePhoto($photoId)
|
||||
{
|
||||
try {
|
||||
$photo = Photo::findOrFail($photoId);
|
||||
|
||||
// Make API request to the PhotoController
|
||||
app(\App\Http\Controllers\PhotoController::class)->destroy($photo);
|
||||
|
||||
$this->loadPhotos();
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'success',
|
||||
'message' => 'Photo deleted successfully'
|
||||
]);
|
||||
|
||||
if ($this->selectedPhoto && $this->selectedPhoto->id === $photoId) {
|
||||
$this->selectedPhoto = null;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deleting photo: ' . $e->getMessage());
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'error',
|
||||
'message' => 'Failed to delete photo: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleViewMode()
|
||||
{
|
||||
$this->viewMode = $this->viewMode === 'grid' ? 'carousel' : 'grid';
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.photo-gallery-component');
|
||||
}
|
||||
}
|
||||
100
app/Livewire/PhotoManagerComponent.php
Normal file
100
app/Livewire/PhotoManagerComponent.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PhotoManagerComponent extends Component
|
||||
{
|
||||
public Park $park;
|
||||
public $photos = [];
|
||||
public $isLoading = true;
|
||||
public $error = null;
|
||||
public $reordering = false;
|
||||
public $photoOrder = [];
|
||||
|
||||
protected $listeners = ['photoUploaded' => 'loadPhotos'];
|
||||
|
||||
public function mount(Park $park)
|
||||
{
|
||||
$this->park = $park;
|
||||
$this->loadPhotos();
|
||||
}
|
||||
|
||||
public function loadPhotos()
|
||||
{
|
||||
$this->isLoading = true;
|
||||
$this->error = null;
|
||||
|
||||
try {
|
||||
$this->photos = $this->park->photos()->ordered()->get()->toArray();
|
||||
$this->photoOrder = collect($this->photos)->pluck('id')->toArray();
|
||||
$this->isLoading = false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error loading photos: ' . $e->getMessage());
|
||||
$this->error = 'Failed to load photos: ' . $e->getMessage();
|
||||
$this->isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function startReordering()
|
||||
{
|
||||
$this->reordering = true;
|
||||
}
|
||||
|
||||
public function cancelReordering()
|
||||
{
|
||||
$this->reordering = false;
|
||||
$this->loadPhotos(); // Reset to original order
|
||||
}
|
||||
|
||||
public function saveOrder()
|
||||
{
|
||||
try {
|
||||
// Make API request to the PhotoController
|
||||
app(\App\Http\Controllers\PhotoController::class)->reorder(
|
||||
request: new \Illuminate\Http\Request(['photo_ids' => $this->photoOrder]),
|
||||
park: $this->park
|
||||
);
|
||||
|
||||
$this->reordering = false;
|
||||
$this->loadPhotos();
|
||||
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'success',
|
||||
'message' => 'Photo order updated successfully'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error reordering photos: ' . $e->getMessage());
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'error',
|
||||
'message' => 'Failed to update photo order: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function moveUp($index)
|
||||
{
|
||||
if ($index > 0) {
|
||||
$temp = $this->photoOrder[$index - 1];
|
||||
$this->photoOrder[$index - 1] = $this->photoOrder[$index];
|
||||
$this->photoOrder[$index] = $temp;
|
||||
}
|
||||
}
|
||||
|
||||
public function moveDown($index)
|
||||
{
|
||||
if ($index < count($this->photoOrder) - 1) {
|
||||
$temp = $this->photoOrder[$index + 1];
|
||||
$this->photoOrder[$index + 1] = $this->photoOrder[$index];
|
||||
$this->photoOrder[$index] = $temp;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.photo-manager-component');
|
||||
}
|
||||
}
|
||||
92
app/Livewire/PhotoUploadComponent.php
Normal file
92
app/Livewire/PhotoUploadComponent.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PhotoUploadComponent extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public Park $park;
|
||||
public $photo;
|
||||
public $title;
|
||||
public $description;
|
||||
public $alt_text;
|
||||
public $credit;
|
||||
public $source_url;
|
||||
public $is_featured = false;
|
||||
public $uploading = false;
|
||||
public $uploadError = null;
|
||||
public $uploadSuccess = false;
|
||||
|
||||
protected $rules = [
|
||||
'photo' => 'required|image|max:10240', // 10MB max
|
||||
'title' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'alt_text' => 'nullable|string|max:255',
|
||||
'credit' => 'nullable|string|max:255',
|
||||
'source_url' => 'nullable|url|max:255',
|
||||
'is_featured' => 'boolean',
|
||||
];
|
||||
|
||||
public function mount(Park $park)
|
||||
{
|
||||
$this->park = $park;
|
||||
}
|
||||
|
||||
public function updatedPhoto()
|
||||
{
|
||||
$this->validate([
|
||||
'photo' => 'image|max:10240', // 10MB max
|
||||
]);
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->uploading = true;
|
||||
$this->uploadError = null;
|
||||
$this->uploadSuccess = false;
|
||||
|
||||
try {
|
||||
$this->validate();
|
||||
|
||||
// Create form data for the API request
|
||||
$formData = [
|
||||
'photo' => $this->photo,
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'alt_text' => $this->alt_text,
|
||||
'credit' => $this->credit,
|
||||
'source_url' => $this->source_url,
|
||||
'is_featured' => $this->is_featured,
|
||||
];
|
||||
|
||||
// Make API request to the PhotoController
|
||||
$response = app(\App\Http\Controllers\PhotoController::class)->store(
|
||||
request: new \Illuminate\Http\Request($formData),
|
||||
park: $this->park
|
||||
);
|
||||
|
||||
// Reset form
|
||||
$this->reset(['photo', 'title', 'description', 'alt_text', 'credit', 'source_url', 'is_featured']);
|
||||
$this->uploadSuccess = true;
|
||||
|
||||
// Emit event to refresh the photo gallery
|
||||
$this->dispatch('photoUploaded');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Photo upload error: ' . $e->getMessage());
|
||||
$this->uploadError = 'Failed to upload photo: ' . $e->getMessage();
|
||||
} finally {
|
||||
$this->uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.photo-upload-component');
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Location extends Model
|
||||
{
|
||||
use \Spatie\Activitylog\LogsActivity;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@@ -41,7 +40,7 @@ class Location extends Model
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'coordinates' => 'point',
|
||||
// 'coordinates' => 'point', // This cast is not available in Laravel by default
|
||||
'latitude' => 'decimal:6',
|
||||
'longitude' => 'decimal:6',
|
||||
'elevation' => 'decimal:2',
|
||||
@@ -51,23 +50,6 @@ class Location extends Model
|
||||
'is_approximate' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be logged.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $logAttributes = [
|
||||
'name',
|
||||
'location_type',
|
||||
'address',
|
||||
'city',
|
||||
'state',
|
||||
'country',
|
||||
'postal_code',
|
||||
'coordinates',
|
||||
'latitude',
|
||||
'longitude',
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot the model.
|
||||
|
||||
@@ -6,8 +6,10 @@ use App\Enums\ParkStatus;
|
||||
use App\Traits\HasLocation;
|
||||
use App\Traits\HasSlugHistory;
|
||||
use App\Traits\HasParkStatistics;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
@@ -95,6 +97,95 @@ class Park extends Model
|
||||
return $this->hasMany(ParkArea::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the photos for the park.
|
||||
*/
|
||||
public function photos(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Photo::class, 'photoable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the featured photo for the park.
|
||||
*/
|
||||
public function featuredPhoto()
|
||||
{
|
||||
return $this->photos()->where('is_featured', true)->first()
|
||||
?? $this->photos()->orderBy('position')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of the featured photo or a default image.
|
||||
*/
|
||||
public function getFeaturedPhotoUrlAttribute(): string
|
||||
{
|
||||
$photo = $this->featuredPhoto();
|
||||
return $photo ? $photo->url : asset('images/placeholders/default-park.jpg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a photo to the park.
|
||||
*
|
||||
* @param array<string, mixed> $attributes
|
||||
* @return \App\Models\Photo
|
||||
*/
|
||||
public function addPhoto(array $attributes): Photo
|
||||
{
|
||||
// Set position to be the last in the collection if not specified
|
||||
if (!isset($attributes['position'])) {
|
||||
$lastPosition = $this->photos()->max('position') ?? 0;
|
||||
$attributes['position'] = $lastPosition + 1;
|
||||
}
|
||||
|
||||
// If this is the first photo or is_featured is true, make it featured
|
||||
if ($this->photos()->count() === 0 || ($attributes['is_featured'] ?? false)) {
|
||||
$attributes['is_featured'] = true;
|
||||
} else {
|
||||
$attributes['is_featured'] = false;
|
||||
}
|
||||
|
||||
return $this->photos()->create($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a photo as the featured photo.
|
||||
*
|
||||
* @param \App\Models\Photo|int $photo
|
||||
* @return bool
|
||||
*/
|
||||
public function setFeaturedPhoto($photo): bool
|
||||
{
|
||||
if (is_numeric($photo)) {
|
||||
$photo = $this->photos()->findOrFail($photo);
|
||||
}
|
||||
|
||||
return $photo->setAsFeatured();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder photos.
|
||||
*
|
||||
* @param array<int, int> $photoIds Ordered array of photo IDs
|
||||
* @return bool
|
||||
*/
|
||||
public function reorderPhotos(array $photoIds): bool
|
||||
{
|
||||
// Begin transaction
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($photoIds as $position => $photoId) {
|
||||
$this->photos()->where('id', $photoId)->update(['position' => $position + 1]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted website URL (ensures proper URL format).
|
||||
*/
|
||||
@@ -165,6 +256,50 @@ class Park extends Model
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted location for the park.
|
||||
*/
|
||||
public function getFormattedLocationAttribute(): string
|
||||
{
|
||||
return $this->formatted_address ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the absolute URL for the park detail page.
|
||||
*/
|
||||
public function getAbsoluteUrl(): string
|
||||
{
|
||||
return route('parks.show', ['slug' => $this->slug]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a park by its current or historical slug.
|
||||
*
|
||||
* @param string $slug
|
||||
* @return array{0: \App\Models\Park|null, 1: bool} Park and whether a historical slug was used
|
||||
*/
|
||||
public static function getBySlug(string $slug): array
|
||||
{
|
||||
// Try current slug
|
||||
$park = static::where('slug', $slug)->first();
|
||||
if ($park) {
|
||||
return [$park, false];
|
||||
}
|
||||
|
||||
// Try historical slug
|
||||
$slugHistory = SlugHistory::where('slug', $slug)
|
||||
->where('sluggable_type', static::class)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($slugHistory) {
|
||||
$park = static::find($slugHistory->sluggable_id);
|
||||
return [$park, true];
|
||||
}
|
||||
|
||||
return [null, false];
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the model.
|
||||
*/
|
||||
|
||||
131
app/Models/Photo.php
Normal file
131
app/Models/Photo.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Photo extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'description',
|
||||
'file_path',
|
||||
'file_name',
|
||||
'file_size',
|
||||
'mime_type',
|
||||
'width',
|
||||
'height',
|
||||
'position',
|
||||
'is_featured',
|
||||
'alt_text',
|
||||
'credit',
|
||||
'source_url',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'width' => 'integer',
|
||||
'height' => 'integer',
|
||||
'file_size' => 'integer',
|
||||
'position' => 'integer',
|
||||
'is_featured' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the parent photoable model.
|
||||
*/
|
||||
public function photoable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the photo.
|
||||
*/
|
||||
public function getUrlAttribute(): string
|
||||
{
|
||||
return asset('storage/' . $this->file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the thumbnail URL for the photo.
|
||||
*/
|
||||
public function getThumbnailUrlAttribute(): string
|
||||
{
|
||||
$pathInfo = pathinfo($this->file_path);
|
||||
$thumbnailPath = $pathInfo['dirname'] . '/' . $pathInfo['filename'] . '_thumb.' . $pathInfo['extension'];
|
||||
|
||||
return asset('storage/' . $thumbnailPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include featured photos.
|
||||
*/
|
||||
public function scopeFeatured($query)
|
||||
{
|
||||
return $query->where('is_featured', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to order by position.
|
||||
*/
|
||||
public function scopeOrdered($query)
|
||||
{
|
||||
return $query->orderBy('position');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this photo as featured and unset others.
|
||||
*/
|
||||
public function setAsFeatured(): bool
|
||||
{
|
||||
if (!$this->photoable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Begin transaction
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// Unset all other photos as featured
|
||||
$this->photoable->photos()
|
||||
->where('id', '!=', $this->id)
|
||||
->update(['is_featured' => false]);
|
||||
|
||||
// Set this photo as featured
|
||||
$this->is_featured = true;
|
||||
$this->save();
|
||||
|
||||
DB::commit();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the position of this photo.
|
||||
*/
|
||||
public function updatePosition(int $position): bool
|
||||
{
|
||||
$this->position = $position;
|
||||
return $this->save();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -19,6 +20,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
// Register the app-layout component
|
||||
Blade::component('components.layouts.app', 'app-layout');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ trait HasSlugHistory
|
||||
|
||||
static::updating(function ($model) {
|
||||
if ($model->isDirty('slug') && $model->getOriginal('slug')) {
|
||||
static::addToSlugHistory($model->getOriginal('slug'));
|
||||
$model->addToSlugHistory($model->getOriginal('slug'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user