# Parks Listing Page Implementation Prompt

## Django Parity Reference
**Django Implementation**: `parks/views.py` - `ParkListView` (lines 135-150+)
**Django Template**: `parks/templates/parks/park_list.html`
**Django Features**: Location-based search, operator filtering, region filtering, park type filtering, statistics display, pagination with HTMX, map integration

## Core Implementation Requirements

### Laravel/Livewire Architecture
Generate the parks listing system using ThrillWiki's custom generators:

```bash
# Generate the main listing component with location optimization
php artisan make:thrillwiki-livewire ParksListing --paginated --cached --with-tests

# Generate location-aware search component
php artisan make:thrillwiki-livewire ParksLocationSearch --reusable --with-tests

# Generate operator-specific park filters
php artisan make:thrillwiki-livewire ParksFilters --reusable --cached

# Generate parks map view component
php artisan make:thrillwiki-livewire ParksMapView --reusable --with-tests

# Generate operator-specific park listings
php artisan make:thrillwiki-livewire OperatorParksListing --paginated --cached --with-tests

# Generate regional park listings
php artisan make:thrillwiki-livewire RegionalParksListing --paginated --cached --with-tests
```

### Django Parity Features

#### 1. Location-Based Search Functionality
**Django Implementation**: Multi-term search with location awareness across:
- Park name (`name__icontains`)
- Park description (`description__icontains`)
- Location city/state (`location__city__icontains`, `location__state__icontains`)
- Operator name (`operator__name__icontains`)
- Park type (`park_type__icontains`)

**Laravel Implementation**:
```php
public function locationAwareSearch($query, $userLocation = null)
{
    return Park::query()
        ->when($query, function ($q) use ($query) {
            $terms = explode(' ', $query);
            foreach ($terms as $term) {
                $q->where(function ($subQuery) use ($term) {
                    $subQuery->where('name', 'ilike', "%{$term}%")
                            ->orWhere('description', 'ilike', "%{$term}%")
                            ->orWhere('park_type', 'ilike', "%{$term}%")
                            ->orWhereHas('location', function($locQuery) use ($term) {
                                $locQuery->where('city', 'ilike', "%{$term}%")
                                        ->orWhere('state', 'ilike', "%{$term}%")
                                        ->orWhere('country', 'ilike', "%{$term}%");
                            })
                            ->orWhereHas('operator', fn($opQuery) => 
                                $opQuery->where('name', 'ilike', "%{$term}%"));
                });
            }
        })
        ->when($userLocation, function ($q) use ($userLocation) {
            // Add distance-based ordering for location-aware results
            $q->selectRaw('parks.*, 
                (6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) * 
                cos(radians(locations.longitude) - radians(?)) + 
                sin(radians(?)) * sin(radians(locations.latitude)))) AS distance', 
                [$userLocation['lat'], $userLocation['lng'], $userLocation['lat']])
              ->join('locations', 'parks.location_id', '=', 'locations.id')
              ->orderBy('distance');
        })
        ->with(['location', 'operator', 'photos', 'statistics'])
        ->withCount(['rides', 'reviews']);
}
```

#### 2. Advanced Filtering with Geographic Context
**Django Filters**:
- Operator (operator__id)
- Region/State (location__state)
- Country (location__country)
- Park type (park_type)
- Opening year range
- Size range (area_acres)
- Ride count range
- Distance from user location

