From 64b0e90a27324f48c750781b32322400df2535e8 Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Tue, 25 Feb 2025 20:37:19 -0500 Subject: [PATCH] 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 --- app/Enums/LaunchType.php | 38 +++ app/Enums/ReviewStatus.php | 28 ++ app/Enums/RideCategory.php | 42 +++ app/Enums/RideStatus.php | 71 +++++ app/Enums/RollerCoasterType.php | 50 ++++ app/Enums/TrackMaterial.php | 34 +++ app/Livewire/RideDetailComponent.php | 88 +++++++ app/Livewire/RideFormComponent.php | 140 ++++++++++ app/Livewire/RideGalleryComponent.php | 92 +++++++ app/Livewire/RideListComponent.php | 101 +++++++ app/Models/Designer.php | 43 +++ app/Models/HelpfulVote.php | 45 ++++ app/Models/Review.php | 113 ++++++++ app/Models/Ride.php | 118 +++++++++ app/Models/RideModel.php | 58 ++++ app/Models/RollerCoasterStats.php | 79 ++++++ ..._02_25_194500_create_ride_models_table.php | 27 ++ .../2024_02_25_194600_create_rides_table.php | 57 ++++ ...4700_create_roller_coaster_stats_table.php | 48 ++++ ...2024_02_25_203100_create_reviews_table.php | 53 ++++ ...2_25_203200_create_helpful_votes_table.php | 33 +++ memory-bank/activeContext.md | 55 +++- memory-bank/components/RideComponents.md | 60 +++++ memory-bank/design/CodeDuplication.md | 69 +++++ memory-bank/features/RideReviews.md | 133 ++++++++++ memory-bank/features/RidesManagement.md | 169 ++++++++++++ memory-bank/models/ReviewModels.md | 101 +++++++ memory-bank/models/RideEnums.md | 97 +++++++ memory-bank/models/RideModels.md | 141 ++++++++++ memory-bank/models/RidesSchema.md | 107 ++++++++ .../partials/_coaster-stats-form.blade.php | 181 +++++++++++++ .../views/livewire/ride-detail.blade.php | 190 +++++++++++++ resources/views/livewire/ride-form.blade.php | 249 ++++++++++++++++++ .../views/livewire/ride-gallery.blade.php | 121 +++++++++ resources/views/livewire/ride-list.blade.php | 127 +++++++++ 35 files changed, 3157 insertions(+), 1 deletion(-) create mode 100644 app/Enums/LaunchType.php create mode 100644 app/Enums/ReviewStatus.php create mode 100644 app/Enums/RideCategory.php create mode 100644 app/Enums/RideStatus.php create mode 100644 app/Enums/RollerCoasterType.php create mode 100644 app/Enums/TrackMaterial.php create mode 100644 app/Livewire/RideDetailComponent.php create mode 100644 app/Livewire/RideFormComponent.php create mode 100644 app/Livewire/RideGalleryComponent.php create mode 100644 app/Livewire/RideListComponent.php create mode 100644 app/Models/Designer.php create mode 100644 app/Models/HelpfulVote.php create mode 100644 app/Models/Review.php create mode 100644 app/Models/Ride.php create mode 100644 app/Models/RideModel.php create mode 100644 app/Models/RollerCoasterStats.php create mode 100644 database/migrations/2024_02_25_194500_create_ride_models_table.php create mode 100644 database/migrations/2024_02_25_194600_create_rides_table.php create mode 100644 database/migrations/2024_02_25_194700_create_roller_coaster_stats_table.php create mode 100644 database/migrations/2024_02_25_203100_create_reviews_table.php create mode 100644 database/migrations/2024_02_25_203200_create_helpful_votes_table.php create mode 100644 memory-bank/components/RideComponents.md create mode 100644 memory-bank/design/CodeDuplication.md create mode 100644 memory-bank/features/RideReviews.md create mode 100644 memory-bank/features/RidesManagement.md create mode 100644 memory-bank/models/ReviewModels.md create mode 100644 memory-bank/models/RideEnums.md create mode 100644 memory-bank/models/RideModels.md create mode 100644 memory-bank/models/RidesSchema.md create mode 100644 resources/views/livewire/partials/_coaster-stats-form.blade.php create mode 100644 resources/views/livewire/ride-detail.blade.php create mode 100644 resources/views/livewire/ride-form.blade.php create mode 100644 resources/views/livewire/ride-gallery.blade.php create mode 100644 resources/views/livewire/ride-list.blade.php diff --git a/app/Enums/LaunchType.php b/app/Enums/LaunchType.php new file mode 100644 index 0000000..579fd91 --- /dev/null +++ b/app/Enums/LaunchType.php @@ -0,0 +1,38 @@ + '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()); + } +} \ No newline at end of file diff --git a/app/Enums/ReviewStatus.php b/app/Enums/ReviewStatus.php new file mode 100644 index 0000000..d93f98f --- /dev/null +++ b/app/Enums/ReviewStatus.php @@ -0,0 +1,28 @@ + 'Pending', + self::APPROVED => 'Approved', + self::REJECTED => 'Rejected', + }; + } + + public function color(): string + { + return match($this) { + self::PENDING => 'yellow', + self::APPROVED => 'green', + self::REJECTED => 'red', + }; + } +} \ No newline at end of file diff --git a/app/Enums/RideCategory.php b/app/Enums/RideCategory.php new file mode 100644 index 0000000..b6a41da --- /dev/null +++ b/app/Enums/RideCategory.php @@ -0,0 +1,42 @@ + '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()); + } +} \ No newline at end of file diff --git a/app/Enums/RideStatus.php b/app/Enums/RideStatus.php new file mode 100644 index 0000000..3bec9d2 --- /dev/null +++ b/app/Enums/RideStatus.php @@ -0,0 +1,71 @@ + '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) + ); + } +} \ No newline at end of file diff --git a/app/Enums/RollerCoasterType.php b/app/Enums/RollerCoasterType.php new file mode 100644 index 0000000..1ac0a4d --- /dev/null +++ b/app/Enums/RollerCoasterType.php @@ -0,0 +1,50 @@ + '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()); + } +} \ No newline at end of file diff --git a/app/Enums/TrackMaterial.php b/app/Enums/TrackMaterial.php new file mode 100644 index 0000000..5b1ee9e --- /dev/null +++ b/app/Enums/TrackMaterial.php @@ -0,0 +1,34 @@ + '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()); + } +} \ No newline at end of file diff --git a/app/Livewire/RideDetailComponent.php b/app/Livewire/RideDetailComponent.php new file mode 100644 index 0000000..defa371 --- /dev/null +++ b/app/Livewire/RideDetailComponent.php @@ -0,0 +1,88 @@ +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'; + } +} \ No newline at end of file diff --git a/app/Livewire/RideFormComponent.php b/app/Livewire/RideFormComponent.php new file mode 100644 index 0000000..93e58d8 --- /dev/null +++ b/app/Livewire/RideFormComponent.php @@ -0,0 +1,140 @@ + */ + public array $state = []; + + /** @var array|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'); + } +} \ No newline at end of file diff --git a/app/Livewire/RideGalleryComponent.php b/app/Livewire/RideGalleryComponent.php new file mode 100644 index 0000000..5c9459c --- /dev/null +++ b/app/Livewire/RideGalleryComponent.php @@ -0,0 +1,92 @@ +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), + ]); + } +} \ No newline at end of file diff --git a/app/Livewire/RideListComponent.php b/app/Livewire/RideListComponent.php new file mode 100644 index 0000000..c26209e --- /dev/null +++ b/app/Livewire/RideListComponent.php @@ -0,0 +1,101 @@ +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, + ]); + } +} \ No newline at end of file diff --git a/app/Models/Designer.php b/app/Models/Designer.php new file mode 100644 index 0000000..6793dc1 --- /dev/null +++ b/app/Models/Designer.php @@ -0,0 +1,43 @@ + + */ + 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); + } +} \ No newline at end of file diff --git a/app/Models/HelpfulVote.php b/app/Models/HelpfulVote.php new file mode 100644 index 0000000..c5ae92b --- /dev/null +++ b/app/Models/HelpfulVote.php @@ -0,0 +1,45 @@ +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; + } +} \ No newline at end of file diff --git a/app/Models/Review.php b/app/Models/Review.php new file mode 100644 index 0000000..febcd4a --- /dev/null +++ b/app/Models/Review.php @@ -0,0 +1,113 @@ + 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; + } +} \ No newline at end of file diff --git a/app/Models/Ride.php b/app/Models/Ride.php new file mode 100644 index 0000000..70c25a8 --- /dev/null +++ b/app/Models/Ride.php @@ -0,0 +1,118 @@ + 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, + ]); + } +} \ No newline at end of file diff --git a/app/Models/RideModel.php b/app/Models/RideModel.php new file mode 100644 index 0000000..5bf1f9b --- /dev/null +++ b/app/Models/RideModel.php @@ -0,0 +1,58 @@ + + */ + protected $fillable = [ + 'name', + 'manufacturer_id', + 'description', + 'category', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + 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; + } +} \ No newline at end of file diff --git a/app/Models/RollerCoasterStats.php b/app/Models/RollerCoasterStats.php new file mode 100644 index 0000000..90bbf6c --- /dev/null +++ b/app/Models/RollerCoasterStats.php @@ -0,0 +1,79 @@ + + */ + 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 + */ + 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; + } +} \ No newline at end of file diff --git a/database/migrations/2024_02_25_194500_create_ride_models_table.php b/database/migrations/2024_02_25_194500_create_ride_models_table.php new file mode 100644 index 0000000..e7c2292 --- /dev/null +++ b/database/migrations/2024_02_25_194500_create_ride_models_table.php @@ -0,0 +1,27 @@ +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'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_02_25_194600_create_rides_table.php b/database/migrations/2024_02_25_194600_create_rides_table.php new file mode 100644 index 0000000..48261d8 --- /dev/null +++ b/database/migrations/2024_02_25_194600_create_rides_table.php @@ -0,0 +1,57 @@ +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'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_02_25_194700_create_roller_coaster_stats_table.php b/database/migrations/2024_02_25_194700_create_roller_coaster_stats_table.php new file mode 100644 index 0000000..7ad6e40 --- /dev/null +++ b/database/migrations/2024_02_25_194700_create_roller_coaster_stats_table.php @@ -0,0 +1,48 @@ +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'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_02_25_203100_create_reviews_table.php b/database/migrations/2024_02_25_203100_create_reviews_table.php new file mode 100644 index 0000000..6ce5251 --- /dev/null +++ b/database/migrations/2024_02_25_203100_create_reviews_table.php @@ -0,0 +1,53 @@ +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'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_02_25_203200_create_helpful_votes_table.php b/database/migrations/2024_02_25_203200_create_helpful_votes_table.php new file mode 100644 index 0000000..5e339ed --- /dev/null +++ b/database/migrations/2024_02_25_203200_create_helpful_votes_table.php @@ -0,0 +1,33 @@ +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'); + } +}; \ No newline at end of file diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index e7379bb..656efdc 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -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 diff --git a/memory-bank/components/RideComponents.md b/memory-bank/components/RideComponents.md new file mode 100644 index 0000000..83e4a44 --- /dev/null +++ b/memory-bank/components/RideComponents.md @@ -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 \ No newline at end of file diff --git a/memory-bank/design/CodeDuplication.md b/memory-bank/design/CodeDuplication.md new file mode 100644 index 0000000..259841a --- /dev/null +++ b/memory-bank/design/CodeDuplication.md @@ -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 \ No newline at end of file diff --git a/memory-bank/features/RideReviews.md b/memory-bank/features/RideReviews.md new file mode 100644 index 0000000..2cd29ec --- /dev/null +++ b/memory-bank/features/RideReviews.md @@ -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 \ No newline at end of file diff --git a/memory-bank/features/RidesManagement.md b/memory-bank/features/RidesManagement.md new file mode 100644 index 0000000..9296568 --- /dev/null +++ b/memory-bank/features/RidesManagement.md @@ -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 \ No newline at end of file diff --git a/memory-bank/models/ReviewModels.md b/memory-bank/models/ReviewModels.md new file mode 100644 index 0000000..862d0cb --- /dev/null +++ b/memory-bank/models/ReviewModels.md @@ -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(); \ No newline at end of file diff --git a/memory-bank/models/RideEnums.md b/memory-bank/models/RideEnums.md new file mode 100644 index 0000000..ddef302 --- /dev/null +++ b/memory-bank/models/RideEnums.md @@ -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 \ No newline at end of file diff --git a/memory-bank/models/RideModels.md b/memory-bank/models/RideModels.md new file mode 100644 index 0000000..adad17e --- /dev/null +++ b/memory-bank/models/RideModels.md @@ -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 \ No newline at end of file diff --git a/memory-bank/models/RidesSchema.md b/memory-bank/models/RidesSchema.md new file mode 100644 index 0000000..0994a9c --- /dev/null +++ b/memory-bank/models/RidesSchema.md @@ -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 \ No newline at end of file diff --git a/resources/views/livewire/partials/_coaster-stats-form.blade.php b/resources/views/livewire/partials/_coaster-stats-form.blade.php new file mode 100644 index 0000000..25f4794 --- /dev/null +++ b/resources/views/livewire/partials/_coaster-stats-form.blade.php @@ -0,0 +1,181 @@ +{{-- Physical Dimensions --}} +
+
+
+ +
+ + @error('coasterStats.height_ft') {{ $message }} @enderror +
+
+
+ +
+ + @error('coasterStats.length_ft') {{ $message }} @enderror +
+
+
+ +
+
+ +
+ + @error('coasterStats.speed_mph') {{ $message }} @enderror +
+
+
+ +
+ + @error('coasterStats.max_drop_height_ft') {{ $message }} @enderror +
+
+
+
+ +{{-- Track Configuration --}} +
+
+
+ +
+ + @error('coasterStats.track_material') {{ $message }} @enderror +
+
+
+ +
+ + @error('coasterStats.roller_coaster_type') {{ $message }} @enderror +
+
+
+ +
+
+ +
+ + @error('coasterStats.inversions') {{ $message }} @enderror +
+
+
+ +
+ + @error('coasterStats.launch_type') {{ $message }} @enderror +
+
+
+
+ +{{-- Train Configuration --}} +
+
+ +
+ + @error('coasterStats.train_style') {{ $message }} @enderror +
+
+ +
+
+ +
+ + @error('coasterStats.trains_count') {{ $message }} @enderror +
+
+
+ +
+ + @error('coasterStats.cars_per_train') {{ $message }} @enderror +
+
+
+ +
+ + @error('coasterStats.seats_per_car') {{ $message }} @enderror +
+
+
+
\ No newline at end of file diff --git a/resources/views/livewire/ride-detail.blade.php b/resources/views/livewire/ride-detail.blade.php new file mode 100644 index 0000000..af0e210 --- /dev/null +++ b/resources/views/livewire/ride-detail.blade.php @@ -0,0 +1,190 @@ +
+ {{-- Header --}} +
+
+
+

