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,624 @@
# Designers Listing Page Implementation Prompt
## Django Parity Reference
**Django Implementation**: `designers/views.py` - `DesignerListView` (similar patterns to companies views)
**Django Template**: `designers/templates/designers/designer_list.html`
**Django Features**: Creative portfolio showcases, design specialization filtering, innovation timeline display, collaboration networks, award recognition system
## Core Implementation Requirements
### Laravel/Livewire Architecture
Generate the designers listing system using ThrillWiki's custom generators:
```bash
# Generate main designers listing with creative portfolio support
php artisan make:thrillwiki-livewire DesignersListing --paginated --cached --with-tests
# Generate creative specialization filters
php artisan make:thrillwiki-livewire DesignersSpecializationFilter --reusable --with-tests
# Generate portfolio showcase component
php artisan make:thrillwiki-livewire DesignerPortfolioShowcase --reusable --with-tests
# Generate innovation timeline component
php artisan make:thrillwiki-livewire DesignerInnovationTimeline --reusable --cached
# Generate collaboration network visualization
php artisan make:thrillwiki-livewire DesignerCollaborationNetwork --reusable --with-tests
# Generate awards and recognition display
php artisan make:thrillwiki-livewire DesignerAwardsRecognition --reusable --cached
# Generate design influence analysis
php artisan make:thrillwiki-livewire DesignerInfluenceAnalysis --reusable --with-tests
```
### Django Parity Features
#### 1. Creative Portfolio Search Functionality
**Django Implementation**: Multi-faceted search across:
- Designer name (`name__icontains`)
- Design specialization (`specialization__icontains`)
- Notable innovations (`innovations__description__icontains`)
- Career highlights (`career_highlights__icontains`)
- Awards and recognition (`awards__title__icontains`)
- Collaboration partners (`collaborations__partner__name__icontains`)
**Laravel Implementation**:
```php
public function creativePortfolioSearch($query, $specializations = [])
{
return Designer::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('bio', 'ilike', "%{$term}%")
->orWhere('design_philosophy', 'ilike', "%{$term}%")
->orWhere('career_highlights', 'ilike', "%{$term}%")
->orWhereHas('designed_rides', function($rideQuery) use ($term) {
$rideQuery->where('name', 'ilike', "%{$term}%")
->orWhere('description', 'ilike', "%{$term}%");
})
->orWhereHas('awards', function($awardQuery) use ($term) {
$awardQuery->where('title', 'ilike', "%{$term}%")
->orWhere('description', 'ilike', "%{$term}%");
})
->orWhereHas('innovations', function($innQuery) use ($term) {
$innQuery->where('title', 'ilike', "%{$term}%")
->orWhere('description', 'ilike', "%{$term}%");
});
});
}
})
->when($specializations, function ($q) use ($specializations) {
$q->where(function ($specQuery) use ($specializations) {
foreach ($specializations as $spec) {
$specQuery->orWhereJsonContains('specializations', $spec);
}
});
})
->with([
'designed_rides' => fn($q) => $q->with(['park', 'photos'])->limit(5),
'awards' => fn($q) => $q->orderBy('year', 'desc')->limit(3),
'innovations' => fn($q) => $q->orderBy('year', 'desc')->limit(3),
'collaborations' => fn($q) => $q->with('partner')->limit(5)
])
->withCount(['designed_rides', 'awards', 'innovations', 'collaborations']);
}
```
#### 2. Advanced Creative Filtering
**Django Filters**:
- Design specialization (coaster_designer, dark_ride_specialist, theming_expert)
- Experience level (emerging, established, legendary)
- Innovation era (classic, modern, contemporary, cutting_edge)
- Career span (active_years range)
- Award categories (technical, artistic, lifetime_achievement)
- Collaboration type (solo_artist, team_player, cross_industry)
- Geographic influence (regional, national, international)
**Laravel Filters Implementation**:
```php
public function applyCreativeFilters($query, $filters)
{
return $query
->when($filters['specializations'] ?? null, function ($q, $specializations) {
$q->where(function ($specQuery) use ($specializations) {
foreach ($specializations as $spec) {
$specQuery->orWhereJsonContains('specializations', $spec);
}
});
})
->when($filters['experience_level'] ?? null, function ($q, $level) {
$experienceRanges = [
'emerging' => [0, 5],
'established' => [6, 15],
'veteran' => [16, 25],
'legendary' => [26, PHP_INT_MAX]
];
if (isset($experienceRanges[$level])) {
$q->whereRaw('EXTRACT(YEAR FROM NOW()) - career_start_year BETWEEN ? AND ?',
$experienceRanges[$level]);
}
})
->when($filters['innovation_era'] ?? null, function ($q, $era) {
$eraRanges = [
'classic' => [1950, 1979],
'modern' => [1980, 1999],
'contemporary' => [2000, 2009],
'cutting_edge' => [2010, date('Y')]
];
if (isset($eraRanges[$era])) {
$q->whereHas('innovations', function ($innQuery) use ($eraRanges, $era) {
$innQuery->whereBetween('year', $eraRanges[$era]);
});
}
})
->when($filters['career_start_from'] ?? null, fn($q, $year) =>
$q->where('career_start_year', '>=', $year))
->when($filters['career_start_to'] ?? null, fn($q, $year) =>
$q->where('career_start_year', '<=', $year))
->when($filters['award_categories'] ?? null, function ($q, $categories) {
$q->whereHas('awards', function ($awardQuery) use ($categories) {
$awardQuery->whereIn('category', $categories);
});
})
->when($filters['collaboration_style'] ?? null, function ($q, $style) {
switch ($style) {
case 'solo_artist':
$q->whereDoesntHave('collaborations');
break;
case 'team_player':
$q->whereHas('collaborations', fn($colQ) => $colQ->where('type', 'team'));
break;
case 'cross_industry':
$q->whereHas('collaborations', fn($colQ) => $colQ->where('type', 'cross_industry'));
break;
}
})
->when($filters['geographic_influence'] ?? null, function ($q, $influence) {
switch ($influence) {
case 'regional':
$q->whereHas('designed_rides', function ($rideQ) {
$rideQ->whereHas('park.location', function ($locQ) {
$locQ->havingRaw('COUNT(DISTINCT country) = 1');
});
});
break;
case 'international':
$q->whereHas('designed_rides', function ($rideQ) {
$rideQ->whereHas('park.location', function ($locQ) {
$locQ->havingRaw('COUNT(DISTINCT country) > 3');
});
});
break;
}
});
}
```
#### 3. Innovation Timeline and Portfolio Display
**Creative Metrics**:
- Notable ride designs and their impact
- Innovation timeline with breakthrough moments
- Awards and industry recognition
- Collaboration network and partnerships
- Design philosophy and artistic influence
- Career milestones and achievements
### Screen-Agnostic Design Implementation
#### Mobile Layout (320px - 767px)
- **Designer Cards**: Artist-focused cards with signature designs
- **Portfolio Highlights**: Visual showcase of most notable works
- **Innovation Badges**: Visual indicators of breakthrough innovations
- **Timeline Snapshots**: Condensed career timeline view
**Mobile Component Structure**:
```blade
<div class="designers-mobile-layout">
<!-- Creative Search Bar -->
<div class="sticky top-0 bg-white dark:bg-gray-900 z-20 p-4">
<livewire:designers-creative-search />
<div class="flex items-center mt-2 space-x-2">
<button wire:click="filterBySpecialization('coaster_designer')"
class="flex items-center space-x-1 px-3 py-1 {{ $activeSpec === 'coaster_designer' ? 'bg-red-500 text-white' : 'bg-red-100 dark:bg-red-900' }} rounded-full">
<span class="text-sm">Coasters</span>
</button>
<button wire:click="filterBySpecialization('dark_ride_specialist')"
class="flex items-center space-x-1 px-3 py-1 {{ $activeSpec === 'dark_ride_specialist' ? 'bg-purple-500 text-white' : 'bg-purple-100 dark:bg-purple-900' }} rounded-full">
<span class="text-sm">Dark Rides</span>
</button>
<button wire:click="filterBySpecialization('theming_expert')"
class="flex items-center space-x-1 px-3 py-1 {{ $activeSpec === 'theming_expert' ? 'bg-green-500 text-white' : 'bg-green-100 dark:bg-green-900' }} rounded-full">
<span class="text-sm">Theming</span>
</button>
</div>
</div>
<!-- Creative Inspiration Banner -->
<div class="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white p-4 m-4 rounded-lg">
<livewire:designers-inspiration-stats :compact="true" />
</div>
<!-- Quick Filters -->
<div class="horizontal-scroll p-4 pb-2">
<livewire:designers-quick-filters />
</div>
<!-- Designer Cards -->
<div class="space-y-4 p-4">
@foreach($designers as $designer)
<livewire:designer-mobile-card :designer="$designer" :show-portfolio="true" :key="$designer->id" />
@endforeach
</div>
<!-- Mobile Pagination -->
<div class="sticky bottom-0 bg-white dark:bg-gray-900 p-4">
{{ $designers->links('pagination.mobile') }}
</div>
</div>
```
#### Tablet Layout (768px - 1023px)
- **Portfolio Gallery**: Visual grid of signature designs
- **Innovation Timeline**: Interactive career progression
- **Collaboration Network**: Visual relationship mapping
- **Awards Showcase**: Comprehensive recognition display
**Tablet Component Structure**:
```blade
<div class="designers-tablet-layout flex h-screen">
<!-- Creative Filter Sidebar -->
<div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
<div class="p-6">
<livewire:designers-creative-search :advanced="true" />
<div class="mt-6">
<livewire:designers-specialization-filter :expanded="true" />
</div>
<div class="mt-6">
<livewire:designers-creative-filters :show-awards="true" />
</div>
<div class="mt-6">
<livewire:designers-inspiration-stats :detailed="true" />
</div>
</div>
</div>
<!-- Main Content Area -->
<div class="flex-1 flex flex-col">
<!-- Creative Header -->
<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">{{ $designers->total() }} Visionary Designers</h2>
<livewire:designers-industry-overview />
</div>
<div class="flex items-center space-x-2">
<livewire:designers-sort-selector />
<livewire:designers-view-toggle />
</div>
</div>
</div>
<!-- Content Display -->
<div class="flex-1 overflow-y-auto p-6">
@if($view === 'grid')
<div class="grid grid-cols-2 gap-6">
@foreach($designers as $designer)
<livewire:designer-tablet-card :designer="$designer" :portfolio="true" :key="$designer->id" />
@endforeach
</div>
@elseif($view === 'timeline')
<div class="space-y-8">
@foreach($designers as $designer)
<livewire:designer-timeline-showcase :designer="$designer" :key="$designer->id" />
@endforeach
</div>
@else
<livewire:designers-innovation-analysis :designers="$designers" />
@endif
<div class="mt-6">
{{ $designers->links() }}
</div>
</div>
</div>
</div>
```
#### Desktop Layout (1024px - 1919px)
- **Comprehensive Portfolio Views**: Detailed design showcases
- **Interactive Innovation Timeline**: Full career progression with milestones
- **Collaboration Network Visualization**: Complex relationship mapping
- **Creative Influence Analysis**: Industry impact visualization
**Desktop Component Structure**:
```blade
<div class="designers-desktop-layout flex h-screen">
<!-- Advanced Creative Filters -->
<div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
<div class="p-6">
<livewire:designers-creative-search :advanced="true" :autocomplete="true" />
<div class="mt-6">
<livewire:designers-specialization-filter :advanced="true" :show-statistics="true" />
</div>
<div class="mt-6">
<livewire:designers-creative-filters :advanced="true" :show-awards="true" />
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col">
<!-- Creative Dashboard 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">{{ $designers->total() }} Creative Visionaries</h1>
<livewire:designers-creative-summary />
</div>
<div class="flex items-center space-x-4">
<livewire:designers-sort-selector :advanced="true" />
<livewire:designers-view-selector />
<livewire:designers-export-options />
</div>
</div>
<livewire:designers-advanced-search />
</div>
<!-- Content Area -->
<div class="flex-1 overflow-y-auto">
@if($view === 'portfolio')
<div class="p-6">
<div class="grid grid-cols-3 xl:grid-cols-4 gap-6">
@foreach($designers as $designer)
<livewire:designer-desktop-card :designer="$designer" :comprehensive="true" :key="$designer->id" />
@endforeach
</div>
<div class="mt-8">
{{ $designers->links('pagination.desktop') }}
</div>
</div>
@elseif($view === 'timeline')
<div class="p-6 space-y-8">
@foreach($designers as $designer)
<livewire:designer-innovation-timeline :designer="$designer" :detailed="true" :key="$designer->id" />
@endforeach
</div>
@elseif($view === 'network')
<div class="p-6">
<livewire:designer-collaboration-network :designers="$designers" :interactive="true" />
</div>
@else
<div class="p-6">
<livewire:designers-creative-dashboard :designers="$designers" />
</div>
@endif
</div>
</div>
<!-- Creative Insights Panel -->
<div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
<div class="p-6">
<livewire:designers-creative-insights />
<div class="mt-6">
<livewire:designers-innovation-trends />
</div>
<div class="mt-6">
<livewire:designers-featured-works />
</div>
</div>
</div>
</div>
```
#### Large Screen Layout (1920px+)
- **Creative Studio Interface**: Comprehensive design analysis
- **Multi-Panel Innovation Views**: Simultaneous portfolio and timeline analysis
- **Advanced Visualization**: Creative influence networks and innovation patterns
- **Immersive Portfolio Experience**: Full-screen design showcases
### Performance Optimization Strategy
#### Creative Portfolio Caching
```php
public function mount()
{
$this->creativeStats = Cache::remember(
'designers.creative.stats',
now()->addHours(8),
fn() => $this->calculateCreativeStatistics()
);
$this->innovationTrends = Cache::remember(
'designers.innovation.trends',
now()->addHours(24),
fn() => $this->loadInnovationTrends()
);
}
public function getDesignersProperty()
{
$cacheKey = "designers.listing." . md5(serialize([
'search' => $this->search,
'filters' => $this->filters,
'specialization_filter' => $this->specializationFilter,
'sort' => $this->sort,
'page' => $this->page
]));
return Cache::remember($cacheKey, now()->addMinutes(45), function() {
return $this->creativePortfolioSearch($this->search, $this->specializationFilter)
->applyCreativeFilters($this->filters)
->orderBy($this->sort['column'], $this->sort['direction'])
->paginate(16);
});
}
```
#### Portfolio Media Optimization
```php
// Optimized query for portfolio and innovation data
public function optimizedPortfolioQuery()
{
return Designer::select([
'designers.*',
DB::raw('COALESCE(rides_count.count, 0) as designed_rides_count'),
DB::raw('COALESCE(awards_count.count, 0) as awards_count'),
DB::raw('COALESCE(innovations_count.count, 0) as innovations_count'),
DB::raw('CASE
WHEN EXTRACT(YEAR FROM NOW()) - career_start_year > 25 THEN "legendary"
WHEN EXTRACT(YEAR FROM NOW()) - career_start_year > 15 THEN "veteran"
WHEN EXTRACT(YEAR FROM NOW()) - career_start_year > 5 THEN "established"
ELSE "emerging"
END as experience_level_category')
])
->leftJoin(DB::raw('(SELECT designer_id, COUNT(*) as count FROM rides GROUP BY designer_id) as rides_count'),
'designers.id', '=', 'rides_count.designer_id')
->leftJoin(DB::raw('(SELECT designer_id, COUNT(*) as count FROM designer_awards GROUP BY designer_id) as awards_count'),
'designers.id', '=', 'awards_count.designer_id')
->leftJoin(DB::raw('(SELECT designer_id, COUNT(*) as count FROM designer_innovations GROUP BY designer_id) as innovations_count'),
'designers.id', '=', 'innovations_count.designer_id')
->with([
'designed_rides:id,designer_id,name,ride_type,opening_date',
'awards:id,designer_id,title,year,category',
'innovations:id,designer_id,title,year,description',
'collaborations' => fn($q) => $q->with('partner:id,name,type')
]);
}
```
### Component Reuse Strategy
#### Shared Components
- **`DesignersSpecializationFilter`**: Multi-specialization filtering with visual indicators
- **`DesignerPortfolioShowcase`**: Comprehensive portfolio display with media
- **`DesignerInnovationTimeline`**: Interactive career progression visualization
- **`DesignerCreativeMetrics`**: Portfolio statistics and creative impact metrics
#### Context Variations
- **`CoasterDesignersListing`**: Coaster designers with ride performance metrics
- **`ThemingExpertsListing`**: Theming specialists with environmental design focus
- **`DarkRideDesignersListing`**: Dark ride specialists with storytelling emphasis
- **`EmergingDesignersListing`**: New talent showcase with potential indicators
### Testing Requirements
#### Feature Tests
```php
/** @test */
public function can_filter_designers_by_specialization()
{
$coasterDesigner = Designer::factory()->create([
'name' => 'John Wardley',
'specializations' => ['coaster_designer', 'theming_expert']
]);
$coasterDesigner->designed_rides()->create(['name' => 'The Smiler', 'ride_type' => 'roller-coaster']);
$darkRideDesigner = Designer::factory()->create([
'name' => 'Tony Baxter',
'specializations' => ['dark_ride_specialist', 'imagineer']
]);
$darkRideDesigner->designed_rides()->create(['name' => 'Indiana Jones Adventure', 'ride_type' => 'dark-ride']);
Livewire::test(DesignersListing::class)
->set('specializationFilter', ['coaster_designer'])
->assertSee($coasterDesigner->name)
->assertDontSee($darkRideDesigner->name);
}
/** @test */
public function calculates_experience_level_correctly()
{
$legendary = Designer::factory()->create(['career_start_year' => 1985]);
$veteran = Designer::factory()->create(['career_start_year' => 2000]);
$established = Designer::factory()->create(['career_start_year' => 2010]);
$emerging = Designer::factory()->create(['career_start_year' => 2020]);
$component = Livewire::test(DesignersListing::class);
$designers = $component->get('designers');
$this->assertEquals('legendary', $designers->where('id', $legendary->id)->first()->experience_level_category);
$this->assertEquals('veteran', $designers->where('id', $veteran->id)->first()->experience_level_category);
}
/** @test */
public function maintains_django_parity_performance_with_portfolio_data()
{
Designer::factory()->count(40)->create();
$start = microtime(true);
Livewire::test(DesignersListing::class);
$end = microtime(true);
$this->assertLessThan(0.5, $end - $start); // < 500ms with portfolio data
}
```
#### Creative Portfolio Tests
```php
/** @test */
public function displays_portfolio_metrics_accurately()
{
$designer = Designer::factory()->create();
$designer->designed_rides()->createMany(8, ['name' => 'Test Ride']);
$designer->awards()->createMany(3, ['title' => 'Test Award']);
$designer->innovations()->createMany(2, ['title' => 'Test Innovation']);
$component = Livewire::test(DesignersListing::class);
$portfolioData = $component->get('designers')->first();
$this->assertEquals(8, $portfolioData->designed_rides_count);
$this->assertEquals(3, $portfolioData->awards_count);
$this->assertEquals(2, $portfolioData->innovations_count);
}
/** @test */
public function handles_collaboration_network_visualization()
{
$designer1 = Designer::factory()->create(['name' => 'Designer One']);
$designer2 = Designer::factory()->create(['name' => 'Designer Two']);
$designer1->collaborations()->create([
'partner_id' => $designer2->id,
'type' => 'team',
'project_name' => 'Joint Project'
]);
$component = Livewire::test(DesignersListing::class);
$collaborationData = $component->get('designers')->first()->collaborations;
$this->assertCount(1, $collaborationData);
$this->assertEquals('Joint Project', $collaborationData->first()->project_name);
}
```
### Performance Targets
#### Universal Performance Standards with Creative Content
- **Initial Load**: < 500ms (including portfolio thumbnails)
- **Portfolio Rendering**: < 300ms for 20 designers
- **Innovation Timeline**: < 200ms for complex career data
- **Collaboration Network**: < 1 second for network visualization
- **Creative Statistics**: < 150ms (cached)
#### Creative Content Caching Strategy
- **Innovation Trends**: 24 hours (industry trends stable)
- **Creative Statistics**: 8 hours (portfolio metrics change)
- **Portfolio Thumbnails**: 48 hours (visual content stable)
- **Designer Profiles**: 12 hours (career data relatively stable)
### Success Criteria Checklist
#### Django Parity Verification
- [ ] Creative portfolio search matches Django behavior exactly
- [ ] Specialization filtering provides same results as Django
- [ ] Innovation timeline displays identically to Django
- [ ] Awards and recognition match Django structure
- [ ] Collaboration networks visualize like Django implementation
#### Screen-Agnostic Compliance
- [ ] Mobile layout optimized for creative content consumption
- [ ] Tablet layout provides effective portfolio browsing
- [ ] Desktop layout maximizes creative visualization
- [ ] Large screen layout provides immersive portfolio experience
- [ ] All layouts handle rich media content gracefully
#### Performance Benchmarks
- [ ] Initial load under 500ms including portfolio media
- [ ] Portfolio rendering under 300ms
- [ ] Innovation timeline under 200ms
- [ ] Creative statistics under 150ms (cached)
- [ ] Portfolio caching reduces server load by 65%
#### Creative Feature Completeness
- [ ] Specialization filtering works across all design disciplines
- [ ] Portfolio showcases provide comprehensive creative overviews
- [ ] Innovation timelines visualize career progression accurately
- [ ] Collaboration networks display meaningful relationships
- [ ] Awards and recognition systems provide proper attribution
This prompt ensures complete Django parity while providing comprehensive creative portfolio capabilities that showcase designer talent and innovation while maintaining ThrillWiki's screen-agnostic design principles.

