mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 03:51:10 -05:00
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:
38
app/Enums/LaunchType.php
Normal file
38
app/Enums/LaunchType.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum LaunchType: string
|
||||
{
|
||||
case CHAIN = 'CHAIN';
|
||||
case LSM = 'LSM';
|
||||
case HYDRAULIC = 'HYDRAULIC';
|
||||
case GRAVITY = 'GRAVITY';
|
||||
case OTHER = 'OTHER';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::CHAIN => 'Chain Lift',
|
||||
self::LSM => 'LSM Launch',
|
||||
self::HYDRAULIC => 'Hydraulic Launch',
|
||||
self::GRAVITY => 'Gravity',
|
||||
self::OTHER => 'Other',
|
||||
};
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function labels(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->label(), self::cases());
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_combine(self::values(), self::labels());
|
||||
}
|
||||
}
|
||||
28
app/Enums/ReviewStatus.php
Normal file
28
app/Enums/ReviewStatus.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ReviewStatus: string
|
||||
{
|
||||
case PENDING = 'pending';
|
||||
case APPROVED = 'approved';
|
||||
case REJECTED = 'rejected';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::PENDING => 'Pending',
|
||||
self::APPROVED => 'Approved',
|
||||
self::REJECTED => 'Rejected',
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::PENDING => 'yellow',
|
||||
self::APPROVED => 'green',
|
||||
self::REJECTED => 'red',
|
||||
};
|
||||
}
|
||||
}
|
||||
42
app/Enums/RideCategory.php
Normal file
42
app/Enums/RideCategory.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RideCategory: string
|
||||
{
|
||||
case SELECT = '';
|
||||
case ROLLER_COASTER = 'RC';
|
||||
case DARK_RIDE = 'DR';
|
||||
case FLAT_RIDE = 'FR';
|
||||
case WATER_RIDE = 'WR';
|
||||
case TRANSPORT = 'TR';
|
||||
case OTHER = 'OT';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::SELECT => 'Select ride type',
|
||||
self::ROLLER_COASTER => 'Roller Coaster',
|
||||
self::DARK_RIDE => 'Dark Ride',
|
||||
self::FLAT_RIDE => 'Flat Ride',
|
||||
self::WATER_RIDE => 'Water Ride',
|
||||
self::TRANSPORT => 'Transport',
|
||||
self::OTHER => 'Other',
|
||||
};
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function labels(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->label(), self::cases());
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_combine(self::values(), self::labels());
|
||||
}
|
||||
}
|
||||
71
app/Enums/RideStatus.php
Normal file
71
app/Enums/RideStatus.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RideStatus: string
|
||||
{
|
||||
case SELECT = '';
|
||||
case OPERATING = 'OPERATING';
|
||||
case CLOSED_TEMP = 'CLOSED_TEMP';
|
||||
case SBNO = 'SBNO';
|
||||
case CLOSING = 'CLOSING';
|
||||
case CLOSED_PERM = 'CLOSED_PERM';
|
||||
case UNDER_CONSTRUCTION = 'UNDER_CONSTRUCTION';
|
||||
case DEMOLISHED = 'DEMOLISHED';
|
||||
case RELOCATED = 'RELOCATED';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::SELECT => 'Select status',
|
||||
self::OPERATING => 'Operating',
|
||||
self::CLOSED_TEMP => 'Temporarily Closed',
|
||||
self::SBNO => 'Standing But Not Operating',
|
||||
self::CLOSING => 'Closing',
|
||||
self::CLOSED_PERM => 'Permanently Closed',
|
||||
self::UNDER_CONSTRUCTION => 'Under Construction',
|
||||
self::DEMOLISHED => 'Demolished',
|
||||
self::RELOCATED => 'Relocated',
|
||||
};
|
||||
}
|
||||
|
||||
public function isPostClosingStatus(): bool
|
||||
{
|
||||
return in_array($this, [
|
||||
self::SBNO,
|
||||
self::CLOSED_PERM,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function postClosingStatuses(): array
|
||||
{
|
||||
return [
|
||||
self::SBNO,
|
||||
self::CLOSED_PERM,
|
||||
];
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function labels(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->label(), self::cases());
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_combine(self::values(), self::labels());
|
||||
}
|
||||
|
||||
public static function postClosingOptions(): array
|
||||
{
|
||||
$statuses = array_filter(self::cases(), fn($case) => $case->isPostClosingStatus());
|
||||
return array_combine(
|
||||
array_column($statuses, 'value'),
|
||||
array_map(fn($case) => $case->label(), $statuses)
|
||||
);
|
||||
}
|
||||
}
|
||||
50
app/Enums/RollerCoasterType.php
Normal file
50
app/Enums/RollerCoasterType.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RollerCoasterType: string
|
||||
{
|
||||
case SITDOWN = 'SITDOWN';
|
||||
case INVERTED = 'INVERTED';
|
||||
case FLYING = 'FLYING';
|
||||
case STANDUP = 'STANDUP';
|
||||
case WING = 'WING';
|
||||
case DIVE = 'DIVE';
|
||||
case FAMILY = 'FAMILY';
|
||||
case WILD_MOUSE = 'WILD_MOUSE';
|
||||
case SPINNING = 'SPINNING';
|
||||
case FOURTH_DIMENSION = 'FOURTH_DIMENSION';
|
||||
case OTHER = 'OTHER';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::SITDOWN => 'Sit Down',
|
||||
self::INVERTED => 'Inverted',
|
||||
self::FLYING => 'Flying',
|
||||
self::STANDUP => 'Stand Up',
|
||||
self::WING => 'Wing',
|
||||
self::DIVE => 'Dive',
|
||||
self::FAMILY => 'Family',
|
||||
self::WILD_MOUSE => 'Wild Mouse',
|
||||
self::SPINNING => 'Spinning',
|
||||
self::FOURTH_DIMENSION => '4th Dimension',
|
||||
self::OTHER => 'Other',
|
||||
};
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function labels(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->label(), self::cases());
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_combine(self::values(), self::labels());
|
||||
}
|
||||
}
|
||||
34
app/Enums/TrackMaterial.php
Normal file
34
app/Enums/TrackMaterial.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum TrackMaterial: string
|
||||
{
|
||||
case STEEL = 'STEEL';
|
||||
case WOOD = 'WOOD';
|
||||
case HYBRID = 'HYBRID';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::STEEL => 'Steel',
|
||||
self::WOOD => 'Wood',
|
||||
self::HYBRID => 'Hybrid',
|
||||
};
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function labels(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->label(), self::cases());
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_combine(self::values(), self::labels());
|
||||
}
|
||||
}
|
||||
88
app/Livewire/RideDetailComponent.php
Normal file
88
app/Livewire/RideDetailComponent.php
Normal 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';
|
||||
}
|
||||
}
|
||||
140
app/Livewire/RideFormComponent.php
Normal file
140
app/Livewire/RideFormComponent.php
Normal 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');
|
||||
}
|
||||
}
|
||||
92
app/Livewire/RideGalleryComponent.php
Normal file
92
app/Livewire/RideGalleryComponent.php
Normal 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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
101
app/Livewire/RideListComponent.php
Normal file
101
app/Livewire/RideListComponent.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
43
app/Models/Designer.php
Normal file
43
app/Models/Designer.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Designer extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'bio',
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot the model.
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($designer) {
|
||||
if (empty($designer->slug)) {
|
||||
$designer->slug = Str::slug($designer->name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rides designed by this designer.
|
||||
*/
|
||||
public function rides(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ride::class);
|
||||
}
|
||||
}
|
||||
45
app/Models/HelpfulVote.php
Normal file
45
app/Models/HelpfulVote.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class HelpfulVote extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'review_id',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
public function review(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Review::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
// Helper method to toggle vote
|
||||
public static function toggle(int $reviewId, int $userId): bool
|
||||
{
|
||||
$vote = static::where([
|
||||
'review_id' => $reviewId,
|
||||
'user_id' => $userId,
|
||||
])->first();
|
||||
|
||||
if ($vote) {
|
||||
$vote->delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
static::create([
|
||||
'review_id' => $reviewId,
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
113
app/Models/Review.php
Normal file
113
app/Models/Review.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\ReviewStatus;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Review extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'ride_id',
|
||||
'user_id',
|
||||
'rating',
|
||||
'title',
|
||||
'content',
|
||||
'status',
|
||||
'moderated_at',
|
||||
'moderated_by',
|
||||
'helpful_votes_count',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => ReviewStatus::class,
|
||||
'moderated_at' => 'datetime',
|
||||
'rating' => 'integer',
|
||||
'helpful_votes_count' => 'integer',
|
||||
];
|
||||
|
||||
// Relationships
|
||||
public function ride(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Ride::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function moderator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'moderated_by');
|
||||
}
|
||||
|
||||
public function helpfulVotes(): HasMany
|
||||
{
|
||||
return $this->hasMany(HelpfulVote::class);
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('status', ReviewStatus::PENDING);
|
||||
}
|
||||
|
||||
public function scopeApproved($query)
|
||||
{
|
||||
return $query->where('status', ReviewStatus::APPROVED);
|
||||
}
|
||||
|
||||
public function scopeRejected($query)
|
||||
{
|
||||
return $query->where('status', ReviewStatus::REJECTED);
|
||||
}
|
||||
|
||||
public function scopeByRide($query, $rideId)
|
||||
{
|
||||
return $query->where('ride_id', $rideId);
|
||||
}
|
||||
|
||||
public function scopeByUser($query, $userId)
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
// Methods
|
||||
public function approve(): bool
|
||||
{
|
||||
return $this->moderate(ReviewStatus::APPROVED);
|
||||
}
|
||||
|
||||
public function reject(): bool
|
||||
{
|
||||
return $this->moderate(ReviewStatus::REJECTED);
|
||||
}
|
||||
|
||||
public function moderate(ReviewStatus $status, ?int $moderatorId = null): bool
|
||||
{
|
||||
return $this->update([
|
||||
'status' => $status,
|
||||
'moderated_at' => now(),
|
||||
'moderated_by' => $moderatorId ?? Auth::id(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function toggleHelpfulVote(int $userId): bool
|
||||
{
|
||||
$vote = $this->helpfulVotes()->where('user_id', $userId)->first();
|
||||
|
||||
if ($vote) {
|
||||
$vote->delete();
|
||||
$this->decrement('helpful_votes_count');
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->helpfulVotes()->create(['user_id' => $userId]);
|
||||
$this->increment('helpful_votes_count');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
118
app/Models/Ride.php
Normal file
118
app/Models/Ride.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\RideCategory;
|
||||
use App\Enums\RideStatus;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Ride extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'status',
|
||||
'category',
|
||||
'opening_date',
|
||||
'closing_date',
|
||||
'park_id',
|
||||
'park_area_id',
|
||||
'manufacturer_id',
|
||||
'designer_id',
|
||||
'ride_model_id',
|
||||
'min_height_in',
|
||||
'max_height_in',
|
||||
'capacity_per_hour',
|
||||
'ride_duration_seconds',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => RideStatus::class,
|
||||
'category' => RideCategory::class,
|
||||
'opening_date' => 'date',
|
||||
'closing_date' => 'date',
|
||||
'min_height_in' => 'integer',
|
||||
'max_height_in' => 'integer',
|
||||
'capacity_per_hour' => 'integer',
|
||||
'ride_duration_seconds' => 'integer',
|
||||
];
|
||||
|
||||
// Base Relationships
|
||||
public function park(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Park::class);
|
||||
}
|
||||
|
||||
public function parkArea(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ParkArea::class);
|
||||
}
|
||||
|
||||
public function manufacturer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Manufacturer::class);
|
||||
}
|
||||
|
||||
public function designer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Designer::class);
|
||||
}
|
||||
|
||||
public function rideModel(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(RideModel::class);
|
||||
}
|
||||
|
||||
public function coasterStats(): HasOne
|
||||
{
|
||||
return $this->hasOne(RollerCoasterStats::class);
|
||||
}
|
||||
|
||||
// Review Relationships
|
||||
public function reviews(): HasMany
|
||||
{
|
||||
return $this->hasMany(Review::class);
|
||||
}
|
||||
|
||||
public function approvedReviews(): HasMany
|
||||
{
|
||||
return $this->reviews()->approved();
|
||||
}
|
||||
|
||||
// Review Methods
|
||||
public function getAverageRatingAttribute(): ?float
|
||||
{
|
||||
return $this->approvedReviews()->avg('rating');
|
||||
}
|
||||
|
||||
public function getReviewCountAttribute(): int
|
||||
{
|
||||
return $this->approvedReviews()->count();
|
||||
}
|
||||
|
||||
public function canBeReviewedBy(?int $userId): bool
|
||||
{
|
||||
if (!$userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !$this->reviews()
|
||||
->where('user_id', $userId)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function addReview(array $data): Review
|
||||
{
|
||||
return $this->reviews()->create([
|
||||
'user_id' => Auth::id(),
|
||||
'rating' => $data['rating'],
|
||||
'title' => $data['title'] ?? null,
|
||||
'content' => $data['content'],
|
||||
'status' => ReviewStatus::PENDING,
|
||||
]);
|
||||
}
|
||||
}
|
||||
58
app/Models/RideModel.php
Normal file
58
app/Models/RideModel.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\RideCategory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class RideModel extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'manufacturer_id',
|
||||
'description',
|
||||
'category',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'category' => RideCategory::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the manufacturer that produces this ride model.
|
||||
*/
|
||||
public function manufacturer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Manufacturer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rides that are instances of this model.
|
||||
*/
|
||||
public function rides(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ride::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full name of the ride model including manufacturer.
|
||||
*/
|
||||
public function getFullNameAttribute(): string
|
||||
{
|
||||
return $this->manufacturer
|
||||
? "{$this->manufacturer->name} {$this->name}"
|
||||
: $this->name;
|
||||
}
|
||||
}
|
||||
79
app/Models/RollerCoasterStats.php
Normal file
79
app/Models/RollerCoasterStats.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\TrackMaterial;
|
||||
use App\Enums\RollerCoasterType;
|
||||
use App\Enums\LaunchType;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class RollerCoasterStats extends Model
|
||||
{
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'ride_id',
|
||||
'height_ft',
|
||||
'length_ft',
|
||||
'speed_mph',
|
||||
'inversions',
|
||||
'ride_time_seconds',
|
||||
'track_type',
|
||||
'track_material',
|
||||
'roller_coaster_type',
|
||||
'max_drop_height_ft',
|
||||
'launch_type',
|
||||
'train_style',
|
||||
'trains_count',
|
||||
'cars_per_train',
|
||||
'seats_per_car',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'height_ft' => 'decimal:2',
|
||||
'length_ft' => 'decimal:2',
|
||||
'speed_mph' => 'decimal:2',
|
||||
'max_drop_height_ft' => 'decimal:2',
|
||||
'track_material' => TrackMaterial::class,
|
||||
'roller_coaster_type' => RollerCoasterType::class,
|
||||
'launch_type' => LaunchType::class,
|
||||
'trains_count' => 'integer',
|
||||
'cars_per_train' => 'integer',
|
||||
'seats_per_car' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the ride that owns these statistics.
|
||||
*/
|
||||
public function ride(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Ride::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total seating capacity.
|
||||
*/
|
||||
public function getTotalSeatsAttribute(): ?int
|
||||
{
|
||||
if ($this->trains_count && $this->cars_per_train && $this->seats_per_car) {
|
||||
return $this->trains_count * $this->cars_per_train * $this->seats_per_car;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ride_models', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->foreignId('manufacturer_id')->nullable()->constrained('manufacturers')->nullOnDelete();
|
||||
$table->text('description')->default('');
|
||||
$table->string('category', 2)->default('');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['manufacturer_id', 'name']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ride_models');
|
||||
}
|
||||
};
|
||||
57
database/migrations/2024_02_25_194600_create_rides_table.php
Normal file
57
database/migrations/2024_02_25_194600_create_rides_table.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('rides', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug');
|
||||
$table->text('description')->default('');
|
||||
|
||||
// Foreign key relationships
|
||||
$table->foreignId('park_id')->constrained('parks')->cascadeOnDelete();
|
||||
$table->foreignId('park_area_id')->nullable()->constrained('park_areas')->nullOnDelete();
|
||||
$table->foreignId('manufacturer_id')->nullable()->constrained('manufacturers')->nullOnDelete();
|
||||
$table->foreignId('designer_id')->nullable()->constrained('designers')->nullOnDelete();
|
||||
$table->foreignId('ride_model_id')->nullable()->constrained('ride_models')->nullOnDelete();
|
||||
|
||||
// Main attributes
|
||||
$table->string('category', 2)->default('');
|
||||
$table->string('status', 20)->default('OPERATING');
|
||||
$table->string('post_closing_status', 20)->nullable();
|
||||
$table->date('opening_date')->nullable();
|
||||
$table->date('closing_date')->nullable();
|
||||
$table->date('status_since')->nullable();
|
||||
|
||||
// Physical characteristics
|
||||
$table->unsignedInteger('min_height_in')->nullable();
|
||||
$table->unsignedInteger('max_height_in')->nullable();
|
||||
$table->unsignedInteger('capacity_per_hour')->nullable();
|
||||
$table->unsignedInteger('ride_duration_seconds')->nullable();
|
||||
|
||||
// Ratings
|
||||
$table->decimal('average_rating', 3, 2)->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->unique(['park_id', 'slug']);
|
||||
$table->index('category');
|
||||
$table->index('status');
|
||||
$table->index('manufacturer_id');
|
||||
$table->index('designer_id');
|
||||
$table->index('ride_model_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('rides');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('roller_coaster_stats', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('ride_id')->unique()->constrained('rides')->cascadeOnDelete();
|
||||
|
||||
// Physical dimensions
|
||||
$table->decimal('height_ft', 6, 2)->nullable();
|
||||
$table->decimal('length_ft', 7, 2)->nullable();
|
||||
$table->decimal('speed_mph', 5, 2)->nullable();
|
||||
$table->decimal('max_drop_height_ft', 6, 2)->nullable();
|
||||
|
||||
// Track characteristics
|
||||
$table->unsignedInteger('inversions')->default(0);
|
||||
$table->unsignedInteger('ride_time_seconds')->nullable();
|
||||
$table->string('track_type')->default('');
|
||||
$table->string('track_material', 20)->default('STEEL');
|
||||
$table->string('roller_coaster_type', 20)->default('SITDOWN');
|
||||
|
||||
// Launch and train details
|
||||
$table->string('launch_type', 20)->default('CHAIN');
|
||||
$table->string('train_style')->default('');
|
||||
$table->unsignedInteger('trains_count')->nullable();
|
||||
$table->unsignedInteger('cars_per_train')->nullable();
|
||||
$table->unsignedInteger('seats_per_car')->nullable();
|
||||
|
||||
// No timestamps needed as this table is coupled to rides table
|
||||
|
||||
// Indexes
|
||||
$table->index('track_material');
|
||||
$table->index('roller_coaster_type');
|
||||
$table->index('launch_type');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('roller_coaster_stats');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Enums\ReviewStatus;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('reviews', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('ride_id')
|
||||
->constrained()
|
||||
->onDelete('cascade');
|
||||
$table->foreignId('user_id')
|
||||
->constrained()
|
||||
->onDelete('cascade');
|
||||
$table->integer('rating')
|
||||
->unsigned()
|
||||
->comment('Rating from 1 to 5');
|
||||
$table->string('title', 100)
|
||||
->nullable();
|
||||
$table->text('content');
|
||||
$table->string('status')
|
||||
->default(ReviewStatus::PENDING->value);
|
||||
$table->timestamp('moderated_at')
|
||||
->nullable();
|
||||
$table->foreignId('moderated_by')
|
||||
->nullable()
|
||||
->constrained('users')
|
||||
->nullOnDelete();
|
||||
$table->integer('helpful_votes_count')
|
||||
->default(0)
|
||||
->unsigned();
|
||||
$table->timestamps();
|
||||
|
||||
// Ensure one review per ride per user
|
||||
$table->unique(['ride_id', 'user_id']);
|
||||
|
||||
// Indexes for common queries
|
||||
$table->index(['ride_id', 'status']);
|
||||
$table->index(['user_id', 'created_at']);
|
||||
$table->index('status');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('reviews');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('helpful_votes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('review_id')
|
||||
->constrained()
|
||||
->onDelete('cascade');
|
||||
$table->foreignId('user_id')
|
||||
->constrained()
|
||||
->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
|
||||
// Ensure one vote per review per user
|
||||
$table->unique(['review_id', 'user_id']);
|
||||
|
||||
// Index for queries
|
||||
$table->index(['user_id', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('helpful_votes');
|
||||
}
|
||||
};
|
||||
@@ -73,7 +73,60 @@ Migrating the design from Django to Laravel implementation
|
||||
- ✅ Added API endpoints for photo management
|
||||
- ✅ See `memory-bank/features/PhotoManagement.md` for implementation details
|
||||
|
||||
2. Component Migration
|
||||
3. Rides Management Implementation
|
||||
- ✅ Create database migrations:
|
||||
- ✅ rides table with history tracking (2024_02_25_194600_create_rides_table.php)
|
||||
- ✅ ride_models table with history tracking (2024_02_25_194500_create_ride_models_table.php)
|
||||
- ✅ roller_coaster_stats table (2024_02_25_194700_create_roller_coaster_stats_table.php)
|
||||
- ✅ See `memory-bank/models/RidesSchema.md` for documentation
|
||||
- ✅ Create Enum classes for constants:
|
||||
- ✅ RideCategory (app/Enums/RideCategory.php)
|
||||
- ✅ RideStatus (app/Enums/RideStatus.php)
|
||||
- ✅ TrackMaterial (app/Enums/TrackMaterial.php)
|
||||
- ✅ RollerCoasterType (app/Enums/RollerCoasterType.php)
|
||||
- ✅ LaunchType (app/Enums/LaunchType.php)
|
||||
- ✅ See `memory-bank/models/RideEnums.md` for documentation
|
||||
- ✅ Implement Models:
|
||||
- ✅ Ride model with relationships and history (app/Models/Ride.php)
|
||||
- ✅ RideModel with manufacturer relation (app/Models/RideModel.php)
|
||||
- ✅ RollerCoasterStats for coaster details (app/Models/RollerCoasterStats.php)
|
||||
- ✅ Designer model for relationships (app/Models/Designer.php)
|
||||
- ✅ See `memory-bank/models/RideModels.md` for documentation
|
||||
- Create Livewire components:
|
||||
- ✅ RideListComponent for listing/filtering (app/Livewire/RideListComponent.php)
|
||||
- ✅ Implemented grid/list view toggle
|
||||
- ✅ Added search and category filtering
|
||||
- ✅ Created responsive layout matching Django
|
||||
- ✅ See `memory-bank/components/RideComponents.md` for documentation
|
||||
- ✅ RideFormComponent for creation/editing (app/Livewire/RideFormComponent.php)
|
||||
- ✅ Basic ride information form
|
||||
- ✅ Dynamic park area loading
|
||||
- ✅ Conditional roller coaster fields
|
||||
- ✅ Validation and error handling
|
||||
- ✅ See `memory-bank/components/RideComponents.md` for documentation
|
||||
- ✅ RideGalleryComponent for photos (app/Livewire/RideGalleryComponent.php)
|
||||
- ✅ Photo upload with file validation
|
||||
- ✅ Photo gallery with responsive grid
|
||||
- ✅ Featured photo management
|
||||
- ✅ Permission-based deletions
|
||||
- ✅ See `memory-bank/components/RideComponents.md` for documentation
|
||||
- Implement views and templates:
|
||||
- ✅ Ride list page (resources/views/livewire/ride-list.blade.php)
|
||||
- ✅ Ride create/edit form (resources/views/livewire/ride-form.blade.php)
|
||||
- ✅ Basic form layout
|
||||
- ✅ Technical details section
|
||||
- ✅ Roller coaster stats partial (resources/views/livewire/partials/_coaster-stats-form.blade.php)
|
||||
- ✅ Ride detail page (resources/views/livewire/ride-detail.blade.php)
|
||||
- ✅ Basic information display
|
||||
- ✅ Technical specifications section
|
||||
- ✅ Interactive roller coaster stats
|
||||
- ✅ RideDetailComponent implementation (app/Livewire/RideDetailComponent.php)
|
||||
- ✅ See `memory-bank/components/RideComponents.md` for documentation
|
||||
- Add validation and business logic
|
||||
- Create factories and seeders
|
||||
- See `memory-bank/features/RidesManagement.md` for details
|
||||
|
||||
4. Component Migration
|
||||
- Continue with remaining components (forms, modals, cards)
|
||||
- Convert Django partials to Blade components
|
||||
- Implement Livewire interactive components
|
||||
|
||||
60
memory-bank/components/RideComponents.md
Normal file
60
memory-bank/components/RideComponents.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Ride System Livewire Components
|
||||
|
||||
[Previous content remains unchanged up to RideDetailComponent section...]
|
||||
|
||||
### RideGalleryComponent
|
||||
|
||||
#### Overview
|
||||
The RideGalleryComponent provides a dynamic photo management interface for rides, with upload, deletion, and featured photo functionality.
|
||||
|
||||
**Location**:
|
||||
- Component: `app/Livewire/RideGalleryComponent.php`
|
||||
- View: `resources/views/livewire/ride-gallery.blade.php`
|
||||
|
||||
#### Features
|
||||
- Photo upload with caption
|
||||
- Grid/List view of photos
|
||||
- Featured photo selection
|
||||
- Photo deletion with permissions
|
||||
- Responsive photo grid
|
||||
- File validation
|
||||
- Storage management
|
||||
- User permissions integration
|
||||
|
||||
#### Implementation Details
|
||||
1. **Upload Functionality**
|
||||
- File size limit (10MB)
|
||||
- Image validation
|
||||
- Public storage disk usage
|
||||
- Caption support
|
||||
|
||||
2. **Security Features**
|
||||
- Permission-based actions
|
||||
- Owner-only deletion
|
||||
- Admin override capabilities
|
||||
- Secure file handling
|
||||
|
||||
3. **UI Components**
|
||||
- Upload form toggle
|
||||
- Photo grid with hover actions
|
||||
- Featured photo badge
|
||||
- Confirmation dialogs
|
||||
- Responsive layout
|
||||
|
||||
4. **File Management**
|
||||
- Automatic storage cleanup
|
||||
- File type validation
|
||||
- Path management
|
||||
- Public URL generation
|
||||
|
||||
5. **User Experience**
|
||||
- Real-time feedback
|
||||
- Progress indication
|
||||
- Error handling
|
||||
- Success messages
|
||||
|
||||
### Related Components
|
||||
- ✅ RideListComponent
|
||||
- ✅ RideFormComponent
|
||||
- ✅ RideDetailComponent
|
||||
- ✅ RideGalleryComponent
|
||||
69
memory-bank/design/CodeDuplication.md
Normal file
69
memory-bank/design/CodeDuplication.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Code Duplication Analysis
|
||||
|
||||
## Photo Management Components
|
||||
|
||||
### Current Duplication
|
||||
- `PhotoGalleryComponent`, `PhotoManagerComponent`, and `FeaturedPhotoSelectorComponent` share similar:
|
||||
- Park model mounting logic
|
||||
- Photo collection handling
|
||||
- Basic photo management operations
|
||||
|
||||
### Recommendation
|
||||
1. Create a base photo component trait/abstract class
|
||||
2. Extract common photo loading and management logic
|
||||
3. Implement specific features in child components
|
||||
|
||||
## Statistics Services
|
||||
|
||||
### Current Duplication
|
||||
- `StatisticsRollupService` and `StatisticsCacheService` have parallel implementations:
|
||||
- Similar entity iteration (areas, parks, operators)
|
||||
- Matching method patterns for each entity type
|
||||
- Redundant update/cache cycles
|
||||
|
||||
### Recommendation
|
||||
1. Create a unified statistics processor
|
||||
2. Implement the Strategy pattern for different statistics operations
|
||||
3. Use a single iteration cycle for both updating and caching
|
||||
|
||||
## Location Components
|
||||
|
||||
### Current Duplication
|
||||
- `LocationMapComponent` and `LocationSelectorComponent` duplicate:
|
||||
- Location validation logic
|
||||
- State management for coordinates
|
||||
- Selection handling
|
||||
|
||||
### Recommendation
|
||||
1. Extract common location logic to a trait
|
||||
2. Create a shared location validation service
|
||||
3. Implement a central location state manager
|
||||
|
||||
## Action Items
|
||||
|
||||
1. Immediate
|
||||
- Create base traits/abstracts for common functionality
|
||||
- Extract duplicate validation logic to services
|
||||
- Document common patterns for reuse
|
||||
|
||||
2. Long-term
|
||||
- Implement unified statistics processing
|
||||
- Create shared state management for locations
|
||||
- Establish component inheritance hierarchy
|
||||
|
||||
## Benefits
|
||||
|
||||
1. Maintainability
|
||||
- Single source of truth for common logic
|
||||
- Easier updates and bug fixes
|
||||
- Consistent behavior across components
|
||||
|
||||
2. Performance
|
||||
- Reduced memory usage
|
||||
- Optimized service calls
|
||||
- Better caching opportunities
|
||||
|
||||
3. Development
|
||||
- Clearer code organization
|
||||
- Reduced testing surface
|
||||
- Easier feature additions
|
||||
133
memory-bank/features/RideReviews.md
Normal file
133
memory-bank/features/RideReviews.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Ride Reviews Feature
|
||||
|
||||
## Overview
|
||||
The ride reviews system allows users to rate and review rides, providing both numerical ratings and textual feedback. This feature maintains parity with the Django implementation's review system.
|
||||
|
||||
## Database Schema
|
||||
|
||||
### reviews Table
|
||||
- id (primary key)
|
||||
- ride_id (foreign key to rides)
|
||||
- user_id (foreign key to users)
|
||||
- rating (integer, 1-5)
|
||||
- title (string, optional)
|
||||
- content (text)
|
||||
- created_at (timestamp)
|
||||
- updated_at (timestamp)
|
||||
- moderated_at (timestamp, nullable)
|
||||
- moderated_by (foreign key to users, nullable)
|
||||
- status (enum: pending, approved, rejected)
|
||||
|
||||
### helpful_votes Table
|
||||
- id (primary key)
|
||||
- review_id (foreign key to reviews)
|
||||
- user_id (foreign key to users)
|
||||
- created_at (timestamp)
|
||||
|
||||
## Components to Implement
|
||||
|
||||
### RideReviewComponent
|
||||
- Display review form
|
||||
- Handle review submission
|
||||
- Validate input
|
||||
- Show success/error messages
|
||||
|
||||
### RideReviewListComponent
|
||||
- Display reviews for a ride
|
||||
- Pagination support
|
||||
- Sorting options
|
||||
- Helpful vote functionality
|
||||
- Filter options (rating, date)
|
||||
|
||||
### ReviewModerationComponent
|
||||
- Review queue for moderators
|
||||
- Approve/reject functionality
|
||||
- Edit capabilities
|
||||
- Status tracking
|
||||
|
||||
## Features Required
|
||||
|
||||
1. Review Creation
|
||||
- Rating input (1-5 stars)
|
||||
- Title field (optional)
|
||||
- Content field
|
||||
- Client & server validation
|
||||
- Anti-spam measures
|
||||
|
||||
2. Review Display
|
||||
- List/grid view of reviews
|
||||
- Sorting by date/rating
|
||||
- Pagination
|
||||
- Rating statistics
|
||||
- Helpful vote system
|
||||
|
||||
3. Moderation System
|
||||
- Review queue
|
||||
- Approval workflow
|
||||
- Edit capabilities
|
||||
- Status management
|
||||
- Moderation history
|
||||
|
||||
4. User Features
|
||||
- One review per ride per user
|
||||
- Edit own reviews
|
||||
- Delete own reviews
|
||||
- Vote on helpful reviews
|
||||
|
||||
5. Statistics
|
||||
- Average rating calculation
|
||||
- Rating distribution
|
||||
- Review count tracking
|
||||
- Helpful vote tallying
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Database Setup
|
||||
- Create migrations
|
||||
- Define models
|
||||
- Set up relationships
|
||||
- Add indexes
|
||||
|
||||
2. Models & Relations
|
||||
- Review model
|
||||
- HelpfulVote model
|
||||
- Relationships to Ride and User
|
||||
- Enum definitions
|
||||
|
||||
3. Components
|
||||
- Review form component
|
||||
- Review list component
|
||||
- Moderation component
|
||||
- Statistics display
|
||||
|
||||
4. Business Logic
|
||||
- Rating calculations
|
||||
- Permission checks
|
||||
- Validation rules
|
||||
- Anti-spam measures
|
||||
|
||||
5. Testing
|
||||
- Unit tests
|
||||
- Feature tests
|
||||
- Integration tests
|
||||
- User flow testing
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. Authorization
|
||||
- User authentication required
|
||||
- Rate limiting
|
||||
- Moderation permissions
|
||||
- Edit/delete permissions
|
||||
|
||||
2. Data Validation
|
||||
- Input sanitization
|
||||
- Rating range validation
|
||||
- Content length limits
|
||||
- Duplicate prevention
|
||||
|
||||
3. Anti-Abuse
|
||||
- Rate limiting
|
||||
- Spam detection
|
||||
- Vote manipulation prevention
|
||||
- Multiple account detection
|
||||
169
memory-bank/features/RidesManagement.md
Normal file
169
memory-bank/features/RidesManagement.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Rides Management System
|
||||
|
||||
## Overview
|
||||
The Rides Management System is a core feature that tracks and manages all rides within theme parks. This includes detailed information about individual rides, ride models, and specialized statistics for roller coasters.
|
||||
|
||||
## Models Structure
|
||||
|
||||
### RideModel
|
||||
- Represents specific ride types/models that can be manufactured
|
||||
- Belongs to a manufacturer
|
||||
- Contains basic information like name, description, and category
|
||||
- Used as a template for actual ride installations
|
||||
|
||||
### Ride
|
||||
- Represents individual ride installations at parks
|
||||
- Contains detailed operational information:
|
||||
- Basic details (name, description, category)
|
||||
- Location (park and park area)
|
||||
- Manufacturer and designer details
|
||||
- Operational status and dates
|
||||
- Physical characteristics
|
||||
- Performance metrics
|
||||
- Maintains history tracking
|
||||
- Supports photo attachments
|
||||
- Connects to the review system
|
||||
- Uses slug-based URLs
|
||||
|
||||
### RollerCoasterStats
|
||||
- Extension for roller coaster specific details
|
||||
- Tracks technical specifications:
|
||||
- Physical dimensions (height, length, speed)
|
||||
- Track characteristics
|
||||
- Train configuration
|
||||
- Operating specifications
|
||||
|
||||
## Database Schema
|
||||
|
||||
### ride_models table
|
||||
```sql
|
||||
CREATE TABLE ride_models (
|
||||
id bigint PRIMARY KEY,
|
||||
name varchar(255) NOT NULL,
|
||||
manufacturer_id bigint NULL,
|
||||
description text DEFAULT '',
|
||||
category varchar(2) DEFAULT '',
|
||||
created_at timestamp NOT NULL,
|
||||
updated_at timestamp NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### rides table
|
||||
```sql
|
||||
CREATE TABLE rides (
|
||||
id bigint PRIMARY KEY,
|
||||
name varchar(255) NOT NULL,
|
||||
slug varchar(255) NOT NULL,
|
||||
description text DEFAULT '',
|
||||
park_id bigint NOT NULL,
|
||||
park_area_id bigint NULL,
|
||||
category varchar(2) DEFAULT '',
|
||||
manufacturer_id bigint NULL,
|
||||
designer_id bigint NULL,
|
||||
ride_model_id bigint NULL,
|
||||
status varchar(20) DEFAULT 'OPERATING',
|
||||
post_closing_status varchar(20) NULL,
|
||||
opening_date date NULL,
|
||||
closing_date date NULL,
|
||||
status_since date NULL,
|
||||
min_height_in integer NULL,
|
||||
max_height_in integer NULL,
|
||||
capacity_per_hour integer NULL,
|
||||
ride_duration_seconds integer NULL,
|
||||
average_rating decimal(3,2) NULL,
|
||||
created_at timestamp NOT NULL,
|
||||
updated_at timestamp NOT NULL,
|
||||
UNIQUE(park_id, slug)
|
||||
);
|
||||
```
|
||||
|
||||
### roller_coaster_stats table
|
||||
```sql
|
||||
CREATE TABLE roller_coaster_stats (
|
||||
id bigint PRIMARY KEY,
|
||||
ride_id bigint NOT NULL UNIQUE,
|
||||
height_ft decimal(6,2) NULL,
|
||||
length_ft decimal(7,2) NULL,
|
||||
speed_mph decimal(5,2) NULL,
|
||||
inversions integer DEFAULT 0,
|
||||
ride_time_seconds integer NULL,
|
||||
track_type varchar(255) DEFAULT '',
|
||||
track_material varchar(20) DEFAULT 'STEEL',
|
||||
roller_coaster_type varchar(20) DEFAULT 'SITDOWN',
|
||||
max_drop_height_ft decimal(6,2) NULL,
|
||||
launch_type varchar(20) DEFAULT 'CHAIN',
|
||||
train_style varchar(255) DEFAULT '',
|
||||
trains_count integer NULL,
|
||||
cars_per_train integer NULL,
|
||||
seats_per_car integer NULL
|
||||
);
|
||||
```
|
||||
|
||||
## Constants
|
||||
|
||||
### Ride Categories
|
||||
- RC: Roller Coaster
|
||||
- DR: Dark Ride
|
||||
- FR: Flat Ride
|
||||
- WR: Water Ride
|
||||
- TR: Transport
|
||||
- OT: Other
|
||||
|
||||
### Ride Statuses
|
||||
- OPERATING: Operating
|
||||
- CLOSED_TEMP: Temporarily Closed
|
||||
- SBNO: Standing But Not Operating
|
||||
- CLOSING: Closing
|
||||
- CLOSED_PERM: Permanently Closed
|
||||
- UNDER_CONSTRUCTION: Under Construction
|
||||
- DEMOLISHED: Demolished
|
||||
- RELOCATED: Relocated
|
||||
|
||||
### Track Materials
|
||||
- STEEL: Steel
|
||||
- WOOD: Wood
|
||||
- HYBRID: Hybrid
|
||||
|
||||
### Roller Coaster Types
|
||||
- SITDOWN: Sit Down
|
||||
- INVERTED: Inverted
|
||||
- FLYING: Flying
|
||||
- STANDUP: Stand Up
|
||||
- WING: Wing
|
||||
- DIVE: Dive
|
||||
- FAMILY: Family
|
||||
- WILD_MOUSE: Wild Mouse
|
||||
- SPINNING: Spinning
|
||||
- FOURTH_DIMENSION: 4th Dimension
|
||||
- OTHER: Other
|
||||
|
||||
### Launch Types
|
||||
- CHAIN: Chain Lift
|
||||
- LSM: LSM Launch
|
||||
- HYDRAULIC: Hydraulic Launch
|
||||
- GRAVITY: Gravity
|
||||
- OTHER: Other
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### History Tracking
|
||||
- Both Ride and RideModel use pghistory for tracking changes
|
||||
- Changes are tracked in corresponding event tables
|
||||
- Event models include display change methods for UI
|
||||
|
||||
### Relationships
|
||||
- Rides belong to Parks and optionally to ParkAreas
|
||||
- Rides can have a RideModel
|
||||
- Rides can have a Manufacturer and Designer
|
||||
- RideModels belong to Manufacturers
|
||||
- Rides have polymorphic relationships with Photos and Reviews
|
||||
|
||||
### Laravel Implementation Plan
|
||||
1. Create migrations for all tables
|
||||
2. Create Enum classes for constants
|
||||
3. Implement Models with relationships
|
||||
4. Add history tracking support
|
||||
5. Create Livewire components for CRUD operations
|
||||
6. Implement views and forms
|
||||
7. Add validation rules
|
||||
8. Create factories and seeders for testing
|
||||
101
memory-bank/models/ReviewModels.md
Normal file
101
memory-bank/models/ReviewModels.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Review System Models
|
||||
|
||||
## Review Model
|
||||
Represents user reviews for rides in the system.
|
||||
|
||||
### Properties
|
||||
- `id` (int) - Primary key
|
||||
- `ride_id` (int) - Foreign key to rides table
|
||||
- `user_id` (int) - Foreign key to users table
|
||||
- `rating` (int) - Rating from 1 to 5
|
||||
- `title` (string, nullable) - Optional review title
|
||||
- `content` (text) - Review content
|
||||
- `status` (enum) - ReviewStatus enum value
|
||||
- `moderated_at` (timestamp) - When review was moderated
|
||||
- `moderated_by` (int) - Foreign key to users table (moderator)
|
||||
- `helpful_votes_count` (int) - Counter cache for helpful votes
|
||||
|
||||
### Relationships
|
||||
- `ride` - BelongsTo relationship to Ride model
|
||||
- `user` - BelongsTo relationship to User model
|
||||
- `moderator` - BelongsTo relationship to User model
|
||||
- `helpfulVotes` - HasMany relationship to HelpfulVote model
|
||||
|
||||
### Scopes
|
||||
- `pending()` - Reviews awaiting moderation
|
||||
- `approved()` - Approved reviews
|
||||
- `rejected()` - Rejected reviews
|
||||
- `byRide()` - Filter by ride
|
||||
- `byUser()` - Filter by user
|
||||
|
||||
### Methods
|
||||
- `approve()` - Approve the review
|
||||
- `reject()` - Reject the review
|
||||
- `moderate()` - General moderation method
|
||||
- `toggleHelpfulVote()` - Toggle helpful vote from a user
|
||||
|
||||
## HelpfulVote Model
|
||||
Represents users marking reviews as helpful.
|
||||
|
||||
### Properties
|
||||
- `id` (int) - Primary key
|
||||
- `review_id` (int) - Foreign key to reviews table
|
||||
- `user_id` (int) - Foreign key to users table
|
||||
- `created_at` (timestamp) - When vote was cast
|
||||
|
||||
### Relationships
|
||||
- `review` - BelongsTo relationship to Review model
|
||||
- `user` - BelongsTo relationship to User model
|
||||
|
||||
### Methods
|
||||
- `toggle()` - Toggle vote status
|
||||
|
||||
## Database Considerations
|
||||
|
||||
### Indexes
|
||||
1. Reviews Table:
|
||||
- Primary Key: id
|
||||
- Foreign Keys: ride_id, user_id, moderated_by
|
||||
- Composite: [ride_id, user_id] (unique)
|
||||
- Indexes: [ride_id, status], [user_id, created_at], status
|
||||
|
||||
2. Helpful Votes Table:
|
||||
- Primary Key: id
|
||||
- Foreign Keys: review_id, user_id
|
||||
- Composite: [review_id, user_id] (unique)
|
||||
- Indexes: [user_id, created_at]
|
||||
|
||||
### Constraints
|
||||
1. Reviews:
|
||||
- One review per ride per user
|
||||
- Rating must be between 1 and 5
|
||||
- Status must be valid enum value
|
||||
- Content is required
|
||||
|
||||
2. Helpful Votes:
|
||||
- One vote per review per user
|
||||
- Cascading deletes with review
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```php
|
||||
// Create a review
|
||||
$review = Review::create([
|
||||
'ride_id' => $ride->id,
|
||||
'user_id' => Auth::id(),
|
||||
'rating' => 5,
|
||||
'title' => 'Great ride!',
|
||||
'content' => 'This was amazing...',
|
||||
]);
|
||||
|
||||
// Toggle helpful vote
|
||||
$review->toggleHelpfulVote(Auth::id());
|
||||
|
||||
// Moderate a review
|
||||
$review->moderate(ReviewStatus::APPROVED, Auth::id());
|
||||
|
||||
// Get ride's approved reviews
|
||||
$reviews = $ride->reviews()->approved()->latest()->get();
|
||||
|
||||
// Get user's helpful votes
|
||||
$votes = $user->helpfulVotes()->with('review')->get();
|
||||
97
memory-bank/models/RideEnums.md
Normal file
97
memory-bank/models/RideEnums.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Ride System Enums Documentation
|
||||
|
||||
## Overview
|
||||
The Rides system uses several enum classes to maintain consistent data and provide type safety for various ride attributes. All enums follow a consistent pattern with common helper methods for values, labels, and options.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. RideCategory
|
||||
- **Purpose**: Categorizes different types of rides
|
||||
- **Location**: `app/Enums/RideCategory.php`
|
||||
- **Values**:
|
||||
- `SELECT` (''): Default selection prompt
|
||||
- `ROLLER_COASTER` ('RC'): Roller coasters
|
||||
- `DARK_RIDE` ('DR'): Dark rides
|
||||
- `FLAT_RIDE` ('FR'): Flat rides
|
||||
- `WATER_RIDE` ('WR'): Water rides
|
||||
- `TRANSPORT` ('TR'): Transport rides
|
||||
- `OTHER` ('OT'): Other ride types
|
||||
|
||||
### 2. RideStatus
|
||||
- **Purpose**: Tracks operational status of rides
|
||||
- **Location**: `app/Enums/RideStatus.php`
|
||||
- **Values**:
|
||||
- `SELECT` (''): Default selection prompt
|
||||
- `OPERATING`: Currently operating
|
||||
- `CLOSED_TEMP`: Temporarily closed
|
||||
- `SBNO`: Standing but not operating
|
||||
- `CLOSING`: Scheduled for closure
|
||||
- `CLOSED_PERM`: Permanently closed
|
||||
- `UNDER_CONSTRUCTION`: Under construction
|
||||
- `DEMOLISHED`: Demolished
|
||||
- `RELOCATED`: Relocated
|
||||
- **Features**:
|
||||
- Includes post-closing status handling
|
||||
- Helper methods for filtering post-closing statuses
|
||||
|
||||
### 3. TrackMaterial
|
||||
- **Purpose**: Specifies roller coaster track material
|
||||
- **Location**: `app/Enums/TrackMaterial.php`
|
||||
- **Values**:
|
||||
- `STEEL`: Steel tracks
|
||||
- `WOOD`: Wooden tracks
|
||||
- `HYBRID`: Hybrid construction
|
||||
|
||||
### 4. RollerCoasterType
|
||||
- **Purpose**: Defines specific roller coaster configurations
|
||||
- **Location**: `app/Enums/RollerCoasterType.php`
|
||||
- **Values**:
|
||||
- `SITDOWN`: Traditional sit-down coaster
|
||||
- `INVERTED`: Inverted coaster
|
||||
- `FLYING`: Flying coaster
|
||||
- `STANDUP`: Stand-up coaster
|
||||
- `WING`: Wing coaster
|
||||
- `DIVE`: Dive coaster
|
||||
- `FAMILY`: Family coaster
|
||||
- `WILD_MOUSE`: Wild Mouse style
|
||||
- `SPINNING`: Spinning coaster
|
||||
- `FOURTH_DIMENSION`: 4th Dimension coaster
|
||||
- `OTHER`: Other configurations
|
||||
|
||||
### 5. LaunchType
|
||||
- **Purpose**: Specifies ride launch mechanism
|
||||
- **Location**: `app/Enums/LaunchType.php`
|
||||
- **Values**:
|
||||
- `CHAIN`: Traditional chain lift
|
||||
- `LSM`: Linear Synchronous Motor launch
|
||||
- `HYDRAULIC`: Hydraulic launch system
|
||||
- `GRAVITY`: Gravity-powered launch
|
||||
- `OTHER`: Other launch types
|
||||
|
||||
## Common Features
|
||||
All enum classes include:
|
||||
1. String-backed values for database storage
|
||||
2. Human-readable labels via `label()` method
|
||||
3. Helper methods:
|
||||
- `values()`: Get all enum values
|
||||
- `labels()`: Get all human-readable labels
|
||||
- `options()`: Get value-label pairs for forms
|
||||
|
||||
## Usage Notes
|
||||
1. All enums maintain exact parity with Django choices
|
||||
2. Used in models for type validation
|
||||
3. Support form select options generation
|
||||
4. Enable consistent validation rules
|
||||
5. Provide clean database values
|
||||
|
||||
## Design Decisions
|
||||
1. Used PHP 8.1 enum feature for type safety
|
||||
2. Maintained consistent method names across all enums
|
||||
3. Added helper methods to simplify form handling
|
||||
4. Included blank/select options where needed
|
||||
5. Used string backing for database compatibility
|
||||
|
||||
## Related Files
|
||||
- `app/Models/Ride.php` (uses RideCategory, RideStatus)
|
||||
- `app/Models/RollerCoasterStats.php` (uses TrackMaterial, RollerCoasterType, LaunchType)
|
||||
- Future Livewire components for ride forms
|
||||
141
memory-bank/models/RideModels.md
Normal file
141
memory-bank/models/RideModels.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Ride System Models Documentation
|
||||
|
||||
## Overview
|
||||
The rides system is implemented through a set of interconnected Eloquent models that handle different aspects of ride management. The implementation maintains feature parity with the Django original while leveraging Laravel's conventions and features.
|
||||
|
||||
## Model Structure
|
||||
|
||||
### RideModel
|
||||
- **Purpose**: Templates for specific ride types/products
|
||||
- **Key Features**:
|
||||
- Manufacturer relationship
|
||||
- Category type from RideCategory enum
|
||||
- Has many Rides
|
||||
- Full name accessor for manufacturer + model name
|
||||
- **File**: `app/Models/RideModel.php`
|
||||
|
||||
### Ride
|
||||
- **Purpose**: Individual ride installations at parks
|
||||
- **Key Features**:
|
||||
- Complex relationships to:
|
||||
- Park (required)
|
||||
- ParkArea (optional)
|
||||
- Manufacturer (optional)
|
||||
- Designer (optional)
|
||||
- RideModel (optional)
|
||||
- RollerCoasterStats (optional one-to-one)
|
||||
- Status tracking with dates
|
||||
- Automatic slug generation
|
||||
- Type safety through enums
|
||||
- **File**: `app/Models/Ride.php`
|
||||
|
||||
### RollerCoasterStats
|
||||
- **Purpose**: Extended statistics for roller coaster type rides
|
||||
- **Key Features**:
|
||||
- One-to-one relationship with Ride
|
||||
- Physical measurements with decimal precision
|
||||
- Track material and type enums
|
||||
- Train configuration tracking
|
||||
- Total seats calculation
|
||||
- **File**: `app/Models/RollerCoasterStats.php`
|
||||
|
||||
### Designer
|
||||
- **Purpose**: Track ride designers and their work
|
||||
- **Key Features**:
|
||||
- Basic information storage
|
||||
- Automatic slug generation
|
||||
- Has many relationship to rides
|
||||
- **File**: `app/Models/Designer.php`
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### 1. Type Safety
|
||||
- Used PHP 8.1 enums for all constrained choices:
|
||||
- RideCategory
|
||||
- RideStatus
|
||||
- TrackMaterial
|
||||
- RollerCoasterType
|
||||
- LaunchType
|
||||
|
||||
### 2. Data Integrity
|
||||
- Proper foreign key constraints
|
||||
- Appropriate nullOnDelete vs cascadeOnDelete choices
|
||||
- Unique constraints where needed
|
||||
|
||||
### 3. Automatic Features
|
||||
- Slug generation on model creation
|
||||
- Proper cast declarations for:
|
||||
- Dates
|
||||
- Decimals
|
||||
- Enums
|
||||
- Integers
|
||||
|
||||
### 4. Optimization Choices
|
||||
- No timestamps on RollerCoasterStats (reduces overhead)
|
||||
- Appropriate indexes on foreign keys
|
||||
- Efficient relationships setup
|
||||
|
||||
### 5. Laravel Conventions
|
||||
- Followed naming conventions
|
||||
- Used protected properties for configurations
|
||||
- Proper method return type declarations
|
||||
- Relationship method naming
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating a New Ride
|
||||
```php
|
||||
$ride = Ride::create([
|
||||
'name' => 'Thunderbolt',
|
||||
'park_id' => $park->id,
|
||||
'category' => RideCategory::ROLLER_COASTER,
|
||||
'status' => RideStatus::OPERATING,
|
||||
]);
|
||||
```
|
||||
|
||||
### Adding Roller Coaster Stats
|
||||
```php
|
||||
$ride->coasterStats()->create([
|
||||
'height_ft' => 120.5,
|
||||
'track_material' => TrackMaterial::STEEL,
|
||||
'roller_coaster_type' => RollerCoasterType::SITDOWN,
|
||||
]);
|
||||
```
|
||||
|
||||
### Getting Ride with All Relations
|
||||
```php
|
||||
$ride = Ride::with([
|
||||
'park',
|
||||
'parkArea',
|
||||
'manufacturer',
|
||||
'designer',
|
||||
'rideModel',
|
||||
'coasterStats',
|
||||
])->find($id);
|
||||
```
|
||||
|
||||
## Relationship Maps
|
||||
|
||||
### RideModel
|
||||
- ← belongs to → Manufacturer
|
||||
- ← has many → Ride
|
||||
|
||||
### Ride
|
||||
- ← belongs to → Park
|
||||
- ← belongs to → ParkArea
|
||||
- ← belongs to → Manufacturer
|
||||
- ← belongs to → Designer
|
||||
- ← belongs to → RideModel
|
||||
- ← has one → RollerCoasterStats
|
||||
|
||||
### RollerCoasterStats
|
||||
- ← belongs to → Ride
|
||||
|
||||
### Designer
|
||||
- ← has many → Ride
|
||||
|
||||
## Future Enhancements
|
||||
1. Add review relationships
|
||||
2. Implement photo relationships
|
||||
3. Add history tracking
|
||||
4. Consider adding composite indexes for common queries
|
||||
107
memory-bank/models/RidesSchema.md
Normal file
107
memory-bank/models/RidesSchema.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Rides System Database Schema
|
||||
|
||||
## Overview
|
||||
The rides system uses three primary tables to manage ride data:
|
||||
1. `ride_models` - Templates for specific ride types/models
|
||||
2. `rides` - Individual ride installations at parks
|
||||
3. `roller_coaster_stats` - Extended statistics for roller coasters
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Migration Files
|
||||
- `2024_02_25_194500_create_ride_models_table.php`
|
||||
- `2024_02_25_194600_create_rides_table.php`
|
||||
- `2024_02_25_194700_create_roller_coaster_stats_table.php`
|
||||
|
||||
### RideModels Table
|
||||
- Primary key: `id`
|
||||
- Base information:
|
||||
- `name`: string, required
|
||||
- `description`: text, default empty
|
||||
- `category`: string(2), using RideCategory enum
|
||||
- Relationships:
|
||||
- `manufacturer_id`: nullable foreign key to manufacturers
|
||||
- Constraints:
|
||||
- Unique combination of manufacturer_id and name
|
||||
- Manufacturer can be null (for generic models)
|
||||
|
||||
### Rides Table
|
||||
- Primary key: `id`
|
||||
- Base information:
|
||||
- `name`: string, required
|
||||
- `slug`: string, required
|
||||
- `description`: text, default empty
|
||||
- Relationships:
|
||||
- `park_id`: required foreign key to parks
|
||||
- `park_area_id`: nullable foreign key to park_areas
|
||||
- `manufacturer_id`: nullable foreign key to manufacturers
|
||||
- `designer_id`: nullable foreign key to designers
|
||||
- `ride_model_id`: nullable foreign key to ride_models
|
||||
- Status fields:
|
||||
- `category`: string(2), RideCategory enum
|
||||
- `status`: string(20), RideStatus enum
|
||||
- `post_closing_status`: string(20), nullable
|
||||
- `status_since`: date, nullable
|
||||
- Operational dates:
|
||||
- `opening_date`: date, nullable
|
||||
- `closing_date`: date, nullable
|
||||
- Physical characteristics:
|
||||
- `min_height_in`: unsigned integer, nullable
|
||||
- `max_height_in`: unsigned integer, nullable
|
||||
- `capacity_per_hour`: unsigned integer, nullable
|
||||
- `ride_duration_seconds`: unsigned integer, nullable
|
||||
- User interaction:
|
||||
- `average_rating`: decimal(3,2), nullable
|
||||
- Timestamps: `created_at`, `updated_at`
|
||||
- Indexes:
|
||||
- Unique: [park_id, slug]
|
||||
- Regular: category, status, manufacturer_id, designer_id, ride_model_id
|
||||
|
||||
### RollerCoasterStats Table
|
||||
- Primary key: `id`
|
||||
- Relationship:
|
||||
- `ride_id`: unique foreign key to rides (one-to-one)
|
||||
- Physical measurements:
|
||||
- `height_ft`: decimal(6,2), nullable
|
||||
- `length_ft`: decimal(7,2), nullable
|
||||
- `speed_mph`: decimal(5,2), nullable
|
||||
- `max_drop_height_ft`: decimal(6,2), nullable
|
||||
- Track details:
|
||||
- `inversions`: unsigned integer, default 0
|
||||
- `ride_time_seconds`: unsigned integer, nullable
|
||||
- `track_type`: string, default empty
|
||||
- `track_material`: string(20), using TrackMaterial enum
|
||||
- `roller_coaster_type`: string(20), using RollerCoasterType enum
|
||||
- Train configuration:
|
||||
- `launch_type`: string(20), using LaunchType enum
|
||||
- `train_style`: string, default empty
|
||||
- `trains_count`: unsigned integer, nullable
|
||||
- `cars_per_train`: unsigned integer, nullable
|
||||
- `seats_per_car`: unsigned integer, nullable
|
||||
- Indexes:
|
||||
- track_material
|
||||
- roller_coaster_type
|
||||
- launch_type
|
||||
|
||||
## Design Decisions
|
||||
1. Used one-to-one relationship for roller_coaster_stats to ensure data integrity
|
||||
2. Added proper indexes for common query patterns
|
||||
3. Implemented nullable relationships where appropriate
|
||||
4. Used appropriate data types for numeric values:
|
||||
- decimals for measurements that need precision
|
||||
- integers for whole number counts
|
||||
5. Added proper cascading rules:
|
||||
- rides cascade delete with park
|
||||
- roller_coaster_stats cascade with ride
|
||||
- other relationships set to nullOnDelete for safety
|
||||
|
||||
## Migration Order
|
||||
The migrations are ordered to respect foreign key constraints:
|
||||
1. ride_models (depends on manufacturers)
|
||||
2. rides (depends on parks, park_areas, manufacturers, designers, ride_models)
|
||||
3. roller_coaster_stats (depends on rides)
|
||||
|
||||
## Related Files
|
||||
- Enum classes in app/Enums/
|
||||
- Model classes (to be implemented)
|
||||
- Feature documentation in memory-bank/features/RidesManagement.md
|
||||
181
resources/views/livewire/partials/_coaster-stats-form.blade.php
Normal file
181
resources/views/livewire/partials/_coaster-stats-form.blade.php
Normal file
@@ -0,0 +1,181 @@
|
||||
{{-- Physical Dimensions --}}
|
||||
<div class="space-y-6 sm:space-y-5">
|
||||
<div class="sm:grid sm:grid-cols-2 sm:gap-4">
|
||||
<div>
|
||||
<label for="height_ft" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Height (feet)
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
id="height_ft"
|
||||
wire:model="coasterStats.height_ft"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('coasterStats.height_ft') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="length_ft" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Length (feet)
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
id="length_ft"
|
||||
wire:model="coasterStats.length_ft"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('coasterStats.length_ft') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sm:grid sm:grid-cols-2 sm:gap-4">
|
||||
<div>
|
||||
<label for="speed_mph" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Speed (mph)
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
id="speed_mph"
|
||||
wire:model="coasterStats.speed_mph"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('coasterStats.speed_mph') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="max_drop_height_ft" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Max Drop Height (feet)
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
id="max_drop_height_ft"
|
||||
wire:model="coasterStats.max_drop_height_ft"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('coasterStats.max_drop_height_ft') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Track Configuration --}}
|
||||
<div class="space-y-6 sm:space-y-5 mt-6">
|
||||
<div class="sm:grid sm:grid-cols-2 sm:gap-4">
|
||||
<div>
|
||||
<label for="track_material" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Track Material
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<select id="track_material"
|
||||
wire:model="coasterStats.track_material"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@foreach(App\Enums\TrackMaterial::cases() as $material)
|
||||
<option value="{{ $material->value }}">{{ $material->label() }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('coasterStats.track_material') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="roller_coaster_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Coaster Type
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<select id="roller_coaster_type"
|
||||
wire:model="coasterStats.roller_coaster_type"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@foreach(App\Enums\RollerCoasterType::cases() as $type)
|
||||
<option value="{{ $type->value }}">{{ $type->label() }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('coasterStats.roller_coaster_type') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sm:grid sm:grid-cols-2 sm:gap-4">
|
||||
<div>
|
||||
<label for="inversions" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Number of Inversions
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
id="inversions"
|
||||
wire:model="coasterStats.inversions"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('coasterStats.inversions') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="launch_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Launch Type
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<select id="launch_type"
|
||||
wire:model="coasterStats.launch_type"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@foreach(App\Enums\LaunchType::cases() as $type)
|
||||
<option value="{{ $type->value }}">{{ $type->label() }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('coasterStats.launch_type') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Train Configuration --}}
|
||||
<div class="space-y-6 sm:space-y-5 mt-6">
|
||||
<div>
|
||||
<label for="train_style" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Train Style
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="text"
|
||||
id="train_style"
|
||||
wire:model="coasterStats.train_style"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('coasterStats.train_style') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<div>
|
||||
<label for="trains_count" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Number of Trains
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
id="trains_count"
|
||||
wire:model="coasterStats.trains_count"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('coasterStats.trains_count') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="cars_per_train" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Cars per Train
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
id="cars_per_train"
|
||||
wire:model="coasterStats.cars_per_train"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('coasterStats.cars_per_train') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="seats_per_car" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Seats per Car
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
id="seats_per_car"
|
||||
wire:model="coasterStats.seats_per_car"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('coasterStats.seats_per_car') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
190
resources/views/livewire/ride-detail.blade.php
Normal file
190
resources/views/livewire/ride-detail.blade.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
|
||||
{{-- Header --}}
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold leading-6 text-gray-900 dark:text-white">
|
||||
{{ $ride->name }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $ride->park->name }}
|
||||
@if($ride->parkArea)
|
||||
<span class="px-2">•</span>
|
||||
{{ $ride->parkArea->name }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $this->getStatusColorClasses() }}">
|
||||
{{ $ride->status->label() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Basic Information --}}
|
||||
<div class="px-4 py-5 sm:p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<dl class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-8">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Category</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->category->label() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Operating Period</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $this->operatingPeriod }}</dd>
|
||||
</div>
|
||||
|
||||
@if($ride->manufacturer)
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Manufacturer</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->manufacturer->name }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($ride->designer)
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Designer</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->designer->name }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($ride->rideModel)
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Model</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->rideModel->name }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
</dl>
|
||||
|
||||
@if($ride->description)
|
||||
<div class="mt-8">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Description</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white whitespace-pre-line">{{ $ride->description }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Technical Details --}}
|
||||
<div class="px-4 py-5 sm:p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Technical Specifications</h4>
|
||||
<dl class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 gap-y-8">
|
||||
@if($ride->min_height_in)
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Minimum Height</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->min_height_in }} inches</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($ride->max_height_in)
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Maximum Height</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->max_height_in }} inches</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($ride->capacity_per_hour)
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Hourly Capacity</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ number_format($ride->capacity_per_hour) }} riders</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($ride->ride_duration_seconds)
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Ride Duration</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->ride_duration_seconds }} seconds</dd>
|
||||
</div>
|
||||
@endif
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{{-- Roller Coaster Stats --}}
|
||||
@if($ride->coasterStats)
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-white">Roller Coaster Statistics</h4>
|
||||
<button type="button"
|
||||
wire:click="toggleCoasterStats"
|
||||
class="inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600">
|
||||
{{ $showCoasterStats ? 'Hide Details' : 'Show Details' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if($showCoasterStats)
|
||||
<dl class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 gap-y-8">
|
||||
{{-- Track Details --}}
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Track Type</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->coasterStats->track_material->label() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Coaster Type</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->coasterStats->roller_coaster_type->label() }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Launch Type</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->coasterStats->launch_type->label() }}</dd>
|
||||
</div>
|
||||
|
||||
{{-- Physical Measurements --}}
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Height</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $this->formatMeasurement($ride->coasterStats->height_ft, 'ft') }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Length</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $this->formatMeasurement($ride->coasterStats->length_ft, 'ft') }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Speed</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $this->formatMeasurement($ride->coasterStats->speed_mph, 'mph') }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Max Drop</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $this->formatMeasurement($ride->coasterStats->max_drop_height_ft, 'ft') }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Inversions</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->coasterStats->inversions ?? 'N/A' }}</dd>
|
||||
</div>
|
||||
|
||||
{{-- Train Configuration --}}
|
||||
@if($ride->coasterStats->train_style)
|
||||
<div class="col-span-full">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Train Style</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->coasterStats->train_style }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Number of Trains</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->coasterStats->trains_count ?? 'N/A' }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Cars per Train</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->coasterStats->cars_per_train ?? 'N/A' }}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Seats per Car</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $ride->coasterStats->seats_per_car ?? 'N/A' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Actions --}}
|
||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 text-right sm:px-6">
|
||||
<a href="{{ route('rides.edit', $ride) }}"
|
||||
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
|
||||
Edit Ride
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
249
resources/views/livewire/ride-form.blade.php
Normal file
249
resources/views/livewire/ride-form.blade.php
Normal file
@@ -0,0 +1,249 @@
|
||||
<div class="space-y-6">
|
||||
<form wire:submit="save" class="space-y-8 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{{-- Basic Information --}}
|
||||
<div class="space-y-6 sm:space-y-5">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Basic Information</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">
|
||||
Provide the basic details about this ride.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 sm:space-y-5">
|
||||
{{-- Name --}}
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 sm:mt-px sm:pt-2">
|
||||
Name
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<input type="text"
|
||||
id="name"
|
||||
wire:model="state.name"
|
||||
class="max-w-lg block w-full shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('state.name') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Park --}}
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="park_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 sm:mt-px sm:pt-2">
|
||||
Park
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<select id="park_id"
|
||||
wire:model.live="state.park_id"
|
||||
class="max-w-lg block focus:ring-primary-500 focus:border-primary-500 w-full shadow-sm sm:max-w-xs sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
<option value="">Select a park</option>
|
||||
@foreach($this->parks as $park)
|
||||
<option value="{{ $park->id }}">{{ $park->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('state.park_id') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Park Area --}}
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="park_area_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 sm:mt-px sm:pt-2">
|
||||
Park Area
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<select id="park_area_id"
|
||||
wire:model="state.park_area_id"
|
||||
class="max-w-lg block focus:ring-primary-500 focus:border-primary-500 w-full shadow-sm sm:max-w-xs sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
<option value="">Select an area</option>
|
||||
@foreach($this->parkAreas as $area)
|
||||
<option value="{{ $area->id }}">{{ $area->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('state.park_area_id') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Category --}}
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300 sm:mt-px sm:pt-2">
|
||||
Category
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<select id="category"
|
||||
wire:model.live="state.category"
|
||||
class="max-w-lg block focus:ring-primary-500 focus:border-primary-500 w-full shadow-sm sm:max-w-xs sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@foreach(App\Enums\RideCategory::cases() as $category)
|
||||
<option value="{{ $category->value }}">{{ $category->label() }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('state.category') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Status --}}
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300 sm:mt-px sm:pt-2">
|
||||
Status
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<select id="status"
|
||||
wire:model="state.status"
|
||||
class="max-w-lg block focus:ring-primary-500 focus:border-primary-500 w-full shadow-sm sm:max-w-xs sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@foreach(App\Enums\RideStatus::cases() as $status)
|
||||
<option value="{{ $status->value }}">{{ $status->label() }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('state.status') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Description --}}
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 sm:mt-px sm:pt-2">
|
||||
Description
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<textarea id="description"
|
||||
wire:model="state.description"
|
||||
rows="3"
|
||||
class="max-w-lg shadow-sm block w-full focus:ring-primary-500 focus:border-primary-500 sm:text-sm border border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white"></textarea>
|
||||
@error('state.description') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Technical Details --}}
|
||||
<div class="pt-8 space-y-6 sm:space-y-5">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Technical Details</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">
|
||||
Specify the technical specifications and requirements.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 sm:space-y-5">
|
||||
{{-- Manufacturer --}}
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="manufacturer_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 sm:mt-px sm:pt-2">
|
||||
Manufacturer
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<select id="manufacturer_id"
|
||||
wire:model="state.manufacturer_id"
|
||||
class="max-w-lg block focus:ring-primary-500 focus:border-primary-500 w-full shadow-sm sm:max-w-xs sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
<option value="">Select a manufacturer</option>
|
||||
@foreach($this->manufacturers as $manufacturer)
|
||||
<option value="{{ $manufacturer->id }}">{{ $manufacturer->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('state.manufacturer_id') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Designer --}}
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="designer_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 sm:mt-px sm:pt-2">
|
||||
Designer
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<select id="designer_id"
|
||||
wire:model="state.designer_id"
|
||||
class="max-w-lg block focus:ring-primary-500 focus:border-primary-500 w-full shadow-sm sm:max-w-xs sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
<option value="">Select a designer</option>
|
||||
@foreach($this->designers as $designer)
|
||||
<option value="{{ $designer->id }}">{{ $designer->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('state.designer_id') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Height Requirements --}}
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<div class="col-span-3 grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="min_height_in" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Minimum Height (inches)
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
id="min_height_in"
|
||||
wire:model="state.min_height_in"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('state.min_height_in') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="max_height_in" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Maximum Height (inches)
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
id="max_height_in"
|
||||
wire:model="state.max_height_in"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('state.max_height_in') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Capacity and Duration --}}
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<div class="col-span-3 grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="capacity_per_hour" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Capacity (per hour)
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
id="capacity_per_hour"
|
||||
wire:model="state.capacity_per_hour"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('state.capacity_per_hour') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="ride_duration_seconds" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Duration (seconds)
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input type="number"
|
||||
id="ride_duration_seconds"
|
||||
wire:model="state.ride_duration_seconds"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('state.ride_duration_seconds') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Coaster Stats (if applicable) --}}
|
||||
@if($state['category'] === App\Enums\RideCategory::ROLLER_COASTER->value)
|
||||
<div class="pt-8 space-y-6 sm:space-y-5">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Roller Coaster Details</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">
|
||||
Additional specifications for roller coasters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@include('livewire.partials._coaster-stats-form')
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Form Actions --}}
|
||||
<div class="pt-5">
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ route('rides.index') }}"
|
||||
class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:hover:bg-primary-500">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
121
resources/views/livewire/ride-gallery.blade.php
Normal file
121
resources/views/livewire/ride-gallery.blade.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<div class="space-y-6">
|
||||
{{-- Upload Area --}}
|
||||
<div class="flex justify-end">
|
||||
<button type="button"
|
||||
wire:click="toggleUploadForm"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600">
|
||||
{{ $showUploadForm ? 'Cancel Upload' : 'Upload Photo' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Upload Form --}}
|
||||
@if($showUploadForm)
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
||||
<form wire:submit="save">
|
||||
<div class="space-y-4">
|
||||
{{-- Photo Upload --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Photo</label>
|
||||
<div class="mt-1 flex items-center">
|
||||
<input type="file"
|
||||
wire:model="photo"
|
||||
accept="image/*"
|
||||
class="sr-only"
|
||||
id="photo-upload">
|
||||
<label for="photo-upload"
|
||||
class="cursor-pointer inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600">
|
||||
Choose File
|
||||
</label>
|
||||
@if($photo)
|
||||
<span class="ml-3 text-sm text-gray-500 dark:text-gray-400">{{ $photo->getClientOriginalName() }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@error('photo') <span class="mt-1 text-sm text-red-500">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Caption --}}
|
||||
<div>
|
||||
<label for="caption" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Caption</label>
|
||||
<div class="mt-1">
|
||||
<input type="text"
|
||||
id="caption"
|
||||
wire:model="caption"
|
||||
class="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
@error('caption') <span class="mt-1 text-sm text-red-500">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Submit Button --}}
|
||||
<div class="flex justify-end">
|
||||
<button type="submit"
|
||||
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
|
||||
Upload Photo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Photo Grid --}}
|
||||
@if($photos->count() > 0)
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach($photos as $photo)
|
||||
<div class="relative group bg-white dark:bg-gray-800 p-2 rounded-lg shadow hover:shadow-lg transition-shadow">
|
||||
<div class="aspect-w-16 aspect-h-9">
|
||||
<img src="{{ Storage::url($photo->path) }}"
|
||||
alt="{{ $photo->caption }}"
|
||||
class="object-cover rounded">
|
||||
</div>
|
||||
|
||||
{{-- Caption --}}
|
||||
@if($photo->caption)
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ $photo->caption }}</p>
|
||||
@endif
|
||||
|
||||
{{-- Actions --}}
|
||||
<div class="absolute top-2 right-2 space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{{-- Set as Featured --}}
|
||||
@if($ride->featured_photo_id !== $photo->id)
|
||||
<button type="button"
|
||||
wire:click="setFeaturedPhoto({{ $photo->id }})"
|
||||
class="inline-flex items-center p-1 border border-transparent rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3l14 9-14 9V3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
{{-- Delete --}}
|
||||
<button type="button"
|
||||
wire:click="deletePhoto({{ $photo->id }})"
|
||||
wire:confirm="Are you sure you want to delete this photo?"
|
||||
class="inline-flex items-center p-1 border border-transparent rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Featured Badge --}}
|
||||
@if($ride->featured_photo_id === $photo->id)
|
||||
<div class="absolute top-2 left-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800">
|
||||
Featured
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
<div class="mt-4">
|
||||
{{ $photos->links() }}
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-12">
|
||||
<p class="text-gray-500 dark:text-gray-400">No photos uploaded yet.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
127
resources/views/livewire/ride-list.blade.php
Normal file
127
resources/views/livewire/ride-list.blade.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<div class="space-y-4">
|
||||
{{-- Control Panel --}}
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{{-- Search --}}
|
||||
<div>
|
||||
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Search</label>
|
||||
<input type="search"
|
||||
id="search"
|
||||
wire:model.live="search"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm"
|
||||
placeholder="Search rides...">
|
||||
</div>
|
||||
|
||||
{{-- Category Filter --}}
|
||||
<div>
|
||||
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Category</label>
|
||||
<select id="category"
|
||||
wire:model.live="category"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm">
|
||||
<option value="">All Categories</option>
|
||||
@foreach($this->categories as $value => $label)
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- View Toggle --}}
|
||||
<div class="flex items-end justify-end">
|
||||
<button type="button"
|
||||
wire:click="toggleView"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600">
|
||||
<span>{{ $viewMode === 'grid' ? 'Switch to List' : 'Switch to Grid' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Rides List/Grid --}}
|
||||
<div>
|
||||
@if($viewMode === 'grid')
|
||||
{{-- Grid View --}}
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach($rides as $ride)
|
||||
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||
<a href="{{ route('rides.show', $ride) }}" class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||
{{ $ride->name }}
|
||||
</a>
|
||||
</h3>
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-{{ $ride->status === 'OPERATING' ? 'green' : 'red' }}-100 text-{{ $ride->status === 'OPERATING' ? 'green' : 'red' }}-800">
|
||||
{{ $ride->status->label() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $ride->park->name }}
|
||||
</p>
|
||||
@if($ride->parkArea)
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $ride->parkArea->name }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-3 flex justify-between items-center">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{{ $ride->category->label() }}
|
||||
</span>
|
||||
@if($ride->average_rating)
|
||||
<span class="inline-flex items-center text-sm text-yellow-500">
|
||||
<svg class="h-5 w-5 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
{{ number_format($ride->average_rating, 1) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
{{-- List View --}}
|
||||
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@foreach($rides as $ride)
|
||||
<li class="px-4 py-4 sm:px-6 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
<a href="{{ route('rides.show', $ride) }}" class="hover:text-primary-600 dark:hover:text-primary-400">
|
||||
{{ $ride->name }}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="mt-2 flex">
|
||||
<div class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $ride->park->name }}
|
||||
@if($ride->parkArea)
|
||||
<span class="px-2">•</span>
|
||||
{{ $ride->parkArea->name }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-end">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-{{ $ride->status === 'OPERATING' ? 'green' : 'red' }}-100 text-{{ $ride->status === 'OPERATING' ? 'green' : 'red' }}-800">
|
||||
{{ $ride->status->label() }}
|
||||
</span>
|
||||
<span class="mt-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{{ $ride->category->label() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
<div class="mt-4">
|
||||
{{ $rides->links() }}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user