**Laravel Filters Implementation**:
```php
public function applyFilters($query, $filters, $userLocation = null)
{
    return $query
        ->when($filters['operator_id'] ?? null, fn($q, $operatorId) => 
            $q->where('operator_id', $operatorId))
        ->when($filters['region'] ?? null, fn($q, $region) => 
            $q->whereHas('location', fn($locQ) => $locQ->where('state', $region)))
        ->when($filters['country'] ?? null, fn($q, $country) => 
            $q->whereHas('location', fn($locQ) => $locQ->where('country', $country)))
        ->when($filters['park_type'] ?? null, fn($q, $type) => 
            $q->where('park_type', $type))
        ->when($filters['opening_year_from'] ?? null, fn($q, $year) => 
            $q->where('opening_date', '>=', "{$year}-01-01"))
        ->when($filters['opening_year_to'] ?? null, fn($q, $year) => 
            $q->where('opening_date', '<=', "{$year}-12-31"))
        ->when($filters['min_area'] ?? null, fn($q, $area) => 
            $q->where('area_acres', '>=', $area))
        ->when($filters['max_area'] ?? null, fn($q, $area) => 
            $q->where('area_acres', '<=', $area))
        ->when($filters['min_rides'] ?? null, fn($q, $count) => 
            $q->whereHas('rides', fn($rideQ) => $rideQ->havingRaw('COUNT(*) >= ?', [$count])))
        ->when($filters['max_distance'] ?? null && $userLocation, function($q) use ($filters, $userLocation) {
            $q->whereRaw('(6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) * 
                cos(radians(locations.longitude) - radians(?)) + 
                sin(radians(?)) * sin(radians(locations.latitude)))) <= ?', 
                [$userLocation['lat'], $userLocation['lng'], $userLocation['lat'], $filters['max_distance']]);
        });
}
```

#### 3. Context-Aware Views with Statistics
**Global Listing**: All parks worldwide with statistics
**Operator-Specific Listing**: Parks filtered by specific operator with comparisons
**Regional Listing**: Parks filtered by geographic region with local insights
**Nearby Listing**: Location-based parks with distance calculations

### Screen-Agnostic Design Implementation

#### Mobile Layout (320px - 767px)
- **Single Column**: Full-width park cards with essential info
- **Location Services**: GPS-enabled "Near Me" functionality
- **Touch-Optimized Maps**: Pinch-to-zoom, tap-to-select functionality
- **Swipe Navigation**: Horizontal scrolling for quick filters
- **Bottom Sheet**: Map/list toggle with smooth transitions

**Mobile Component Structure**:
```blade
<div class="parks-mobile-layout">
    <!-- GPS-Enabled Search Bar -->
    <div class="sticky top-0 bg-white dark:bg-gray-900 z-20 p-4">
        <livewire:parks-location-search :enable-gps="true" />
        <div class="flex items-center mt-2 space-x-2">
            <button wire:click="toggleNearbyMode" class="flex items-center space-x-1 px-3 py-1 bg-blue-100 dark:bg-blue-900 rounded-full">
                <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">...</svg>
                <span class="text-sm">Near Me</span>
            </button>
            <button wire:click="toggleMapView" class="flex items-center space-x-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
                <span class="text-sm">{{ $showMap ? 'List' : 'Map' }}</span>
            </button>
        </div>
    </div>
    
    <!-- Quick Filters -->
    <div class="horizontal-scroll p-4 pb-2">
        <livewire:parks-quick-filters />
    </div>
    
    @if($showMap)
        <!-- Mobile Map View -->
        <div class="h-64 relative">
            <livewire:parks-map-view :parks="$parks" :compact="true" />
        </div>
        <!-- Bottom Sheet Park List -->
        <div class="bg-white dark:bg-gray-900 rounded-t-xl shadow-lg mt-4">
            <div class="p-4 border-b border-gray-200 dark:border-gray-700">
                <h3 class="text-lg font-semibold">{{ $parks->count() }} Parks Found</h3>
            </div>
            <div class="max-h-96 overflow-y-auto">
                @foreach($parks as $park)
                    <livewire:park-mobile-card :park="$park" :show-distance="true" :key="$park->id" />
                @endforeach
            </div>
        </div>
    @else
        <!-- Park Cards -->
        <div class="space-y-4 p-4">
            @foreach($parks as $park)
                <livewire:park-mobile-card :park="$park" :show-distance="$nearbyMode" :key="$park->id" />
            @endforeach
        </div>
    @endif
    
    <!-- Mobile Pagination -->
    <div class="sticky bottom-0 bg-white dark:bg-gray-900 p-4">
        {{ $parks->links('pagination.mobile') }}
    </div>
</div>
```

#### Tablet Layout (768px - 1023px)
- **Dual-Pane with Map**: Filter sidebar + map/list split view
- **Advanced Filtering**: Expandable regional and operator filters
- **Split-Screen Mode**: Map on one side, detailed list on the other
- **Touch + External Input**: Keyboard shortcuts for power users