View File

@@ -0,0 +1,596 @@
# Operators Listing Page Implementation Prompt
## Django Parity Reference
**Django Implementation**: `companies/views.py` - `CompanyListView` & `ManufacturerListView` (lines 62-126)
**Django Template**: `companies/templates/companies/company_list.html`
**Django Features**: Dual-role filtering (park operators vs ride manufacturers), industry statistics, portfolio showcases, corporate hierarchy display, market analysis
## Core Implementation Requirements
### Laravel/Livewire Architecture
Generate the operators listing system using ThrillWiki's custom generators:
```bash
# Generate unified operators listing with dual-role support
php artisan make:thrillwiki-livewire OperatorsListing --paginated --cached --with-tests
# Generate role-specific filtering component
php artisan make:thrillwiki-livewire OperatorsRoleFilter --reusable --with-tests
# Generate portfolio showcase component
php artisan make:thrillwiki-livewire OperatorPortfolioCard --reusable --with-tests
# Generate industry statistics dashboard
php artisan make:thrillwiki-livewire OperatorsIndustryStats --reusable --cached
# Generate corporate hierarchy visualization
php artisan make:thrillwiki-livewire OperatorHierarchyView --reusable --with-tests
# Generate market analysis component
php artisan make:thrillwiki-livewire OperatorsMarketAnalysis --reusable --cached
```
### Django Parity Features
#### 1. Dual-Role Search Functionality
**Django Implementation**: Multi-role search across:
- Operator name (`name__icontains`)
- Company description (`description__icontains`)
- Founded year range (`founded_year__range`)
- Headquarters location (`headquarters__city__icontains`)
- Role-specific filtering (park_operator, ride_manufacturer, or both)
- Industry sector (`industry_sector__icontains`)
**Laravel Implementation**:
```php
public function dualRoleSearch($query, $roles = [])
{
return Operator::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('industry_sector', 'ilike', "%{$term}%")
->orWhereHas('location', function($locQuery) use ($term) {
$locQuery->where('city', 'ilike', "%{$term}%")
->orWhere('state', 'ilike', "%{$term}%")
->orWhere('country', 'ilike', "%{$term}%");
});
});
}
})
->when($roles, function ($q) use ($roles) {
$q->where(function ($roleQuery) use ($roles) {
if (in_array('park_operator', $roles)) {
$roleQuery->whereExists(function ($exists) {
$exists->select(DB::raw(1))
->from('parks')
->whereRaw('parks.operator_id = operators.id');
});
}
if (in_array('ride_manufacturer', $roles)) {
$roleQuery->orWhereExists(function ($exists) {
$exists->select(DB::raw(1))
->from('rides')
->whereRaw('rides.manufacturer_id = operators.id');
});
}
if (in_array('ride_designer', $roles)) {
$roleQuery->orWhereExists(function ($exists) {
$exists->select(DB::raw(1))
->from('rides')
->whereRaw('rides.designer_id = operators.id');
});
}
});
})
->with(['location', 'parks', 'manufactured_rides', 'designed_rides'])
->withCount(['parks', 'manufactured_rides', 'designed_rides']);
}
```
#### 2. Advanced Industry Filtering
**Django Filters**:
- Role type (park_operator, manufacturer, designer, mixed)
- Industry sector (entertainment, manufacturing, technology)
- Company size (small, medium, large, enterprise)
- Founded year range
- Geographic presence (regional, national, international)
- Market capitalization range
- Annual revenue range
**Laravel Filters Implementation**:
```php
public function applyIndustryFilters($query, $filters)
{
return $query
->when($filters['role_type'] ?? null, function ($q, $roleType) {
switch ($roleType) {
case 'park_operator_only':
$q->whereHas('parks')->whereDoesntHave('manufactured_rides');
break;
case 'manufacturer_only':
$q->whereHas('manufactured_rides')->whereDoesntHave('parks');
break;
case 'mixed':
$q->whereHas('parks')->whereHas('manufactured_rides');
break;
case 'designer':
$q->whereHas('designed_rides');
break;
}
})
->when($filters['industry_sector'] ?? null, fn($q, $sector) =>
$q->where('industry_sector', $sector))
->when($filters['company_size'] ?? null, function ($q, $size) {
$ranges = [
'small' => [1, 100],
'medium' => [101, 1000],
'large' => [1001, 10000],
'enterprise' => [10001, PHP_INT_MAX]
];
if (isset($ranges[$size])) {
$q->whereBetween('employee_count', $ranges[$size]);
}
})
->when($filters['founded_year_from'] ?? null, fn($q, $year) =>
$q->where('founded_year', '>=', $year))
->when($filters['founded_year_to'] ?? null, fn($q, $year) =>
$q->where('founded_year', '<=', $year))
->when($filters['geographic_presence'] ?? null, function ($q, $presence) {
switch ($presence) {
case 'regional':
$q->whereHas('parks', function ($parkQ) {
$parkQ->whereHas('location', function ($locQ) {
$locQ->havingRaw('COUNT(DISTINCT country) = 1');
});
});
break;
case 'international':
$q->whereHas('parks', function ($parkQ) {
$parkQ->whereHas('location', function ($locQ) {
$locQ->havingRaw('COUNT(DISTINCT country) > 1');
});
});
break;
}
})
->when($filters['min_revenue'] ?? null, fn($q, $revenue) =>
$q->where('annual_revenue', '>=', $revenue))
->when($filters['max_revenue'] ?? null, fn($q, $revenue) =>
$q->where('annual_revenue', '<=', $revenue));
}
```
#### 3. Portfolio and Statistics Display
**Portfolio Metrics**:
- Total parks operated
- Total rides manufactured/designed
- Geographic reach (countries, continents)
- Market share analysis
- Revenue and financial metrics
- Industry influence score
### Screen-Agnostic Design Implementation
#### Mobile Layout (320px - 767px)
- **Corporate Cards**: Compact operator cards with key metrics
- **Role Badges**: Visual indicators for operator/manufacturer/designer roles
- **Portfolio Highlights**: Key statistics prominently displayed
- **Industry Filters**: Simplified filtering for mobile users
**Mobile Component Structure**:
```blade
<div class="operators-mobile-layout">
<!-- Industry Search Bar -->
<div class="sticky top-0 bg-white dark:bg-gray-900 z-20 p-4">
<livewire:operators-industry-search />
<div class="flex items-center mt-2 space-x-2">
<button wire:click="filterByRole('park_operator')"
class="flex items-center space-x-1 px-3 py-1 {{ $activeRole === 'park_operator' ? 'bg-blue-500 text-white' : 'bg-blue-100 dark:bg-blue-900' }} rounded-full">
<span class="text-sm">Operators</span>
</button>
<button wire:click="filterByRole('manufacturer')"
class="flex items-center space-x-1 px-3 py-1 {{ $activeRole === 'manufacturer' ? 'bg-green-500 text-white' : 'bg-green-100 dark:bg-green-900' }} rounded-full">
<span class="text-sm">Manufacturers</span>
</button>
</div>
</div>
<!-- Industry Statistics Banner -->
<div class="bg-gradient-to-r from-blue-500 to-purple-600 text-white p-4 m-4 rounded-lg">
<livewire:operators-industry-stats :compact="true" />
</div>
<!-- Quick Filters -->
<div class="horizontal-scroll p-4 pb-2">
<livewire:operators-quick-filters />
</div>
<!-- Operator Cards -->
<div class="space-y-4 p-4">
@foreach($operators as $operator)
<livewire:operator-mobile-card :operator="$operator" :show-portfolio="true" :key="$operator->id" />
@endforeach
</div>
<!-- Mobile Pagination -->
<div class="sticky bottom-0 bg-white dark:bg-gray-900 p-4">
{{ $operators->links('pagination.mobile') }}
</div>
</div>
```
#### Tablet Layout (768px - 1023px)
- **Dual-Pane Layout**: Filter sidebar + operator grid
- **Portfolio Showcases**: Detailed portfolio cards for each operator
- **Industry Dashboard**: Real-time industry statistics and trends
- **Comparison Mode**: Side-by-side operator comparisons
**Tablet Component Structure**:
```blade
<div class="operators-tablet-layout flex h-screen">
<!-- Industry Filter Sidebar -->
<div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
<div class="p-6">
<livewire:operators-industry-search :advanced="true" />
<div class="mt-6">
<livewire:operators-role-filter :expanded="true" />
</div>
<div class="mt-6">
<livewire:operators-industry-filters :show-financial="true" />
</div>
<div class="mt-6">
<livewire:operators-industry-stats :detailed="true" />
</div>
</div>
</div>
<!-- Main Content Area -->
<div class="flex-1 flex flex-col">
<!-- Industry Header -->
<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">{{ $operators->total() }} Industry Leaders</h2>
<livewire:operators-market-overview />
</div>
<div class="flex items-center space-x-2">
<livewire:operators-sort-selector />
<livewire:operators-view-toggle />
</div>
</div>
</div>
<!-- Content Grid -->
<div class="flex-1 overflow-y-auto p-6">
@if($view === 'grid')
<div class="grid grid-cols-2 gap-6">
@foreach($operators as $operator)
<livewire:operator-tablet-card :operator="$operator" :detailed="true" :key="$operator->id" />
@endforeach
</div>
@elseif($view === 'portfolio')
<div class="space-y-6">
@foreach($operators as $operator)
<livewire:operator-portfolio-showcase :operator="$operator" :key="$operator->id" />
@endforeach
</div>
@else
<livewire:operators-market-analysis :operators="$operators" />
@endif
<div class="mt-6">
{{ $operators->links() }}
</div>
</div>
</div>
</div>
```
#### Desktop Layout (1024px - 1919px)
- **Three-Pane Layout**: Filters + main content + industry insights
- **Advanced Analytics**: Market share analysis and industry trends
- **Corporate Hierarchies**: Visual representation of corporate structures
- **Portfolio Deep Dives**: Comprehensive portfolio analysis
**Desktop Component Structure**:
```blade
<div class="operators-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:operators-industry-search :advanced="true" :autocomplete="true" />
<div class="mt-6">
<livewire:operators-role-filter :advanced="true" :show-statistics="true" />
</div>
<div class="mt-6">
<livewire:operators-industry-filters :advanced="true" :show-financial="true" />
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col">
<!-- Industry Dashboard 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">{{ $operators->total() }} Industry Operators</h1>
<livewire:operators-market-summary />
</div>
<div class="flex items-center space-x-4">
<livewire:operators-sort-selector :advanced="true" />
<livewire:operators-view-selector />
<livewire:operators-export-options />
</div>
</div>
<livewire:operators-advanced-search />
</div>
<!-- Content Area -->
<div class="flex-1 overflow-y-auto">
@if($view === 'grid')
<div class="p-6">
<div class="grid grid-cols-3 xl:grid-cols-4 gap-6">
@foreach($operators as $operator)
<livewire:operator-desktop-card :operator="$operator" :comprehensive="true" :key="$operator->id" />
@endforeach
</div>
<div class="mt-8">
{{ $operators->links('pagination.desktop') }}
</div>
</div>
@elseif($view === 'portfolio')
<div class="p-6 space-y-8">
@foreach($operators as $operator)
<livewire:operator-portfolio-detailed :operator="$operator" :key="$operator->id" />
@endforeach
</div>
@elseif($view === 'hierarchy')
<div class="p-6">
<livewire:operators-hierarchy-visualization :operators="$operators" />
</div>
@else
<div class="p-6">
<livewire:operators-market-dashboard :operators="$operators" />
</div>
@endif
</div>
</div>
<!-- Industry Insights Panel -->
<div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
<div class="p-6">
<livewire:operators-industry-insights />
<div class="mt-6">
<livewire:operators-market-trends />
</div>
<div class="mt-6">
<livewire:operators-recent-activity />
</div>
</div>
</div>
</div>
```
#### Large Screen Layout (1920px+)
- **Dashboard-Style Interface**: Comprehensive industry analytics
- **Multi-Panel Views**: Simultaneous portfolio and market analysis
- **Advanced Visualizations**: Corporate network maps and market dynamics
- **Real-Time Market Data**: Live industry statistics and trends
### Performance Optimization Strategy
#### Industry-Specific Caching
```php
public function mount()
{
$this->industryStats = Cache::remember(
'operators.industry.stats',
now()->addHours(6),
fn() => $this->calculateIndustryStatistics()
);
$this->marketData = Cache::remember(
'operators.market.data',
now()->addHours(12),
fn() => $this->loadMarketAnalysis()
);
}
public function getOperatorsProperty()
{
$cacheKey = "operators.listing." . md5(serialize([
'search' => $this->search,
'filters' => $this->filters,
'role_filter' => $this->roleFilter,
'sort' => $this->sort,
'page' => $this->page
]));
return Cache::remember($cacheKey, now()->addMinutes(30), function() {
return $this->dualRoleSearch($this->search, $this->roleFilter)
->applyIndustryFilters($this->filters)
->orderBy($this->sort['column'], $this->sort['direction'])
->paginate(20);
});
}
```
#### Financial Data Optimization
```php
// Optimized query for financial and portfolio data
public function optimizedFinancialQuery()
{
return Operator::select([
'operators.*',
DB::raw('COALESCE(parks_count.count, 0) as parks_count'),
DB::raw('COALESCE(rides_count.count, 0) as manufactured_rides_count'),
DB::raw('COALESCE(designed_rides_count.count, 0) as designed_rides_count'),
DB::raw('CASE
WHEN annual_revenue > 10000000000 THEN "enterprise"
WHEN annual_revenue > 1000000000 THEN "large"
WHEN annual_revenue > 100000000 THEN "medium"
ELSE "small"
END as company_size_category')
])
->leftJoin(DB::raw('(SELECT operator_id, COUNT(*) as count FROM parks GROUP BY operator_id) as parks_count'),
'operators.id', '=', 'parks_count.operator_id')
->leftJoin(DB::raw('(SELECT manufacturer_id, COUNT(*) as count FROM rides GROUP BY manufacturer_id) as rides_count'),
'operators.id', '=', 'rides_count.manufacturer_id')
->leftJoin(DB::raw('(SELECT designer_id, COUNT(*) as count FROM rides GROUP BY designer_id) as designed_rides_count'),
'operators.id', '=', 'designed_rides_count.designer_id')
->with([
'location:id,city,state,country',
'parks:id,operator_id,name,opening_date',
'manufactured_rides:id,manufacturer_id,name,ride_type',
'designed_rides:id,designer_id,name,ride_type'
]);
}
```
### Component Reuse Strategy
#### Shared Components
- **`OperatorsRoleFilter`**: Multi-role filtering with statistics
- **`OperatorPortfolioCard`**: Comprehensive portfolio display
- **`OperatorsIndustryStats`**: Real-time industry analytics
- **`OperatorFinancialMetrics`**: Financial performance indicators
#### Context Variations
- **`ParkOperatorsListing`**: Park operators only with park portfolios
- **`ManufacturersListing`**: Ride manufacturers with product catalogs
- **`DesignersListing`**: Ride designers with design portfolios
- **`CorporateGroupsListing`**: Corporate hierarchies and subsidiaries
### Testing Requirements
#### Feature Tests
```php
/** @test */
public function can_filter_operators_by_dual_roles()
{
$pureOperator = Operator::factory()->create(['name' => 'Disney Parks']);
$pureOperator->parks()->create(['name' => 'Magic Kingdom']);
$pureManufacturer = Operator::factory()->create(['name' => 'Intamin']);
$pureManufacturer->manufactured_rides()->create(['name' => 'Millennium Force']);
$mixedOperator = Operator::factory()->create(['name' => 'Universal']);
$mixedOperator->parks()->create(['name' => 'Universal Studios']);
$mixedOperator->manufactured_rides()->create(['name' => 'Custom Ride']);
Livewire::test(OperatorsListing::class)
->set('roleFilter', ['park_operator'])
->assertSee($pureOperator->name)
->assertSee($mixedOperator->name)
->assertDontSee($pureManufacturer->name);
}
/** @test */
public function calculates_industry_statistics_correctly()
{
Operator::factory()->count(10)->create(['industry_sector' => 'entertainment']);
Operator::factory()->count(5)->create(['industry_sector' => 'manufacturing']);
$component = Livewire::test(OperatorsListing::class);
$stats = $component->get('industryStats');
$this->assertEquals(15, $stats['total_operators']);
$this->assertEquals(10, $stats['entertainment_operators']);
$this->assertEquals(5, $stats['manufacturing_operators']);
}
/** @test */
public function maintains_django_parity_performance_with_portfolio_data()
{
Operator::factory()->count(50)->create();
$start = microtime(true);
Livewire::test(OperatorsListing::class);
$end = microtime(true);
$this->assertLessThan(0.5, $end - $start); // < 500ms with portfolio data
}
```
#### Financial Data Tests
```php
/** @test */
public function categorizes_company_size_correctly()
{
$enterprise = Operator::factory()->create(['annual_revenue' => 15000000000]);
$large = Operator::factory()->create(['annual_revenue' => 5000000000]);
$medium = Operator::factory()->create(['annual_revenue' => 500000000]);
$small = Operator::factory()->create(['annual_revenue' => 50000000]);
Livewire::test(OperatorsListing::class)
->set('filters.company_size', 'enterprise')
->assertSee($enterprise->name)
->assertDontSee($large->name);
}
/** @test */
public function handles_portfolio_metrics_calculation()
{
$operator = Operator::factory()->create();
$operator->parks()->createMany(3, ['name' => 'Test Park']);
$operator->manufactured_rides()->createMany(5, ['name' => 'Test Ride']);
$component = Livewire::test(OperatorsListing::class);
$portfolioData = $component->get('operators')->first();
$this->assertEquals(3, $portfolioData->parks_count);
$this->assertEquals(5, $portfolioData->manufactured_rides_count);
}
```
### Performance Targets
#### Universal Performance Standards with Financial Data
- **Initial Load**: < 500ms (including industry statistics)
- **Portfolio Calculation**: < 200ms for 100 operators
- **Financial Filtering**: < 150ms with complex criteria
- **Market Analysis**: < 1 second for trend calculations
- **Industry Statistics**: < 100ms (cached)
#### Industry-Specific Caching Strategy
- **Market Data Cache**: 12 hours (financial markets change)
- **Industry Statistics**: 6 hours (relatively stable)
- **Portfolio Metrics**: 1 hour (operational data)
- **Company Profiles**: 24 hours (corporate data stable)
### Success Criteria Checklist
#### Django Parity Verification
- [ ] Dual-role filtering matches Django behavior exactly
- [ ] Industry statistics calculated identically to Django
- [ ] Portfolio metrics match Django calculations
- [ ] Financial filtering provides same results as Django
- [ ] Corporate hierarchy display matches Django structure
#### Screen-Agnostic Compliance
- [ ] Mobile layout optimized for corporate data consumption
- [ ] Tablet layout provides effective portfolio comparisons
- [ ] Desktop layout maximizes industry analytics
- [ ] Large screen layout provides comprehensive market view
- [ ] All layouts handle complex financial data gracefully
#### Performance Benchmarks
- [ ] Initial load under 500ms including portfolio data
- [ ] Financial calculations under 200ms
- [ ] Industry statistics under 100ms (cached)
- [ ] Market analysis under 1 second
- [ ] Portfolio caching reduces server load by 60%
#### Industry Feature Completeness
- [ ] Dual-role filtering works across all operator types
- [ ] Financial metrics display accurately
- [ ] Portfolio showcases provide comprehensive overviews
- [ ] Market analysis provides meaningful insights
- [ ] Corporate hierarchies visualize relationships correctly
This prompt ensures complete Django parity while providing comprehensive industry analysis capabilities that leverage modern data visualization and maintain ThrillWiki's screen-agnostic design principles.

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.