+ {{ $ride->name }} +

+

+ {{ $ride->park->name }} + @if($ride->parkArea) + + {{ $ride->parkArea->name }} + @endif +

+
+ + {{ $ride->status->label() }} + +
+
+ + {{-- Basic Information --}} +
+
+
+
Category
+
{{ $ride->category->label() }}
+
+ +
+
Operating Period
+
{{ $this->operatingPeriod }}
+
+ + @if($ride->manufacturer) +
+
Manufacturer
+
{{ $ride->manufacturer->name }}
+
+ @endif + + @if($ride->designer) +
+
Designer
+
{{ $ride->designer->name }}
+
+ @endif + + @if($ride->rideModel) +
+
Model
+
{{ $ride->rideModel->name }}
+
+ @endif +
+ + @if($ride->description) +
+
Description
+
{{ $ride->description }}
+
+ @endif +
+ + {{-- Technical Details --}} +
+

Technical Specifications

+
+ @if($ride->min_height_in) +
+
Minimum Height
+
{{ $ride->min_height_in }} inches
+
+ @endif + + @if($ride->max_height_in) +
+
Maximum Height
+
{{ $ride->max_height_in }} inches
+
+ @endif + + @if($ride->capacity_per_hour) +
+
Hourly Capacity
+
{{ number_format($ride->capacity_per_hour) }} riders
+
+ @endif + + @if($ride->ride_duration_seconds) +
+
Ride Duration
+
{{ $ride->ride_duration_seconds }} seconds
+
+ @endif +
+
+ + {{-- Roller Coaster Stats --}} + @if($ride->coasterStats) +
+
+

