Add comprehensive implementation prompts for Reviews and Rides listing pages with Django parity, Laravel/Livewire architecture, and screen-agnostic design principles

This commit is contained in:
pacnpal
2025-06-23 10:21:54 -04:00
parent ecf237d592
commit c2f3532469
9 changed files with 2995 additions and 183 deletions

View File

@@ -0,0 +1,551 @@
# 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.