View File

@@ -0,0 +1,629 @@
# Reviews Listing Page Implementation Prompt
## Django Parity Reference
**Django Implementation**: `reviews/views.py` - `ReviewListView` (similar patterns to other listing views)
**Django Template**: `reviews/templates/reviews/review_list.html`
**Django Features**: Social interaction display, sentiment analysis, review verification, context-aware filtering, real-time engagement metrics
## Core Implementation Requirements
### Laravel/Livewire Architecture
Generate the reviews listing system using ThrillWiki's custom generators:
```bash
# Generate main reviews listing with social interaction support
php artisan make:thrillwiki-livewire ReviewsListing --paginated --cached --with-tests
# Generate social interaction components
php artisan make:thrillwiki-livewire ReviewSocialInteractions --reusable --with-tests
# Generate sentiment analysis display
php artisan make:thrillwiki-livewire ReviewSentimentAnalysis --reusable --cached
# Generate review verification system
php artisan make:thrillwiki-livewire ReviewVerificationBadges --reusable --with-tests
# Generate context-aware filters
php artisan make:thrillwiki-livewire ReviewsContextFilters --reusable --cached
# Generate real-time engagement metrics
php artisan make:thrillwiki-livewire ReviewEngagementMetrics --reusable --with-tests
# Generate review quality indicators
php artisan make:thrillwiki-livewire ReviewQualityIndicators --reusable --cached
# Generate user credibility system
php artisan make:thrillwiki-livewire UserCredibilityBadges --reusable --with-tests
```
### Django Parity Features
#### 1. Social Review Search Functionality
**Django Implementation**: Multi-faceted search across:
- Review content (`content__icontains`)
- Reviewer username (`user__username__icontains`)
- Reviewable entity (`reviewable__name__icontains`)
- Review tags (`tags__name__icontains`)
- Experience context (`experience_context__icontains`)
- Visit verification status (`verified_visit`)
**Laravel Implementation**:
```php
public function socialReviewSearch($query, $context = 'all')
{
return Review::query()
->when($query, function ($q) use ($query) {
$terms = explode(' ', $query);
foreach ($terms as $term) {
$q->where(function ($subQuery) use ($term) {
$subQuery->where('content', 'ilike', "%{$term}%")
->orWhere('title', 'ilike', "%{$term}%")
->orWhere('experience_context', 'ilike', "%{$term}%")
->orWhereHas('user', function($userQuery) use ($term) {
$userQuery->where('username', 'ilike', "%{$term}%")
->orWhere('display_name', 'ilike', "%{$term}%");
})
->orWhereHas('reviewable', function($entityQuery) use ($term) {
$entityQuery->where('name', 'ilike', "%{$term}%");
})
->orWhereHas('tags', function($tagQuery) use ($term) {
$tagQuery->where('name', 'ilike', "%{$term}%");
});
});
}
})
->when($context !== 'all', function ($q) use ($context) {
$q->where('reviewable_type', $this->getModelClass($context));
})
->with([
'user' => fn($q) => $q->with(['profile', 'credibilityBadges']),
'reviewable',
'likes' => fn($q) => $q->with('user:id,username'),
'comments' => fn($q) => $q->with('user:id,username')->limit(3),
'tags',
'verificationBadges'
])
->withCount(['likes', 'dislikes', 'comments', 'shares'])
->addSelect([
'engagement_score' => DB::raw('(likes_count * 2 + comments_count * 3 + shares_count * 4)')
]);
}
```
#### 2. Advanced Social Filtering
**Django Filters**:
- Review rating (1-5 stars)
- Verification status (verified, unverified, disputed)
- Sentiment analysis (positive, neutral, negative)
- Social engagement level (high, medium, low)
- Review recency (last_day, last_week, last_month, last_year)
- User credibility level (expert, trusted, verified, new)
- Review context (solo_visit, group_visit, family_visit, enthusiast_visit)
- Review completeness (photos, detailed, brief)
**Laravel Filters Implementation**:
```php
public function applySocialFilters($query, $filters)
{
return $query
->when($filters['rating_range'] ?? null, function ($q, $range) {
[$min, $max] = explode('-', $range);
$q->whereBetween('rating', [$min, $max]);
})
->when($filters['verification_status'] ?? null, function ($q, $status) {
switch ($status) {
case 'verified':
$q->where('verified_visit', true);
break;
case 'unverified':
$q->where('verified_visit', false);
break;
case 'disputed':
$q->where('verification_disputed', true);
break;
}
})
->when($filters['sentiment'] ?? null, function ($q, $sentiment) {
$sentimentRanges = [
'positive' => [0.6, 1.0],
'neutral' => [0.4, 0.6],
'negative' => [0.0, 0.4]
];
if (isset($sentimentRanges[$sentiment])) {
$q->whereBetween('sentiment_score', $sentimentRanges[$sentiment]);
}
})
->when($filters['engagement_level'] ?? null, function ($q, $level) {
$engagementThresholds = [
'high' => 20,
'medium' => 5,
'low' => 0
];
if (isset($engagementThresholds[$level])) {
$q->havingRaw('(likes_count + comments_count + shares_count) >= ?',
[$engagementThresholds[$level]]);
}
})
->when($filters['recency'] ?? null, function ($q, $recency) {
$timeRanges = [
'last_day' => now()->subDay(),
'last_week' => now()->subWeek(),
'last_month' => now()->subMonth(),
'last_year' => now()->subYear()
];
if (isset($timeRanges[$recency])) {
$q->where('created_at', '>=', $timeRanges[$recency]);
}
})
->when($filters['user_credibility'] ?? null, function ($q, $credibility) {
$q->whereHas('user', function ($userQuery) use ($credibility) {
switch ($credibility) {
case 'expert':
$userQuery->whereHas('credibilityBadges', fn($badge) =>
$badge->where('type', 'expert'));
break;
case 'trusted':
$userQuery->where('trust_score', '>=', 80);
break;
case 'verified':
$userQuery->whereNotNull('email_verified_at');
break;
case 'new':
$userQuery->where('created_at', '>=', now()->subMonths(3));
break;
}
});
})
->when($filters['review_context'] ?? null, function ($q, $context) {
$q->where('visit_context', $context);
})
->when($filters['completeness'] ?? null, function ($q, $completeness) {
switch ($completeness) {
case 'photos':
$q->whereHas('photos');
break;
case 'detailed':
$q->whereRaw('LENGTH(content) > 500');
break;
case 'brief':
$q->whereRaw('LENGTH(content) <= 200');
break;
}
});
}
```
#### 3. Real-Time Social Engagement Display
**Social Metrics**:
- Like/dislike counts with user attribution
- Comment threads with nested replies
- Share counts across platforms
- User credibility and verification badges
- Sentiment analysis visualization
- Engagement trend tracking
### Screen-Agnostic Design Implementation
#### Mobile Layout (320px - 767px)
- **Social Review Cards**: Compact cards with engagement metrics
- **Touch Interactions**: Swipe-to-like, pull-to-refresh, tap interactions
- **Social Actions**: Prominent like/comment/share buttons
- **User Attribution**: Clear reviewer identification with badges
**Mobile Component Structure**:
```blade
<div class="reviews-mobile-layout">
<!-- Social Search Bar -->
<div class="sticky top-0 bg-white dark:bg-gray-900 z-20 p-4">
<livewire:reviews-social-search />
<div class="flex items-center mt-2 space-x-2">
<button wire:click="filterByContext('park')"
class="flex items-center space-x-1 px-3 py-1 {{ $activeContext === 'park' ? 'bg-blue-500 text-white' : 'bg-blue-100 dark:bg-blue-900' }} rounded-full">
<span class="text-sm">Parks</span>
</button>
<button wire:click="filterByContext('ride')"
class="flex items-center space-x-1 px-3 py-1 {{ $activeContext === 'ride' ? 'bg-green-500 text-white' : 'bg-green-100 dark:bg-green-900' }} rounded-full">
<span class="text-sm">Rides</span>
</button>
<button wire:click="toggleVerifiedOnly"
class="flex items-center space-x-1 px-2 py-1 {{ $verifiedOnly ? 'bg-orange-500 text-white' : 'bg-orange-100 dark:bg-orange-900' }} rounded-full">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<span class="text-xs">Verified</span>
</button>
</div>
</div>
<!-- Community Engagement Banner -->
<div class="bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white p-4 m-4 rounded-lg">
<livewire:reviews-community-stats :compact="true" />
</div>
<!-- Quick Filters -->
<div class="horizontal-scroll p-4 pb-2">
<livewire:reviews-quick-filters />
</div>
<!-- Review Cards -->
<div class="space-y-4 p-4">
@foreach($reviews as $review)
<livewire:review-mobile-card :review="$review" :show-social="true" :key="$review->id" />
@endforeach
</div>
<!-- Mobile Pagination -->
<div class="sticky bottom-0 bg-white dark:bg-gray-900 p-4">
{{ $reviews->links('pagination.mobile') }}
</div>
</div>
```
#### Tablet Layout (768px - 1023px)
- **Social Stream Layout**: Two-column review stream with engagement sidebar
- **Interactive Comments**: Expandable comment threads
- **Multi-Touch Gestures**: Pinch-to-zoom on photos, swipe between reviews
- **Social Activity Feed**: Real-time updates on review interactions
**Tablet Component Structure**:
```blade
<div class="reviews-tablet-layout flex h-screen">
<!-- Social Filter Sidebar -->
<div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
<div class="p-6">
<livewire:reviews-social-search :advanced="true" />
<div class="mt-6">
<livewire:reviews-context-filters :expanded="true" />
</div>
<div class="mt-6">
<livewire:reviews-social-filters :show-engagement="true" />
</div>
<div class="mt-6">
<livewire:reviews-community-stats :detailed="true" />
</div>
</div>
</div>
<!-- Main Content Area -->
<div class="flex-1 flex flex-col">
<!-- Social Header -->
<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">{{ $reviews->total() }} Community Reviews</h2>
<livewire:reviews-engagement-overview />
</div>
<div class="flex items-center space-x-2">
<livewire:reviews-sort-selector />
<livewire:reviews-view-toggle />
</div>
</div>
</div>
<!-- Content Stream -->
<div class="flex-1 overflow-y-auto p-6">
@if($view === 'stream')
<div class="space-y-6">
@foreach($reviews as $review)
<livewire:review-tablet-card :review="$review" :interactive="true" :key="$review->id" />
@endforeach
</div>
@elseif($view === 'sentiment')
<livewire:reviews-sentiment-analysis :reviews="$reviews" />
@else
<livewire:reviews-engagement-dashboard :reviews="$reviews" />
@endif
<div class="mt-6">
{{ $reviews->links() }}
</div>
</div>
</div>
</div>
```
#### Desktop Layout (1024px - 1919px)
- **Three-Pane Social Layout**: Filters + reviews + activity feed
- **Advanced Social Features**: Real-time notifications, user following
- **Rich Interaction**: Hover states, contextual menus, drag-and-drop
- **Community Moderation**: Flagging, reporting, and moderation tools
**Desktop Component Structure**:
```blade
<div class="reviews-desktop-layout flex h-screen">
<!-- Advanced Social Filters -->
<div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
<div class="p-6">
<livewire:reviews-social-search :advanced="true" :autocomplete="true" />
<div class="mt-6">
<livewire:reviews-context-filters :advanced="true" :show-statistics="true" />
</div>
<div class="mt-6">
<livewire:reviews-social-filters :advanced="true" :show-engagement="true" />
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col">
<!-- Social Dashboard 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">{{ $reviews->total() }} Community Reviews</h1>
<livewire:reviews-social-summary />
</div>
<div class="flex items-center space-x-4">
<livewire:reviews-sort-selector :advanced="true" />
<livewire:reviews-view-selector />
<livewire:reviews-moderation-tools />
</div>
</div>
<livewire:reviews-advanced-search />
</div>
<!-- Content Area -->
<div class="flex-1 overflow-y-auto">
@if($view === 'feed')
<div class="p-6 space-y-6">
@foreach($reviews as $review)
<livewire:review-desktop-card :review="$review" :comprehensive="true" :key="$review->id" />
@endforeach
<div class="mt-8">
{{ $reviews->links('pagination.desktop') }}
</div>
</div>
@elseif($view === 'sentiment')
<div class="p-6">
<livewire:reviews-sentiment-dashboard :reviews="$reviews" :interactive="true" />
</div>
@elseif($view === 'moderation')
<div class="p-6">
<livewire:reviews-moderation-dashboard :reviews="$reviews" />
</div>
@else
<div class="p-6">
<livewire:reviews-social-analytics :reviews="$reviews" />
</div>
@endif
</div>
</div>
<!-- Social Activity Panel -->
<div class="w-80 bg-gray-50 dark:bg-gray-800 overflow-y-auto">
<div class="p-6">
<livewire:reviews-social-activity />
<div class="mt-6">
<livewire:reviews-trending-topics />
</div>
<div class="mt-6">
<livewire:reviews-featured-reviewers />
</div>
</div>
</div>
</div>
```
#### Large Screen Layout (1920px+)
- **Dashboard-Style Social Interface**: Comprehensive community analytics
- **Multi-Panel Views**: Simultaneous review streams and analytics
- **Advanced Visualizations**: Sentiment analysis charts and engagement networks
- **Community Management**: Advanced moderation and user management tools
### Performance Optimization Strategy
#### Social Engagement Caching
```php
public function mount()
{
$this->socialStats = Cache::remember(
'reviews.social.stats',
now()->addMinutes(15),
fn() => $this->calculateSocialStatistics()
);
$this->trendingTopics = Cache::remember(
'reviews.trending.topics',
now()->addHours(1),
fn() => $this->loadTrendingTopics()
);
}
public function getReviewsProperty()
{
$cacheKey = "reviews.listing." . md5(serialize([
'search' => $this->search,
'filters' => $this->filters,
'context_filter' => $this->contextFilter,
'sort' => $this->sort,
'page' => $this->page,
'user_id' => auth()->id() // For personalized content
]));
return Cache::remember($cacheKey, now()->addMinutes(10), function() {
return $this->socialReviewSearch($this->search, $this->contextFilter)
->applySocialFilters($this->filters)
->orderBy($this->sort['column'], $this->sort['direction'])
->paginate(12);
});
}
```
#### Real-Time Social Features
```php
// Optimized query for social engagement data
public function optimizedSocialQuery()
{
return Review::select([
'reviews.*',
DB::raw('COALESCE(likes_count.count, 0) as likes_count'),
DB::raw('COALESCE(comments_count.count, 0) as comments_count'),
DB::raw('COALESCE(shares_count.count, 0) as shares_count'),
DB::raw('(COALESCE(likes_count.count, 0) * 2 +
COALESCE(comments_count.count, 0) * 3 +
COALESCE(shares_count.count, 0) * 4) as engagement_score'),
DB::raw('CASE
WHEN sentiment_score >= 0.6 THEN "positive"
WHEN sentiment_score >= 0.4 THEN "neutral"
ELSE "negative"
END as sentiment_category')
])
->leftJoin(DB::raw('(SELECT review_id, COUNT(*) as count FROM review_likes GROUP BY review_id) as likes_count'),
'reviews.id', '=', 'likes_count.review_id')
->leftJoin(DB::raw('(SELECT review_id, COUNT(*) as count FROM review_comments GROUP BY review_id) as comments_count'),
'reviews.id', '=', 'comments_count.review_id')
->leftJoin(DB::raw('(SELECT review_id, COUNT(*) as count FROM review_shares GROUP BY review_id) as shares_count'),
'reviews.id', '=', 'shares_count.review_id')
->with([
'user:id,username,display_name,avatar_url',
'user.credibilityBadges:id,user_id,type,title',
'reviewable:id,name,type',
'verificationBadges:id,review_id,type,verified_at',
'recentLikes' => fn($q) => $q->with('user:id,username')->limit(5),
'topComments' => fn($q) => $q->with('user:id,username')->orderBy('likes_count', 'desc')->limit(3)
]);
}
```
### Component Reuse Strategy
#### Shared Components
- **`ReviewSocialInteractions`**: Like/comment/share functionality across all review contexts
- **`ReviewVerificationBadges`**: Trust and verification indicators for authentic reviews
- **`ReviewEngagementMetrics`**: Real-time engagement tracking and display
- **`UserCredibilityBadges`**: User reputation and expertise indicators
#### Context Variations
- **`ParkReviewsListing`**: Park-specific reviews with location context
- **`RideReviewsListing`**: Ride-specific reviews with experience context
- **`UserReviewsListing`**: User profile reviews with credibility focus
- **`FeaturedReviewsListing`**: High-engagement reviews with community highlights
### Testing Requirements
#### Feature Tests
```php
/** @test */
public function can_filter_reviews_by_social_engagement()
{
$highEngagement = Review::factory()->create(['content' => 'Amazing experience!']);
$highEngagement->likes()->createMany(15, ['user_id' => User::factory()]);
$highEngagement->comments()->createMany(8, ['user_id' => User::factory()]);
$lowEngagement = Review::factory()->create(['content' => 'Okay ride']);
$lowEngagement->likes()->create(['user_id' => User::factory()]);
Livewire::test(ReviewsListing::class)
->set('filters.engagement_level', 'high')
->assertSee($highEngagement->content)
->assertDontSee($lowEngagement->content);
}
/** @test */
public function displays_user_credibility_correctly()
{
$expertUser = User::factory()->create(['username' => 'expert_reviewer']);
$expertUser->credibilityBadges()->create(['type' => 'expert', 'title' => 'Theme Park Expert']);
$expertReview = Review::factory()->create([
'user_id' => $expertUser->id,
'content' => 'Professional analysis'
]);
Livewire::test(ReviewsListing::class)
->assertSee('Theme Park Expert')
->assertSee($expertReview->content);
}
/** @test */
public function maintains_django_parity_performance_with_social_data()
{
Review::factory()->count(30)->create();
$start = microtime(true);
Livewire::test(ReviewsListing::class);
$end = microtime(true);
$this->assertLessThan(0.5, $end - $start); // < 500ms with social data
}
```
#### Social Interaction Tests
```php
/** @test */
public function calculates_engagement_scores_accurately()
{
$review = Review::factory()->create();
$review->likes()->createMany(10, ['user_id' => User::factory()]);
$review->comments()->createMany(5, ['user_id' => User::factory()]);
$review->shares()->createMany(2, ['user_id' => User::factory()]);
$component = Livewire::test(ReviewsListing::class);
$reviewData = $component->get('reviews')->first();
// Engagement score = (likes * 2) + (comments * 3) + (shares * 4)
$expectedScore = (10 * 2) + (5 * 3) + (2 * 4); // 43
$this->assertEquals($expectedScore, $reviewData->engagement_score);
}
/** @test */
public function handles_real_time_social_updates()
{
$review = Review::factory()->create();
$component = Livewire::test(ReviewsListing::class);
// Simulate real-time like
$review->likes()->create(['user_id' => User::factory()->create()]);
$component->call('refreshEngagement', $review->id)
->assertSee('1 like');
}
```
### Performance Targets
#### Universal Performance Standards with Social Features
- **Initial Load**: < 500ms (including engagement metrics)
- **Social Interaction Response**: < 200ms for like/comment actions
- **Real-time Updates**: < 100ms for engagement refresh
- **Sentiment Analysis**: < 150ms for sentiment visualization
- **Community Statistics**: < 100ms (cached)
#### Social Content Caching Strategy
- **Engagement Metrics**: 10 minutes (frequently changing)
- **Trending Topics**: 1 hour (community trends)
- **User Credibility**: 6 hours (reputation changes slowly)
- **Social Statistics**: 15 minutes (community activity)
### Success Criteria Checklist
#### Django Parity Verification
- [ ] Social review search matches Django behavior exactly
- [ ] Engagement metrics calculated identically to Django
- [ ] Verification systems work like Django implementation
- [ ] Sentiment analysis provides same results as Django
- [ ] Community features match Django social functionality
#### Screen-Agnostic Compliance
- [ ] Mobile layout optimized for social interaction
- [ ] Tablet layout provides effective community browsing
- [ ] Desktop layout maximizes social engagement features
- [ ] Large screen layout provides comprehensive community management
- [ ] All layouts handle real-time social updates gracefully
#### Performance Benchmarks
- [ ] Initial load under 500ms including social data
- [ ] Social interactions under 200ms response time
- [ ] Real-time updates under 100ms
- [ ] Community statistics under 100ms (cached)
- [ ] Social caching reduces server load by 70%
#### Social Feature Completeness
- [ ] Engagement metrics display accurately across all contexts
- [ ] User credibility systems provide meaningful trust indicators
- [ ] Verification badges work for authentic experience validation
- [ ] Community moderation tools function effectively
- [ ] Real-time social updates work seamlessly across devices
This prompt ensures complete Django parity while providing comprehensive social review capabilities that foster authentic community engagement while maintaining ThrillWiki's screen-agnostic design principles.