Roller Coaster Statistics

+ +
+ + @if($showCoasterStats) +
+ {{-- Track Details --}} +
+
Track Type
+
{{ $ride->coasterStats->track_material->label() }}
+
+ +
+
Coaster Type
+
{{ $ride->coasterStats->roller_coaster_type->label() }}
+
+ +
+
Launch Type
+
{{ $ride->coasterStats->launch_type->label() }}
+
+ + {{-- Physical Measurements --}} +
+
Height
+
{{ $this->formatMeasurement($ride->coasterStats->height_ft, 'ft') }}
+
+ +
+
Length
+
{{ $this->formatMeasurement($ride->coasterStats->length_ft, 'ft') }}
+
+ +
+
Speed
+
{{ $this->formatMeasurement($ride->coasterStats->speed_mph, 'mph') }}
+
+ +
+
Max Drop
+
{{ $this->formatMeasurement($ride->coasterStats->max_drop_height_ft, 'ft') }}
+
+ +
+
Inversions
+
{{ $ride->coasterStats->inversions ?? 'N/A' }}
+
+ + {{-- Train Configuration --}} + @if($ride->coasterStats->train_style) +
+
Train Style
+
{{ $ride->coasterStats->train_style }}
+
+ @endif + +
+
Number of Trains
+
{{ $ride->coasterStats->trains_count ?? 'N/A' }}
+
+ +
+
Cars per Train
+
{{ $ride->coasterStats->cars_per_train ?? 'N/A' }}
+
+ +
+
Seats per Car
+
{{ $ride->coasterStats->seats_per_car ?? 'N/A' }}
+
+
+ @endif +
+ @endif + + {{-- Actions --}} + +
\ No newline at end of file diff --git a/resources/views/livewire/ride-form.blade.php b/resources/views/livewire/ride-form.blade.php new file mode 100644 index 0000000..dee645c --- /dev/null +++ b/resources/views/livewire/ride-form.blade.php @@ -0,0 +1,249 @@ +
+
+ {{-- Basic Information --}} +
+
+

