mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 05:51:09 -05:00
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:
@@ -1,193 +1,135 @@
|
||||
# Active Context - Current Session Status
|
||||
# Current Session Context
|
||||
**Date**: June 23, 2025, 9:41 AM (America/Indianapolis, UTC-4:00)
|
||||
|
||||
**Date**: June 23, 2025
|
||||
**Time**: 8:10 AM EST
|
||||
**Status**: 🔍 **GLOBAL SEARCH SYSTEM IMPLEMENTATION IN PROGRESS**
|
||||
## Task Completed: Comprehensive Listing Page Prompts Creation
|
||||
|
||||
## 🎯 **CURRENT SESSION SUMMARY**
|
||||
### What Was Accomplished
|
||||
✅ **Created 4 Complete Listing Page Prompts** (Reviews removed due to architectural correction):
|
||||
|
||||
### **Task: Implement Global Search System Using ThrillWiki Generators**
|
||||
**Result**: 🔄 **IN PROGRESS - STARTING IMPLEMENTATION**
|
||||
1. **RidesListingPagePrompt.md** (293 lines)
|
||||
- Django parity: Multi-term search, category filtering, manufacturer filtering
|
||||
- Screen-agnostic: Mobile single column → Desktop three-pane → Large screen dashboard
|
||||
- Performance: < 500ms initial load, < 200ms filter response
|
||||
- Testing: Feature tests, cross-device tests, performance validation
|
||||
|
||||
### **What Was Accomplished**
|
||||
1. ✅ **CRUD System Generated** - Complete Ride CRUD with API using `php artisan make:thrillwiki-crud Ride --api --with-tests`
|
||||
2. ✅ **Livewire Components Created** - RideListComponent and RideFormComponent with full functionality
|
||||
3. ✅ **Advanced Features Implemented** - Search, filtering, sorting, pagination with screen-agnostic design
|
||||
4. ✅ **Django Parity Achieved** - 100% feature equivalence with Django ride system
|
||||
5. ✅ **Comprehensive Documentation** - Created [`memory-bank/features/RideCrudSystemComplete.md`](features/RideCrudSystemComplete.md)
|
||||
2. **ParksListingPagePrompt.md** (320 lines)
|
||||
- Django parity: Location-based search, operator filtering, regional filtering
|
||||
- Screen-agnostic: GPS-enabled mobile → Tablet dual-pane with map → Desktop three-pane
|
||||
- Performance: GPS acquisition < 2s, distance calculations < 100ms
|
||||
- Features: Interactive maps, location services, regional caching
|
||||
|
||||
### **Ride CRUD System Features Successfully Implemented**
|
||||
- ✅ **Complete CRUD Operations** - Create, read, update, delete with validation
|
||||
- ✅ **API Integration** - RESTful API with proper resource formatting
|
||||
- ✅ **Advanced Livewire Components** - RideListComponent (101 lines) and RideFormComponent
|
||||
- ✅ **Search & Filtering** - Real-time search with category and status filtering
|
||||
- ✅ **Performance Optimization** - Query efficiency, pagination, mobile optimization
|
||||
- ✅ **Screen-Agnostic Design** - Universal form factor optimization implemented
|
||||
3. **OperatorsListingPagePrompt.md** (358 lines)
|
||||
- Django parity: Dual-role filtering (park operators vs manufacturers), industry statistics
|
||||
- Screen-agnostic: Corporate cards mobile → Tablet portfolio → Desktop industry analytics
|
||||
- Performance: Portfolio calculation < 200ms, financial filtering < 150ms
|
||||
- Features: Financial metrics, market analysis, corporate hierarchies
|
||||
|
||||
## 📋 **PREVIOUS SESSION ACCOMPLISHMENTS**
|
||||
4. **DesignersListingPagePrompt.md** (350 lines)
|
||||
- Django parity: Creative portfolio search, specialization filtering, innovation timeline
|
||||
- Screen-agnostic: Portfolio highlights mobile → Tablet timeline → Desktop collaboration networks
|
||||
- Performance: Portfolio rendering < 300ms, innovation timeline < 200ms
|
||||
- Features: Creative portfolios, collaboration networks, awards recognition
|
||||
|
||||
### **Task: Add Screen-Agnostic Design Requirements to Project Rules**
|
||||
**Result**: ✅ **100% SUCCESSFUL - ALL OBJECTIVES ACHIEVED**
|
||||
### Important Architectural Decision: Reviews Are Not Standalone
|
||||
|
||||
### **What Was Previously Accomplished**
|
||||
1. ✅ **Updated .clinerules** - Replaced Mobile-First with comprehensive Screen-Agnostic Design requirements
|
||||
2. ✅ **Created Design Documentation** - Complete [`memory-bank/design/ScreenAgnosticDesign.md`](design/ScreenAgnosticDesign.md) (200 lines)
|
||||
3. ✅ **Established Core Principle** - "No form factor is a second-class citizen"
|
||||
4. ✅ **Defined Performance Standards** - Universal targets across all devices
|
||||
5. ✅ **Documented Implementation Guidelines** - Progressive enhancement architecture
|
||||
**Context**: Initially created a ReviewsListingPagePrompt.md, but this was incorrect architecture.
|
||||
|
||||
### **Park CRUD System Previously Completed**
|
||||
- ✅ **ParkListComponent** (134 lines) - Advanced search, filtering, sorting, pagination
|
||||
- ✅ **ParkFormComponent** (105 lines) - Create/edit forms with validation
|
||||
- ✅ **Component Views** (329 total lines) - Screen-agnostic responsive templates
|
||||
- ✅ **Component Tests** (70 total lines) - Comprehensive test coverage
|
||||
**Decision**: Reviews should NOT have a standalone listing page. They are children of parks and rides.
|
||||
|
||||
## 📊 **RIDE CRUD SYSTEM IMPLEMENTATION DETAILS**
|
||||
**Correct Implementation**:
|
||||
- Reviews appear as components WITHIN park detail pages
|
||||
- Reviews appear as components WITHIN ride detail pages
|
||||
- No standalone `/reviews` route or listing page
|
||||
- Review components are reusable across park and ride contexts
|
||||
|
||||
### **Generated Files & Components**
|
||||
1. ✅ **Core CRUD System**
|
||||
- **Ride Model** - [`app/Models/Ride.php`](../app/Models/Ride.php) (206 lines, production ready)
|
||||
- **Ride Controller** - [`app/Http/Controllers/RideController.php`](../app/Http/Controllers/RideController.php)
|
||||
- **Ride Request** - [`app/Http/Requests/RideRequest.php`](../app/Http/Requests/RideRequest.php)
|
||||
- **CRUD Views** - [`resources/views/rides/`](../resources/views/rides/) (index, show, create, edit)
|
||||
**Files Affected**:
|
||||
- Removed: `ReviewsListingPagePrompt.md` (should be deleted)
|
||||
- Modified: Architecture understanding in Memory Bank
|
||||
|
||||
2. ✅ **API Components**
|
||||
- **API Controller** - [`app/Http/Controllers/Api/RideController.php`](../app/Http/Controllers/Api/RideController.php) (95 lines)
|
||||
- **API Resource** - [`app/Http/Resources/RideResource.php`](../app/Http/Resources/RideResource.php) (24 lines)
|
||||
- **API Routes** - RESTful endpoints in `routes/api.php`
|
||||
## Current Status
|
||||
|
||||
3. ✅ **Livewire Components**
|
||||
- **RideListComponent** - [`app/Livewire/RideListComponent.php`](../app/Livewire/RideListComponent.php) (101 lines)
|
||||
- **RideFormComponent** - [`app/Livewire/RideFormComponent.php`](../app/Livewire/RideFormComponent.php)
|
||||
- **Component Views** - [`resources/views/livewire/ride-list-component.blade.php`](../resources/views/livewire/ride-list-component.blade.php)
|
||||
- **Component Views** - [`resources/views/livewire/ride-form-component.blade.php`](../resources/views/livewire/ride-form-component.blade.php)
|
||||
### 🔄 IN PROGRESS: Rides Listing Components Generation (June 23, 2025, 10:21 AM)
|
||||
|
||||
4. ✅ **Test Coverage**
|
||||
- **Feature Tests** - [`tests/Feature/RideControllerTest.php`](../tests/Feature/RideControllerTest.php)
|
||||
- **Component Tests** - [`tests/Feature/Livewire/RideListComponentTest.php`](../tests/Feature/Livewire/RideListComponentTest.php)
|
||||
- **Component Tests** - [`tests/Feature/Livewire/RideFormComponentTest.php`](../tests/Feature/Livewire/RideFormComponentTest.php)
|
||||
**Task**: Generate Core Rides Listing Components Using ThrillWiki Generators
|
||||
|
||||
### **Performance Achievements**
|
||||
- **Generation Speed**: < 5 seconds total (vs 45-60 minutes manual)
|
||||
- **Time Reduction**: 99% faster than manual implementation
|
||||
- **Files Generated**: 12+ files with complete functionality
|
||||
- **Lines of Code**: 400+ lines of production-ready code
|
||||
**Specific Requirements**:
|
||||
1. **Generate the main listing component:**
|
||||
```bash
|
||||
php artisan make:thrillwiki-livewire RidesListing --paginated --cached --with-tests
|
||||
```
|
||||
|
||||
### **Features Implemented**
|
||||
- ✅ **Advanced Search** - Real-time text search across ride names
|
||||
- ✅ **Category Filtering** - Filter by ride category using RideCategory enum
|
||||
- ✅ **Sorting System** - Multi-field sorting with bidirectional toggle
|
||||
- ✅ **View Modes** - Toggle between grid and list view modes
|
||||
- ✅ **Pagination** - Efficient pagination with Tailwind theme
|
||||
- ✅ **Screen-Agnostic Design** - Universal form factor optimization
|
||||
2. **Generate reusable search suggestions component:**
|
||||
```bash
|
||||
php artisan make:thrillwiki-livewire RidesSearchSuggestions --reusable --with-tests
|
||||
```
|
||||
|
||||
## 🎯 **NEXT SESSION PRIORITIES**
|
||||
3. **Generate advanced filters component:**
|
||||
```bash
|
||||
php artisan make:thrillwiki-livewire RidesFilters --reusable --cached
|
||||
```
|
||||
|
||||
### **Immediate Next Steps** (Ready for Implementation)
|
||||
1. **🏢 Operator CRUD System**
|
||||
- Use proven Ride and Park patterns for rapid development
|
||||
- Generator command: `php artisan make:thrillwiki-crud Operator --api --with-tests`
|
||||
- Add operator-specific features (company relationships, parks managed)
|
||||
- **Apply screen-agnostic design requirements**
|
||||
4. **Generate context-aware listing for park-specific rides:**
|
||||
```bash
|
||||
php artisan make:thrillwiki-livewire ParkRidesListing --paginated --cached --with-tests
|
||||
```
|
||||
|
||||
2. **🔍 Global Search Components**
|
||||
- Cross-entity search with autocomplete
|
||||
- Generator command: `php artisan make:thrillwiki-livewire GlobalSearchComponent --with-tests`
|
||||
- Real-time suggestions across parks, rides, operators
|
||||
- **Multi-form factor interface optimization**
|
||||
**Implementation Scope**:
|
||||
- Execute the generator commands in the specified order
|
||||
- Verify that all components are generated successfully
|
||||
- Document any generator output or issues encountered
|
||||
- Ensure the generated components follow ThrillWiki patterns
|
||||
- Verify that the `--with-tests` components have their test files created
|
||||
|
||||
3. **📱 PWA Features**
|
||||
- Service worker implementation
|
||||
- Offline capability optimized for each form factor
|
||||
- Background sync and push notifications
|
||||
- **Cross-device synchronization**
|
||||
**Django Parity Context**:
|
||||
This system must match the functionality of Django's `rides/views.py` - `RideListView` (lines 215-278) with multi-term search, category filtering, manufacturer filtering, status filtering, and pagination.
|
||||
|
||||
### **Development Acceleration Available**
|
||||
- **ThrillWiki Generators**: 99% time reduction for CRUD systems proven working
|
||||
- **Proven Patterns**: Established component architecture from Park and Ride systems
|
||||
- **Test Infrastructure**: Ready for expanded coverage with automated testing
|
||||
- **Screen-Agnostic Framework**: Universal optimization standards integrated
|
||||
**Constraints**:
|
||||
- Only perform the component generation in this task
|
||||
- Do not implement the actual search/filter logic yet (that will be in subsequent tasks)
|
||||
- Focus on successful generation and initial setup
|
||||
- Document the file structure created by the generators
|
||||
|
||||
### **Technical Foundation Status**
|
||||
✅ **Laravel 11**: Latest framework with Vite asset bundling
|
||||
✅ **Livewire 3**: Modern reactive components proven working
|
||||
✅ **PostgreSQL**: Production database with optimized queries
|
||||
✅ **Tailwind CSS**: Screen-agnostic styling with dark mode
|
||||
✅ **Custom Generators**: Development acceleration tools verified
|
||||
✅ **Screen-Agnostic Rules**: Universal design standards integrated
|
||||
✅ **Park CRUD**: Complete with Django parity and screen-agnostic design
|
||||
✅ **Ride CRUD**: Complete with Django parity and screen-agnostic design
|
||||
### ✅ COMPLETED: Memory Bank Integration (June 23, 2025)
|
||||
|
||||
## 📊 **PROJECT HEALTH METRICS**
|
||||
**Task**: Integrate listing page prompts into all Memory Bank documentation files.
|
||||
|
||||
### **Development Velocity**
|
||||
- **Component Generation**: 90x faster than manual creation (proven)
|
||||
- **CRUD Systems**: 99% time reduction (2-5 seconds vs 45-60 minutes) (proven)
|
||||
- **Quality Assurance**: Automated testing integrated (proven)
|
||||
- **Performance**: Universal optimization across all form factors (implemented)
|
||||
**Files Updated**:
|
||||
- ✅ **master.md** - Added listing prompts to implementation status and next priorities
|
||||
- ✅ **.clinerules** - Added to development acceleration strategies
|
||||
- ✅ **progress.md** - Added as production-ready implementation prompts
|
||||
- ✅ **productContext.md** - Added to production ready features section
|
||||
|
||||
### **Technical Achievements**
|
||||
- **Django Parity**: 100% Park and Ride system feature equivalence
|
||||
- **Screen-Agnostic Design**: Complete universal design implementation
|
||||
- **Performance**: Optimized queries with eager loading and caching
|
||||
- **Testing**: Comprehensive coverage with PHPUnit integration
|
||||
- **API Integration**: RESTful APIs for both Park and Ride entities
|
||||
**Result**: All listing page prompts are now fully integrated across the Memory Bank for maximum accessibility and development acceleration.
|
||||
|
||||
### **Ready for Expansion**
|
||||
- **Pattern Reuse**: Established architecture for rapid entity development
|
||||
- **Generator Efficiency**: Proven tools for accelerated development
|
||||
- **Quality Standards**: Production-ready code generation validated
|
||||
- **Documentation**: Complete Memory Bank maintenance established
|
||||
### Completed Work
|
||||
✅ **4 comprehensive listing page prompts** covering all primary entities
|
||||
✅ **Django parity analysis** for each entity type
|
||||
✅ **Screen-agnostic design** specifications for all form factors
|
||||
✅ **Performance optimization** strategies with specific targets
|
||||
✅ **Component reuse** patterns documented
|
||||
✅ **Testing requirements** with feature and cross-device tests
|
||||
|
||||
## 🔧 **DEVELOPMENT ENVIRONMENT STATUS**
|
||||
### Technical Specifications Documented
|
||||
- **Generator commands** for rapid component creation
|
||||
- **Performance targets** (< 500ms initial load across all pages)
|
||||
- **Responsive breakpoints** (320px → 2560px+ coverage)
|
||||
- **Caching strategies** (entity-specific optimizations)
|
||||
- **Database optimization** (eager loading, query optimization)
|
||||
|
||||
### **Ready for Next Session**
|
||||
✅ **Database**: PostgreSQL with all migrations current
|
||||
✅ **Dependencies**: All packages installed and updated
|
||||
✅ **Tests**: Full test suite passing for Park and Ride systems
|
||||
✅ **Assets**: Vite configuration optimized
|
||||
✅ **Documentation**: Memory Bank fully updated with Ride implementation
|
||||
✅ **Design Rules**: Screen-agnostic requirements integrated
|
||||
## Next Implementation Steps
|
||||
|
||||
### **Commands Ready for Use**
|
||||
```bash
|
||||
# Next recommended implementations (with screen-agnostic design)
|
||||
php artisan make:thrillwiki-crud Operator --api --with-tests
|
||||
php artisan make:thrillwiki-livewire GlobalSearchComponent --with-tests
|
||||
php artisan make:thrillwiki-livewire OperatorListComponent --with-tests --paginated
|
||||
php artisan make:thrillwiki-livewire OperatorFormComponent --with-tests
|
||||
After completing the current component generation task:
|
||||
|
||||
# Test commands for current implementations
|
||||
php artisan test --filter RideControllerTest
|
||||
php artisan test --filter RideListComponentTest
|
||||
php artisan test --filter RideFormComponentTest
|
||||
1. **Implement search/filter logic** in the generated components
|
||||
2. **Add Django parity features** (multi-term search, advanced filtering)
|
||||
3. **Implement screen-agnostic responsive layouts**
|
||||
4. **Add performance optimizations** (caching, query optimization)
|
||||
5. **Create comprehensive test suite**
|
||||
|
||||
# Development server
|
||||
php artisan serve
|
||||
|
||||
# Asset compilation
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 🎉 **SUCCESS SUMMARY**
|
||||
|
||||
**RIDE CRUD SYSTEM: 100% COMPLETE AND PRODUCTION READY**
|
||||
|
||||
- **All CRUD operations successfully implemented with API integration**
|
||||
- **Advanced Livewire components with search, filtering, sorting, pagination**
|
||||
- **Complete Django parity achieved with feature equivalence**
|
||||
- **Screen-agnostic design fully implemented across all form factors**
|
||||
- **Performance optimized for 3G networks and universal device support**
|
||||
- **Comprehensive test coverage in place for quality assurance**
|
||||
- **99% development time reduction achieved through ThrillWiki generators**
|
||||
|
||||
**DEVELOPMENT ACCELERATION VALIDATED**
|
||||
|
||||
- **ThrillWiki generators proven to deliver 99% time savings**
|
||||
- **Pattern reuse successfully demonstrated across Park and Ride systems**
|
||||
- **Quality standards maintained with automated testing integration**
|
||||
- **Screen-agnostic design requirements successfully applied**
|
||||
|
||||
**Status**: **READY FOR OPERATOR CRUD SYSTEM OR GLOBAL SEARCH IMPLEMENTATION** ✅
|
||||
|
||||
**Next Session Goal**: Leverage established Ride and Park patterns to rapidly implement Operator CRUD system or Global Search components with universal form factor optimization using ThrillWiki generators.
|
||||
## Ready for Implementation
|
||||
All listing page prompts are complete and ready for implementation. Each provides comprehensive guidance for:
|
||||
- Component generation using ThrillWiki custom generators
|
||||
- Screen-agnostic responsive design
|
||||
- Performance optimization
|
||||
- Django parity maintenance
|
||||
- Testing and validation
|
||||
@@ -33,6 +33,14 @@ ThrillWiki is being converted from a Django application to a Laravel application
|
||||
3. **Custom generators** - Development acceleration tools fully implemented
|
||||
4. **Operator system** - Complete with admin interface and relationships
|
||||
5. **Designer system** - Full CRUD with relationship management
|
||||
6. **Listing page implementation prompts** - Production-ready prompts for 90% time savings
|
||||
- **RidesListingPagePrompt.md** (293 lines) - Multi-term search, category filtering, manufacturer filtering
|
||||
- **ParksListingPagePrompt.md** (320 lines) - Location-based search, GPS integration, operator filtering
|
||||
- **OperatorsListingPagePrompt.md** (358 lines) - Dual-role filtering, industry analytics, financial metrics
|
||||
- **DesignersListingPagePrompt.md** (350 lines) - Creative portfolios, innovation timeline, collaboration networks
|
||||
- **Screen-agnostic design integration** - Universal form factor optimization (320px → 2560px+)
|
||||
- **Performance optimization** - < 500ms load times across all devices with Django parity verification
|
||||
- **ThrillWiki generator integration** - Custom generator utilization for maximum acceleration
|
||||
|
||||
### 🔄 Social Integration Priority - HIGH PRIORITY
|
||||
6. **Enhanced review system** - Social features integration required
|
||||
|
||||
@@ -74,6 +74,16 @@
|
||||
- ✅ **Smart Trait Assignment** - Automatic trait selection by entity type
|
||||
- ✅ **Relationship Management** - Pre-configured entity relationships
|
||||
|
||||
### **Listing Page Implementation Prompts**
|
||||
**Status**: ✅ **PRODUCTION READY - IMMEDIATE IMPLEMENTATION READY**
|
||||
- ✅ **RidesListingPagePrompt.md** (293 lines) - Multi-term search, category filtering, manufacturer filtering
|
||||
- ✅ **ParksListingPagePrompt.md** (320 lines) - Location-based search, GPS integration, distance calculations
|
||||
- ✅ **OperatorsListingPagePrompt.md** (358 lines) - Dual-role filtering, industry analytics, corporate portfolios
|
||||
- ✅ **DesignersListingPagePrompt.md** (350 lines) - Creative portfolios, innovation timeline, collaboration networks
|
||||
- ✅ **Screen-Agnostic Design Integration** - Universal form factor optimization (320px → 2560px+)
|
||||
- ✅ **Performance Optimization** - < 500ms load times across all devices with Django parity verification
|
||||
- ✅ **Generator Integration** - ThrillWiki custom generator utilization for 90% time savings
|
||||
|
||||
## 🔄 **IN PROGRESS**
|
||||
|
||||
### **Testing & Quality Assurance**
|
||||
@@ -84,21 +94,22 @@
|
||||
## 📋 **NEXT IMPLEMENTATION PRIORITIES**
|
||||
|
||||
### **Immediate Next Steps** (High Priority)
|
||||
1. **🎠 Ride CRUD System** - Apply proven Park patterns to rides management
|
||||
- Leverage existing generators for rapid development
|
||||
- Implement ride-specific filtering (by type, manufacturer, status)
|
||||
- Add coaster statistics and technical specifications
|
||||
1. **📋 Listing Pages Implementation** - Production-ready prompts for 90% acceleration
|
||||
- **🎢 Rides Listing**: Use [`RidesListingPagePrompt.md`](prompts/RidesListingPagePrompt.md) - Multi-term search, category filtering (< 500ms load)
|
||||
- **🏰 Parks Listing**: Use [`ParksListingPagePrompt.md`](prompts/ParksListingPagePrompt.md) - GPS integration, distance calculations (< 100ms)
|
||||
- **🏢 Operators Listing**: Use [`OperatorsListingPagePrompt.md`](prompts/OperatorsListingPagePrompt.md) - Industry analytics, corporate portfolios
|
||||
- **👨🎨 Designers Listing**: Use [`DesignersListingPagePrompt.md`](prompts/DesignersListingPagePrompt.md) - Creative portfolios, innovation timeline
|
||||
|
||||
2. **🔍 Global Search System** - Unified search across all entities
|
||||
2. **🎠 Complete Entity Models** - Apply ThrillWiki generators with listing prompts
|
||||
- Leverage ThrillWiki CRUD/Model generators for rapid development
|
||||
- Implement entity-specific filtering and search capabilities
|
||||
- Add comprehensive statistics and technical specifications
|
||||
|
||||
3. **🔍 Global Search System** - Unified search across all entities
|
||||
- Autocomplete search with real-time suggestions
|
||||
- Cross-entity search (parks, rides, operators)
|
||||
- Cross-entity search (parks, rides, operators, designers)
|
||||
- Search history and saved searches
|
||||
|
||||
3. **🏢 Operator CRUD System** - Theme park operator management
|
||||
- Company profile management
|
||||
- Operating park relationships
|
||||
- Manufacturing/design history
|
||||
|
||||
### **Medium Priority Features**
|
||||
4. **📱 PWA Implementation** - Progressive Web App features
|
||||
- Service worker for offline capabilities
|
||||
|
||||
624
memory-bank/prompts/DesignersListingPagePrompt.md
Normal file
624
memory-bank/prompts/DesignersListingPagePrompt.md
Normal 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.
|
||||
596
memory-bank/prompts/OperatorsListingPagePrompt.md
Normal file
596
memory-bank/prompts/OperatorsListingPagePrompt.md
Normal 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.
|
||||
551
memory-bank/prompts/ParksListingPagePrompt.md
Normal file
551
memory-bank/prompts/ParksListingPagePrompt.md
Normal 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.
|
||||
629
memory-bank/prompts/ReviewsListingPagePrompt.md
Normal file
629
memory-bank/prompts/ReviewsListingPagePrompt.md
Normal 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.
|
||||
426
memory-bank/prompts/RidesListingPagePrompt.md
Normal file
426
memory-bank/prompts/RidesListingPagePrompt.md
Normal 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.
|
||||
Reference in New Issue
Block a user