**Tablet Component Structure**:
```blade
<div class="parks-tablet-layout flex h-screen">
    <!-- Filter Sidebar -->
    <div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
        <div class="p-6">
            <livewire:parks-location-search :advanced="true" />
            <div class="mt-6">
                <livewire:parks-filters :expanded="true" :show-regional="true" />
            </div>
        </div>
    </div>
    
    <!-- Main Content Area -->
    <div class="flex-1 flex flex-col">
        <!-- View Toggle and Stats -->
        <div class="bg-white dark:bg-gray-900 p-4 border-b border-gray-200 dark:border-gray-700">
            <div class="flex items-center justify-between">
                <div class="flex items-center space-x-4">
                    <h2 class="text-xl font-semibold">{{ $parks->total() }} Parks</h2>
                    <livewire:parks-statistics-summary />
                </div>
                <div class="flex items-center space-x-2">
                    <button wire:click="setView('list')" class="px-3 py-2 {{ $view === 'list' ? 'bg-blue-500 text-white' : 'bg-gray-200 dark:bg-gray-700' }} rounded">
                        List
                    </button>
                    <button wire:click="setView('map')" class="px-3 py-2 {{ $view === 'map' ? 'bg-blue-500 text-white' : 'bg-gray-200 dark:bg-gray-700' }} rounded">
                        Map
                    </button>
                    <button wire:click="setView('split')" class="px-3 py-2 {{ $view === 'split' ? 'bg-blue-500 text-white' : 'bg-gray-200 dark:bg-gray-700' }} rounded">
                        Split
                    </button>
                </div>
            </div>
        </div>
        
        <!-- Content Area -->
        <div class="flex-1 flex">
            @if($view === 'list')
                <!-- Full List View -->
                <div class="flex-1 overflow-y-auto p-6">
                    <div class="grid grid-cols-2 gap-6">
                        @foreach($parks as $park)
                            <livewire:park-tablet-card :park="$park" :key="$park->id" />
                        @endforeach
                    </div>
                    <div class="mt-6">
                        {{ $parks->links() }}
                    </div>
                </div>
            @elseif($view === 'map')
                <!-- Full Map View -->
                <div class="flex-1">
                    <livewire:parks-map-view :parks="$parks" :interactive="true" />
                </div>
            @else
                <!-- Split View -->
                <div class="flex-1">
                    <livewire:parks-map-view :parks="$parks" :interactive="true" />
                </div>
                <div class="w-96 bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-700 overflow-y-auto">
                    <div class="p-4">
                        @foreach($parks as $park)
                            <livewire:park-compact-card :park="$park" :key="$park->id" />
                        @endforeach
                    </div>
                </div>
            @endif
        </div>
    </div>
</div>
```

#### Desktop Layout (1024px - 1919px)
- **Three-Pane Layout**: Filters + map/list + park details
- **Advanced Map Integration**: Multiple layers, clustering, detailed overlays
- **Keyboard Navigation**: Full keyboard shortcuts and accessibility
- **Multi-Window Support**: Optimal for external monitor setups

