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

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

38
app/Enums/LaunchType.php Normal file
View 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());
}
}

View 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',
};
}
}

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

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

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

View File

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

View File

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

View File

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

View File

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

43
app/Models/Designer.php Normal file
View 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);
}
}

View 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
View 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
View 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
View 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;
}
}

View 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;
}
}

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -73,7 +73,60 @@ Migrating the design from Django to Laravel implementation
- ✅ Added API endpoints for photo management - ✅ Added API endpoints for photo management
- ✅ See `memory-bank/features/PhotoManagement.md` for implementation details - ✅ 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) - Continue with remaining components (forms, modals, cards)
- Convert Django partials to Blade components - Convert Django partials to Blade components
- Implement Livewire interactive components - Implement Livewire interactive components

View 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

View 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

View 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

View 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

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

View 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

View 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

View 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

View 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>

View 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>

View 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>

View 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>

View 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>