Add enums for ReviewStatus, TrackMaterial, LaunchType, RideCategory, and RollerCoasterType; implement Designer and RideModel models; create migrations for ride_models and helpful_votes tables; enhance RideGalleryComponent documentation

This commit is contained in:
pacnpal
2025-02-25 20:37:19 -05:00
parent 8951e59f49
commit 64b0e90a27
35 changed files with 3157 additions and 1 deletions

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Livewire;
use App\Models\Ride;
use Livewire\Component;
class RideDetailComponent extends Component
{
/** @var Ride */
public Ride $ride;
/** @var bool */
public bool $showCoasterStats = false;
public function mount(Ride $ride): void
{
$this->ride = $ride->load([
'park',
'parkArea',
'manufacturer',
'designer',
'rideModel',
'coasterStats',
]);
$this->showCoasterStats = $ride->category === 'RC' && $ride->coasterStats !== null;
}
public function toggleCoasterStats(): void
{
$this->showCoasterStats = !$this->showCoasterStats;
}
public function render()
{
return view('livewire.ride-detail');
}
/**
* Get the opening year of the ride.
*/
public function getOpeningYearAttribute(): ?string
{
return $this->ride->opening_date?->format('Y');
}
/**
* Get a formatted date range for the ride's operation.
*/
public function getOperatingPeriodAttribute(): string
{
$start = $this->ride->opening_date?->format('Y');
$end = $this->ride->closing_date?->format('Y');
if (!$start) {
return 'Unknown dates';
}
return $end ? "{$start} - {$end}" : "{$start} - Present";
}
/**
* Get the ride's status badge color classes.
*/
public function getStatusColorClasses(): string
{
return match($this->ride->status) {
'OPERATING' => 'bg-green-100 text-green-800',
'CLOSED_TEMP' => 'bg-yellow-100 text-yellow-800',
'SBNO' => 'bg-red-100 text-red-800',
'CLOSING' => 'bg-orange-100 text-orange-800',
'CLOSED_PERM' => 'bg-gray-100 text-gray-800',
'UNDER_CONSTRUCTION' => 'bg-blue-100 text-blue-800',
'DEMOLISHED' => 'bg-gray-100 text-gray-800',
'RELOCATED' => 'bg-purple-100 text-purple-800',
default => 'bg-gray-100 text-gray-800',
};
}
/**
* Format a measurement value with units.
*/
public function formatMeasurement(?float $value, string $unit): string
{
return $value !== null ? number_format($value, 2) . ' ' . $unit : 'N/A';
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Livewire;
use App\Models\Ride;
use App\Models\Park;
use App\Models\ParkArea;
use App\Models\RideModel;
use App\Models\Designer;
use App\Models\Manufacturer;
use App\Enums\RideCategory;
use App\Enums\RideStatus;
use Livewire\Component;
use Illuminate\Validation\Rules\Enum;
class RideFormComponent extends Component
{
public ?Ride $ride = null;
/** @var array<string, mixed> */
public array $state = [];
/** @var array<string, mixed>|null */
public ?array $coasterStats = null;
/** @var int|null */
public ?int $parkId = null;
protected function rules(): array
{
return [
'state.name' => ['required', 'string', 'max:255'],
'state.park_id' => ['required', 'exists:parks,id'],
'state.park_area_id' => ['nullable', 'exists:park_areas,id'],
'state.manufacturer_id' => ['nullable', 'exists:manufacturers,id'],
'state.designer_id' => ['nullable', 'exists:designers,id'],
'state.ride_model_id' => ['nullable', 'exists:ride_models,id'],
'state.category' => ['required', new Enum(RideCategory::class)],
'state.status' => ['required', new Enum(RideStatus::class)],
'state.post_closing_status' => ['nullable', new Enum(RideStatus::class)],
'state.description' => ['nullable', 'string'],
'state.opening_date' => ['nullable', 'date'],
'state.closing_date' => ['nullable', 'date'],
'state.status_since' => ['nullable', 'date'],
'state.min_height_in' => ['nullable', 'integer', 'min:0'],
'state.max_height_in' => ['nullable', 'integer', 'min:0'],
'state.capacity_per_hour' => ['nullable', 'integer', 'min:0'],
'state.ride_duration_seconds' => ['nullable', 'integer', 'min:0'],
];
}
public function mount(?Ride $ride = null): void
{
$this->ride = $ride;
if ($ride) {
$this->state = $ride->only([
'name', 'park_id', 'park_area_id', 'manufacturer_id',
'designer_id', 'ride_model_id', 'category', 'status',
'post_closing_status', 'description', 'opening_date',
'closing_date', 'status_since', 'min_height_in',
'max_height_in', 'capacity_per_hour', 'ride_duration_seconds',
]);
$this->parkId = $ride->park_id;
if ($ride->coasterStats) {
$this->coasterStats = $ride->coasterStats->toArray();
}
}
}
public function updated(string $field): void
{
$this->validateOnly($field);
if ($field === 'state.category') {
$this->coasterStats = $this->state['category'] === RideCategory::ROLLER_COASTER->value
? ($this->coasterStats ?? [])
: null;
}
}
public function save(): void
{
$this->validate();
if (!$this->ride) {
$this->ride = new Ride();
}
$this->ride->fill($this->state)->save();
// Handle coaster stats if applicable
if ($this->state['category'] === RideCategory::ROLLER_COASTER->value && $this->coasterStats) {
$this->ride->coasterStats()->updateOrCreate(
['ride_id' => $this->ride->id],
$this->coasterStats
);
} elseif ($this->ride->coasterStats) {
$this->ride->coasterStats->delete();
}
session()->flash('message', 'Ride saved successfully.');
$this->redirect(route('rides.show', $this->ride));
}
public function getParksProperty()
{
return Park::orderBy('name')->get();
}
public function getParkAreasProperty()
{
return $this->parkId
? ParkArea::where('park_id', $this->parkId)->orderBy('name')->get()
: collect();
}
public function getManufacturersProperty()
{
return Manufacturer::orderBy('name')->get();
}
public function getDesignersProperty()
{
return Designer::orderBy('name')->get();
}
public function getRideModelsProperty()
{
return RideModel::orderBy('name')->get();
}
public function render()
{
return view('livewire.ride-form');
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Livewire;
use App\Models\Ride;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\Attributes\Rule;
class RideGalleryComponent extends Component
{
use WithFileUploads;
/** @var Ride */
public Ride $ride;
/** @var \Illuminate\Http\UploadedFile */
#[Rule('image|max:10240')] // 10MB Max
public $photo;
/** @var string|null */
public ?string $caption = null;
/** @var bool */
public bool $showUploadForm = false;
public function mount(Ride $ride): void
{
$this->ride = $ride;
}
public function toggleUploadForm(): void
{
$this->showUploadForm = !$this->showUploadForm;
$this->reset(['photo', 'caption']);
}
public function save(): void
{
$this->validate([
'photo' => 'required|image|max:10240',
'caption' => 'nullable|string|max:255',
]);
$path = $this->photo->store('ride-photos', 'public');
$this->ride->photos()->create([
'path' => $path,
'caption' => $this->caption,
'uploaded_by' => Auth::id(),
]);
$this->reset(['photo', 'caption']);
$this->showUploadForm = false;
session()->flash('message', 'Photo uploaded successfully.');
}
public function deletePhoto(int $photoId): void
{
$photo = $this->ride->photos()->findOrFail($photoId);
if ($photo->uploaded_by === Auth::id() || Gate::allows('delete-any-photo')) {
Storage::disk('public')->delete($photo->path);
$photo->delete();
session()->flash('message', 'Photo deleted successfully.');
} else {
session()->flash('error', 'You do not have permission to delete this photo.');
}
}
public function setFeaturedPhoto(int $photoId): void
{
if (Gate::allows('edit', $this->ride)) {
$this->ride->featured_photo_id = $photoId;
$this->ride->save();
session()->flash('message', 'Featured photo updated.');
} else {
session()->flash('error', 'You do not have permission to set the featured photo.');
}
}
public function render()
{
return view('livewire.ride-gallery', [
'photos' => $this->ride->photos()->latest()->paginate(12),
]);
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Livewire;
use App\Enums\RideCategory;
use App\Models\Ride;
use Livewire\Component;
use Livewire\WithPagination;
class RideListComponent extends Component
{
use WithPagination;
/** @var string */
public $search = '';
/** @var string */
public $category = '';
/** @var string */
public $sortField = 'name';
/** @var string */
public $sortDirection = 'asc';
/** @var string */
public $viewMode = 'grid';
/**
* Update search term and reset pagination.
*/
public function updatingSearch(): void
{
$this->resetPage();
}
/**
* Update category filter and reset pagination.
*/
public function updatingCategory(): void
{
$this->resetPage();
}
/**
* Sort results by the given field.
*/
public function sortBy(string $field): void
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortDirection = 'asc';
}
$this->sortField = $field;
}
/**
* Toggle between grid and list views.
*/
public function toggleView(): void
{
$this->viewMode = $this->viewMode === 'grid' ? 'list' : 'grid';
}
/**
* Get all available ride categories.
*/
public function getCategoriesProperty(): array
{
return RideCategory::options();
}
/**
* Get the filtered and sorted rides query.
*/
private function getRidesQuery()
{
return Ride::query()
->when($this->search, fn($query, $search) =>
$query->where('name', 'like', "%{$search}%")
)
->when($this->category, fn($query, $category) =>
$query->where('category', $category)
)
->orderBy($this->sortField, $this->sortDirection);
}
/**
* Render the component.
*/
public function render()
{
$rides = $this->getRidesQuery()->paginate(12);
return view('livewire.ride-list', [
'rides' => $rides,
]);
}
}