Files
thrillwiki_laravel/resources/views/livewire/parks-listing-universal.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

217 lines
14 KiB
PHP

<div>
{{-- Universal Listing Component Integration --}}
<x-universal-listing
:entity-type="$entityType"
:items="$parks"
:search="$search"
:sort-by="$sortBy"
:sort-direction="$sortDirection"
:show-filters="$showFilters"
:active-filters="$activeFilters"
:location-enabled="$locationEnabled"
:location-loading="$locationLoading"
:user-location="$userLocation"
wire:model.live="search"
>
{{-- Custom Location Controls Slot --}}
<x-slot name="locationControls">
@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
</x-slot>
{{-- Custom Filters Slot for Parks-Specific Filters --}}
<x-slot name="customFilters">
<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>
</x-slot>
{{-- Custom Sort Options Slot --}}
<x-slot name="customSortOptions">
@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>
</x-slot>
{{-- Custom Card Content for Parks --}}
<x-slot name="customCardContent" :item="null">
@foreach($parks as $park)
<x-universal-listing-card
:item="$park"
:entity-type="$entityType"
:location-enabled="$locationEnabled"
:user-location="$userLocation"
>
{{-- Custom Park-Specific Content --}}
<x-slot name="customContent">
@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
</x-slot>
</x-universal-listing-card>
@endforeach
</x-slot>
</x-universal-listing>
{{-- JavaScript for Location Services (GPS Integration) --}}
<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>
</div>