Add photo management features, update database configuration, and enhance park model seeding

This commit is contained in:
pacnpal
2025-02-25 15:44:21 -05:00
parent 15b2d4ebcf
commit b4462ba89e
31 changed files with 2700 additions and 71 deletions

View File

@@ -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.

View File

@@ -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
View 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();
}
}