**Desktop Component Structure**:
```blade
<div class="parks-desktop-layout flex h-screen">
    <!-- Advanced Filter Sidebar -->
    <div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
        <div class="p-6">
            <livewire:parks-location-search :advanced="true" :autocomplete="true" />
            <div class="mt-6">
                <livewire:parks-filters :expanded="true" :advanced="true" :show-statistics="true" />
            </div>
        </div>
    </div>
    
    <!-- Main Content -->
    <div class="flex-1 flex flex-col">
        <!-- Advanced 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 mb-4">
                <div class="flex items-center space-x-6">
                    <h1 class="text-2xl font-bold">{{ $parks->total() }} Theme Parks</h1>
                    <livewire:parks-statistics-dashboard />
                </div>
                <div class="flex items-center space-x-4">
                    <livewire:parks-sort-selector :options="$advancedSortOptions" />
                    <livewire:parks-view-selector />
                    <livewire:parks-export-options />
                </div>
            </div>
            <livewire:parks-advanced-search-bar />
        </div>
        
        <!-- Content Area -->
        <div class="flex-1 flex">
            @if($view === 'grid')
                <!-- Advanced Grid View -->
                <div class="flex-1 overflow-y-auto p-6">
                    <div class="grid grid-cols-3 xl:grid-cols-4 gap-6">
                        @foreach($parks as $park)
                            <livewire:park-desktop-card :park="$park" :detailed="true" :key="$park->id" />
                        @endforeach
                    </div>
                    <div class="mt-8">
                        {{ $parks->links('pagination.desktop') }}
                    </div>
                </div>
            @elseif($view === 'map')
                <!-- Advanced Map View -->
                <div class="flex-1">
                    <livewire:parks-advanced-map :parks="$parks" :clustering="true" :layers="true" />
                </div>
            @else
                <!-- Dashboard View -->
                <div class="flex-1 p-6">
                    <livewire:parks-dashboard :parks="$parks" />
                </div>
            @endif
        </div>
    </div>
    
    <!-- Quick Info Panel -->
    <div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
        <div class="p-6">
            <livewire:parks-quick-info />
            <div class="mt-6">
                <livewire:parks-recent-activity />
            </div>
        </div>
    </div>
</div>
```

#### Large Screen Layout (1920px+)
- **Dashboard-Style Interface**: Multi-column with comprehensive analytics
- **Ultra-Wide Map Integration**: Immersive geographic visualization
- **Advanced Data Visualization**: Charts, graphs, and statistical overlays
- **Multi-Monitor Optimization**: Designed for extended desktop setups

### Performance Optimization Strategy

#### Location-Aware Caching
```php
public function mount()
{
    $this->userLocation = $this->getUserLocation();
    
    $this->cachedFilters = Cache::remember(
        "parks.filters.{$this->userLocation['region']}",
        now()->addHours(2),
        fn() => $this->loadRegionalFilterOptions()
    );
}

public function getParksProperty()
{
    $cacheKey = "parks.listing." . md5(serialize([
        'search' => $this->search,
        'filters' => $this->filters,
        'location' => $this->userLocation,
        'sort' => $this->sort,
        'page' => $this->page
    ]));
    
    return Cache::remember($cacheKey, now()->addMinutes(20), function() {
        return $this->locationAwareSearch($this->search, $this->userLocation)
            ->applyFilters($this->filters, $this->userLocation)
            ->orderBy($this->sort['column'], $this->sort['direction'])
            ->paginate(18);
    });
}
```

#### Geographic Query Optimization
```php
// Optimized query with spatial indexing
public function optimizedLocationQuery()
{
    return Park::select([
        'parks.*',
        DB::raw('(6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) * 
                cos(radians(locations.longitude) - radians(?)) + 
                sin(radians(?)) * sin(radians(locations.latitude)))) AS distance
        ')
    ])
    ->join('locations', 'parks.location_id', '=', 'locations.id')
    ->with([
        'location:id,city,state,country,latitude,longitude',
        'operator:id,name,slug',
        'photos' => fn($q) => $q->select(['id', 'park_id', 'url', 'thumbnail_url'])->limit(3),
        'statistics:park_id,total_rides,total_reviews,average_rating'
    ])
    ->withCount(['rides', 'reviews', 'favorites'])
    ->addBinding([$this->userLat, $this->userLng, $this->userLat], 'select');
}
```

### Component Reuse Strategy

#### Shared Components
- **`ParksLocationSearch`**: GPS-enabled search with autocomplete
- **`ParksFilters`**: Regional and operator filtering with statistics
- **`ParksMapView`**: Interactive map with clustering and layers
- **`ParkCard`**: Responsive park display with distance calculations

#### Context Variations
- **`GlobalParksListing`**: All parks worldwide with regional grouping
- **`OperatorParksListing`**: Operator-specific parks with comparisons
- **`RegionalParksListing`**: Geographic region parks with local insights
- **`NearbyParksListing`**: Location-based parks with travel information

### Testing Requirements