Basic Information

+

+ Provide the basic details about this ride. +

+
+ +
+ {{-- Name --}} +
+ +
+ + @error('state.name') {{ $message }} @enderror +
+
+ + {{-- Park --}} +
+ +
+ + @error('state.park_id') {{ $message }} @enderror +
+
+ + {{-- Park Area --}} +
+ +
+ + @error('state.park_area_id') {{ $message }} @enderror +
+
+ + {{-- Category --}} +
+ +
+ + @error('state.category') {{ $message }} @enderror +
+
+ + {{-- Status --}} +
+ +
+ + @error('state.status') {{ $message }} @enderror +
+
+ + {{-- Description --}} +
+ +
+ + @error('state.description') {{ $message }} @enderror +
+
+
+
+ + {{-- Technical Details --}} +
+
+

Technical Details

+

+ Specify the technical specifications and requirements. +

+
+ +
+ {{-- Manufacturer --}} +
+ +
+ + @error('state.manufacturer_id') {{ $message }} @enderror +
+
+ + {{-- Designer --}} +
+ +
+ + @error('state.designer_id') {{ $message }} @enderror +
+
+ + {{-- Height Requirements --}} +
+
+
+ +
+ + @error('state.min_height_in') {{ $message }} @enderror +
+
+
+ +
+ + @error('state.max_height_in') {{ $message }} @enderror +
+
+
+
+ + {{-- Capacity and Duration --}} +
+
+
+ +
+ + @error('state.capacity_per_hour') {{ $message }} @enderror +
+
+
+ +
+ + @error('state.ride_duration_seconds') {{ $message }} @enderror +
+
+
+
+
+
+ + {{-- Coaster Stats (if applicable) --}} + @if($state['category'] === App\Enums\RideCategory::ROLLER_COASTER->value) +
+
+

