mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 05:31:10 -05:00
- Marked Rides Listing Components Generation as completed with detailed results. - Implemented search/filter logic in RidesListing component for Django parity. - Created ParkRidesListing, RidesFilters, and RidesSearchSuggestions components with caching and pagination. - Developed corresponding Blade views for each component. - Added comprehensive tests for ParkRidesListing, RidesListing, and RidesSearchSuggestions components to ensure functionality and adherence to patterns.
348 lines
19 KiB
PHP
348 lines
19 KiB
PHP
{{-- ThrillWiki RidesListing: Django Parity Search & Filter Interface --}}
|
|
<div class="thrillwiki-rides-listing">
|
|
{{-- Header Section --}}
|
|
<div class="mb-6">
|
|
<h1 class="text-2xl md:text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white mb-2">
|
|
Rides Directory
|
|
</h1>
|
|
<p class="text-gray-600 dark:text-gray-400 text-sm md:text-base">
|
|
Discover and explore theme park rides from around the world
|
|
</p>
|
|
</div>
|
|
|
|
{{-- Search & Filter Section --}}
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-6">
|
|
{{-- Main Search Bar --}}
|
|
<div class="p-4 md:p-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="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
wire:model.live.debounce.300ms="search"
|
|
placeholder="Search rides, parks, manufacturers, or designers..."
|
|
class="block w-full pl-10 pr-3 py-3 md:py-4 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 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-transparent text-sm md:text-base"
|
|
style="min-height: 44px;"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Advanced Filters --}}
|
|
<div class="border-t border-gray-200 dark:border-gray-700">
|
|
<div class="p-4 md:p-6">
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
{{-- Category Filter --}}
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Category
|
|
</label>
|
|
<select
|
|
wire:model.live="category"
|
|
class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
|
style="min-height: 44px;"
|
|
>
|
|
<option value="">All Categories</option>
|
|
@foreach($filterOptions['categories'] as $value => $label)
|
|
<option value="{{ $value }}">{{ ucfirst($label) }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
{{-- Status Filter --}}
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Status
|
|
</label>
|
|
<select
|
|
wire:model.live="status"
|
|
class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
|
style="min-height: 44px;"
|
|
>
|
|
<option value="">All Statuses</option>
|
|
@foreach($filterOptions['statuses'] as $value => $label)
|
|
<option value="{{ $value }}">{{ ucfirst($label) }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
{{-- Manufacturer Filter --}}
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Manufacturer
|
|
</label>
|
|
<select
|
|
wire:model.live="manufacturerId"
|
|
class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
|
style="min-height: 44px;"
|
|
>
|
|
<option value="">All Manufacturers</option>
|
|
@foreach($filterOptions['manufacturers'] as $id => $name)
|
|
<option value="{{ $id }}">{{ $name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
{{-- Park Filter --}}
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Park
|
|
</label>
|
|
<select
|
|
wire:model.live="parkId"
|
|
class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
|
style="min-height: 44px;"
|
|
>
|
|
<option value="">All Parks</option>
|
|
@foreach($filterOptions['parks'] as $id => $name)
|
|
<option value="{{ $id }}">{{ $name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Year Range Filters --}}
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mt-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Opening Year From
|
|
</label>
|
|
<input
|
|
type="number"
|
|
wire:model.live.debounce.500ms="openingYearFrom"
|
|
placeholder="e.g. 1990"
|
|
min="1800"
|
|
max="{{ date('Y') + 5 }}"
|
|
class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 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-transparent text-sm"
|
|
style="min-height: 44px;"
|
|
>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Opening Year To
|
|
</label>
|
|
<input
|
|
type="number"
|
|
wire:model.live.debounce.500ms="openingYearTo"
|
|
placeholder="e.g. 2024"
|
|
min="1800"
|
|
max="{{ date('Y') + 5 }}"
|
|
class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 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-transparent text-sm"
|
|
style="min-height: 44px;"
|
|
>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Min Height (cm)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
wire:model.live.debounce.500ms="minHeight"
|
|
placeholder="e.g. 100"
|
|
min="0"
|
|
max="300"
|
|
class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 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-transparent text-sm"
|
|
style="min-height: 44px;"
|
|
>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Max Height (cm)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
wire:model.live.debounce.500ms="maxHeight"
|
|
placeholder="e.g. 200"
|
|
min="0"
|
|
max="300"
|
|
class="block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 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-transparent text-sm"
|
|
style="min-height: 44px;"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Clear Filters Button --}}
|
|
@if($search || $category || $status || $manufacturerId || $openingYearFrom || $openingYearTo || $minHeight || $maxHeight || $parkId)
|
|
<div class="mt-4 flex justify-end">
|
|
<button
|
|
wire:click="clearFilters"
|
|
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200"
|
|
style="min-height: 44px;"
|
|
>
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
Clear Filters
|
|
</button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Results Section --}}
|
|
<div class="mb-6">
|
|
{{-- Results Count & Loading State --}}
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-4">
|
|
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2 sm:mb-0">
|
|
@if($rides->total() > 0)
|
|
Showing {{ $rides->firstItem() }}-{{ $rides->lastItem() }} of {{ $rides->total() }} rides
|
|
@else
|
|
No rides found
|
|
@endif
|
|
</div>
|
|
|
|
<div wire:loading class="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
|
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" 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>
|
|
Loading...
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Rides Grid --}}
|
|
@if($rides->count() > 0)
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6">
|
|
@foreach($rides as $ride)
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-md transition-shadow duration-200">
|
|
{{-- Ride Image --}}
|
|
<div class="aspect-w-16 aspect-h-9 bg-gray-200 dark:bg-gray-700">
|
|
@if($ride->photos->count() > 0)
|
|
<img
|
|
src="{{ $ride->photos->first()->url }}"
|
|
alt="{{ $ride->name }}"
|
|
class="w-full h-48 object-cover"
|
|
loading="lazy"
|
|
>
|
|
@else
|
|
<div class="w-full h-48 flex items-center justify-center">
|
|
<svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<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"></path>
|
|
</svg>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Ride Info --}}
|
|
<div class="p-4">
|
|
<h3 class="font-semibold text-gray-900 dark:text-white text-lg mb-1 line-clamp-1">
|
|
{{ $ride->name }}
|
|
</h3>
|
|
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
|
{{ $ride->park->name }}
|
|
</p>
|
|
|
|
@if($ride->ride_type)
|
|
<span class="inline-block px-2 py-1 text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full mb-2">
|
|
{{ ucfirst($ride->ride_type) }}
|
|
</span>
|
|
@endif
|
|
|
|
@if($ride->status)
|
|
<span class="inline-block px-2 py-1 text-xs font-medium rounded-full mb-2 ml-1
|
|
@if($ride->status === 'operating') bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200
|
|
@elseif($ride->status === 'closed') bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200
|
|
@else bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200
|
|
@endif">
|
|
{{ ucfirst($ride->status) }}
|
|
</span>
|
|
@endif
|
|
|
|
@if($ride->description)
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-3">
|
|
{{ $ride->description }}
|
|
</p>
|
|
@endif
|
|
|
|
{{-- Ride Details --}}
|
|
<div class="space-y-1 text-xs text-gray-500 dark:text-gray-400">
|
|
@if($ride->manufacturer)
|
|
<div class="flex items-center">
|
|
<span class="font-medium">Manufacturer:</span>
|
|
<span class="ml-1">{{ $ride->manufacturer->name }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
@if($ride->opening_date)
|
|
<div class="flex items-center">
|
|
<span class="font-medium">Opened:</span>
|
|
<span class="ml-1">{{ $ride->opening_date->format('Y') }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
@if($ride->height_requirement)
|
|
<div class="flex items-center">
|
|
<span class="font-medium">Height Req:</span>
|
|
<span class="ml-1">{{ $ride->height_requirement }}cm</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Action Button --}}
|
|
<div class="mt-4">
|
|
<a
|
|
href="{{ route('rides.show', $ride) }}"
|
|
class="block w-full text-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
style="min-height: 44px; display: flex; align-items: center; justify-content: center;"
|
|
>
|
|
View Details
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
{{-- Pagination --}}
|
|
<div class="mt-8">
|
|
{{ $rides->links() }}
|
|
</div>
|
|
@else
|
|
{{-- Empty State --}}
|
|
<div class="text-center py-12">
|
|
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0120 12a8 8 0 10-16 0 7.962 7.962 0 012 5.291z"></path>
|
|
</svg>
|
|
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No rides found</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
Try adjusting your search criteria or filters.
|
|
</p>
|
|
@if($search || $category || $status || $manufacturerId || $openingYearFrom || $openingYearTo || $minHeight || $maxHeight || $parkId)
|
|
<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-blue-600 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-900 dark:text-blue-200 dark:hover:bg-blue-800"
|
|
style="min-height: 44px;"
|
|
>
|
|
Clear all filters
|
|
</button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Custom Styles for Line Clamping --}}
|
|
<style>
|
|
.line-clamp-1 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 1;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.line-clamp-2 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
</style> |