#### Feature Tests
```php
/** @test */
public function can_search_parks_with_location_awareness()
{
    $magicKingdom = Park::factory()->create(['name' => 'Magic Kingdom']);
    $magicKingdom->location()->create([
        'city' => 'Orlando',
        'state' => 'Florida',
        'latitude' => 28.3772,
        'longitude' => -81.5707
    ]);
    
    Livewire::test(ParksListing::class)
        ->set('search', 'Magic Orlando')
        ->set('userLocation', ['lat' => 28.4, 'lng' => -81.6])
        ->assertSee($magicKingdom->name)
        ->assertSee('Orlando');
}

/** @test */
public function filters_parks_by_distance_from_user_location()
{
    $nearPark = Park::factory()->create(['name' => 'Near Park']);
    $nearPark->location()->create(['latitude' => 28.3772, 'longitude' => -81.5707]);
    
    $farPark = Park::factory()->create(['name' => 'Far Park']);
    $farPark->location()->create(['latitude' => 40.7128, 'longitude' => -74.0060]);
    
    Livewire::test(ParksListing::class)
        ->set('userLocation', ['lat' => 28.4, 'lng' => -81.6])
        ->set('filters.max_distance', 50)
        ->assertSee($nearPark->name)
        ->assertDontSee($farPark->name);
}

/** @test */
public function maintains_django_parity_performance_with_location()
{
    Park::factory()->count(100)->create();
    
    $start = microtime(true);
    Livewire::test(ParksListing::class)
        ->set('userLocation', ['lat' => 28.4, 'lng' => -81.6]);
    $end = microtime(true);
    
    $this->assertLessThan(0.5, $end - $start); // < 500ms with location
}
```

#### Location-Specific Tests
```php
/** @test */
public function calculates_accurate_distances_between_parks_and_user()
{
    $park = Park::factory()->create();
    $park->location()->create([
        'latitude' => 28.3772,  // Magic Kingdom coordinates
        'longitude' => -81.5707
    ]);
    
    $component = Livewire::test(ParksListing::class)
        ->set('userLocation', ['lat' => 28.4, 'lng' => -81.6]);
        
    $distance = $component->get('parks')->first()->distance;
    $this->assertLessThan(5, $distance); // Should be less than 5km
}

/** @test */
public function handles_gps_permission_denied_gracefully()
{
    Livewire::test(ParksListing::class)
        ->set('gpsPermissionDenied', true)
        ->assertSee('Enter your location manually')
        ->assertDontSee('Near Me');
}
```

### Performance Targets

#### Universal Performance Standards with Location
- **Initial Load**: < 500ms (matches Django with location services)
- **GPS Location Acquisition**: < 2 seconds
- **Distance Calculation**: < 100ms for 100 parks
- **Map Rendering**: < 1 second for initial load
- **Filter Response**: < 200ms with location context

#### Location-Aware Caching Strategy
- **Regional Filter Cache**: 2 hours (changes infrequently)
- **Distance Calculations**: 30 minutes (user location dependent)
- **Map Tile Cache**: 24 hours (geographic data stable)
- **Nearby Parks Cache**: 15 minutes (location and time sensitive)

### Success Criteria Checklist

#### Django Parity Verification
- [ ] Location-based search matches Django behavior exactly
- [ ] All geographic filters implemented and functional
- [ ] Distance calculations accurate within 1% of Django results
- [ ] Regional grouping works identically to Django
- [ ] Statistics display matches Django formatting

#### Screen-Agnostic Compliance
- [ ] Mobile layout optimized with GPS integration
- [ ] Tablet layout provides effective split-screen experience
- [ ] Desktop layout maximizes map and data visualization
- [ ] Large screen layout provides comprehensive dashboard
- [ ] All layouts handle location permissions gracefully

#### Performance Benchmarks
- [ ] Initial load under 500ms including location services
- [ ] GPS acquisition under 2 seconds
- [ ] Map rendering under 1 second
- [ ] Distance calculations under 100ms
- [ ] Regional caching reduces server load by 70%

#### Geographic Feature Completeness
- [ ] GPS location services work on all supported devices
- [ ] Distance calculations accurate across all coordinate systems
- [ ] Map integration functional on all screen sizes
- [ ] Regional filtering provides meaningful results
- [ ] Location search provides relevant autocomplete suggestions

This prompt ensures complete Django parity while adding location-aware enhancements that leverage modern browser capabilities and maintain ThrillWiki's screen-agnostic design principles.