mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-26 12:06:57 -05:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user