Files
thrillwiki_laravel/resources/views/livewire/parks-listing.blade.php
pacnpal 97a7682eb7 Add Livewire components for parks, rides, and manufacturers
- Implemented ParksLocationSearch component with loading state and refresh functionality.
- Created ParksMapView component with similar structure and functionality.
- Added RegionalParksListing component for displaying regional parks.
- Developed RidesListingUniversal component for universal listing integration.
- Established ManufacturersListing view with navigation and Livewire integration.
- Added feature tests for various Livewire components including OperatorHierarchyView, OperatorParksListing, OperatorPortfolioCard, OperatorsListing, OperatorsRoleFilter, ParksListing, ParksLocationSearch, ParksMapView, and RegionalParksListing to ensure proper rendering and adherence to patterns.
2025-06-23 21:31:05 -04:00

405 lines
27 KiB
PHP

<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
{{-- Header Section --}}
<div class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
{{-- Title and Stats --}}
<div class="flex-1">
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white">
Theme Parks
</h1>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ $parks->total() }} parks found
@if($locationEnabled)
<span class="inline-flex items-center ml-2 px-2 py-1 rounded-full text-xs bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
</svg>
Location enabled
</span>
@endif
</p>
</div>
{{-- Location Controls --}}
<div class="flex items-center gap-3">
@if(!$locationEnabled)
<button
wire:click="enableLocation"
wire:loading.attr="disabled"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg wire:loading.remove wire:target="enableLocation" class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
</svg>
<svg wire:loading wire:target="enableLocation" class="animate-spin w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span wire:loading.remove wire:target="enableLocation">Find Parks Near Me</span>
<span wire:loading wire:target="enableLocation">Getting Location...</span>
</button>
@endif
{{-- Filters Toggle --}}
<button
wire:click="toggleFilters"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z" clip-rule="evenodd"></path>
</svg>
Filters
@if(count($activeFilters) > 0)
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{{ count($activeFilters) }}
</span>
@endif
</button>
</div>
</div>
{{-- Search Bar --}}
<div class="mt-6">
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"></path>
</svg>
</div>
<input
wire:model.live.debounce.300ms="search"
type="text"
placeholder="Search parks by name, location, operator, or type..."
class="block w-full pl-10 pr-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg leading-5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
</div>
</div>
{{-- Active Filters Display --}}
@if(count($activeFilters) > 0)
<div class="mt-4 flex flex-wrap items-center gap-2">
<span class="text-sm text-gray-600 dark:text-gray-400">Active filters:</span>
@foreach($activeFilters as $filter)
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{{ $filter }}
</span>
@endforeach
<button
wire:click="clearFilters"
class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600"
>
Clear all
</button>
</div>
@endif
</div>
</div>
{{-- Filters Panel --}}
@if($showFilters)
<div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{{-- Operator Filter --}}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Operator</label>
<select wire:model.live="operatorId" class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<option value="">All Operators</option>
@foreach($operators as $operator)
<option value="{{ $operator->id }}">{{ $operator->name }}</option>
@endforeach
</select>
</div>
{{-- Region Filter --}}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Region</label>
<select wire:model.live="region" class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<option value="">All Regions</option>
@foreach($regions as $regionOption)
<option value="{{ $regionOption }}">{{ $regionOption }}</option>
@endforeach
</select>
</div>
{{-- Country Filter --}}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Country</label>
<select wire:model.live="country" class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<option value="">All Countries</option>
@foreach($countries as $countryOption)
<option value="{{ $countryOption }}">{{ $countryOption }}</option>
@endforeach
</select>
</div>
{{-- Park Type Filter --}}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Park Type</label>
<select wire:model.live="parkType" class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<option value="">All Types</option>
@foreach($parkTypes as $type)
<option value="{{ $type }}">{{ $type }}</option>
@endforeach
</select>
</div>
{{-- Opening Year Range --}}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Opening Year</label>
<div class="grid grid-cols-2 gap-2">
<input wire:model.live="openingYearFrom" type="number" placeholder="From" min="1800" max="{{ date('Y') }}" class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<input wire:model.live="openingYearTo" type="number" placeholder="To" min="1800" max="{{ date('Y') }}" class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
</div>
{{-- Area Range --}}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Area (acres)</label>
<div class="grid grid-cols-2 gap-2">
<input wire:model.live="minArea" type="number" placeholder="Min" min="0" class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<input wire:model.live="maxArea" type="number" placeholder="Max" min="0" class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
</div>
{{-- Minimum Rides --}}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Minimum Rides</label>
<input wire:model.live="minRides" type="number" placeholder="Min rides" min="0" class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
{{-- Distance Filter (only if location enabled) --}}
@if($locationEnabled)
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Max Distance (km)</label>
<input wire:model.live="maxDistance" type="number" placeholder="Distance" min="1" class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
@endif
</div>
</div>
</div>
@endif
{{-- Sorting Controls --}}
<div class="bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-600 dark:text-gray-400">Sort by:</span>
<div class="flex items-center space-x-2">
<button wire:click="sortBy('name')" class="inline-flex items-center px-3 py-1 rounded-md text-sm {{ $sortBy === 'name' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white' }}">
Name
@if($sortBy === 'name')
<svg class="ml-1 w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="{{ $sortDirection === 'asc' ? 'M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z' : 'M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' }}" clip-rule="evenodd"></path>
</svg>
@endif
</button>
@if($locationEnabled)
<button wire:click="sortBy('distance')" class="inline-flex items-center px-3 py-1 rounded-md text-sm {{ $sortBy === 'distance' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white' }}">
Distance
@if($sortBy === 'distance')
<svg class="ml-1 w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="{{ $sortDirection === 'asc' ? 'M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z' : 'M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' }}" clip-rule="evenodd"></path>
</svg>
@endif
</button>
@endif
<button wire:click="sortBy('rides_count')" class="inline-flex items-center px-3 py-1 rounded-md text-sm {{ $sortBy === 'rides_count' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white' }}">
Rides
@if($sortBy === 'rides_count')
<svg class="ml-1 w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="{{ $sortDirection === 'asc' ? 'M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z' : 'M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' }}" clip-rule="evenodd"></path>
</svg>
@endif
</button>
<button wire:click="sortBy('opening_date')" class="inline-flex items-center px-3 py-1 rounded-md text-sm {{ $sortBy === 'opening_date' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white' }}">
Opening Date
@if($sortBy === 'opening_date')
<svg class="ml-1 w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="{{ $sortDirection === 'asc' ? 'M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z' : 'M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' }}" clip-rule="evenodd"></path>
</svg>
@endif
</button>
</div>
</div>
</div>
</div>
</div>
{{-- Parks Grid --}}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
@if($parks->count() > 0)
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
@foreach($parks as $park)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 overflow-hidden">
{{-- Park Image --}}
<div class="aspect-w-16 aspect-h-9 bg-gray-200 dark:bg-gray-700">
@if($park->photos->count() > 0)
<img src="{{ $park->photos->first()->url }}" alt="{{ $park->name }}" class="w-full h-48 object-cover">
@else
<div class="w-full h-48 flex items-center justify-center">
<svg class="w-12 h-12 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"></path>
</svg>
</div>
@endif
</div>
{{-- Park Info --}}
<div class="p-4">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate">
<a href="{{ route('parks.show', $park) }}" class="hover:text-blue-600 dark:hover:text-blue-400">
{{ $park->name }}
</a>
</h3>
@if($park->location)
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
{{ $park->location->city }}, {{ $park->location->state }}
@if($park->location->country !== 'United States')
, {{ $park->location->country }}
@endif
</p>
@endif
@if($park->operator)
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">
{{ $park->operator->name }}
</p>
@endif
</div>
@if($locationEnabled && isset($park->distance))
<div class="ml-2 flex-shrink-0">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{{ number_format($park->distance, 1) }} km
</span>
</div>
@endif
</div>
{{-- Park Stats --}}
<div class="mt-3 flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
<div class="flex items-center space-x-4">
<span class="flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{{ $park->rides_count }} rides
</span>
@if($park->reviews_count > 0)
<span class="flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
{{ $park->reviews_count }} reviews
</span>
@endif
</div>
@if($park->opening_date)
<span class="text-xs">
{{ $park->opening_date->format('Y') }}
</span>
@endif
</div>
{{-- Park Type --}}
@if($park->park_type)
<div class="mt-2">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
{{ $park->park_type }}
</span>
</div>
@endif
</div>
</div>
@endforeach
</div>
{{-- Pagination --}}
<div class="mt-8">
{{ $parks->links() }}
</div>
@else
{{-- Empty State --}}
<div class="text-center py-12">
<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 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"></path>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No parks found</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Try adjusting your search criteria or filters.
</p>
@if(count($activeFilters) > 0)
<div class="mt-6">
<button
wire:click="clearFilters"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Clear
Clear All Filters
</button>
</div>
@endif
</div>
@endif
</div>
{{-- Loading State --}}
<div wire:loading.delay class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 flex items-center space-x-3">
<svg class="animate-spin h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="text-gray-900 dark:text-white">Loading parks...</span>
</div>
</div>
</div>
{{-- JavaScript for Location Services --}}
<script>
document.addEventListener('livewire:init', () => {
Livewire.on('request-location', () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
function(position) {
Livewire.dispatch('locationReceived', {
lat: position.coords.latitude,
lng: position.coords.longitude
});
},
function(error) {
let message = 'Unable to get your location.';
switch(error.code) {
case error.PERMISSION_DENIED:
message = 'Location access denied by user.';
break;
case error.POSITION_UNAVAILABLE:
message = 'Location information is unavailable.';
break;
case error.TIMEOUT:
message = 'Location request timed out.';
break;
}
Livewire.dispatch('locationError', { message: message });
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 300000 // 5 minutes
}
);
} else {
Livewire.dispatch('locationError', {
message: 'Geolocation is not supported by this browser.'
});
}
});
Livewire.on('location-error', (event) => {
alert('Location Error: ' + event.message);
});
});
</script>