Roller Coaster Details

+

+ Additional specifications for roller coasters. +

+
+ + @include('livewire.partials._coaster-stats-form') +
+ @endif + + {{-- Form Actions --}} +
+
+ + Cancel + + +
+
+
+
\ No newline at end of file diff --git a/resources/views/livewire/ride-gallery.blade.php b/resources/views/livewire/ride-gallery.blade.php new file mode 100644 index 0000000..fe0bb19 --- /dev/null +++ b/resources/views/livewire/ride-gallery.blade.php @@ -0,0 +1,121 @@ +
+ {{-- Upload Area --}} +
+ +
+ + {{-- Upload Form --}} + @if($showUploadForm) +
+
+
+ {{-- Photo Upload --}} +
+ +
+ + + @if($photo) + {{ $photo->getClientOriginalName() }} + @endif +
+ @error('photo') {{ $message }} @enderror +
+ + {{-- Caption --}} +
+ +
+ + @error('caption') {{ $message }} @enderror +
+
+ + {{-- Submit Button --}} +
+ +
+
+
+
+ @endif + + {{-- Photo Grid --}} + @if($photos->count() > 0) +
+ @foreach($photos as $photo) +
+
+ {{ $photo->caption }} +
+ + {{-- Caption --}} + @if($photo->caption) +

