feat: Implement rides management with CRUD functionality

- Added rides index view with search and filter options.
- Created rides show view to display ride details.
- Implemented API routes for rides.
- Developed authentication routes for user registration, login, and email verification.
- Created tests for authentication, email verification, password reset, and user profile management.
- Added feature tests for rides and operators, including creation, updating, deletion, and searching.
- Implemented soft deletes and caching for rides and operators.
- Enhanced manufacturer and operator model tests for various functionalities.
This commit is contained in:
pacnpal
2025-06-19 22:34:10 -04:00
parent 86263db9d9
commit cc33781245
148 changed files with 14026 additions and 2482 deletions

View File

@@ -1,163 +1,200 @@
<x-app-layout>
<x-slot name="title">{{ $park->name }}</x-slot>
<div class="container mx-auto px-4 py-8">
<div class="mb-6">
<div class="flex flex-wrap items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ $park->name }}</h1>
<div class="flex items-center mt-2 text-sm text-gray-600 dark:text-gray-300">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $park->status_classes }} mr-2">
{{ $park->status->name }}
</span>
@if ($park->operator)
<span>Operated by <a href="{{ route('operators.show', $park->operator) }}" class="text-blue-600 dark:text-blue-400 hover:underline">{{ $park->operator->name }}</a></span>
@endif
<x-slot name="header">
<div class="flex justify-between items-center">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ $park->name }}
</h2>
@auth
<div class="flex space-x-2">
<a href="{{ route('parks.edit', $park) }}"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<svg class="-ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit Park
</a>
</div>
</div>
<div class="mt-4 md:mt-0 flex space-x-2">
<a href="{{ route('parks.edit', $park) }}" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600">
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-500 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
Edit Park
</a>
</div>
@endauth
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Main Content -->
<div class="lg:col-span-2 space-y-6">
<!-- Featured Photo -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
<div class="aspect-video bg-gray-100 dark:bg-gray-700">
<img
src="{{ $park->featured_photo_url }}"
alt="{{ $park->name }}"
class="w-full h-full object-cover"
>
</x-slot>
<div class="py-8">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<!-- Park Header -->
<div class="bg-white shadow rounded-lg mb-8">
<div class="px-6 py-8">
<div class="flex flex-col lg:flex-row lg:items-start lg:space-x-8">
<!-- Park Image Placeholder -->
<div class="w-full lg:w-1/3 mb-6 lg:mb-0">
<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center">
@if($park->photos->count() > 0)
<img src="#" alt="{{ $park->name }}" class="w-full h-full object-cover rounded-lg">
@else
<div class="text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p class="mt-2 text-sm text-gray-500">No photos available</p>
</div>
@endif
</div>
</div>
<!-- Park Details -->
<div class="flex-1">
<div class="flex flex-wrap gap-2 mb-4">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium {{ $park->status_classes }}">
{{ $park->status->label() }}
</span>
@if($park->opening_date)
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800">
Opened {{ date('F j, Y', strtotime($park->opening_date)) }}
</span>
@endif
</div>
@if($park->description)
<p class="text-gray-700 text-lg mb-6">{{ $park->description }}</p>
@endif
<!-- Park Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
@if($park->ride_count)
<div class="text-center p-4 bg-blue-50 rounded-lg">
<div class="text-2xl font-bold text-blue-600">{{ $park->ride_count }}</div>
<div class="text-sm text-blue-600">Total Rides</div>
</div>
@endif
@if($park->coaster_count)
<div class="text-center p-4 bg-purple-50 rounded-lg">
<div class="text-2xl font-bold text-purple-600">{{ $park->coaster_count }}</div>
<div class="text-sm text-purple-600">Roller Coasters</div>
</div>
@endif
@if($park->size_acres)
<div class="text-center p-4 bg-green-50 rounded-lg">
<div class="text-2xl font-bold text-green-600">{{ number_format($park->size_acres) }}</div>
<div class="text-sm text-green-600">Acres</div>
</div>
@endif
@if($park->annual_attendance)
<div class="text-center p-4 bg-orange-50 rounded-lg">
<div class="text-2xl font-bold text-orange-600">{{ number_format($park->annual_attendance) }}</div>
<div class="text-sm text-orange-600">Annual Visitors</div>
</div>
@endif
</div>
<!-- Location and Operator -->
<div class="space-y-4">
@if($park->location)
<div class="flex items-start space-x-3">
<svg class="h-5 w-5 text-gray-400 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<div>
<div class="font-medium text-gray-900">Location</div>
<div class="text-gray-600">
{{ $park->location->city }}{{ $park->location->state ? ', ' . $park->location->state : '' }}{{ $park->location->country ? ', ' . $park->location->country : '' }}
</div>
</div>
</div>
@endif
@if($park->operator)
<div class="flex items-start space-x-3">
<svg class="h-5 w-5 text-gray-400 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<div>
<div class="font-medium text-gray-900">Operator</div>
<div class="text-gray-600">{{ $park->operator->name }}</div>
</div>
</div>
@endif
@if($park->website)
<div class="flex items-start space-x-3">
<svg class="h-5 w-5 text-gray-400 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
<div>
<div class="font-medium text-gray-900">Website</div>
<a href="{{ $park->website }}" target="_blank" class="text-blue-600 hover:text-blue-800">{{ $park->website }}</a>
</div>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
<!-- Description -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">About {{ $park->name }}</h2>
<div class="prose dark:prose-invert max-w-none">
@if ($park->description)
<p>{{ $park->description }}</p>
@else
<p class="text-gray-500 dark:text-gray-400">No description available.</p>
@endif
<!-- Park Areas and Rides -->
@if($park->areas->count() > 0)
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Park Areas & Attractions</h3>
</div>
<div class="p-6">
<div class="space-y-6">
@foreach($park->areas as $area)
<div class="border border-gray-200 rounded-lg p-4">
<div class="flex justify-between items-start mb-4">
<div>
<h4 class="text-lg font-semibold text-gray-900">{{ $area->name }}</h4>
@if($area->description)
<p class="text-gray-600 mt-1">{{ $area->description }}</p>
@endif
</div>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{{ $area->type }}
</span>
</div>
@if($area->rides->count() > 0)
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
@foreach($area->rides as $ride)
<div class="border border-gray-100 rounded p-3 hover:bg-gray-50">
<div class="flex justify-between items-start">
<div class="flex-1">
<h5 class="font-medium text-gray-900">{{ $ride->name }}</h5>
<p class="text-sm text-gray-600">{{ $ride->category->label() }}</p>
@if($ride->opening_date)
<p class="text-xs text-gray-500">Opened {{ date('Y', strtotime($ride->opening_date)) }}</p>
@endif
</div>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium {{ $ride->status_classes }}">
{{ $ride->status->label() }}
</span>
</div>
</div>
@endforeach
</div>
@else
<p class="text-gray-500 text-sm">No attractions listed for this area.</p>
@endif
</div>
@endforeach
</div>
</div>
</div>
</div>
<!-- Photo Gallery -->
<livewire:photo-gallery-component :park="$park" />
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Park Info -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h2 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Park Information</h2>
<dl class="space-y-3 text-sm">
@if ($park->opening_date)
<div class="flex justify-between">
<dt class="font-medium text-gray-500 dark:text-gray-400">Opened:</dt>
<dd class="text-gray-900 dark:text-white">{{ $park->opening_date->format('F j, Y') }}</dd>
</div>
@endif
@if ($park->closing_date)
<div class="flex justify-between">
<dt class="font-medium text-gray-500 dark:text-gray-400">Closed:</dt>
<dd class="text-gray-900 dark:text-white">{{ $park->closing_date->format('F j, Y') }}</dd>
</div>
@endif
@if ($park->size_acres)
<div class="flex justify-between">
<dt class="font-medium text-gray-500 dark:text-gray-400">Size:</dt>
<dd class="text-gray-900 dark:text-white">{{ $park->size_display }}</dd>
</div>
@endif
@if ($park->operating_season)
<div class="flex justify-between">
<dt class="font-medium text-gray-500 dark:text-gray-400">Season:</dt>
<dd class="text-gray-900 dark:text-white">{{ $park->operating_season }}</dd>
</div>
@endif
@if ($park->website)
<div class="flex justify-between">
<dt class="font-medium text-gray-500 dark:text-gray-400">Website:</dt>
<dd class="text-gray-900 dark:text-white">
<a href="{{ $park->website_url }}" target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline">
Visit Website
</a>
</dd>
</div>
@endif
<div class="flex justify-between">
<dt class="font-medium text-gray-500 dark:text-gray-400">Location:</dt>
<dd class="text-gray-900 dark:text-white">{{ $park->formatted_location ?: 'Unknown' }}</dd>
@else
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-8 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No areas or attractions</h3>
<p class="mt-1 text-sm text-gray-500">This park doesn't have any areas or attractions listed yet.</p>
</div>
</dl>
</div>
<!-- Statistics -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h2 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Statistics</h2>
<dl class="space-y-3 text-sm">
<div class="flex justify-between">
<dt class="font-medium text-gray-500 dark:text-gray-400">Total Rides:</dt>
<dd class="text-gray-900 dark:text-white">{{ $park->total_rides ?: 0 }}</dd>
</div>
<div class="flex justify-between">
<dt class="font-medium text-gray-500 dark:text-gray-400">Roller Coasters:</dt>
<dd class="text-gray-900 dark:text-white">{{ $park->total_coasters ?: 0 }}</dd>
</div>
<div class="flex justify-between">
<dt class="font-medium text-gray-500 dark:text-gray-400">Flat Rides:</dt>
<dd class="text-gray-900 dark:text-white">{{ $park->total_flat_rides ?: 0 }}</dd>
</div>
<div class="flex justify-between">
<dt class="font-medium text-gray-500 dark:text-gray-400">Water Rides:</dt>
<dd class="text-gray-900 dark:text-white">{{ $park->total_water_rides ?: 0 }}</dd>
</div>
<div class="flex justify-between">
<dt class="font-medium text-gray-500 dark:text-gray-400">Areas:</dt>
<dd class="text-gray-900 dark:text-white">{{ $park->total_areas ?: 0 }}</dd>
</div>
</dl>
</div>
<!-- Location -->
@if ($park->location)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
<livewire:location.location-map-component :location="$park->location" :height="300" />
</div>
@endif
<!-- Photo Upload -->
<livewire:photo-upload-component :park="$park" />
<!-- Photo Management -->
<livewire:photo-manager-component :park="$park" />
<!-- Featured Photo Selector -->
<livewire:featured-photo-selector-component :park="$park" />
</div>
</div>
</div>
</x-app-layout>
</x-app-layout>