View File

@@ -0,0 +1,426 @@
# Rides Listing Page Implementation Prompt
## Django Parity Reference
**Django Implementation**: `rides/views.py` - `RideListView` (lines 215-278)
**Django Template**: `rides/templates/rides/ride_list.html`
**Django Features**: Multi-term search, category filtering, manufacturer filtering, status filtering, pagination with HTMX, eager loading optimization
## Core Implementation Requirements
### Laravel/Livewire Architecture
Generate the rides listing system using ThrillWiki's custom generators:
```bash
# Generate the main listing component with optimizations
php artisan make:thrillwiki-livewire RidesListing --paginated --cached --with-tests
# Generate reusable search suggestions component
php artisan make:thrillwiki-livewire RidesSearchSuggestions --reusable --with-tests
# Generate advanced filters component
php artisan make:thrillwiki-livewire RidesFilters --reusable --cached
# Generate context-aware listing for park-specific rides
php artisan make:thrillwiki-livewire ParkRidesListing --paginated --cached --with-tests
```
### Django Parity Features
#### 1. Search Functionality
**Django Implementation**: Multi-term search across:
- Ride name (`name__icontains`)
- Ride description (`description__icontains`)
- Park name (`park__name__icontains`)
- Manufacturer name (`manufacturer__name__icontains`)
- Designer name (`designer__name__icontains`)
**Laravel Implementation**:
```php
public function search($query)
{
return Ride::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}%")
->orWhereHas('park', fn($q) => $q->where('name', 'ilike', "%{$term}%"))
->orWhereHas('manufacturer', fn($q) => $q->where('name', 'ilike', "%{$term}%"))
->orWhereHas('designer', fn($q) => $q->where('name', 'ilike', "%{$term}%"));
});
}
})
->with(['park', 'manufacturer', 'designer', 'photos'])
->orderBy('name');
}
```
#### 2. Advanced Filtering
**Django Filters**:
- Category (ride_type)
- Status (status)
- Manufacturer (manufacturer__id)
- Opening year range
- Height restrictions
- Park context (when viewing park-specific rides)
**Laravel Filters Implementation**:
```php
public function applyFilters($query, $filters)
{
return $query
->when($filters['category'] ?? null, fn($q, $category) =>
$q->where('ride_type', $category))
->when($filters['status'] ?? null, fn($q, $status) =>
$q->where('status', $status))
->when($filters['manufacturer_id'] ?? null, fn($q, $manufacturerId) =>
$q->where('manufacturer_id', $manufacturerId))
->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_height'] ?? null, fn($q, $height) =>
$q->where('height_requirement', '>=', $height))
->when($filters['max_height'] ?? null, fn($q, $height) =>
$q->where('height_requirement', '<=', $height));
}
```
#### 3. Context-Aware Views
**Global Listing**: All rides across all parks
**Park-Specific Listing**: Rides filtered by specific park
**Category-Specific Listing**: Rides filtered by ride type/category
### Screen-Agnostic Design Implementation
#### Mobile Layout (320px - 767px)
- **Single Column**: Full-width ride cards
- **Touch Targets**: Minimum 44px touch areas
- **Gesture Support**: Pull-to-refresh, swipe navigation
- **Bottom Navigation**: Sticky filters and search
- **Thumb Navigation**: Search and filter controls within thumb reach
**Mobile Component Structure**:
```blade
<div class="rides-mobile-layout">
<!-- Sticky Search Bar -->
<div class="sticky top-0 bg-white dark:bg-gray-900 z-10 p-4">
<livewire:rides-search-suggestions />
</div>
<!-- Quick Filters -->
<div class="horizontal-scroll p-4">
<livewire:rides-quick-filters />
</div>
<!-- Ride Cards -->
<div class="space-y-4 p-4">
@foreach($rides as $ride)
<livewire:ride-mobile-card :ride="$ride" :key="$ride->id" />
@endforeach
</div>
<!-- Mobile Pagination -->
<div class="sticky bottom-0 bg-white dark:bg-gray-900 p-4">
{{ $rides->links('pagination.mobile') }}
</div>
</div>
```
#### Tablet Layout (768px - 1023px)
- **Dual-Pane**: Filter sidebar + main content
- **Grid Layout**: 2-column ride cards
- **Advanced Filters**: Expandable filter panels
- **Touch + Keyboard**: Support both interaction modes
**Tablet Component Structure**:
```blade
<div class="rides-tablet-layout flex">
<!-- Filter Sidebar -->
<div class="w-80 bg-gray-50 dark:bg-gray-800 p-6">
<livewire:rides-filters :expanded="true" />
</div>
<!-- Main Content -->
<div class="flex-1 p-6">
<!-- Search and Sort -->
<div class="mb-6 flex items-center space-x-4">
<livewire:rides-search-suggestions class="flex-1" />
<livewire:rides-sort-selector />
</div>
<!-- Grid Layout -->
<div class="grid grid-cols-2 gap-6 mb-6">
@foreach($rides as $ride)
<livewire:ride-tablet-card :ride="$ride" :key="$ride->id" />
@endforeach
</div>
<!-- Pagination -->
{{ $rides->links() }}
</div>
</div>
```
#### Desktop Layout (1024px - 1919px)
- **Three-Pane**: Filter sidebar + main content + quick info panel
- **Advanced Grid**: 3-4 column layout
- **Keyboard Navigation**: Full keyboard shortcuts
- **Mouse Interactions**: Hover effects, context menus
**Desktop Component Structure**:
```blade
<div class="rides-desktop-layout flex">
<!-- Filter Sidebar -->
<div class="w-80 bg-gray-50 dark:bg-gray-800 p-6">
<livewire:rides-filters :expanded="true" :advanced="true" />
</div>
<!-- Main Content -->
<div class="flex-1 p-8">
<!-- Advanced Search Bar -->
<div class="mb-8 flex items-center space-x-6">
<livewire:rides-search-suggestions class="flex-1" :advanced="true" />
<livewire:rides-sort-selector :options="$advancedSortOptions" />
<livewire:rides-view-selector />
</div>
<!-- Grid Layout -->
<div class="grid grid-cols-3 xl:grid-cols-4 gap-6 mb-8">
@foreach($rides as $ride)
<livewire:ride-desktop-card :ride="$ride" :key="$ride->id" />
@endforeach
</div>
<!-- Advanced Pagination -->
{{ $rides->links('pagination.desktop') }}
</div>
<!-- Quick Info Panel -->
<div class="w-80 bg-gray-50 dark:bg-gray-800 p-6">
<livewire:rides-quick-info />
</div>
</div>
```
#### Large Screen Layout (1920px+)
- **Dashboard Style**: Multi-column layout with statistics
- **Ultra-Wide Optimization**: Up to 6-column grid
- **Advanced Analytics**: Statistics panels and data visualization
- **Multi-Monitor Support**: Optimized for extended displays
### Performance Optimization Strategy
#### Caching Implementation
```php
public function mount()
{
$this->cachedFilters = Cache::remember(
"rides.filters.{$this->currentUser->id}",
now()->addHours(1),
fn() => $this->loadFilterOptions()
);
}
public function getRidesProperty()
{
$cacheKey = "rides.listing." . md5(serialize([
'search' => $this->search,
'filters' => $this->filters,
'sort' => $this->sort,
'page' => $this->page
]));
return Cache::remember($cacheKey, now()->addMinutes(15), function() {
return $this->search($this->search)
->applyFilters($this->filters)
->orderBy($this->sort['column'], $this->sort['direction'])
->paginate(24);
});
}
```
#### Database Optimization
```php
// Query optimization with eager loading
public function optimizedQuery()
{
return Ride::select([
'id', 'name', 'description', 'ride_type', 'status',
'park_id', 'manufacturer_id', 'designer_id', 'opening_date',
'height_requirement', 'created_at', 'updated_at'
])
->with([
'park:id,name,slug',
'manufacturer:id,name,slug',
'designer:id,name,slug',
'photos' => fn($q) => $q->select(['id', 'ride_id', 'url', 'thumbnail_url'])->limit(1)
])
->withCount(['reviews', 'favorites']);
}
```
### Component Reuse Strategy
#### Shared Components
- **`RidesSearchSuggestions`**: Reusable across all ride-related pages
- **`RidesFilters`**: Extensible filter component with device-aware UI
- **`RideCard`**: Responsive ride display component
- **`RideQuickView`**: Modal/sidebar quick view component
#### Context Variations
- **`GlobalRidesListing`**: All rides across all parks
- **`ParkRidesListing`**: Park-specific rides (extends base listing)
- **`CategoryRidesListing`**: Category-specific rides (extends base listing)
- **`UserFavoriteRides`**: User's favorite rides (extends base listing)
### Testing Requirements
#### Feature Tests
```php
/** @test */
public function can_search_rides_across_multiple_fields()
{
// Test multi-term search across name, description, park, manufacturer
$ride = Ride::factory()->create(['name' => 'Space Mountain']);
$park = $ride->park;
$park->update(['name' => 'Magic Kingdom']);
Livewire::test(RidesListing::class)
->set('search', 'Space Magic')
->assertSee($ride->name)
->assertSee($park->name);
}
/** @test */
public function filters_rides_by_multiple_criteria()
{
$coaster = Ride::factory()->create(['ride_type' => 'roller-coaster']);
$kiddie = Ride::factory()->create(['ride_type' => 'kiddie']);
Livewire::test(RidesListing::class)
->set('filters.category', 'roller-coaster')
->assertSee($coaster->name)
->assertDontSee($kiddie->name);
}
/** @test */
public function maintains_django_parity_performance()
{
Ride::factory()->count(100)->create();
$start = microtime(true);
Livewire::test(RidesListing::class);
$end = microtime(true);
$this->assertLessThan(0.5, $end - $start); // < 500ms initial load
}
```
#### Cross-Device Tests
```php
/** @test */
public function renders_appropriately_on_mobile()
{
$this->browse(function (Browser $browser) {
$browser->resize(375, 667) // iPhone dimensions
->visit('/rides')
->assertVisible('.rides-mobile-layout')
->assertMissing('.rides-desktop-layout');
});
}
/** @test */
public function supports_touch_gestures_on_tablet()
{
$this->browse(function (Browser $browser) {
$browser->resize(768, 1024) // iPad dimensions
->visit('/rides')
->assertVisible('.rides-tablet-layout')
->swipeLeft('.horizontal-scroll')
->assertMissing('.rides-mobile-layout');
});
}
```
### Performance Targets
#### Universal Performance Standards
- **Initial Load**: < 500ms (Django parity requirement)
- **Filter Response**: < 200ms
- **Search Response**: < 300ms
- **3G Network**: < 3 seconds total page load
- **First Contentful Paint**: < 1.5 seconds across all devices
#### Device-Specific Targets
- **Mobile (3G)**: Core functionality in < 3 seconds
- **Tablet (WiFi)**: Full functionality in < 2 seconds
- **Desktop (Broadband)**: Advanced features in < 1 second
- **Large Screen**: Dashboard mode in < 1.5 seconds
### Success Criteria Checklist
#### Django Parity Verification
- [ ] Multi-term search matches Django behavior exactly
- [ ] All Django filters implemented and functional
- [ ] Pagination performance matches or exceeds Django
- [ ] Eager loading prevents N+1 queries like Django
- [ ] Context-aware views work identically to Django
#### Screen-Agnostic Compliance
- [ ] Mobile layout optimized for 320px+ screens
- [ ] Tablet layout utilizes dual-pane effectively
- [ ] Desktop layout provides advanced functionality
- [ ] Large screen layout maximizes available space
- [ ] All touch targets meet 44px minimum requirement
- [ ] Keyboard navigation works on all layouts
#### Performance Benchmarks
- [ ] Initial load under 500ms (matches Django target)
- [ ] Filter/search responses under 200ms
- [ ] 3G network performance under 3 seconds
- [ ] Memory usage optimized with proper caching
- [ ] Database queries optimized with eager loading
#### Component Reusability
- [ ] Search component reusable across ride-related pages
- [ ] Filter component extensible for different contexts
- [ ] Card components work across all screen sizes
- [ ] Modal/sidebar quick view components functional
#### Testing Coverage
- [ ] All Django functionality covered by feature tests
- [ ] Performance tests validate speed requirements
- [ ] Cross-device browser tests pass
- [ ] Component integration tests complete
- [ ] User interaction tests cover all form factors
## Implementation Priority Order
1. **Generate Base Components** (Day 1)
- Use ThrillWiki generators for rapid scaffolding
- Implement core search and filter functionality
- Set up responsive layouts
2. **Django Parity Implementation** (Day 2)
- Implement exact search behavior
- Add all Django filter options
- Optimize database queries
3. **Screen-Agnostic Optimization** (Day 3)
- Fine-tune responsive layouts
- Implement device-specific features
- Add touch and keyboard interactions
4. **Performance Optimization** (Day 4)
- Implement caching strategies
- Optimize database queries
- Add lazy loading where appropriate
5. **Testing and Validation** (Day 5)
- Complete test suite implementation
- Validate Django parity
- Verify performance targets
This prompt ensures complete Django parity while leveraging Laravel/Livewire advantages and maintaining ThrillWiki's screen-agnostic design principles.