{{ $photo->caption }}

+ @endif + + {{-- Actions --}} +
+ {{-- Set as Featured --}} + @if($ride->featured_photo_id !== $photo->id) + + @endif + + {{-- Delete --}} + +
+ + {{-- Featured Badge --}} + @if($ride->featured_photo_id === $photo->id) +
+ + Featured + +
+ @endif +
+ @endforeach +
+ + {{-- Pagination --}} +
+ {{ $photos->links() }} +
+ @else +
+

No photos uploaded yet.

+
+ @endif +
\ No newline at end of file diff --git a/resources/views/livewire/ride-list.blade.php b/resources/views/livewire/ride-list.blade.php new file mode 100644 index 0000000..6e7a9fe --- /dev/null +++ b/resources/views/livewire/ride-list.blade.php @@ -0,0 +1,127 @@ +
+ {{-- Control Panel --}} +
+
+ {{-- Search --}} +
+ + +
+ + {{-- Category Filter --}} +
+ + +
+ + {{-- View Toggle --}} +
+ +
+
+
+ + {{-- Rides List/Grid --}} +
+ @if($viewMode === 'grid') + {{-- Grid View --}} +
+ @foreach($rides as $ride) +
+
+
+

+ + {{ $ride->name }} + +

+ + {{ $ride->status->label() }} + +
+
+

+ {{ $ride->park->name }} +

+ @if($ride->parkArea) +

+ {{ $ride->parkArea->name }} +

+ @endif +
+
+ + {{ $ride->category->label() }} + + @if($ride->average_rating) + + + + + {{ number_format($ride->average_rating, 1) }} + + @endif +
+
+
+ @endforeach +
+ @else + {{-- List View --}} +
+
    + @foreach($rides as $ride) +
  • +
    +
    +

    + + {{ $ride->name }} + +

    +
    +
    + {{ $ride->park->name }} + @if($ride->parkArea) + + {{ $ride->parkArea->name }} + @endif +
    +
    +
    +
    + + {{ $ride->status->label() }} + + + {{ $ride->category->label() }} + +
    +
    +
  • + @endforeach +
+
+ @endif +
+ + {{-- Pagination --}} +
+ {{ $rides->links() }} +
+
\ No newline at end of file