Files
thrillwiki_laravel/resources/views/components/universal-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

513 lines
31 KiB
PHP

@props([
'entityType' => 'items',
'entityConfig' => [],
'items' => collect(),
'filters' => [],
'statistics' => [],
'viewModes' => ['grid', 'list'],
'currentViewMode' => 'grid',
'searchPlaceholder' => 'Search...',
'title' => 'Items',
'description' => 'Browse and discover items',
'emptyStateMessage' => 'No items found',
'emptyStateDescription' => 'Try adjusting your search or filters.',
'livewireComponent' => null
])
@php
$config = collect($entityConfig);
$cardFields = $config->get('cardFields', []);
$filterConfig = $config->get('filters', []);
$statisticsConfig = $config->get('statistics', []);
$badgeConfig = $config->get('badges', []);
$sortOptions = $config->get('sortOptions', []);
$colorScheme = $config->get('colorScheme', [
'primary' => 'blue',
'secondary' => 'green',
'accent' => 'purple'
]);
@endphp
<div class="universal-listing-container" x-data="{ viewMode: '{{ $currentViewMode }}' }">
{{-- Mobile Layout (320px - 767px) --}}
<div class="block md:hidden">
{{-- Mobile Header with Search --}}
<div class="sticky top-0 bg-white dark:bg-gray-900 z-20 p-4 border-b border-gray-200 dark:border-gray-700">
<div class="space-y-3">
{{-- Search Input --}}
<div class="relative">
<input
type="text"
@if($livewireComponent) wire:model.live.debounce.300ms="search" @endif
placeholder="{{ $searchPlaceholder }}"
class="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-{{ $colorScheme['primary'] }}-500 focus:border-transparent"
>
<svg class="absolute left-3 top-3.5 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>
{{-- Quick Filter Buttons --}}
@if(isset($filterConfig['quickFilters']))
<div class="flex flex-wrap gap-2">
@foreach($filterConfig['quickFilters'] as $filter)
<button
@if($livewireComponent) wire:click="toggleFilter('{{ $filter['key'] }}', '{{ $filter['value'] }}')" @endif
class="px-3 py-1.5 text-sm rounded-full border transition-colors {{ $filter['active'] ?? false ? 'bg-' . $colorScheme['primary'] . '-500 text-white border-' . $colorScheme['primary'] . '-500' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600' }}"
>
{{ $filter['label'] }}
@if(isset($filter['count']))
<span class="ml-1 text-xs opacity-75">({{ $filter['count'] }})</span>
@endif
</button>
@endforeach
</div>
@endif
</div>
</div>
{{-- Statistics Banner --}}
@if(!empty($statistics))
<div class="bg-gradient-to-r from-{{ $colorScheme['primary'] }}-500 to-{{ $colorScheme['accent'] }}-600 text-white p-4 m-4 rounded-lg">
<div class="text-center">
<h3 class="text-lg font-semibold mb-2">{{ $statistics['title'] ?? 'Overview' }}</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
@foreach(array_slice($statistics['items'] ?? [], 0, 2) as $stat)
<div>
<div class="text-2xl font-bold">{{ $stat['value'] }}</div>
<div class="opacity-90">{{ $stat['label'] }}</div>
</div>
@endforeach
</div>
</div>
</div>
@endif
{{-- Item Cards --}}
<div class="space-y-4 p-4">
@forelse($items as $item)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 border border-gray-200 dark:border-gray-700">
{{-- Item Header --}}
<div class="flex items-start justify-between mb-3">
<div class="flex-1">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
{{ data_get($item, $cardFields['title'] ?? 'name') }}
</h3>
@if(isset($cardFields['subtitle']) && data_get($item, $cardFields['subtitle']))
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ data_get($item, $cardFields['subtitle']) }}
</p>
@endif
</div>
@if(isset($cardFields['score']) && data_get($item, $cardFields['score']))
<div class="text-right">
<div class="text-sm font-medium text-{{ $colorScheme['primary'] }}-600 dark:text-{{ $colorScheme['primary'] }}-400">
{{ data_get($item, $cardFields['score']) }}
</div>
<div class="text-xs text-gray-500">{{ $cardFields['scoreLabel'] ?? 'Score' }}</div>
</div>
@endif
</div>
{{-- Badges --}}
@if(isset($badgeConfig['fields']))
<div class="flex flex-wrap gap-2 mb-3">
@foreach($badgeConfig['fields'] as $badgeField)
@if(data_get($item, $badgeField['field']))
<span class="px-2 py-1 text-xs bg-{{ $badgeField['color'] ?? $colorScheme['primary'] }}-100 dark:bg-{{ $badgeField['color'] ?? $colorScheme['primary'] }}-900 text-{{ $badgeField['color'] ?? $colorScheme['primary'] }}-800 dark:text-{{ $badgeField['color'] ?? $colorScheme['primary'] }}-200 rounded-full">
{{ $badgeField['prefix'] ?? '' }}{{ data_get($item, $badgeField['field']) }}{{ $badgeField['suffix'] ?? '' }}
</span>
@endif
@endforeach
</div>
@endif
{{-- Key Metrics --}}
@if(isset($cardFields['metrics']))
<div class="grid grid-cols-3 gap-4 text-center text-sm">
@foreach(array_slice($cardFields['metrics'], 0, 3) as $metric)
@if(data_get($item, $metric['field']))
<div>
<div class="font-semibold text-gray-900 dark:text-gray-100">
{{ $metric['format'] ? sprintf($metric['format'], data_get($item, $metric['field'])) : data_get($item, $metric['field']) }}
</div>
<div class="text-gray-600 dark:text-gray-400">{{ $metric['label'] }}</div>
</div>
@endif
@endforeach
</div>
@endif
</div>
@empty
<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="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-gray-100">{{ $emptyStateMessage }}</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ $emptyStateDescription }}</p>
</div>
@endforelse
</div>
{{-- Mobile Pagination --}}
@if(method_exists($items, 'hasPages') && $items->hasPages())
<div class="sticky bottom-0 bg-white dark:bg-gray-900 p-4 border-t border-gray-200 dark:border-gray-700">
{{ $items->links('pagination.mobile') }}
</div>
@endif
</div>
{{-- Tablet Layout (768px - 1023px) --}}
<div class="hidden md:block lg:hidden">
<div class="flex h-screen">
{{-- Filter Sidebar --}}
<div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto border-r border-gray-200 dark:border-gray-700">
<div class="p-6">
{{-- Search --}}
<div class="relative mb-6">
<input
type="text"
@if($livewireComponent) wire:model.live.debounce.300ms="search" @endif
placeholder="{{ $searchPlaceholder }}"
class="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<svg class="absolute left-3 top-3.5 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>
{{-- Dynamic Filters --}}
@if(isset($filterConfig['sections']))
@foreach($filterConfig['sections'] as $section)
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">{{ $section['title'] }}</h3>
@if($section['type'] === 'checkboxes')
<div class="space-y-2">
@foreach($section['options'] as $option)
<label class="flex items-center">
<input
type="checkbox"
@if($livewireComponent) wire:model.live="{{ $section['model'] }}" @endif
value="{{ $option['value'] }}"
class="rounded border-gray-300 text-{{ $colorScheme['primary'] }}-600 focus:ring-{{ $colorScheme['primary'] }}-500"
>
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">
{{ $option['label'] }}
@if(isset($option['count']))
({{ $option['count'] }})
@endif
</span>
</label>
@endforeach
</div>
@elseif($section['type'] === 'select')
<select @if($livewireComponent) wire:model.live="{{ $section['model'] }}" @endif class="w-full text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
<option value="">{{ $section['placeholder'] ?? 'All Options' }}</option>
@foreach($section['options'] as $option)
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
@endforeach
</select>
@elseif($section['type'] === 'range')
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">{{ $section['fromLabel'] ?? 'From' }}</label>
<input
type="number"
@if($livewireComponent) wire:model.live="{{ $section['fromModel'] }}" @endif
placeholder="{{ $section['fromPlaceholder'] ?? '' }}"
class="w-full text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">{{ $section['toLabel'] ?? 'To' }}</label>
<input
type="number"
@if($livewireComponent) wire:model.live="{{ $section['toModel'] }}" @endif
placeholder="{{ $section['toPlaceholder'] ?? '' }}"
class="w-full text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
</div>
</div>
@endif
</div>
@endforeach
@endif
{{-- Statistics Panel --}}
@if(!empty($statistics))
<div class="bg-{{ $colorScheme['primary'] }}-50 dark:bg-{{ $colorScheme['primary'] }}-900/20 rounded-lg p-4">
<h3 class="text-sm font-medium text-{{ $colorScheme['primary'] }}-900 dark:text-{{ $colorScheme['primary'] }}-100 mb-3">{{ $statistics['title'] ?? 'Statistics' }}</h3>
<div class="space-y-2 text-sm">
@foreach($statistics['items'] ?? [] as $stat)
<div class="flex justify-between">
<span class="text-{{ $colorScheme['primary'] }}-700 dark:text-{{ $colorScheme['primary'] }}-300">{{ $stat['label'] }}</span>
<span class="font-medium text-{{ $colorScheme['primary'] }}-900 dark:text-{{ $colorScheme['primary'] }}-100">{{ $stat['value'] }}</span>
</div>
@endforeach
</div>
</div>
@endif
</div>
</div>
{{-- Main Content --}}
<div class="flex-1 flex flex-col">
{{-- Header --}}
<div class="bg-white dark:bg-gray-900 p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
{{ method_exists($items, 'total') ? $items->total() : $items->count() }} {{ $title }}
</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ $description }}</p>
</div>
<div class="flex items-center space-x-4">
{{-- Sort Selector --}}
@if(!empty($sortOptions))
<select @if($livewireComponent) wire:model.live="sortBy" @endif class="text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
@foreach($sortOptions as $option)
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
@endforeach
</select>
@endif
{{-- View Toggle --}}
@if(count($viewModes) > 1)
<div class="flex rounded-md border border-gray-300 dark:border-gray-600">
@foreach($viewModes as $mode)
<button
@if($livewireComponent) wire:click="setViewMode('{{ $mode }}')" @endif
x-on:click="viewMode = '{{ $mode }}'"
class="px-3 py-1 text-sm transition-colors"
:class="viewMode === '{{ $mode }}' ? 'bg-{{ $colorScheme['primary'] }}-500 text-white' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300'"
>
{{ ucfirst($mode) }}
</button>
@endforeach
</div>
@endif
</div>
</div>
</div>
{{-- Content Grid --}}
<div class="flex-1 overflow-y-auto p-6">
<div x-show="viewMode === 'grid'" class="grid grid-cols-2 gap-6">
@foreach($items as $item)
<x-universal-listing-card
:item="$item"
:config="$cardFields"
:badges="$badgeConfig"
:colorScheme="$colorScheme"
layout="grid"
/>
@endforeach
</div>
<div x-show="viewMode === 'list'" class="space-y-4">
@foreach($items as $item)
<x-universal-listing-card
:item="$item"
:config="$cardFields"
:badges="$badgeConfig"
:colorScheme="$colorScheme"
layout="list"
/>
@endforeach
</div>
{{-- Pagination --}}
@if(method_exists($items, 'hasPages') && $items->hasPages())
<div class="mt-8">
{{ $items->links() }}
</div>
@endif
</div>
</div>
</div>
</div>
{{-- Desktop Layout (1024px+) --}}
<div class="hidden lg:block">
<div class="flex h-screen">
{{-- Advanced Filter Sidebar --}}
<div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto border-r border-gray-200 dark:border-gray-700">
<div class="p-6">
{{-- Search --}}
<div class="relative mb-6">
<input
type="text"
@if($livewireComponent) wire:model.live.debounce.300ms="search" @endif
placeholder="{{ $searchPlaceholder }}"
class="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<svg class="absolute left-3 top-3.5 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>
{{-- Advanced Filters (Same as tablet but with more options) --}}
@if(isset($filterConfig['sections']))
@foreach($filterConfig['sections'] as $section)
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">{{ $section['title'] }}</h3>
@if($section['type'] === 'checkboxes')
<div class="space-y-2">
@foreach($section['options'] as $option)
<label class="flex items-center">
<input
type="checkbox"
@if($livewireComponent) wire:model.live="{{ $section['model'] }}" @endif
value="{{ $option['value'] }}"
class="rounded border-gray-300 text-{{ $colorScheme['primary'] }}-600 focus:ring-{{ $colorScheme['primary'] }}-500"
>
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">
{{ $option['label'] }}
@if(isset($option['count']))
({{ $option['count'] }})
@endif
</span>
</label>
@endforeach
</div>
@elseif($section['type'] === 'select')
<select @if($livewireComponent) wire:model.live="{{ $section['model'] }}" @endif class="w-full text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
<option value="">{{ $section['placeholder'] ?? 'All Options' }}</option>
@foreach($section['options'] as $option)
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
@endforeach
</select>
@elseif($section['type'] === 'range')
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">{{ $section['fromLabel'] ?? 'From' }}</label>
<input
type="number"
@if($livewireComponent) wire:model.live="{{ $section['fromModel'] }}" @endif
placeholder="{{ $section['fromPlaceholder'] ?? '' }}"
class="w-full text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">{{ $section['toLabel'] ?? 'To' }}</label>
<input
type="number"
@if($livewireComponent) wire:model.live="{{ $section['toModel'] }}" @endif
placeholder="{{ $section['toPlaceholder'] ?? '' }}"
class="w-full text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
</div>
</div>
@endif
</div>
@endforeach
@endif
{{-- Enhanced Statistics Panel --}}
@if(!empty($statistics))
<div class="bg-{{ $colorScheme['primary'] }}-50 dark:bg-{{ $colorScheme['primary'] }}-900/20 rounded-lg p-4">
<h3 class="text-sm font-medium text-{{ $colorScheme['primary'] }}-900 dark:text-{{ $colorScheme['primary'] }}-100 mb-3">{{ $statistics['title'] ?? 'Statistics' }}</h3>
<div class="space-y-2 text-sm">
@foreach($statistics['items'] ?? [] as $stat)
<div class="flex justify-between">
<span class="text-{{ $colorScheme['primary'] }}-700 dark:text-{{ $colorScheme['primary'] }}-300">{{ $stat['label'] }}</span>
<span class="font-medium text-{{ $colorScheme['primary'] }}-900 dark:text-{{ $colorScheme['primary'] }}-100">{{ $stat['value'] }}</span>
</div>
@endforeach
</div>
</div>
@endif
</div>
</div>
{{-- Main Content Area --}}
<div class="flex-1 flex flex-col">
{{-- Enhanced Header --}}
<div class="bg-white dark:bg-gray-900 p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{{ method_exists($items, 'total') ? $items->total() : $items->count() }} {{ $title }}
</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ $description }}</p>
</div>
<div class="flex items-center space-x-4">
{{-- Sort Selector --}}
@if(!empty($sortOptions))
<select @if($livewireComponent) wire:model.live="sortBy" @endif class="text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
@foreach($sortOptions as $option)
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
@endforeach
</select>
@endif
{{-- Enhanced View Toggle --}}
@if(count($viewModes) > 1)
<div class="flex rounded-md border border-gray-300 dark:border-gray-600">
@foreach($viewModes as $mode)
<button
@if($livewireComponent) wire:click="setViewMode('{{ $mode }}')" @endif
x-on:click="viewMode = '{{ $mode }}'"
class="px-4 py-2 text-sm transition-colors"
:class="viewMode === '{{ $mode }}' ? 'bg-{{ $colorScheme['primary'] }}-500 text-white' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300'"
>
{{ ucfirst($mode) }}
</button>
@endforeach
</div>
@endif
</div>
</div>
</div>
{{-- Enhanced Content Area --}}
<div class="flex-1 overflow-y-auto p-6">
<div x-show="viewMode === 'grid'" class="grid grid-cols-3 gap-6">
@foreach($items as $item)
<x-universal-listing-card
:item="$item"
:config="$cardFields"
:badges="$badgeConfig"
:colorScheme="$colorScheme"
layout="grid"
/>
@endforeach
</div>
<div x-show="viewMode === 'list'" class="space-y-4">
@foreach($items as $item)
<x-universal-listing-card
:item="$item"
:config="$cardFields"
:badges="$badgeConfig"
:colorScheme="$colorScheme"
layout="list"
/>
@endforeach
</div>
<div x-show="viewMode === 'portfolio'" class="space-y-6">
@foreach($items as $item)
<x-universal-listing-card
:item="$item"
:config="$cardFields"
:badges="$badgeConfig"
:colorScheme="$colorScheme"
layout="portfolio"
/>
@endforeach
</div>
{{-- Pagination --}}
@if(method_exists($items, 'hasPages') && $items->hasPages())
<div class="mt-8">
{{ $items->links() }}
</div>
@endif
</div>
</div>
</div>
</div>
</div>