mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 02:31:09 -05:00
- 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.
357 lines
19 KiB
PHP
357 lines
19 KiB
PHP
<div class="space-y-6">
|
|
<!-- Park Header with Stats -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
|
|
<div>
|
|
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white">
|
|
{{ $park->name }} Rides
|
|
</h1>
|
|
<p class="mt-2 text-gray-600 dark:text-gray-400">
|
|
Explore all rides at {{ $park->name }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Park Statistics -->
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 lg:gap-6">
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
|
{{ $parkStats['total_rides'] }}
|
|
</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Total Rides</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-green-600 dark:text-green-400">
|
|
{{ $parkStats['operating_rides'] }}
|
|
</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Operating</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-purple-600 dark:text-purple-400">
|
|
{{ $parkStats['categories'] }}
|
|
</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Categories</div>
|
|
</div>
|
|
@if($parkStats['avg_rating'])
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-yellow-600 dark:text-yellow-400">
|
|
{{ $parkStats['avg_rating'] }}★
|
|
</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Avg Rating</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search and Filter Controls -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
|
<!-- Search Bar -->
|
|
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
|
<div class="flex flex-col sm:flex-row gap-4">
|
|
<div class="flex-1">
|
|
<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="searchTerm"
|
|
placeholder="Search rides by name, description, manufacturer, or designer..."
|
|
class="block w-full pl-10 pr-3 py-2 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-blue-500"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Toggle (Mobile) -->
|
|
<button
|
|
wire:click="toggleFilters"
|
|
class="sm:hidden flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600"
|
|
>
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.207A1 1 0 013 6.5V4z"></path>
|
|
</svg>
|
|
Filters
|
|
@if($activeFiltersCount > 0)
|
|
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
|
{{ $activeFiltersCount }}
|
|
</span>
|
|
@endif
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters Section -->
|
|
<div class="transition-all duration-300 {{ $showFilters ? 'block' : 'hidden' }} sm:block">
|
|
<div class="p-4 space-y-4">
|
|
<!-- Category and Status Filters -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<!-- Categories -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Category
|
|
</label>
|
|
<div class="flex flex-wrap gap-2">
|
|
@foreach($categories as $category)
|
|
<button
|
|
wire:click="setCategory('{{ $category['value'] }}')"
|
|
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium transition-colors
|
|
{{ $selectedCategory === $category['value']
|
|
? 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-700'
|
|
: 'bg-gray-100 text-gray-700 border-gray-200 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600' }} border"
|
|
>
|
|
{{ $category['label'] }}
|
|
<span class="ml-1 text-xs">({{ $category['count'] }})</span>
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statuses -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Status
|
|
</label>
|
|
<div class="flex flex-wrap gap-2">
|
|
@foreach($statuses as $status)
|
|
<button
|
|
wire:click="setStatus('{{ $status['value'] }}')"
|
|
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium transition-colors
|
|
{{ $selectedStatus === $status['value']
|
|
? 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-300 dark:border-green-700'
|
|
: 'bg-gray-100 text-gray-700 border-gray-200 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600' }} border"
|
|
>
|
|
{{ $status['label'] }}
|
|
<span class="ml-1 text-xs">({{ $status['count'] }})</span>
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sort and Actions -->
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<!-- Sort Options -->
|
|
<div class="flex items-center space-x-4">
|
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Sort by:</label>
|
|
<select
|
|
wire:model.live="sortBy"
|
|
class="rounded-lg border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:border-blue-500 focus:ring-blue-500"
|
|
>
|
|
@foreach($sortOptions as $value => $label)
|
|
<option value="{{ $value }}">{{ $label }}</option>
|
|
@endforeach
|
|
</select>
|
|
|
|
<button
|
|
wire:click="setSortBy('{{ $sortBy }}')"
|
|
class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
title="Toggle sort direction"
|
|
>
|
|
<svg class="w-4 h-4 transform {{ $sortDirection === 'desc' ? 'rotate-180' : '' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Clear Filters -->
|
|
@if($activeFiltersCount > 0)
|
|
<button
|
|
wire:click="clearFilters"
|
|
class="text-sm text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 font-medium"
|
|
>
|
|
Clear all filters ({{ $activeFiltersCount }})
|
|
</button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Count and Loading -->
|
|
<div class="flex items-center justify-between">
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
|
@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 space-x-2 text-gray-600 dark:text-gray-400">
|
|
<svg class="animate-spin 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>
|
|
<span class="text-sm">Loading...</span>
|
|
</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 2xl:grid-cols-5 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">
|
|
<!-- 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">
|
|
<div class="flex items-start justify-between">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate">
|
|
<a href="{{ route('rides.show', $ride) }}" class="hover:text-blue-600 dark:hover:text-blue-400">
|
|
{{ $ride->name }}
|
|
</a>
|
|
</h3>
|
|
|
|
<!-- Status Badge -->
|
|
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
|
|
{{ $ride->status === 'operating' ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' : 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300' }}">
|
|
{{ ucfirst($ride->status) }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Category -->
|
|
<div class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
{{ ucfirst(str_replace('_', ' ', $ride->category)) }}
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
@if($ride->description)
|
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
|
{{ $ride->description }}
|
|
</p>
|
|
@endif
|
|
|
|
<!-- Ride Details -->
|
|
<div class="mt-3 space-y-1 text-xs text-gray-500 dark:text-gray-400">
|
|
@if($ride->opening_year)
|
|
<div>Opened: {{ $ride->opening_year }}</div>
|
|
@endif
|
|
@if($ride->height_requirement)
|
|
<div>Height: {{ $ride->height_requirement }}cm minimum</div>
|
|
@endif
|
|
@if($ride->manufacturer)
|
|
<div>Manufacturer: {{ $ride->manufacturer->name }}</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Rating -->
|
|
@if($ride->reviews_avg_rating)
|
|
<div class="mt-3 flex items-center">
|
|
<div class="flex items-center">
|
|
@for($i = 1; $i <= 5; $i++)
|
|
<svg class="w-4 h-4 {{ $i <= round($ride->reviews_avg_rating) ? 'text-yellow-400' : 'text-gray-300 dark:text-gray-600' }}" 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>
|
|
@endfor
|
|
</div>
|
|
<span class="ml-1 text-sm text-gray-600 dark:text-gray-400">
|
|
{{ round($ride->reviews_avg_rating, 1) }} ({{ $ride->reviews_count }})
|
|
</span>
|
|
</div>
|
|
@endif
|
|
</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 0118 12a8 8 0 01-8 8 8 8 0 01-8-8 8 8 0 018-8c2.152 0 4.139.851 5.582 2.236"></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">
|
|
@if($activeFiltersCount > 0)
|
|
Try adjusting your search criteria or clearing filters.
|
|
@else
|
|
This park doesn't have any rides yet.
|
|
@endif
|
|
</p>
|
|
@if($activeFiltersCount > 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-blue-700 bg-blue-100 hover:bg-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:hover:bg-blue-900/30"
|
|
>
|
|
Clear all filters
|
|
</button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Screen-Agnostic Responsive Styles -->
|
|
<style>
|
|
/* Mobile-first responsive design */
|
|
@media (max-width: 320px) {
|
|
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
}
|
|
|
|
@media (min-width: 1024px) {
|
|
.lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
}
|
|
|
|
@media (min-width: 1280px) {
|
|
.xl\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
}
|
|
|
|
@media (min-width: 1536px) {
|
|
.2xl\:grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
|
}
|
|
|
|
@media (min-width: 1920px) {
|
|
.2xl\:grid-cols-5 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
|
}
|
|
|
|
@media (min-width: 2560px) {
|
|
.2xl\:grid-cols-5 { grid-template-columns: repeat(8, minmax(0, 1fr)); }
|
|
}
|
|
|
|
/* Touch-friendly targets for mobile */
|
|
@media (max-width: 768px) {
|
|
button, select, input {
|
|
min-height: 44px;
|
|
}
|
|
}
|
|
|
|
/* Line clamp utility */
|
|
.line-clamp-2 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* High DPI display optimizations */
|
|
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
|
.border { border-width: 0.5px; }
|
|
}
|
|
</style> |