Compare commits

..

19 Commits

Author SHA1 Message Date
pacnpal
97a7682eb7 Add Livewire components for parks, rides, and manufacturers
- Implemented ParksLocationSearch component with loading state and refresh functionality.
- Created ParksMapView component with similar structure and functionality.
- Added RegionalParksListing component for displaying regional parks.
- Developed RidesListingUniversal component for universal listing integration.
- Established ManufacturersListing view with navigation and Livewire integration.
- Added feature tests for various Livewire components including OperatorHierarchyView, OperatorParksListing, OperatorPortfolioCard, OperatorsListing, OperatorsRoleFilter, ParksListing, ParksLocationSearch, ParksMapView, and RegionalParksListing to ensure proper rendering and adherence to patterns.
2025-06-23 21:31:05 -04:00
pacnpal
5caa148a89 feat: Complete generation and implementation of Rides Listing components
- Marked Rides Listing Components Generation as completed with detailed results.
- Implemented search/filter logic in RidesListing component for Django parity.
- Created ParkRidesListing, RidesFilters, and RidesSearchSuggestions components with caching and pagination.
- Developed corresponding Blade views for each component.
- Added comprehensive tests for ParkRidesListing, RidesListing, and RidesSearchSuggestions components to ensure functionality and adherence to patterns.
2025-06-23 11:34:13 -04:00
pacnpal
c2f3532469 Add comprehensive implementation prompts for Reviews and Rides listing pages with Django parity, Laravel/Livewire architecture, and screen-agnostic design principles 2025-06-23 10:21:54 -04:00
pacnpal
ecf237d592 feat: Implement Global Search component with caching and tests 2025-06-23 08:12:56 -04:00
pacnpal
bd08111971 feat: Complete implementation of Ride CRUD system with full functionality and testing
- Added Ride CRUD system documentation detailing implementation summary, generated components, and performance metrics.
- Created Ride CRUD system prompt for future development with core requirements and implementation strategy.
- Established relationships between rides and parks, ensuring Django parity and optimized performance.
- Implemented waiting for user command execution documentation for Park CRUD generation.
- Developed Livewire components for RideForm and RideList with basic structure.
- Created feature tests for Park and Ride components, ensuring proper rendering and functionality.
- Added comprehensive tests for ParkController, ReviewImage, and ReviewReport models, validating CRUD operations and relationships.
2025-06-23 08:10:04 -04:00
pacnpal
5c68845f44 feat: add GitHub MCP server configuration to mcp.json 2025-06-21 11:25:52 -04:00
pacnpal
fcd6fe3054 feat: add initial mcp.json configuration for server commands 2025-06-21 11:20:29 -04:00
pacnpal
48646570d8 fix: update user menu link to settings and add alt attribute to background image 2025-06-19 22:37:44 -04:00
pacnpal
cc33781245 feat: Implement rides management with CRUD functionality
- Added rides index view with search and filter options.
- Created rides show view to display ride details.
- Implemented API routes for rides.
- Developed authentication routes for user registration, login, and email verification.
- Created tests for authentication, email verification, password reset, and user profile management.
- Added feature tests for rides and operators, including creation, updating, deletion, and searching.
- Implemented soft deletes and caching for rides and operators.
- Enhanced manufacturer and operator model tests for various functionalities.
2025-06-19 22:34:10 -04:00
pacnpal
86263db9d9 refactor: remove designers table migration and update layout structure for improved readability and organization 2025-03-25 11:33:47 -04:00
pacnpal
8eac13d51b feat: create designers table and update Park model to use Operator for ownership 2025-03-23 15:13:03 -04:00
pacnpal
ea7af68d99 feat: add application kernel and middleware structure; implement console and exception handling kernels 2025-02-27 12:35:34 -05:00
pacnpal
2436e8cec6 feat: add middleware for cookie encryption, CSRF verification, string trimming, and maintenance request prevention; implement Designer resource management with CRUD pages and permissions 2025-02-26 21:28:02 -05:00
pacnpal
0e61f7d694 feat: enhance documentation system with Handoffs integration and structured guidelines 2025-02-26 20:13:36 -05:00
pacnpal
ce137acd58 chore: add RooCode-Tips-Tricks-main to .gitignore 2025-02-26 20:05:22 -05:00
pacnpal
7cc3349b0e feat: document project analysis, implementation priorities, and technical dependencies 2025-02-26 20:04:53 -05:00
pacnpal
1a88c35fa8 feat: implement autocomplete functionality for park search with keyboard navigation 2025-02-26 13:00:42 -05:00
pacnpal
82d99a8161 refactor: remove Company model as it is no longer needed 2025-02-26 13:00:23 -05:00
pacnpal
bcfab9fb74 feat: add Company model and TrackedModel trait; update Park model to use Company as owner 2025-02-26 12:58:46 -05:00
303 changed files with 36544 additions and 3291 deletions

2
.gitignore vendored
View File

@@ -24,3 +24,5 @@ yarn-error.log
.clinerules
.clinerules
.clinerules
RooCode-Tips-Tricks-main
.DS_Store

48
.roo/mcp.json Normal file
View File

@@ -0,0 +1,48 @@
{
"mcpServers": {
"git": {
"command": "uvx",
"args": [
"mcp-server-git",
"--repository",
"/Volumes/macminissd/Projects/ThrillWiki/thrillwiki_laravel"
]
},
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Volumes/macminissd/Projects/ThrillWiki/thrillwiki_laravel"
]
},
"postgres": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://192.168.86.3:5432"
]
},
"github": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"-e",
"GITHUB_TOOLSETS",
"-e",
"GITHUB_READ_ONLY",
"ghcr.io/github/github-mcp-server"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "",
"GITHUB_TOOLSETS": "",
"GITHUB_READ_ONLY": ""
}
}
}
}

128
README.md
View File

@@ -1,66 +1,106 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
# ThrillWiki - Laravel/Livewire Implementation
## About Laravel
This is the Laravel/Livewire implementation of ThrillWiki, maintaining feature parity with the original Django project.
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
## Prerequisites
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
- PHP 8.1 or higher
- PostgreSQL
- Node.js and npm
- Composer
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Setup Instructions
## Learning Laravel
### 1. Environment Configuration
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
```bash
# Copy the example environment file
cp .env.example .env
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
# Generate application key
php artisan key:generate
```
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
Configure your `.env` file with the following essential settings:
## Laravel Sponsors
```env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=thrillwiki
DB_USERNAME=your_username
DB_PASSWORD=your_password
```
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### 2. Database Setup
### Premium Partners
1. Create PostgreSQL database:
```sql
CREATE DATABASE thrillwiki;
```
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
2. Run migrations and seed the database:
```bash
php artisan migrate:fresh --seed
```
## Contributing
### 3. Install Dependencies
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
```bash
# Install PHP dependencies
composer install
## Code of Conduct
# Install Node.js dependencies
npm install
```
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
### 4. Start Development Servers
## Security Vulnerabilities
Run these commands in separate terminal windows:
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
```bash
# Start Laravel development server
php artisan serve
## License
# Start Vite development server for asset compilation
npm run dev
```
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
For production:
```bash
npm run build
```
### 5. Clear Cache (If Needed)
```bash
php artisan cache:clear && php artisan config:clear && php artisan route:clear && php artisan view:clear
```
## Troubleshooting
1. **Assets Not Loading**
- Ensure Vite is running (`npm run dev`)
- For production, make sure assets are built (`npm run build`)
2. **Database Connection Issues**
- Verify PostgreSQL is running
- Check credentials in `.env` file
- Ensure database exists and is accessible
3. **Migration Errors**
- Check migration order in `database/migrations`
- Ensure database is empty when running `migrate:fresh`
## Development Guidelines
This implementation maintains strict feature parity with the original Django project. Key requirements:
- Feature-to-Feature matching with Django implementation
- Identical API responses and data structures
- Consistent UI/UX with original
- Test coverage matching Django functionality
For detailed development guidelines, refer to the project documentation.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,392 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
class MakeThrillWikiLivewire extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'make:thrillwiki-livewire {name : The name of the component}
{--reusable : Generate a reusable component with optimization traits}
{--with-tests : Generate test files for the component}
{--cached : Add caching optimization to the component}
{--paginated : Add pagination support to the component}
{--force : Overwrite existing files}';
/**
* The console command description.
*/
protected $description = 'Create a ThrillWiki-optimized Livewire component with built-in patterns and performance optimization';
protected Filesystem $files;
public function __construct(Filesystem $files)
{
parent::__construct();
$this->files = $files;
}
/**
* Execute the console command.
*/
public function handle(): int
{
$name = $this->argument('name');
$className = Str::studly($name);
$kebabName = Str::kebab($name);
$this->info("🚀 Generating ThrillWiki Livewire Component: {$className}");
// Generate the component class
$this->generateComponent($className, $kebabName);
// Generate the view file
$this->generateView($className, $kebabName);
// Generate tests if requested
if ($this->option('with-tests')) {
$this->generateTest($className);
}
$this->displaySummary($className, $kebabName);
return Command::SUCCESS;
}
protected function generateComponent(string $className, string $kebabName): void
{
$componentPath = app_path("Livewire/{$className}.php");
if ($this->files->exists($componentPath) && !$this->option('force')) {
$this->error("Component {$className} already exists! Use --force to overwrite.");
return;
}
$stub = $this->getComponentStub();
$content = $this->replaceStubPlaceholders($stub, $className, $kebabName);
$this->files->ensureDirectoryExists(dirname($componentPath));
$this->files->put($componentPath, $content);
$this->info("✅ Component created: app/Livewire/{$className}.php");
}
protected function generateView(string $className, string $kebabName): void
{
$viewPath = resource_path("views/livewire/{$kebabName}.blade.php");
if ($this->files->exists($viewPath) && !$this->option('force')) {
$this->error("View {$kebabName}.blade.php already exists! Use --force to overwrite.");
return;
}
$stub = $this->getViewStub();
$content = $this->replaceViewPlaceholders($stub, $className, $kebabName);
$this->files->ensureDirectoryExists(dirname($viewPath));
$this->files->put($viewPath, $content);
$this->info("✅ View created: resources/views/livewire/{$kebabName}.blade.php");
}
protected function generateTest(string $className): void
{
$testPath = base_path("tests/Feature/Livewire/{$className}Test.php");
if ($this->files->exists($testPath) && !$this->option('force')) {
$this->error("Test {$className}Test already exists! Use --force to overwrite.");
return;
}
$stub = $this->getTestStub();
$content = $this->replaceTestPlaceholders($stub, $className);
$this->files->ensureDirectoryExists(dirname($testPath));
$this->files->put($testPath, $content);
$this->info("✅ Test created: tests/Feature/Livewire/{$className}Test.php");
}
protected function getComponentStub(): string
{
$traits = [];
$imports = ['use Livewire\Component;'];
$properties = [];
$methods = [];
// Add pagination if requested
if ($this->option('paginated')) {
$imports[] = 'use Livewire\WithPagination;';
$traits[] = 'WithPagination';
$properties[] = ' protected $paginationTheme = \'tailwind\';';
}
// Add caching optimization if requested
if ($this->option('cached') || $this->option('reusable')) {
$imports[] = 'use Illuminate\Support\Facades\Cache;';
$methods[] = $this->getCachingMethods();
}
// Build traits string
$traitsString = empty($traits) ? '' : "\n use " . implode(', ', $traits) . ";\n";
// Build properties string
$propertiesString = empty($properties) ? '' : "\n" . implode("\n", $properties) . "\n";
// Build methods string
$methodsString = implode("\n\n", $methods);
return <<<PHP
<?php
namespace App\Livewire;
{IMPORTS}
class {CLASS_NAME} extends Component
{{TRAITS}{PROPERTIES}
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.{VIEW_NAME}');
}{METHODS}
}
PHP;
}
protected function getViewStub(): string
{
if ($this->option('reusable')) {
return <<<BLADE
{{-- ThrillWiki Reusable Component: {CLASS_NAME} --}}
<div class="thrillwiki-component"
x-data="{ loading: false }"
wire:loading.class="opacity-50">
{{-- Component Header --}}
<div class="component-header mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{CLASS_NAME}
</h3>
</div>
{{-- Component Content --}}
<div class="component-content">
<p class="text-gray-600 dark:text-gray-400">
ThrillWiki component content goes here.
</p>
{{-- Example interactive element --}}
<button wire:click="\$refresh"
class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
Refresh Component
</button>
</div>
{{-- Loading State --}}
<div wire:loading wire:target="\$refresh"
class="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
</div>
BLADE;
}
return <<<BLADE
{{-- ThrillWiki Component: {CLASS_NAME} --}}
<div class="thrillwiki-component">
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
{CLASS_NAME}
</h3>
<p class="text-gray-600 dark:text-gray-400">
ThrillWiki component content goes here.
</p>
</div>
BLADE;
}
protected function getTestStub(): string
{
return <<<PHP
<?php
namespace Tests\Feature\Livewire;
use App\Livewire\{CLASS_NAME};
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
class {CLASS_NAME}Test extends TestCase
{
use RefreshDatabase;
/** @test */
public function component_can_render(): void
{
Livewire::test({CLASS_NAME}::class)
->assertStatus(200)
->assertSee('{CLASS_NAME}');
}
/** @test */
public function component_can_mount_successfully(): void
{
Livewire::test({CLASS_NAME}::class)
->assertStatus(200);
}
/** @test */
public function component_follows_thrillwiki_patterns(): void
{
Livewire::test({CLASS_NAME}::class)
->assertViewIs('livewire.{VIEW_NAME}');
}
}
PHP;
}
protected function getCachingMethods(): string
{
return <<<PHP
/**
* Get cache key for this component
*/
protected function getCacheKey(string \$suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . \$suffix;
}
/**
* Remember data with caching
*/
protected function remember(string \$key, \$callback, int \$ttl = 3600)
{
return Cache::remember(\$this->getCacheKey(\$key), \$ttl, \$callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string \$key = null): void
{
if (\$key) {
Cache::forget(\$this->getCacheKey(\$key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
PHP;
}
protected function replaceStubPlaceholders(string $stub, string $className, string $kebabName): string
{
$imports = ['use Livewire\Component;'];
$traits = [];
if ($this->option('paginated')) {
$imports[] = 'use Livewire\WithPagination;';
$traits[] = 'WithPagination';
}
if ($this->option('cached') || $this->option('reusable')) {
$imports[] = 'use Illuminate\Support\Facades\Cache;';
}
$traitsString = empty($traits) ? '' : "\n use " . implode(', ', $traits) . ";\n";
$importsString = implode("\n", $imports);
$methodsString = '';
if ($this->option('cached') || $this->option('reusable')) {
$methodsString = "\n\n" . $this->getCachingMethods();
}
return str_replace(
['{IMPORTS}', '{CLASS_NAME}', '{VIEW_NAME}', '{TRAITS}', '{PROPERTIES}', '{METHODS}'],
[$importsString, $className, $kebabName, $traitsString, '', $methodsString],
$stub
);
}
protected function replaceViewPlaceholders(string $stub, string $className, string $kebabName): string
{
return str_replace(
['{CLASS_NAME}', '{VIEW_NAME}'],
[$className, $kebabName],
$stub
);
}
protected function replaceTestPlaceholders(string $stub, string $className): string
{
return str_replace(
['{CLASS_NAME}', '{VIEW_NAME}'],
[$className, Str::kebab($className)],
$stub
);
}
protected function displaySummary(string $className, string $kebabName): void
{
$this->newLine();
$this->info("🎉 ThrillWiki Livewire Component '{$className}' created successfully!");
$this->newLine();
$this->comment("📁 Files Generated:");
$this->line(" • app/Livewire/{$className}.php");
$this->line(" • resources/views/livewire/{$kebabName}.blade.php");
if ($this->option('with-tests')) {
$this->line(" • tests/Feature/Livewire/{$className}Test.php");
}
$this->newLine();
$this->comment("🚀 Features Added:");
if ($this->option('reusable')) {
$this->line(" • Reusable component patterns with optimization traits");
}
if ($this->option('cached')) {
$this->line(" • Caching optimization methods");
}
if ($this->option('paginated')) {
$this->line(" • Pagination support with Tailwind theme");
}
if ($this->option('with-tests')) {
$this->line(" • Automated test suite with ThrillWiki patterns");
}
$this->newLine();
$this->comment("📝 Next Steps:");
$this->line(" 1. Customize the component logic in app/Livewire/{$className}.php");
$this->line(" 2. Update the view template in resources/views/livewire/{$kebabName}.blade.php");
$this->line(" 3. Include the component in your templates with <livewire:{$kebabName} />");
if ($this->option('with-tests')) {
$this->line(" 4. Run tests with: php artisan test --filter {$className}Test");
}
$this->newLine();
$this->info("✨ Happy coding with ThrillWiki acceleration patterns!");
}
}

View File

@@ -0,0 +1,857 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\File;
class MakeThrillWikiModel extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'make:thrillwiki-model {name : The name of the model}
{--migration : Generate a migration file}
{--factory : Generate a model factory}
{--with-relationships : Include common ThrillWiki relationships}
{--cached : Add caching traits and methods}
{--api-resource : Generate API resource class}
{--with-tests : Generate model tests}
{--force : Overwrite existing files}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate a ThrillWiki model with optimized patterns, traits, and optional related files';
/**
* ThrillWiki traits for different model types
*/
protected array $thrillWikiTraits = [
'HasLocation' => 'App\\Traits\\HasLocation',
'HasSlugHistory' => 'App\\Traits\\HasSlugHistory',
'HasStatistics' => 'App\\Traits\\HasStatistics',
'HasCaching' => 'App\\Traits\\HasCaching',
'HasSoftDeletes' => 'Illuminate\\Database\\Eloquent\\SoftDeletes',
'HasFactory' => 'Illuminate\\Database\\Eloquent\\Factories\\HasFactory',
];
/**
* Common ThrillWiki relationships by model type
*/
protected array $relationshipPatterns = [
'Park' => [
'areas' => 'hasMany:ParkArea',
'rides' => 'hasManyThrough:Ride,ParkArea',
'operator' => 'belongsTo:Operator',
'photos' => 'morphMany:Photo',
'reviews' => 'morphMany:Review',
],
'Ride' => [
'park' => 'belongsTo:Park',
'area' => 'belongsTo:ParkArea',
'manufacturer' => 'belongsTo:Manufacturer',
'designer' => 'belongsTo:Designer',
'photos' => 'morphMany:Photo',
'reviews' => 'morphMany:Review',
],
'Operator' => [
'parks' => 'hasMany:Park',
],
'Manufacturer' => [
'rides' => 'hasMany:Ride,manufacturer_id',
],
'Designer' => [
'rides' => 'hasMany:Ride,designer_id',
],
'Review' => [
'user' => 'belongsTo:User',
'reviewable' => 'morphTo',
],
];
/**
* Execute the console command.
*/
public function handle()
{
$this->info('🚀 Generating ThrillWiki Model for: ' . $this->argument('name'));
$name = $this->argument('name');
$className = Str::studly($name);
$tableName = Str::snake(Str::plural($name));
// Generate model
$this->generateModel($className);
// Generate optional files
if ($this->option('migration')) {
$this->generateMigration($className, $tableName);
}
if ($this->option('factory')) {
$this->generateFactory($className);
}
if ($this->option('api-resource')) {
$this->generateApiResource($className);
}
if ($this->option('with-tests')) {
$this->generateTests($className);
}
$this->displaySummary($className);
return Command::SUCCESS;
}
/**
* Generate the model file
*/
protected function generateModel(string $className): void
{
$modelPath = app_path("Models/{$className}.php");
if (File::exists($modelPath) && !$this->option('force')) {
$this->error("Model {$className} already exists! Use --force to overwrite.");
return;
}
$modelContent = $this->buildModelContent($className);
$this->ensureDirectoryExists(dirname($modelPath));
File::put($modelPath, $modelContent);
$this->line("✅ Model created: app/Models/{$className}.php");
}
/**
* Build the model content with ThrillWiki patterns
*/
protected function buildModelContent(string $className): string
{
$tableName = Str::snake(Str::plural($className));
$traits = $this->getTraitsForModel($className);
$relationships = $this->getRelationshipsForModel($className);
$cachingMethods = $this->option('cached') ? $this->getCachingMethods($className) : '';
$stub = <<<'PHP'
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
{TRAIT_IMPORTS}
/**
* {CLASS_NAME} Model
*
* Generated by ThrillWiki Model Generator
* Includes ThrillWiki optimization patterns and performance enhancements
*/
class {CLASS_NAME} extends Model
{
{TRAITS}
/**
* The table associated with the model.
*
* @var string
*/
protected $table = '{TABLE_NAME}';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'description',
'is_active',
// Add more fillable attributes as needed
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'is_active' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
// Add more casts as needed
];
/**
* The attributes that should be hidden for arrays.
*
* @var array<int, string>
*/
protected $hidden = [
// Add hidden attributes if needed
];
// Query Scopes
/**
* Scope a query to only include active records.
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Scope for optimized queries with common relationships.
*/
public function scopeOptimized($query)
{
return $query->with($this->getOptimizedRelations());
}
// ThrillWiki Methods
/**
* Get optimized relations for this model.
*/
public function getOptimizedRelations(): array
{
return [
// Define common relationships to eager load
];
}
/**
* Get cache key for this model instance.
*/
public function getCacheKey(string $suffix = ''): string
{
$key = strtolower(class_basename($this)) . '.' . $this->id;
return $suffix ? $key . '.' . $suffix : $key;
}
{RELATIONSHIPS}
{CACHING_METHODS}
}
PHP;
return str_replace([
'{CLASS_NAME}',
'{TABLE_NAME}',
'{TRAIT_IMPORTS}',
'{TRAITS}',
'{RELATIONSHIPS}',
'{CACHING_METHODS}',
], [
$className,
$tableName,
$this->buildTraitImports($traits),
$this->buildTraitUses($traits),
$relationships,
$cachingMethods,
], $stub);
}
/**
* Get traits for the model based on options and model type
*/
protected function getTraitsForModel(string $className): array
{
$traits = ['HasFactory']; // Always include HasFactory
// Add SoftDeletes for most models
$traits[] = 'HasSoftDeletes';
// Add caching if requested
if ($this->option('cached')) {
$traits[] = 'HasCaching';
}
// Add location trait for location-based models
if (in_array($className, ['Park', 'Company', 'ParkArea'])) {
$traits[] = 'HasLocation';
}
// Add slug history for main entities
if (in_array($className, ['Park', 'Ride', 'Company', 'Designer'])) {
$traits[] = 'HasSlugHistory';
}
// Add statistics for countable entities
if (in_array($className, ['Park', 'Ride', 'User'])) {
$traits[] = 'HasStatistics';
}
return $traits;
}
/**
* Build trait import statements
*/
protected function buildTraitImports(array $traits): string
{
$imports = [];
foreach ($traits as $trait) {
if (isset($this->thrillWikiTraits[$trait])) {
$imports[] = "use {$this->thrillWikiTraits[$trait]};";
}
}
return implode("\n", $imports);
}
/**
* Build trait use statements
*/
protected function buildTraitUses(array $traits): string
{
$uses = array_map(function($trait) {
return " use {$trait};";
}, $traits);
return implode("\n", $uses);
}
/**
* Get relationships for the model
*/
protected function getRelationshipsForModel(string $className): string
{
if (!$this->option('with-relationships')) {
return '';
}
if (!isset($this->relationshipPatterns[$className])) {
return '';
}
$relationships = [];
foreach ($this->relationshipPatterns[$className] as $method => $definition) {
$relationships[] = $this->buildRelationshipMethod($method, $definition);
}
return "\n // Relationships\n\n" . implode("\n\n", $relationships);
}
/**
* Build a relationship method
*/
protected function buildRelationshipMethod(string $method, string $definition): string
{
[$type, $model, $foreignKey] = array_pad(explode(',', str_replace(':', ',', $definition)), 3, null);
$methodBody = match($type) {
'hasMany' => $foreignKey ?
"return \$this->hasMany({$model}::class, '{$foreignKey}');" :
"return \$this->hasMany({$model}::class);",
'belongsTo' => $foreignKey ?
"return \$this->belongsTo({$model}::class, '{$foreignKey}');" :
"return \$this->belongsTo({$model}::class);",
'hasManyThrough' => "return \$this->hasManyThrough({$model}::class, {$foreignKey}::class);",
'morphMany' => "return \$this->morphMany({$model}::class, 'morphable');",
'morphTo' => "return \$this->morphTo();",
default => "return \$this->{$type}({$model}::class);"
};
return " /**\n * Get the {$method} relationship.\n */\n public function {$method}()\n {\n {$methodBody}\n }";
}
/**
* Get caching methods for the model
*/
protected function getCachingMethods(string $className): string
{
return <<<'PHP'
// Caching Methods
/**
* Remember a value in cache with model-specific key.
*/
public function remember(string $key, $callback, int $ttl = 3600)
{
return cache()->remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate cache for this model.
*/
public function invalidateCache(string $key = null): void
{
if ($key) {
cache()->forget($this->getCacheKey($key));
} else {
// Clear all cache keys for this model
cache()->forget($this->getCacheKey());
}
}
/**
* Boot method to handle cache invalidation.
*/
protected static function boot()
{
parent::boot();
static::saved(function ($model) {
$model->invalidateCache();
});
static::deleted(function ($model) {
$model->invalidateCache();
});
}
PHP;
}
/**
* Generate migration file
*/
protected function generateMigration(string $className, string $tableName): void
{
$migrationName = 'create_' . $tableName . '_table';
$timestamp = date('Y_m_d_His');
$migrationFile = database_path("migrations/{$timestamp}_{$migrationName}.php");
$migrationContent = $this->buildMigrationContent($className, $tableName, $migrationName);
File::put($migrationFile, $migrationContent);
$this->line("✅ Migration created: database/migrations/{$timestamp}_{$migrationName}.php");
}
/**
* Build migration content
*/
protected function buildMigrationContent(string $className, string $tableName, string $migrationName): string
{
$migrationClass = Str::studly($migrationName);
$stub = <<<'PHP'
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('{TABLE_NAME}', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->boolean('is_active')->default(true);
// Add common ThrillWiki fields
$table->string('slug')->unique();
// Add indexes for performance
$table->index(['is_active']);
$table->index(['name']);
$table->index(['slug']);
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('{TABLE_NAME}');
}
};
PHP;
return str_replace('{TABLE_NAME}', $tableName, $stub);
}
/**
* Generate factory file
*/
protected function generateFactory(string $className): void
{
$factoryPath = database_path("factories/{$className}Factory.php");
if (File::exists($factoryPath) && !$this->option('force')) {
$this->error("Factory {$className}Factory already exists! Use --force to overwrite.");
return;
}
$factoryContent = $this->buildFactoryContent($className);
$this->ensureDirectoryExists(dirname($factoryPath));
File::put($factoryPath, $factoryContent);
$this->line("✅ Factory created: database/factories/{$className}Factory.php");
}
/**
* Build factory content
*/
protected function buildFactoryContent(string $className): string
{
$stub = <<<'PHP'
<?php
namespace Database\Factories;
use App\Models\{CLASS_NAME};
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\{CLASS_NAME}>
*/
class {CLASS_NAME}Factory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = {CLASS_NAME}::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$name = $this->faker->unique()->words(2, true);
return [
'name' => $name,
'slug' => Str::slug($name),
'description' => $this->faker->paragraphs(2, true),
'is_active' => $this->faker->boolean(90), // 90% chance of being active
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
'updated_at' => function (array $attributes) {
return $this->faker->dateTimeBetween($attributes['created_at'], 'now');
},
];
}
/**
* Indicate that the model is active.
*/
public function active(): static
{
return $this->state(fn (array $attributes) => [
'is_active' => true,
]);
}
/**
* Indicate that the model is inactive.
*/
public function inactive(): static
{
return $this->state(fn (array $attributes) => [
'is_active' => false,
]);
}
}
PHP;
return str_replace('{CLASS_NAME}', $className, $stub);
}
/**
* Generate API resource
*/
protected function generateApiResource(string $className): void
{
$resourcePath = app_path("Http/Resources/{$className}Resource.php");
if (File::exists($resourcePath) && !$this->option('force')) {
$this->error("Resource {$className}Resource already exists! Use --force to overwrite.");
return;
}
$resourceContent = $this->buildApiResourceContent($className);
$this->ensureDirectoryExists(dirname($resourcePath));
File::put($resourcePath, $resourceContent);
$this->line("✅ API Resource created: app/Http/Resources/{$className}Resource.php");
}
/**
* Build API resource content
*/
protected function buildApiResourceContent(string $className): string
{
$stub = <<<'PHP'
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* {CLASS_NAME} API Resource
*
* Transforms {CLASS_NAME} model data for API responses
* Includes ThrillWiki optimization patterns
*/
class {CLASS_NAME}Resource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'description' => $this->description,
'is_active' => $this->is_active,
'created_at' => $this->created_at?->toISOString(),
'updated_at' => $this->updated_at?->toISOString(),
// Include relationships when loaded
$this->mergeWhen($this->relationLoaded('relationships'), [
// Add relationship data here
]),
];
}
/**
* Get additional data that should be returned with the resource array.
*
* @return array<string, mixed>
*/
public function with(Request $request): array
{
return [
'meta' => [
'model' => '{CLASS_NAME}',
'generated_at' => now()->toISOString(),
],
];
}
}
PHP;
return str_replace('{CLASS_NAME}', $className, $stub);
}
/**
* Generate test files
*/
protected function generateTests(string $className): void
{
$testPath = base_path("tests/Feature/{$className}Test.php");
if (File::exists($testPath) && !$this->option('force')) {
$this->error("Test {$className}Test already exists! Use --force to overwrite.");
return;
}
$testContent = $this->buildTestContent($className);
$this->ensureDirectoryExists(dirname($testPath));
File::put($testPath, $testContent);
$this->line("✅ Test created: tests/Feature/{$className}Test.php");
}
/**
* Build test content
*/
protected function buildTestContent(string $className): string
{
$stub = <<<'PHP'
<?php
namespace Tests\Feature;
use App\Models\{CLASS_NAME};
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
/**
* {CLASS_NAME} Model Feature Tests
*
* Tests for ThrillWiki {CLASS_NAME} model functionality
*/
class {CLASS_NAME}Test extends TestCase
{
use RefreshDatabase, WithFaker;
/**
* Test model creation.
*/
public function test_can_create_{LOWER_CLASS_NAME}(): void
{
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
$this->assertDatabaseHas('{TABLE_NAME}', [
'id' => ${LOWER_CLASS_NAME}->id,
'name' => ${LOWER_CLASS_NAME}->name,
]);
}
/**
* Test model factory.
*/
public function test_{LOWER_CLASS_NAME}_factory_works(): void
{
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
$this->assertInstanceOf({CLASS_NAME}::class, ${LOWER_CLASS_NAME});
$this->assertNotEmpty(${LOWER_CLASS_NAME}->name);
$this->assertIsBool(${LOWER_CLASS_NAME}->is_active);
}
/**
* Test active scope.
*/
public function test_active_scope_filters_correctly(): void
{
{CLASS_NAME}::factory()->active()->create();
{CLASS_NAME}::factory()->inactive()->create();
$activeCount = {CLASS_NAME}::active()->count();
$totalCount = {CLASS_NAME}::count();
$this->assertEquals(1, $activeCount);
$this->assertEquals(2, $totalCount);
}
/**
* Test cache key generation.
*/
public function test_cache_key_generation(): void
{
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
$cacheKey = ${LOWER_CLASS_NAME}->getCacheKey();
$expectedKey = strtolower('{LOWER_CLASS_NAME}') . '.' . ${LOWER_CLASS_NAME}->id;
$this->assertEquals($expectedKey, $cacheKey);
}
/**
* Test cache key with suffix.
*/
public function test_cache_key_with_suffix(): void
{
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
$cacheKey = ${LOWER_CLASS_NAME}->getCacheKey('details');
$expectedKey = strtolower('{LOWER_CLASS_NAME}') . '.' . ${LOWER_CLASS_NAME}->id . '.details';
$this->assertEquals($expectedKey, $cacheKey);
}
/**
* Test soft deletes.
*/
public function test_soft_deletes_work(): void
{
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
${LOWER_CLASS_NAME}->delete();
$this->assertSoftDeleted(${LOWER_CLASS_NAME});
// Test that it's excluded from normal queries
$this->assertEquals(0, {CLASS_NAME}::count());
// Test that it's included in withTrashed queries
$this->assertEquals(1, {CLASS_NAME}::withTrashed()->count());
}
}
PHP;
return str_replace([
'{CLASS_NAME}',
'{LOWER_CLASS_NAME}',
'{TABLE_NAME}',
], [
$className,
strtolower($className),
Str::snake(Str::plural($className)),
], $stub);
}
/**
* Ensure directory exists
*/
protected function ensureDirectoryExists(string $directory): void
{
if (!File::isDirectory($directory)) {
File::makeDirectory($directory, 0755, true);
}
}
/**
* Display summary of generated files
*/
protected function displaySummary(string $className): void
{
$this->newLine();
$this->info("🎉 ThrillWiki Model '{$className}' created successfully!");
$this->newLine();
$this->line("📁 Files Generated:");
$this->line(" • app/Models/{$className}.php");
if ($this->option('migration')) {
$this->line(" • database/migrations/[timestamp]_create_" . Str::snake(Str::plural($className)) . "_table.php");
}
if ($this->option('factory')) {
$this->line(" • database/factories/{$className}Factory.php");
}
if ($this->option('api-resource')) {
$this->line(" • app/Http/Resources/{$className}Resource.php");
}
if ($this->option('with-tests')) {
$this->line(" • tests/Feature/{$className}Test.php");
}
$this->newLine();
$this->line("🚀 Next Steps:");
if ($this->option('migration')) {
$this->line(" 1. Run migration: php artisan migrate");
}
if ($this->option('with-tests')) {
$this->line(" 2. Run tests: php artisan test --filter {$className}Test");
}
$this->line(" 3. Customize model attributes and relationships");
$this->line(" 4. Update migration with specific fields");
if ($this->option('factory')) {
$this->line(" 5. Customize factory with realistic data");
}
$this->newLine();
}
}

27
app/Console/Kernel.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\DesignerResource\Pages;
use App\Filament\Resources\DesignerResource\RelationManagers;
use App\Models\Designer;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class DesignerResource extends Resource
{
protected static ?string $model = Designer::class;
protected static ?string $navigationIcon = 'heroicon-o-building-office';
protected static ?string $navigationGroup = 'Company Management';
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make('Basic Information')
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(fn ($state, Forms\Set $set) =>
$set('slug', str($state)->slug())),
Forms\Components\TextInput::make('slug')
->required()
->maxLength(255)
->unique(ignoreRecord: true),
Forms\Components\TextInput::make('headquarters')
->maxLength(255),
Forms\Components\DatePicker::make('founded_date')
->label('Founded Date')
->format('Y-m-d'),
])->columns(2),
Forms\Components\Section::make('Additional Details')
->schema([
Forms\Components\TextInput::make('website')
->url()
->prefix('https://')
->maxLength(255),
Forms\Components\RichEditor::make('description')
->columnSpanFull()
->toolbarButtons([
'bold',
'italic',
'link',
'bulletList',
'orderedList',
'h2',
'h3',
]),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('headquarters')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('founded_date')
->date()
->sortable(),
Tables\Columns\TextColumn::make('rides_count')
->counts('rides')
->label('Rides')
->sortable(),
Tables\Columns\TextColumn::make('website')
->searchable()
->url(fn ($state) => str($state)->start('https://')),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(),
])
->filters([
Tables\Filters\Filter::make('has_rides')
->query(fn (Builder $query) => $query->has('rides'))
->label('Has Rides'),
Tables\Filters\Filter::make('no_rides')
->query(fn (Builder $query) => $query->doesntHave('rides'))
->label('No Rides'),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\ViewAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
RelationManagers\RidesRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListDesigners::route('/'),
'create' => Pages\CreateDesigner::route('/create'),
'edit' => Pages\EditDesigner::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Filament\Resources\DesignerResource\Pages;
use App\Filament\Resources\DesignerResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateDesigner extends CreateRecord
{
protected static string $resource = DesignerResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DesignerResource\Pages;
use App\Filament\Resources\DesignerResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditDesigner extends EditRecord
{
protected static string $resource = DesignerResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DesignerResource\Pages;
use App\Filament\Resources\DesignerResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListDesigners extends ListRecords
{
protected static string $resource = DesignerResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Filament\Resources\DesignerResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class RidesRelationManager extends RelationManager
{
protected static string $relationship = 'rides';
protected static ?string $title = 'Rides';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('manufacturer_name')
->maxLength(255),
Forms\Components\TextInput::make('model_name')
->maxLength(255),
Forms\Components\DatePicker::make('opened_date')
->label('Opening Date'),
Forms\Components\DatePicker::make('closed_date')
->label('Closing Date')
->after('opened_date'),
Forms\Components\Textarea::make('description')
->columnSpanFull(),
Forms\Components\Toggle::make('is_active')
->label('Active')
->default(true),
]);
}
public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('manufacturer_name')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('opened_date')
->date()
->sortable(),
Tables\Columns\TextColumn::make('closed_date')
->date()
->sortable(),
Tables\Columns\IconColumn::make('is_active')
->boolean()
->sortable(),
])
->filters([
Tables\Filters\TrashedFilter::make(),
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\RideResource\Pages;
use App\Filament\Resources\RideResource\RelationManagers;
use App\Models\Ride;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class RideResource extends Resource
{
protected static ?string $model = Ride::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('slug')
->required()
->maxLength(255),
Forms\Components\Textarea::make('description')
->required()
->columnSpanFull(),
Forms\Components\Select::make('park_id')
->relationship('park', 'name')
->required(),
Forms\Components\Select::make('park_area_id')
->relationship('parkArea', 'name'),
Forms\Components\Select::make('manufacturer_id')
->relationship('manufacturer', 'name'),
Forms\Components\Select::make('designer_id')
->relationship('designer', 'name'),
Forms\Components\Select::make('ride_model_id')
->relationship('rideModel', 'name'),
Forms\Components\TextInput::make('category')
->required()
->maxLength(2)
->default(''),
Forms\Components\TextInput::make('status')
->required()
->maxLength(20)
->default('OPERATING'),
Forms\Components\TextInput::make('post_closing_status')
->maxLength(20),
Forms\Components\DatePicker::make('opening_date'),
Forms\Components\DatePicker::make('closing_date'),
Forms\Components\DatePicker::make('status_since'),
Forms\Components\TextInput::make('min_height_in')
->numeric(),
Forms\Components\TextInput::make('max_height_in')
->numeric(),
Forms\Components\TextInput::make('capacity_per_hour')
->numeric(),
Forms\Components\TextInput::make('ride_duration_seconds')
->numeric(),
Forms\Components\TextInput::make('average_rating')
->numeric(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('slug')
->searchable(),
Tables\Columns\TextColumn::make('park.name')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('parkArea.name')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('manufacturer.name')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('designer.name')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('rideModel.name')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('category')
->searchable(),
Tables\Columns\TextColumn::make('status')
->searchable(),
Tables\Columns\TextColumn::make('post_closing_status')
->searchable(),
Tables\Columns\TextColumn::make('opening_date')
->date()
->sortable(),
Tables\Columns\TextColumn::make('closing_date')
->date()
->sortable(),
Tables\Columns\TextColumn::make('status_since')
->date()
->sortable(),
Tables\Columns\TextColumn::make('min_height_in')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('max_height_in')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('capacity_per_hour')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('ride_duration_seconds')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('average_rating')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListRides::route('/'),
'create' => Pages\CreateRide::route('/create'),
'edit' => Pages\EditRide::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Filament\Resources\RideResource\Pages;
use App\Filament\Resources\RideResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateRide extends CreateRecord
{
protected static string $resource = RideResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\RideResource\Pages;
use App\Filament\Resources\RideResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditRide extends EditRecord
{
protected static string $resource = RideResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\RideResource\Pages;
use App\Filament\Resources\RideResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListRides extends ListRecords
{
protected static string $resource = RideResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Operator;
use App\Http\Requests\OperatorRequest;
use App\Http\Resources\OperatorResource;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class OperatorController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): JsonResponse
{
$query = Operator::query();
// Search functionality
if ($request->filled('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('name', 'ILIKE', "%{$search}%")
->orWhere('description', 'ILIKE', "%{$search}%");
});
}
// Filter by status
if ($request->filled('status')) {
$query->where('is_active', $request->get('status') === 'active');
}
$operators = $query->latest()->paginate(15);
return response()->json([
'data' => OperatorResource::collection($operators),
'meta' => [
'current_page' => $operators->currentPage(),
'last_page' => $operators->lastPage(),
'per_page' => $operators->perPage(),
'total' => $operators->total(),
]
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(OperatorRequest $request): JsonResponse
{
$operator = Operator::create($request->validated());
return response()->json([
'message' => 'Operator created successfully',
'data' => new OperatorResource($operator)
], 201);
}
/**
* Display the specified resource.
*/
public function show(Operator $operator): JsonResponse
{
return response()->json([
'data' => new OperatorResource($operator)
]);
}
/**
* Update the specified resource in storage.
*/
public function update(OperatorRequest $request, Operator $operator): JsonResponse
{
$operator->update($request->validated());
return response()->json([
'message' => 'Operator updated successfully',
'data' => new OperatorResource($operator)
]);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Operator $operator): JsonResponse
{
$operator->delete();
return response()->json([
'message' => 'Operator deleted successfully'
]);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Ride;
use App\Http\Requests\RideRequest;
use App\Http\Resources\RideResource;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class RideController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): JsonResponse
{
$query = Ride::query();
// Search functionality
if ($request->filled('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('name', 'ILIKE', "%{$search}%")
->orWhere('description', 'ILIKE', "%{$search}%");
});
}
// Filter by status
if ($request->filled('status')) {
$query->where('is_active', $request->get('status') === 'active');
}
$rides = $query->latest()->paginate(15);
return response()->json([
'data' => RideResource::collection($rides),
'meta' => [
'current_page' => $rides->currentPage(),
'last_page' => $rides->lastPage(),
'per_page' => $rides->perPage(),
'total' => $rides->total(),
]
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(RideRequest $request): JsonResponse
{
$ride = Ride::create($request->validated());
return response()->json([
'message' => 'Ride created successfully',
'data' => new RideResource($ride)
], 201);
}
/**
* Display the specified resource.
*/
public function show(Ride $ride): JsonResponse
{
return response()->json([
'data' => new RideResource($ride)
]);
}
/**
* Update the specified resource in storage.
*/
public function update(RideRequest $request, Ride $ride): JsonResponse
{
$ride->update($request->validated());
return response()->json([
'message' => 'Ride updated successfully',
'data' => new RideResource($ride)
]);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Ride $ride): JsonResponse
{
$ride->delete();
return response()->json([
'message' => 'Ride deleted successfully'
]);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers;
use App\Models\Operator;
use App\Http\Requests\OperatorRequest;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
class OperatorController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): View
{
$query = Operator::query();
// Search functionality
if ($request->filled('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('name', 'ILIKE', "%{$search}%")
->orWhere('description', 'ILIKE', "%{$search}%");
});
}
// Filter by status
if ($request->filled('status')) {
$query->where('is_active', $request->get('status') === 'active');
}
$operators = $query->latest()->paginate(15)->withQueryString();
return view('operators.index', compact('operators'));
}
/**
* Show the form for creating a new resource.
*/
public function create(): View
{
return view('operators.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(OperatorRequest $request): RedirectResponse
{
$operator = Operator::create($request->validated());
return redirect()
->route('operators.show', $operator)
->with('success', 'Operator created successfully!');
}
/**
* Display the specified resource.
*/
public function show(Operator $operator): View
{
return view('operators.show', compact('operator'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Operator $operator): View
{
return view('operators.edit', compact('operator'));
}
/**
* Update the specified resource in storage.
*/
public function update(OperatorRequest $request, Operator $operator): RedirectResponse
{
$operator->update($request->validated());
return redirect()
->route('operators.show', $operator)
->with('success', 'Operator updated successfully!');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Operator $operator): RedirectResponse
{
$operator->delete();
return redirect()
->route('operators.index')
->with('success', 'Operator deleted successfully!');
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers;
use App\Models\Park;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ParkController extends Controller
{
/**
* Display a listing of parks.
*/
public function index(): View
{
return view('parks.index');
}
/**
* Show the form for creating a new park.
*/
public function create(): View
{
return view('parks.create');
}
/**
* Display the specified park.
*/
public function show(Park $park): View
{
// Load relationships for the park detail page
$park->load([
'operator',
'location',
'areas.rides' => function ($query) {
$query->orderBy('position')->orderBy('name');
},
'areas' => function ($query) {
$query->orderBy('position')->orderBy('name');
},
'photos' => function ($query) {
$query->orderBy('is_featured', 'desc')->orderBy('created_at', 'desc');
}
]);
return view('parks.show', compact('park'));
}
/**
* Show the form for editing the specified park.
*/
public function edit(Park $park): View
{
return view('parks.edit', compact('park'));
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers;
use App\Models\Ride;
use App\Http\Requests\RideRequest;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
class RideController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): View
{
$query = Ride::query();
// Search functionality
if ($request->filled('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('name', 'ILIKE', "%{$search}%")
->orWhere('description', 'ILIKE', "%{$search}%");
});
}
// Filter by status
if ($request->filled('status')) {
$query->where('is_active', $request->get('status') === 'active');
}
$rides = $query->latest()->paginate(15)->withQueryString();
return view('rides.index', compact('rides'));
}
/**
* Show the form for creating a new resource.
*/
public function create(): View
{
return view('rides.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(RideRequest $request): RedirectResponse
{
$ride = Ride::create($request->validated());
return redirect()
->route('rides.show', $ride)
->with('success', 'Ride created successfully!');
}
/**
* Display the specified resource.
*/
public function show(Ride $ride): View
{
return view('rides.show', compact('ride'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Ride $ride): View
{
return view('rides.edit', compact('ride'));
}
/**
* Update the specified resource in storage.
*/
public function update(RideRequest $request, Ride $ride): RedirectResponse
{
$ride->update($request->validated());
return redirect()
->route('rides.show', $ride)
->with('success', 'Ride updated successfully!');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Ride $ride): RedirectResponse
{
$ride->delete();
return redirect()
->route('rides.index')
->with('success', 'Ride deleted successfully!');
}
}

65
app/Http/Kernel.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
protected $except = [
//
];
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
protected $except = [
//
];
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
protected $proxies;
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
protected $except = [
//
];
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class OperatorRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Add authorization logic as needed
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'is_active' => ['boolean'],
];
// For updates, make name unique except for current record
if ($this->route('operator')) {
$rules['name'][] = 'unique:operators,name,' . $this->route('operator')->id;
} else {
$rules['name'][] = 'unique:operators,name';
}
return $rules;
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'name.required' => 'The operator name is required.',
'name.unique' => 'A operator with this name already exists.',
];
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ParkRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Add authorization logic as needed
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'is_active' => ['boolean'],
];
// For updates, make name unique except for current record
if ($this->route('park')) {
$rules['name'][] = 'unique:parks,name,' . $this->route('park')->id;
} else {
$rules['name'][] = 'unique:parks,name';
}
return $rules;
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'name.required' => 'The park name is required.',
'name.unique' => 'A park with this name already exists.',
];
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RideRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Add authorization logic as needed
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'is_active' => ['boolean'],
];
// For updates, make name unique except for current record
if ($this->route('ride')) {
$rules['name'][] = 'unique:rides,name,' . $this->route('ride')->id;
} else {
$rules['name'][] = 'unique:rides,name';
}
return $rules;
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'name.required' => 'The ride name is required.',
'name.unique' => 'A ride with this name already exists.',
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* Manufacturer API Resource
*
* Transforms Manufacturer model data for API responses
* Includes ThrillWiki optimization patterns
*/
class ManufacturerResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'description' => $this->description,
'is_active' => $this->is_active,
'created_at' => $this->created_at?->toISOString(),
'updated_at' => $this->updated_at?->toISOString(),
// Include relationships when loaded
$this->mergeWhen($this->relationLoaded('relationships'), [
// Add relationship data here
]),
];
}
/**
* Get additional data that should be returned with the resource array.
*
* @return array<string, mixed>
*/
public function with(Request $request): array
{
return [
'meta' => [
'model' => 'Manufacturer',
'generated_at' => now()->toISOString(),
],
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class OperatorResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'is_active' => $this->is_active,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class RideResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'is_active' => $this->is_active,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Livewire\Actions;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
class Logout
{
/**
* Log the current user out of the application.
*/
public function __invoke(): void
{
Auth::guard('web')->logout();
Session::invalidate();
Session::regenerateToken();
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Livewire;
use App\Models\Park;
use App\Models\Ride;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
class AutocompleteComponent extends Component
{
public string $query = '';
public string $type = 'park';
public array $suggestions = [];
public ?string $selectedId = null;
protected $queryString = [
'query' => ['except' => ''],
'type' => ['except' => 'park']
];
public function mount(string $type = 'park'): void
{
$this->type = $type;
}
public function render(): View
{
return view('livewire.autocomplete', [
'suggestions' => $this->suggestions
]);
}
public function updatedQuery(): void
{
if (strlen($this->query) < 2) {
$this->suggestions = [];
return;
}
$this->suggestions = match ($this->type) {
'park' => $this->getParkSuggestions(),
'ride' => $this->getRideSuggestions(),
default => [],
};
}
protected function getParkSuggestions(): array
{
return Park::query()
->select(['id', 'name', 'slug'])
->where('name', 'like', "%{$this->query}%")
->orderBy('name')
->limit(5)
->get()
->map(fn($park) => [
'id' => $park->id,
'text' => $park->name,
'url' => route('parks.show', $park->slug)
])
->toArray();
}
protected function getRideSuggestions(): array
{
return Ride::query()
->select(['id', 'name', 'slug', 'park_id'])
->with('park:id,name')
->where('name', 'like', "%{$this->query}%")
->orderBy('name')
->limit(5)
->get()
->map(fn($ride) => [
'id' => $ride->id,
'text' => "{$ride->name} at {$ride->park->name}",
'url' => route('rides.show', $ride->slug)
])
->toArray();
}
public function selectSuggestion(string $id): void
{
$this->selectedId = $id;
$this->dispatch('suggestion-selected', id: $id);
}
}

View File

@@ -0,0 +1,563 @@
<?php
namespace App\Livewire;
use App\Models\Designer;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class DesignersListingUniversal extends Component
{
use WithPagination;
// Universal Listing System Integration
public string $entityType = 'designers';
#[Url(as: 'q')]
public string $search = '';
#[Url(as: 'specialties')]
public array $specialties = [];
#[Url(as: 'style')]
public string $designStyle = '';
#[Url(as: 'founded_from')]
public string $foundedYearFrom = '';
#[Url(as: 'founded_to')]
public string $foundedYearTo = '';
#[Url(as: 'innovation_min')]
public string $minInnovationScore = '';
#[Url(as: 'innovation_max')]
public string $maxInnovationScore = '';
#[Url(as: 'active_years_min')]
public string $minActiveYears = '';
#[Url(as: 'active_years_max')]
public string $maxActiveYears = '';
#[Url(as: 'sort')]
public string $sortBy = 'name';
#[Url(as: 'dir')]
public string $sortDirection = 'asc';
#[Url(as: 'view')]
public string $viewMode = 'grid';
public int $perPage = 20;
public array $portfolioStats = [];
public array $innovationTimeline = [];
public array $collaborationNetworks = [];
protected $queryString = [
'search' => ['except' => ''],
'specialties' => ['except' => []],
'designStyle' => ['except' => ''],
'foundedYearFrom' => ['except' => ''],
'foundedYearTo' => ['except' => ''],
'minInnovationScore' => ['except' => ''],
'maxInnovationScore' => ['except' => ''],
'minActiveYears' => ['except' => ''],
'maxActiveYears' => ['except' => ''],
'sortBy' => ['except' => 'name'],
'sortDirection' => ['except' => 'asc'],
'viewMode' => ['except' => 'grid'],
'page' => ['except' => 1],
];
/**
* Component initialization
*/
public function mount(): void
{
$this->loadPortfolioStatistics();
$this->loadInnovationTimeline();
$this->loadCollaborationNetworks();
}
/**
* Load creative portfolio statistics with caching
*/
protected function loadPortfolioStatistics(): void
{
$this->portfolioStats = Cache::remember(
'designers.portfolio.stats',
now()->addHours(6),
fn() => $this->calculatePortfolioStatistics()
);
}
/**
* Load innovation timeline data with caching
*/
protected function loadInnovationTimeline(): void
{
$this->innovationTimeline = Cache::remember(
'designers.innovation.timeline',
now()->addHours(12),
fn() => $this->calculateInnovationTimeline()
);
}
/**
* Load collaboration networks with caching
*/
protected function loadCollaborationNetworks(): void
{
$this->collaborationNetworks = Cache::remember(
'designers.collaboration.networks',
now()->addHours(6),
fn() => $this->calculateCollaborationNetworks()
);
}
/**
* Calculate comprehensive portfolio statistics
*/
protected function calculatePortfolioStatistics(): array
{
return [
'total_designers' => Designer::active()->count(),
'coaster_designers' => Designer::active()->specialty('roller_coaster')->count(),
'dark_ride_designers' => Designer::active()->specialty('dark_ride')->count(),
'themed_experience_designers' => Designer::active()->specialty('themed_experience')->count(),
'water_attraction_designers' => Designer::active()->specialty('water_attraction')->count(),
'specialties_distribution' => Designer::active()
->select('specialty', DB::raw('count(*) as count'))
->whereNotNull('specialty')
->groupBy('specialty')
->orderByDesc('count')
->get()
->pluck('count', 'specialty')
->toArray(),
'average_innovation_score' => Designer::active()
->whereNotNull('innovation_score')
->avg('innovation_score'),
'total_designs' => Designer::active()
->withCount('rides')
->get()
->sum('rides_count'),
'top_innovators' => Designer::active()
->orderByDesc('innovation_score')
->take(5)
->get(['name', 'innovation_score', 'specialty'])
->toArray(),
'design_styles' => Designer::active()
->select('design_style', DB::raw('count(*) as count'))
->whereNotNull('design_style')
->groupBy('design_style')
->orderByDesc('count')
->get()
->pluck('count', 'design_style')
->toArray(),
];
}
/**
* Calculate innovation timeline data
*/
protected function calculateInnovationTimeline(): array
{
$timelineData = Designer::active()
->whereNotNull('founded_year')
->whereNotNull('innovation_score')
->select('founded_year', 'innovation_score', 'name', 'specialty')
->orderBy('founded_year')
->get()
->groupBy(function($designer) {
return floor($designer->founded_year / 10) * 10; // Group by decade
})
->map(function($decade) {
return [
'count' => $decade->count(),
'avg_innovation' => $decade->avg('innovation_score'),
'top_designer' => $decade->sortByDesc('innovation_score')->first(),
'specialties' => $decade->countBy('specialty')->toArray()
];
});
return [
'timeline' => $timelineData->toArray(),
'innovation_milestones' => Designer::active()
->where('innovation_score', '>=', 8.5)
->orderByDesc('innovation_score')
->take(10)
->get(['name', 'founded_year', 'innovation_score', 'specialty'])
->toArray(),
'breakthrough_years' => Designer::active()
->whereNotNull('founded_year')
->select('founded_year', DB::raw('count(*) as new_designers'), DB::raw('avg(innovation_score) as avg_innovation'))
->groupBy('founded_year')
->having('new_designers', '>=', 2)
->orderByDesc('avg_innovation')
->take(5)
->get()
->toArray(),
];
}
/**
* Calculate collaboration networks
*/
protected function calculateCollaborationNetworks(): array
{
return [
'collaboration_pairs' => Designer::active()
->whereHas('rides', function($query) {
$query->whereHas('park', function($parkQuery) {
$parkQuery->whereHas('rides', function($rideQuery) {
$rideQuery->whereNotNull('designer_id');
});
});
})
->with(['rides.park.rides.designer'])
->get()
->flatMap(function($designer) {
return $designer->rides->flatMap(function($ride) use ($designer) {
return $ride->park->rides
->where('designer_id', '!=', $designer->id)
->whereNotNull('designer_id')
->pluck('designer.name')
->map(function($collaborator) use ($designer, $ride) {
return [
'designer' => $designer->name,
'collaborator' => $collaborator,
'park' => $ride->park->name
];
});
});
})
->groupBy(function($item) {
$names = [$item['designer'], $item['collaborator']];
sort($names);
return implode(' + ', $names);
})
->map(function($collaborations) {
return [
'count' => $collaborations->count(),
'parks' => $collaborations->pluck('park')->unique()->values()->toArray()
];
})
->sortByDesc('count')
->take(10)
->toArray(),
'network_hubs' => Designer::active()
->withCount('rides')
->having('rides_count', '>=', 3)
->orderByDesc('rides_count')
->take(10)
->get(['name', 'specialty', 'rides_count'])
->toArray(),
'cross_specialty_projects' => Designer::active()
->whereHas('rides', function($query) {
$query->whereHas('park', function($parkQuery) {
$parkQuery->whereHas('rides', function($rideQuery) {
$rideQuery->whereHas('designer', function($designerQuery) {
$designerQuery->whereColumn('specialty', '!=', 'designers.specialty');
});
});
});
})
->with(['rides.park'])
->get()
->flatMap(function($designer) {
return $designer->rides->map(function($ride) use ($designer) {
return [
'designer' => $designer->name,
'specialty' => $designer->specialty,
'park' => $ride->park->name,
'ride' => $ride->name
];
});
})
->take(15)
->toArray(),
];
}
/**
* Django parity creative portfolio search functionality
*/
public function creativePortfolioSearch($query, $specialties = [])
{
return Designer::query()
->when($query, function ($q) use ($query) {
$terms = explode(' ', trim($query));
foreach ($terms as $term) {
if (strlen($term) >= 2) {
$q->where(function ($subQuery) use ($term) {
$subQuery->where('name', 'ilike', "%{$term}%")
->orWhere('description', 'ilike', "%{$term}%")
->orWhere('specialty', 'ilike', "%{$term}%")
->orWhere('design_style', 'ilike', "%{$term}%")
->orWhere('headquarters', 'ilike', "%{$term}%")
->orWhereHas('rides', function($rideQuery) use ($term) {
$rideQuery->where('name', 'ilike', "%{$term}%")
->orWhere('category', 'ilike', "%{$term}%");
})
->orWhereHas('rides.park', function($parkQuery) use ($term) {
$parkQuery->where('name', 'ilike', "%{$term}%");
});
});
}
}
})
->when($specialties, function ($q) use ($specialties) {
$q->whereIn('specialty', $specialties);
})
->active()
->with(['rides:id,designer_id,name,category,park_id', 'rides.park:id,name'])
->withCount(['rides']);
}
/**
* Apply creative and innovation filters
*/
public function applyCreativeFilters($query)
{
return $query
->when($this->designStyle, fn($q, $style) =>
$q->where('design_style', $style))
->when($this->foundedYearFrom, fn($q, $year) =>
$q->where('founded_year', '>=', $year))
->when($this->foundedYearTo, fn($q, $year) =>
$q->where('founded_year', '<=', $year))
->when($this->minInnovationScore, fn($q, $score) =>
$q->where('innovation_score', '>=', $score))
->when($this->maxInnovationScore, fn($q, $score) =>
$q->where('innovation_score', '<=', $score))
->when($this->minActiveYears, fn($q, $years) =>
$q->where('active_years', '>=', $years))
->when($this->maxActiveYears, fn($q, $years) =>
$q->where('active_years', '<=', $years));
}
/**
* Get designers with optimized caching
*/
public function getDesignersProperty()
{
$cacheKey = "designers.listing." . md5(serialize([
'search' => $this->search,
'specialties' => $this->specialties,
'designStyle' => $this->designStyle,
'foundedYearFrom' => $this->foundedYearFrom,
'foundedYearTo' => $this->foundedYearTo,
'minInnovationScore' => $this->minInnovationScore,
'maxInnovationScore' => $this->maxInnovationScore,
'minActiveYears' => $this->minActiveYears,
'maxActiveYears' => $this->maxActiveYears,
'sortBy' => $this->sortBy,
'sortDirection' => $this->sortDirection,
'page' => $this->getPage(),
'perPage' => $this->perPage,
]));
return Cache::remember($cacheKey, now()->addMinutes(30), function() {
$query = $this->creativePortfolioSearch($this->search, $this->specialties);
$query = $this->applyCreativeFilters($query);
// Apply sorting
switch ($this->sortBy) {
case 'name':
$query->orderBy('name', $this->sortDirection);
break;
case 'founded_year':
$query->orderBy('founded_year', $this->sortDirection);
break;
case 'innovation_score':
$query->orderBy('innovation_score', $this->sortDirection);
break;
case 'designed_rides_count':
$query->orderBy('rides_count', $this->sortDirection);
break;
case 'active_years':
$query->orderBy('active_years', $this->sortDirection);
break;
default:
$query->orderBy('name', 'asc');
}
return $query->paginate($this->perPage);
});
}
/**
* Update search and reset pagination
*/
public function updatedSearch(): void
{
$this->resetPage();
$this->invalidateCache();
}
/**
* Update specialties filter and reset pagination
*/
public function updatedSpecialties(): void
{
$this->resetPage();
$this->invalidateCache();
}
/**
* Update any filter and reset pagination
*/
public function updatedDesignStyle(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedFoundedYearFrom(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedFoundedYearTo(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedMinInnovationScore(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedMaxInnovationScore(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedMinActiveYears(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedMaxActiveYears(): void
{
$this->resetPage();
$this->invalidateCache();
}
/**
* Sort by specific column
*/
public function sortBy(string $column): void
{
if ($this->sortBy === $column) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $column;
$this->sortDirection = 'asc';
}
$this->resetPage();
$this->invalidateCache();
}
/**
* Change view mode
*/
public function setViewMode(string $mode): void
{
$this->viewMode = $mode;
}
/**
* Clear all filters
*/
public function clearFilters(): void
{
$this->search = '';
$this->specialties = [];
$this->designStyle = '';
$this->foundedYearFrom = '';
$this->foundedYearTo = '';
$this->minInnovationScore = '';
$this->maxInnovationScore = '';
$this->minActiveYears = '';
$this->maxActiveYears = '';
$this->sortBy = 'name';
$this->sortDirection = 'asc';
$this->resetPage();
$this->invalidateCache();
}
/**
* Toggle specialty filter
*/
public function toggleSpecialtyFilter(string $specialty): void
{
if (in_array($specialty, $this->specialties)) {
$this->specialties = array_values(array_diff($this->specialties, [$specialty]));
} else {
$this->specialties[] = $specialty;
}
$this->resetPage();
$this->invalidateCache();
}
/**
* Invalidate component cache
*/
protected function invalidateCache(): void
{
Cache::forget('designers.portfolio.stats');
Cache::forget('designers.innovation.timeline');
Cache::forget('designers.collaboration.networks');
// Clear listing cache pattern - simplified approach
$cacheKeys = [
'designers.listing.*'
];
foreach ($cacheKeys as $pattern) {
Cache::forget($pattern);
}
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Render the component
*/
public function render()
{
return view('livewire.designers-listing-universal', [
'designers' => $this->designers,
'portfolioStats' => $this->portfolioStats,
'innovationTimeline' => $this->innovationTimeline,
'collaborationNetworks' => $this->collaborationNetworks,
]);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Livewire\Forms;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Validate;
use Livewire\Form;
class LoginForm extends Form
{
#[Validate('required|string|email')]
public string $email = '';
#[Validate('required|string')]
public string $password = '';
#[Validate('boolean')]
public bool $remember = false;
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'form.email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the authentication request is not rate limited.
*/
protected function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout(request()));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'form.email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the authentication rate limiting throttle key.
*/
protected function throttleKey(): string
{
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
class GlobalSearchComponent extends Component
{
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.global-search-component');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,362 @@
<?php
namespace App\Livewire;
use App\Models\Manufacturer;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Support\Facades\Cache;
use Illuminate\Database\Eloquent\Builder;
class ManufacturersListingUniversal extends Component
{
use WithPagination;
// Universal Listing System Integration
public string $entityType = 'manufacturers';
// Search and Filtering
public string $search = '';
public string $sortBy = 'name';
public string $sortDirection = 'asc';
public string $viewMode = 'grid';
public int $perPage = 12;
// Manufacturer-specific filters
public array $specializations = [];
public array $totalRidesRange = [0, 1000];
public array $industryPresenceRange = [0, 100];
public array $foundedYearRange = [1800, 2025];
public bool $activeOnly = false;
public bool $innovationLeadersOnly = false;
// Performance optimization
private string $cacheKeyPrefix = 'manufacturers_listing';
private int $cacheProductPortfolioTtl = 21600; // 6 hours
private int $cacheIndustryPresenceTtl = 43200; // 12 hours
private int $cacheListingTtl = 1800; // 30 minutes
protected $queryString = [
'search' => ['except' => ''],
'sortBy' => ['except' => 'name'],
'sortDirection' => ['except' => 'asc'],
'viewMode' => ['except' => 'grid'],
'perPage' => ['except' => 12],
'specializations' => ['except' => []],
'totalRidesRange' => ['except' => [0, 1000]],
'industryPresenceRange' => ['except' => [0, 100]],
'foundedYearRange' => ['except' => [1800, 2025]],
'activeOnly' => ['except' => false],
'innovationLeadersOnly' => ['except' => false],
'page' => ['except' => 1],
];
public function mount()
{
// Initialize filters with cached values for performance
$this->initializeFilters();
}
public function updatedSearch()
{
$this->resetPage();
}
public function updatedSpecializations()
{
$this->resetPage();
}
public function updatedTotalRidesRange()
{
$this->resetPage();
}
public function updatedIndustryPresenceRange()
{
$this->resetPage();
}
public function updatedFoundedYearRange()
{
$this->resetPage();
}
public function updatedActiveOnly()
{
$this->resetPage();
}
public function updatedInnovationLeadersOnly()
{
$this->resetPage();
}
public function clearFilters()
{
$this->reset([
'search',
'specializations',
'totalRidesRange',
'industryPresenceRange',
'foundedYearRange',
'activeOnly',
'innovationLeadersOnly'
]);
$this->totalRidesRange = [0, 1000];
$this->industryPresenceRange = [0, 100];
$this->foundedYearRange = [1800, 2025];
$this->resetPage();
}
public function setViewMode(string $mode)
{
$this->viewMode = $mode;
}
public function setSortBy(string $field)
{
if ($this->sortBy === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $field;
$this->sortDirection = 'asc';
}
$this->resetPage();
}
public function render()
{
$manufacturers = $this->getManufacturers();
$statistics = $this->getStatistics();
$productPortfolioData = $this->getProductPortfolioData();
$industryPresenceData = $this->getIndustryPresenceData();
return view('livewire.manufacturers-listing-universal', [
'manufacturers' => $manufacturers,
'statistics' => $statistics,
'productPortfolioData' => $productPortfolioData,
'industryPresenceData' => $industryPresenceData,
'hasActiveFilters' => $this->hasActiveFilters(),
]);
}
private function getManufacturers()
{
$cacheKey = $this->generateCacheKey();
return Cache::remember($cacheKey, $this->cacheListingTtl, function () {
$query = Manufacturer::query()
->select([
'id', 'name', 'slug', 'headquarters', 'description', 'website',
'total_rides', 'total_roller_coasters', 'founded_year',
'industry_presence_score', 'specialization', 'is_active',
'is_major_manufacturer', 'market_share_percentage', 'created_at'
]);
// Apply search with Django parity algorithms
if (!empty($this->search)) {
$searchTerms = explode(' ', trim($this->search));
$query->where(function (Builder $q) use ($searchTerms) {
foreach ($searchTerms as $term) {
$q->where(function (Builder $subQ) use ($term) {
$subQ->where('name', 'ILIKE', "%{$term}%")
->orWhere('description', 'ILIKE', "%{$term}%")
->orWhere('headquarters', 'ILIKE', "%{$term}%")
->orWhere('specialization', 'ILIKE', "%{$term}%");
});
}
});
}
// Apply specialization filters
if (!empty($this->specializations)) {
$query->whereIn('specialization', $this->specializations);
}
// Apply total rides range filter
if ($this->totalRidesRange[0] > 0 || $this->totalRidesRange[1] < 1000) {
$query->whereBetween('total_rides', $this->totalRidesRange);
}
// Apply industry presence score range filter
if ($this->industryPresenceRange[0] > 0 || $this->industryPresenceRange[1] < 100) {
$query->whereBetween('industry_presence_score', $this->industryPresenceRange);
}
// Apply founded year range filter
if ($this->foundedYearRange[0] > 1800 || $this->foundedYearRange[1] < 2025) {
$query->whereBetween('founded_year', $this->foundedYearRange);
}
// Apply active filter
if ($this->activeOnly) {
$query->where('is_active', true);
}
// Apply major manufacturers filter
if ($this->innovationLeadersOnly) {
$query->where('is_major_manufacturer', true);
}
// Apply sorting
$query->orderBy($this->sortBy, $this->sortDirection);
// Add secondary sort for consistency
if ($this->sortBy !== 'name') {
$query->orderBy('name', 'asc');
}
return $query->paginate($this->perPage);
});
}
private function getStatistics()
{
$cacheKey = "{$this->cacheKeyPrefix}_statistics";
return Cache::remember($cacheKey, $this->cacheListingTtl, function () {
$baseQuery = Manufacturer::query();
// Apply same filters as main query for accurate statistics
if (!empty($this->search)) {
$searchTerms = explode(' ', trim($this->search));
$baseQuery->where(function (Builder $q) use ($searchTerms) {
foreach ($searchTerms as $term) {
$q->where(function (Builder $subQ) use ($term) {
$subQ->where('name', 'ILIKE', "%{$term}%")
->orWhere('description', 'ILIKE', "%{$term}%")
->orWhere('headquarters', 'ILIKE', "%{$term}%")
->orWhere('specialization', 'ILIKE', "%{$term}%");
});
}
});
}
if (!empty($this->specializations)) {
$baseQuery->whereIn('specialization', $this->specializations);
}
if ($this->totalRidesRange[0] > 0 || $this->totalRidesRange[1] < 1000) {
$baseQuery->whereBetween('total_rides', $this->totalRidesRange);
}
if ($this->industryPresenceRange[0] > 0 || $this->industryPresenceRange[1] < 100) {
$baseQuery->whereBetween('industry_presence_score', $this->industryPresenceRange);
}
if ($this->foundedYearRange[0] > 1800 || $this->foundedYearRange[1] < 2025) {
$baseQuery->whereBetween('founded_year', $this->foundedYearRange);
}
if ($this->activeOnly) {
$baseQuery->where('is_active', true);
}
if ($this->innovationLeadersOnly) {
$baseQuery->where('is_major_manufacturer', true);
}
return [
'count' => $baseQuery->count(),
'active_count' => (clone $baseQuery)->where('is_active', true)->count(),
'total_rides_sum' => $baseQuery->sum('total_rides'),
'avg_industry_presence' => round($baseQuery->avg('industry_presence_score'), 1),
];
});
}
private function getProductPortfolioData()
{
$cacheKey = "{$this->cacheKeyPrefix}_product_portfolio";
return Cache::remember($cacheKey, $this->cacheProductPortfolioTtl, function () {
return [
'specialization_distribution' => Manufacturer::selectRaw('specialization, COUNT(*) as count')
->groupBy('specialization')
->pluck('count', 'specialization')
->toArray(),
'top_manufacturers_by_rides' => Manufacturer::orderBy('total_rides', 'desc')
->limit(10)
->pluck('total_rides', 'name')
->toArray(),
'major_manufacturers_count' => Manufacturer::where('is_major_manufacturer', true)->count(),
'average_market_share' => round(Manufacturer::avg('market_share_percentage'), 2),
];
});
}
private function getIndustryPresenceData()
{
$cacheKey = "{$this->cacheKeyPrefix}_industry_presence";
return Cache::remember($cacheKey, $this->cacheIndustryPresenceTtl, function () {
return [
'presence_score_ranges' => [
'high' => Manufacturer::where('industry_presence_score', '>=', 80)->count(),
'medium' => Manufacturer::whereBetween('industry_presence_score', [50, 79])->count(),
'low' => Manufacturer::where('industry_presence_score', '<', 50)->count(),
],
'founding_decades' => Manufacturer::selectRaw('FLOOR(founded_year / 10) * 10 as decade, COUNT(*) as count')
->groupBy('decade')
->orderBy('decade')
->pluck('count', 'decade')
->toArray(),
'active_vs_inactive' => [
'active' => Manufacturer::where('is_active', true)->count(),
'inactive' => Manufacturer::where('is_active', false)->count(),
],
'market_concentration' => Manufacturer::orderBy('market_share_percentage', 'desc')
->limit(5)
->pluck('market_share_percentage', 'name')
->toArray(),
];
});
}
private function hasActiveFilters(): bool
{
return !empty($this->search) ||
!empty($this->specializations) ||
$this->totalRidesRange !== [0, 1000] ||
$this->industryPresenceRange !== [0, 100] ||
$this->foundedYearRange !== [1800, 2025] ||
$this->activeOnly ||
$this->innovationLeadersOnly;
}
private function generateCacheKey(): string
{
$filterHash = md5(serialize([
'search' => $this->search,
'sortBy' => $this->sortBy,
'sortDirection' => $this->sortDirection,
'specializations' => $this->specializations,
'totalRidesRange' => $this->totalRidesRange,
'industryPresenceRange' => $this->industryPresenceRange,
'foundedYearRange' => $this->foundedYearRange,
'activeOnly' => $this->activeOnly,
'innovationLeadersOnly' => $this->innovationLeadersOnly,
'perPage' => $this->perPage,
'page' => $this->getPage(),
]));
return "{$this->cacheKeyPrefix}_{$filterHash}";
}
private function initializeFilters()
{
// Initialize with sensible defaults for manufacturer filtering
if (empty($this->totalRidesRange)) {
$this->totalRidesRange = [0, 1000];
}
if (empty($this->industryPresenceRange)) {
$this->industryPresenceRange = [0, 100];
}
if (empty($this->foundedYearRange)) {
$this->foundedYearRange = [1800, 2025];
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
class OperatorHierarchyView extends Component
{
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.operator-hierarchy-view');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Support\Facades\Cache;
class OperatorParksListing extends Component
{
use WithPagination;
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.operator-parks-listing');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
class OperatorPortfolioCard extends Component
{
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.operator-portfolio-card');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
class OperatorsIndustryStats extends Component
{
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.operators-industry-stats');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,476 @@
<?php
namespace App\Livewire;
use App\Models\Operator;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class OperatorsListing extends Component
{
use WithPagination;
#[Url(as: 'q')]
public string $search = '';
#[Url(as: 'roles')]
public array $roleFilter = [];
#[Url(as: 'sector')]
public string $industrySector = '';
#[Url(as: 'size')]
public string $companySize = '';
#[Url(as: 'founded_from')]
public string $foundedYearFrom = '';
#[Url(as: 'founded_to')]
public string $foundedYearTo = '';
#[Url(as: 'presence')]
public string $geographicPresence = '';
#[Url(as: 'min_revenue')]
public string $minRevenue = '';
#[Url(as: 'max_revenue')]
public string $maxRevenue = '';
#[Url(as: 'sort')]
public string $sortBy = 'name';
#[Url(as: 'dir')]
public string $sortDirection = 'asc';
#[Url(as: 'view')]
public string $viewMode = 'grid';
public int $perPage = 20;
public array $industryStats = [];
public array $marketData = [];
protected $queryString = [
'search' => ['except' => ''],
'roleFilter' => ['except' => []],
'industrySector' => ['except' => ''],
'companySize' => ['except' => ''],
'foundedYearFrom' => ['except' => ''],
'foundedYearTo' => ['except' => ''],
'geographicPresence' => ['except' => ''],
'minRevenue' => ['except' => ''],
'maxRevenue' => ['except' => ''],
'sortBy' => ['except' => 'name'],
'sortDirection' => ['except' => 'asc'],
'viewMode' => ['except' => 'grid'],
'page' => ['except' => 1],
];
/**
* Component initialization
*/
public function mount(): void
{
$this->loadIndustryStatistics();
$this->loadMarketData();
}
/**
* Load industry statistics with caching
*/
protected function loadIndustryStatistics(): void
{
$this->industryStats = Cache::remember(
'operators.industry.stats',
now()->addHours(6),
fn() => $this->calculateIndustryStatistics()
);
}
/**
* Load market analysis data with caching
*/
protected function loadMarketData(): void
{
$this->marketData = Cache::remember(
'operators.market.data',
now()->addHours(12),
fn() => $this->loadMarketAnalysis()
);
}
/**
* Calculate comprehensive industry statistics
*/
protected function calculateIndustryStatistics(): array
{
return [
'total_operators' => Operator::active()->count(),
'park_operators' => Operator::active()->parkOperators()->count(),
'manufacturers' => Operator::active()->manufacturers()->count(),
'designers' => Operator::active()->designers()->count(),
'mixed_role' => Operator::active()
->whereHas('parks')
->whereHas('manufactured_rides')
->count(),
'sectors' => Operator::active()
->select('industry_sector', DB::raw('count(*) as count'))
->whereNotNull('industry_sector')
->groupBy('industry_sector')
->orderByDesc('count')
->get()
->pluck('count', 'industry_sector')
->toArray(),
'company_sizes' => [
'small' => Operator::active()->companySize('small')->count(),
'medium' => Operator::active()->companySize('medium')->count(),
'large' => Operator::active()->companySize('large')->count(),
'enterprise' => Operator::active()->companySize('enterprise')->count(),
],
'geographic_distribution' => Operator::active()
->whereHas('parks.location')
->with('parks.location')
->get()
->flatMap(fn($op) => $op->parks->pluck('location.country'))
->countBy()
->sortDesc()
->take(10)
->toArray(),
];
}
/**
* Load market analysis data
*/
protected function loadMarketAnalysis(): array
{
return [
'total_market_cap' => Operator::active()
->whereNotNull('market_cap')
->sum('market_cap'),
'total_revenue' => Operator::active()
->whereNotNull('annual_revenue')
->sum('annual_revenue'),
'average_parks_per_operator' => Operator::active()
->parkOperators()
->avg('total_parks'),
'top_operators_by_parks' => Operator::active()
->parkOperators()
->orderByDesc('total_parks')
->take(5)
->get(['name', 'total_parks'])
->toArray(),
'top_manufacturers_by_rides' => Operator::active()
->manufacturers()
->orderByDesc('total_rides_manufactured')
->take(5)
->get(['name', 'total_rides_manufactured'])
->toArray(),
];
}
/**
* Django parity dual-role search functionality
*/
public function dualRoleSearch($query, $roles = [])
{
return Operator::query()
->when($query, function ($q) use ($query) {
$terms = explode(' ', trim($query));
foreach ($terms as $term) {
if (strlen($term) >= 2) {
$q->where(function ($subQuery) use ($term) {
$subQuery->where('name', 'ilike', "%{$term}%")
->orWhere('description', 'ilike', "%{$term}%")
->orWhere('industry_sector', 'ilike', "%{$term}%")
->orWhere('headquarters_location', '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->whereHas('parks');
}
if (in_array('ride_manufacturer', $roles)) {
$roleQuery->orWhereHas('manufactured_rides');
}
if (in_array('ride_designer', $roles)) {
$roleQuery->orWhereHas('designed_rides');
}
});
})
->active()
->with(['location', 'parks:id,operator_id,name', 'manufactured_rides:id,manufacturer_id,name', 'designed_rides:id,designer_id,name'])
->withCount(['parks', 'manufactured_rides', 'designed_rides']);
}
/**
* Apply advanced industry filters
*/
public function applyIndustryFilters($query)
{
return $query
->when($this->industrySector, fn($q, $sector) =>
$q->where('industry_sector', $sector))
->when($this->companySize, fn($q, $size) =>
$q->companySize($size))
->when($this->foundedYearFrom, fn($q, $year) =>
$q->where('founded_year', '>=', $year))
->when($this->foundedYearTo, fn($q, $year) =>
$q->where('founded_year', '<=', $year))
->when($this->geographicPresence, 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($this->minRevenue, fn($q, $revenue) =>
$q->where('annual_revenue', '>=', $revenue))
->when($this->maxRevenue, fn($q, $revenue) =>
$q->where('annual_revenue', '<=', $revenue));
}
/**
* Get operators with optimized caching
*/
public function getOperatorsProperty()
{
$cacheKey = "operators.listing." . md5(serialize([
'search' => $this->search,
'roleFilter' => $this->roleFilter,
'industrySector' => $this->industrySector,
'companySize' => $this->companySize,
'foundedYearFrom' => $this->foundedYearFrom,
'foundedYearTo' => $this->foundedYearTo,
'geographicPresence' => $this->geographicPresence,
'minRevenue' => $this->minRevenue,
'maxRevenue' => $this->maxRevenue,
'sortBy' => $this->sortBy,
'sortDirection' => $this->sortDirection,
'page' => $this->getPage(),
'perPage' => $this->perPage,
]));
return Cache::remember($cacheKey, now()->addMinutes(30), function() {
$query = $this->dualRoleSearch($this->search, $this->roleFilter);
$query = $this->applyIndustryFilters($query);
// Apply sorting
switch ($this->sortBy) {
case 'name':
$query->orderBy('name', $this->sortDirection);
break;
case 'founded_year':
$query->orderBy('founded_year', $this->sortDirection);
break;
case 'parks_count':
$query->orderBy('total_parks', $this->sortDirection);
break;
case 'rides_count':
$query->orderBy('total_rides_manufactured', $this->sortDirection);
break;
case 'revenue':
$query->orderBy('annual_revenue', $this->sortDirection);
break;
case 'market_influence':
$query->orderByRaw('(total_parks * 10 + total_rides_manufactured * 2) ' . $this->sortDirection);
break;
default:
$query->orderBy('name', 'asc');
}
return $query->paginate($this->perPage);
});
}
/**
* Update search and reset pagination
*/
public function updatedSearch(): void
{
$this->resetPage();
$this->invalidateCache();
}
/**
* Update role filter and reset pagination
*/
public function updatedRoleFilter(): void
{
$this->resetPage();
$this->invalidateCache();
}
/**
* Update any filter and reset pagination
*/
public function updatedIndustrySector(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedCompanySize(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedFoundedYearFrom(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedFoundedYearTo(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedGeographicPresence(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedMinRevenue(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedMaxRevenue(): void
{
$this->resetPage();
$this->invalidateCache();
}
/**
* Sort by specific column
*/
public function sortBy(string $column): void
{
if ($this->sortBy === $column) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $column;
$this->sortDirection = 'asc';
}
$this->resetPage();
$this->invalidateCache();
}
/**
* Change view mode
*/
public function setViewMode(string $mode): void
{
$this->viewMode = $mode;
}
/**
* Clear all filters
*/
public function clearFilters(): void
{
$this->search = '';
$this->roleFilter = [];
$this->industrySector = '';
$this->companySize = '';
$this->foundedYearFrom = '';
$this->foundedYearTo = '';
$this->geographicPresence = '';
$this->minRevenue = '';
$this->maxRevenue = '';
$this->sortBy = 'name';
$this->sortDirection = 'asc';
$this->resetPage();
$this->invalidateCache();
}
/**
* Toggle role filter
*/
public function toggleRoleFilter(string $role): void
{
if (in_array($role, $this->roleFilter)) {
$this->roleFilter = array_values(array_diff($this->roleFilter, [$role]));
} else {
$this->roleFilter[] = $role;
}
$this->resetPage();
$this->invalidateCache();
}
/**
* Invalidate component cache
*/
protected function invalidateCache(): void
{
Cache::forget('operators.industry.stats');
Cache::forget('operators.market.data');
// Clear listing cache pattern - simplified approach
$cacheKeys = [
'operators.listing.*'
];
foreach ($cacheKeys as $pattern) {
Cache::forget($pattern);
}
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Render the component
*/
public function render()
{
return view('livewire.operators-listing', [
'operators' => $this->operators,
'industryStats' => $this->industryStats,
'marketData' => $this->marketData,
]);
}
}

View File

@@ -0,0 +1,479 @@
<?php
namespace App\Livewire;
use App\Models\Operator;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class OperatorsListingUniversal extends Component
{
use WithPagination;
// Universal Listing System Integration
public string $entityType = 'operators';
#[Url(as: 'q')]
public string $search = '';
#[Url(as: 'roles')]
public array $roleFilter = [];
#[Url(as: 'sector')]
public string $industrySector = '';
#[Url(as: 'size')]
public string $companySize = '';
#[Url(as: 'founded_from')]
public string $foundedYearFrom = '';
#[Url(as: 'founded_to')]
public string $foundedYearTo = '';
#[Url(as: 'presence')]
public string $geographicPresence = '';
#[Url(as: 'min_revenue')]
public string $minRevenue = '';
#[Url(as: 'max_revenue')]
public string $maxRevenue = '';
#[Url(as: 'sort')]
public string $sortBy = 'name';
#[Url(as: 'dir')]
public string $sortDirection = 'asc';
#[Url(as: 'view')]
public string $viewMode = 'grid';
public int $perPage = 20;
public array $industryStats = [];
public array $marketData = [];
protected $queryString = [
'search' => ['except' => ''],
'roleFilter' => ['except' => []],
'industrySector' => ['except' => ''],
'companySize' => ['except' => ''],
'foundedYearFrom' => ['except' => ''],
'foundedYearTo' => ['except' => ''],
'geographicPresence' => ['except' => ''],
'minRevenue' => ['except' => ''],
'maxRevenue' => ['except' => ''],
'sortBy' => ['except' => 'name'],
'sortDirection' => ['except' => 'asc'],
'viewMode' => ['except' => 'grid'],
'page' => ['except' => 1],
];
/**
* Component initialization
*/
public function mount(): void
{
$this->loadIndustryStatistics();
$this->loadMarketData();
}
/**
* Load industry statistics with caching
*/
protected function loadIndustryStatistics(): void
{
$this->industryStats = Cache::remember(
'operators.industry.stats',
now()->addHours(6),
fn() => $this->calculateIndustryStatistics()
);
}
/**
* Load market analysis data with caching
*/
protected function loadMarketData(): void
{
$this->marketData = Cache::remember(
'operators.market.data',
now()->addHours(12),
fn() => $this->loadMarketAnalysis()
);
}
/**
* Calculate comprehensive industry statistics
*/
protected function calculateIndustryStatistics(): array
{
return [
'total_operators' => Operator::active()->count(),
'park_operators' => Operator::active()->parkOperators()->count(),
'manufacturers' => Operator::active()->manufacturers()->count(),
'designers' => Operator::active()->designers()->count(),
'mixed_role' => Operator::active()
->whereHas('parks')
->whereHas('manufactured_rides')
->count(),
'sectors' => Operator::active()
->select('industry_sector', DB::raw('count(*) as count'))
->whereNotNull('industry_sector')
->groupBy('industry_sector')
->orderByDesc('count')
->get()
->pluck('count', 'industry_sector')
->toArray(),
'company_sizes' => [
'small' => Operator::active()->companySize('small')->count(),
'medium' => Operator::active()->companySize('medium')->count(),
'large' => Operator::active()->companySize('large')->count(),
'enterprise' => Operator::active()->companySize('enterprise')->count(),
],
'geographic_distribution' => Operator::active()
->whereHas('parks.location')
->with('parks.location')
->get()
->flatMap(fn($op) => $op->parks->pluck('location.country'))
->countBy()
->sortDesc()
->take(10)
->toArray(),
];
}
/**
* Load market analysis data
*/
protected function loadMarketAnalysis(): array
{
return [
'total_market_cap' => Operator::active()
->whereNotNull('market_cap')
->sum('market_cap'),
'total_revenue' => Operator::active()
->whereNotNull('annual_revenue')
->sum('annual_revenue'),
'average_parks_per_operator' => Operator::active()
->parkOperators()
->avg('total_parks'),
'top_operators_by_parks' => Operator::active()
->parkOperators()
->orderByDesc('total_parks')
->take(5)
->get(['name', 'total_parks'])
->toArray(),
'top_manufacturers_by_rides' => Operator::active()
->manufacturers()
->orderByDesc('total_rides_manufactured')
->take(5)
->get(['name', 'total_rides_manufactured'])
->toArray(),
];
}
/**
* Django parity dual-role search functionality
*/
public function dualRoleSearch($query, $roles = [])
{
return Operator::query()
->when($query, function ($q) use ($query) {
$terms = explode(' ', trim($query));
foreach ($terms as $term) {
if (strlen($term) >= 2) {
$q->where(function ($subQuery) use ($term) {
$subQuery->where('name', 'ilike', "%{$term}%")
->orWhere('description', 'ilike', "%{$term}%")
->orWhere('industry_sector', 'ilike', "%{$term}%")
->orWhere('headquarters_location', '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->whereHas('parks');
}
if (in_array('ride_manufacturer', $roles)) {
$roleQuery->orWhereHas('manufactured_rides');
}
if (in_array('ride_designer', $roles)) {
$roleQuery->orWhereHas('designed_rides');
}
});
})
->active()
->with(['location', 'parks:id,operator_id,name', 'manufactured_rides:id,manufacturer_id,name', 'designed_rides:id,designer_id,name'])
->withCount(['parks', 'manufactured_rides', 'designed_rides']);
}
/**
* Apply advanced industry filters
*/
public function applyIndustryFilters($query)
{
return $query
->when($this->industrySector, fn($q, $sector) =>
$q->where('industry_sector', $sector))
->when($this->companySize, fn($q, $size) =>
$q->companySize($size))
->when($this->foundedYearFrom, fn($q, $year) =>
$q->where('founded_year', '>=', $year))
->when($this->foundedYearTo, fn($q, $year) =>
$q->where('founded_year', '<=', $year))
->when($this->geographicPresence, 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($this->minRevenue, fn($q, $revenue) =>
$q->where('annual_revenue', '>=', $revenue))
->when($this->maxRevenue, fn($q, $revenue) =>
$q->where('annual_revenue', '<=', $revenue));
}
/**
* Get operators with optimized caching
*/
public function getOperatorsProperty()
{
$cacheKey = "operators.listing." . md5(serialize([
'search' => $this->search,
'roleFilter' => $this->roleFilter,
'industrySector' => $this->industrySector,
'companySize' => $this->companySize,
'foundedYearFrom' => $this->foundedYearFrom,
'foundedYearTo' => $this->foundedYearTo,
'geographicPresence' => $this->geographicPresence,
'minRevenue' => $this->minRevenue,
'maxRevenue' => $this->maxRevenue,
'sortBy' => $this->sortBy,
'sortDirection' => $this->sortDirection,
'page' => $this->getPage(),
'perPage' => $this->perPage,
]));
return Cache::remember($cacheKey, now()->addMinutes(30), function() {
$query = $this->dualRoleSearch($this->search, $this->roleFilter);
$query = $this->applyIndustryFilters($query);
// Apply sorting
switch ($this->sortBy) {
case 'name':
$query->orderBy('name', $this->sortDirection);
break;
case 'founded_year':
$query->orderBy('founded_year', $this->sortDirection);
break;
case 'parks_count':
$query->orderBy('total_parks', $this->sortDirection);
break;
case 'rides_count':
$query->orderBy('total_rides_manufactured', $this->sortDirection);
break;
case 'revenue':
$query->orderBy('annual_revenue', $this->sortDirection);
break;
case 'market_influence':
$query->orderByRaw('(total_parks * 10 + total_rides_manufactured * 2) ' . $this->sortDirection);
break;
default:
$query->orderBy('name', 'asc');
}
return $query->paginate($this->perPage);
});
}
/**
* Update search and reset pagination
*/
public function updatedSearch(): void
{
$this->resetPage();
$this->invalidateCache();
}
/**
* Update role filter and reset pagination
*/
public function updatedRoleFilter(): void
{
$this->resetPage();
$this->invalidateCache();
}
/**
* Update any filter and reset pagination
*/
public function updatedIndustrySector(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedCompanySize(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedFoundedYearFrom(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedFoundedYearTo(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedGeographicPresence(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedMinRevenue(): void
{
$this->resetPage();
$this->invalidateCache();
}
public function updatedMaxRevenue(): void
{
$this->resetPage();
$this->invalidateCache();
}
/**
* Sort by specific column
*/
public function sortBy(string $column): void
{
if ($this->sortBy === $column) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $column;
$this->sortDirection = 'asc';
}
$this->resetPage();
$this->invalidateCache();
}
/**
* Change view mode
*/
public function setViewMode(string $mode): void
{
$this->viewMode = $mode;
}
/**
* Clear all filters
*/
public function clearFilters(): void
{
$this->search = '';
$this->roleFilter = [];
$this->industrySector = '';
$this->companySize = '';
$this->foundedYearFrom = '';
$this->foundedYearTo = '';
$this->geographicPresence = '';
$this->minRevenue = '';
$this->maxRevenue = '';
$this->sortBy = 'name';
$this->sortDirection = 'asc';
$this->resetPage();
$this->invalidateCache();
}
/**
* Toggle role filter
*/
public function toggleRoleFilter(string $role): void
{
if (in_array($role, $this->roleFilter)) {
$this->roleFilter = array_values(array_diff($this->roleFilter, [$role]));
} else {
$this->roleFilter[] = $role;
}
$this->resetPage();
$this->invalidateCache();
}
/**
* Invalidate component cache
*/
protected function invalidateCache(): void
{
Cache::forget('operators.industry.stats');
Cache::forget('operators.market.data');
// Clear listing cache pattern - simplified approach
$cacheKeys = [
'operators.listing.*'
];
foreach ($cacheKeys as $pattern) {
Cache::forget($pattern);
}
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Render the component
*/
public function render()
{
return view('livewire.operators-listing-universal', [
'operators' => $this->operators,
'industryStats' => $this->industryStats,
'marketData' => $this->marketData,
]);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
class OperatorsMarketAnalysis extends Component
{
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.operators-market-analysis');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
class OperatorsRoleFilter extends Component
{
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.operators-role-filter');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,372 @@
<?php
namespace App\Livewire;
use App\Models\Ride;
use App\Models\Park;
use App\Enums\RideCategory;
use App\Enums\RideStatus;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Cache;
use Illuminate\Database\Eloquent\Builder;
class ParkRidesListing extends Component
{
use WithPagination;
// Required park context
public Park $park;
// URL-bound search and filter properties
#[Url(as: 'search')]
public string $searchTerm = '';
#[Url(as: 'category')]
public ?string $selectedCategory = null;
#[Url(as: 'status')]
public ?string $selectedStatus = null;
#[Url(as: 'sort')]
public string $sortBy = 'name';
#[Url(as: 'direction')]
public string $sortDirection = 'asc';
// UI state
public bool $showFilters = false;
public int $perPage = 12;
// Cached data
public array $categories = [];
public array $statuses = [];
public array $sortOptions = [];
/**
* Component initialization
*/
public function mount(Park $park): void
{
$this->park = $park;
$this->loadFilterOptions();
$this->setupSortOptions();
}
/**
* Load filter options specific to this park
*/
protected function loadFilterOptions(): void
{
$cacheKey = "park_rides_filters_{$this->park->id}";
$filterData = Cache::remember($cacheKey, 3600, function() {
// Categories available in this park
$categories = $this->park->rides()
->select('category')
->groupBy('category')
->get()
->map(function($ride) {
$category = RideCategory::from($ride->category);
return [
'value' => $category->value,
'label' => $category->name,
'count' => $this->park->rides()->where('category', $category->value)->count()
];
})
->toArray();
// Statuses available in this park
$statuses = $this->park->rides()
->select('status')
->groupBy('status')
->get()
->map(function($ride) {
$status = RideStatus::from($ride->status);
return [
'value' => $status->value,
'label' => $status->name,
'count' => $this->park->rides()->where('status', $status->value)->count()
];
})
->toArray();
return compact('categories', 'statuses');
});
$this->categories = $filterData['categories'];
$this->statuses = $filterData['statuses'];
}
/**
* Setup sort options
*/
protected function setupSortOptions(): void
{
$this->sortOptions = [
'name' => 'Name',
'opening_year' => 'Opening Year',
'height_requirement' => 'Height Requirement',
'created_at' => 'Date Added',
'updated_at' => 'Last Updated'
];
}
/**
* Update search term and reset pagination
*/
public function updatedSearchTerm(): void
{
$this->resetPage();
}
/**
* Update category filter
*/
public function updatedSelectedCategory(): void
{
$this->resetPage();
}
/**
* Update status filter
*/
public function updatedSelectedStatus(): void
{
$this->resetPage();
}
/**
* Update sort options
*/
public function updatedSortBy(): void
{
$this->resetPage();
}
/**
* Update sort direction
*/
public function updatedSortDirection(): void
{
$this->resetPage();
}
/**
* Set category filter
*/
public function setCategory(?string $category): void
{
$this->selectedCategory = $category === $this->selectedCategory ? null : $category;
$this->resetPage();
}
/**
* Set status filter
*/
public function setStatus(?string $status): void
{
$this->selectedStatus = $status === $this->selectedStatus ? null : $status;
$this->resetPage();
}
/**
* Set sort parameters
*/
public function setSortBy(string $field): void
{
if ($this->sortBy === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $field;
$this->sortDirection = 'asc';
}
$this->resetPage();
}
/**
* Toggle filters visibility
*/
public function toggleFilters(): void
{
$this->showFilters = !$this->showFilters;
}
/**
* Clear all filters
*/
public function clearFilters(): void
{
$this->searchTerm = '';
$this->selectedCategory = null;
$this->selectedStatus = null;
$this->sortBy = 'name';
$this->sortDirection = 'asc';
$this->resetPage();
}
/**
* Get filtered and sorted rides for this park
*/
public function getRidesProperty()
{
$cacheKey = $this->getCacheKey();
return Cache::remember($cacheKey, 300, function() {
$query = $this->park->rides()
->with(['manufacturer', 'designer', 'photos'])
->when($this->searchTerm, function (Builder $query) {
$query->where(function (Builder $subQuery) {
$subQuery->where('name', 'ILIKE', "%{$this->searchTerm}%")
->orWhere('description', 'ILIKE', "%{$this->searchTerm}%")
->orWhereHas('manufacturer', function (Builder $manufacturerQuery) {
$manufacturerQuery->where('name', 'ILIKE', "%{$this->searchTerm}%");
})
->orWhereHas('designer', function (Builder $designerQuery) {
$designerQuery->where('name', 'ILIKE', "%{$this->searchTerm}%");
});
});
})
->when($this->selectedCategory, function (Builder $query) {
$query->where('category', $this->selectedCategory);
})
->when($this->selectedStatus, function (Builder $query) {
$query->where('status', $this->selectedStatus);
});
// Apply sorting
switch ($this->sortBy) {
case 'name':
$query->orderBy('name', $this->sortDirection);
break;
case 'opening_year':
$query->orderBy('opening_year', $this->sortDirection)
->orderBy('name', 'asc');
break;
case 'height_requirement':
$query->orderBy('height_requirement', $this->sortDirection)
->orderBy('name', 'asc');
break;
case 'created_at':
$query->orderBy('created_at', $this->sortDirection);
break;
case 'updated_at':
$query->orderBy('updated_at', $this->sortDirection);
break;
default:
$query->orderBy('name', 'asc');
}
return $query->paginate($this->perPage);
});
}
/**
* Get park statistics
*/
public function getParkStatsProperty(): array
{
$cacheKey = "park_stats_{$this->park->id}";
return Cache::remember($cacheKey, 3600, function() {
$totalRides = $this->park->rides()->count();
$operatingRides = $this->park->rides()->where('status', 'operating')->count();
$categories = $this->park->rides()
->select('category')
->groupBy('category')
->get()
->count();
$avgRating = $this->park->rides()
->whereHas('reviews')
->withAvg('reviews', 'rating')
->get()
->avg('reviews_avg_rating');
return [
'total_rides' => $totalRides,
'operating_rides' => $operatingRides,
'categories' => $categories,
'avg_rating' => $avgRating ? round($avgRating, 1) : null
];
});
}
/**
* Get active filters count
*/
public function getActiveFiltersCountProperty(): int
{
return collect([
$this->searchTerm,
$this->selectedCategory,
$this->selectedStatus
])->filter()->count();
}
/**
* Get cache key for current state
*/
protected function getCacheKey(): string
{
return sprintf(
'park_rides_%d_%s_%s_%s_%s_%s_%d_%d',
$this->park->id,
md5($this->searchTerm),
$this->selectedCategory ?? 'all',
$this->selectedStatus ?? 'all',
$this->sortBy,
$this->sortDirection,
$this->perPage,
$this->getPage()
);
}
/**
* Render the component
*/
public function render()
{
return view('livewire.park-rides-listing', [
'rides' => $this->rides,
'parkStats' => $this->parkStats,
'activeFiltersCount' => $this->activeFiltersCount
]);
}
/**
* Reset pagination when filters change
*/
public function resetPage($pageName = 'page'): void
{
$this->resetPage($pageName);
// Clear cache when filters change
$this->clearComponentCache();
}
/**
* Clear component-specific cache
*/
protected function clearComponentCache(): void
{
$patterns = [
"park_rides_{$this->park->id}_*",
"park_stats_{$this->park->id}",
"park_rides_filters_{$this->park->id}"
];
foreach ($patterns as $pattern) {
Cache::forget($pattern);
}
}
/**
* Get pagination view
*/
public function paginationView(): string
{
return 'livewire.pagination-links';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
class ParksFilters extends Component
{
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.parks-filters');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,476 @@
<?php
namespace App\Livewire;
use App\Models\Park;
use App\Models\Operator;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
class ParksListing extends Component
{
use WithPagination;
// Search and Filter Properties with URL binding
#[Url(as: 'q')]
public string $search = '';
#[Url(as: 'operator')]
public string $operatorId = '';
#[Url(as: 'region')]
public string $region = '';
#[Url(as: 'country')]
public string $country = '';
#[Url(as: 'type')]
public string $parkType = '';
#[Url(as: 'year_from')]
public string $openingYearFrom = '';
#[Url(as: 'year_to')]
public string $openingYearTo = '';
#[Url(as: 'min_area')]
public string $minArea = '';
#[Url(as: 'max_area')]
public string $maxArea = '';
#[Url(as: 'min_rides')]
public string $minRides = '';
#[Url(as: 'max_distance')]
public string $maxDistance = '';
#[Url(as: 'sort')]
public string $sortBy = 'name';
#[Url(as: 'dir')]
public string $sortDirection = 'asc';
// Location Properties
public ?array $userLocation = null;
public bool $locationEnabled = false;
public bool $locationLoading = false;
// UI State
public bool $showFilters = false;
public bool $isLoading = false;
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
$this->resetPage();
}
/**
* Updated hook for reactive properties
*/
public function updated($property): void
{
if (in_array($property, [
'search', 'operatorId', 'region', 'country', 'parkType',
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
'minRides', 'maxDistance', 'sortBy', 'sortDirection'
])) {
$this->resetPage();
$this->invalidateCache('parks');
}
}
/**
* Enable location services
*/
public function enableLocation(): void
{
$this->locationLoading = true;
$this->dispatch('request-location');
}
/**
* Handle location received from JavaScript
*/
public function locationReceived(array $location): void
{
$this->userLocation = $location;
$this->locationEnabled = true;
$this->locationLoading = false;
$this->resetPage();
$this->invalidateCache('parks');
}
/**
* Handle location error
*/
public function locationError(string $error): void
{
$this->locationLoading = false;
$this->dispatch('location-error', message: $error);
}
/**
* Clear all filters
*/
public function clearFilters(): void
{
$this->reset([
'search', 'operatorId', 'region', 'country', 'parkType',
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
'minRides', 'maxDistance'
]);
$this->userLocation = null;
$this->locationEnabled = false;
$this->resetPage();
$this->invalidateCache('parks');
}
/**
* Toggle filters visibility
*/
public function toggleFilters(): void
{
$this->showFilters = !$this->showFilters;
}
/**
* Sort parks by given field
*/
public function sortBy(string $field): void
{
if ($this->sortBy === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $field;
$this->sortDirection = 'asc';
}
$this->resetPage();
$this->invalidateCache('parks');
}
/**
* Get parks with location-aware search and filtering
*/
public function getParksProperty()
{
return $this->remember('parks', function () {
return $this->buildParksQuery()
->paginate(12, ['*'], 'page', $this->getPage());
}, 1200); // 20-minute location-aware caching
}
/**
* Build the parks query with Django parity search and filtering
*/
protected function buildParksQuery(): Builder
{
$query = Park::query();
// Location-aware search functionality (Django parity)
if (!empty($this->search)) {
$query = $this->locationAwareSearch($query, $this->search, $this->userLocation);
}
// Apply advanced filters with geographic context
$query = $this->applyFilters($query, [
'operator_id' => $this->operatorId,
'region' => $this->region,
'country' => $this->country,
'park_type' => $this->parkType,
'opening_year_from' => $this->openingYearFrom,
'opening_year_to' => $this->openingYearTo,
'min_area' => $this->minArea,
'max_area' => $this->maxArea,
'min_rides' => $this->minRides,
'max_distance' => $this->maxDistance,
], $this->userLocation);
// Apply sorting with location-aware options
$query = $this->applySorting($query);
// Eager load relationships for performance
$query->with(['location', 'operator', 'photos', 'statistics'])
->withCount(['rides', 'reviews']);
return $query;
}
/**
* Location-aware search functionality matching Django implementation
*/
protected function locationAwareSearch(Builder $query, string $searchQuery, ?array $userLocation = null): Builder
{
$terms = explode(' ', trim($searchQuery));
foreach ($terms as $term) {
$term = trim($term);
if (empty($term)) continue;
$query->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}%"));
});
}
// Add distance-based ordering for location-aware results
if ($userLocation) {
$query->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');
}
return $query;
}
/**
* Apply advanced filters with geographic context
*/
protected function applyFilters(Builder $query, array $filters, ?array $userLocation = null): Builder
{
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']]);
});
}
/**
* Apply sorting with location-aware options
*/
protected function applySorting(Builder $query): Builder
{
switch ($this->sortBy) {
case 'distance':
if ($this->userLocation) {
// Distance sorting already applied in locationAwareSearch
return $query;
}
// Fallback to name if no location
return $query->orderBy('name', $this->sortDirection);
case 'rides_count':
return $query->orderBy('rides_count', $this->sortDirection);
case 'reviews_count':
return $query->orderBy('reviews_count', $this->sortDirection);
case 'opening_date':
return $query->orderBy('opening_date', $this->sortDirection);
case 'area_acres':
return $query->orderBy('area_acres', $this->sortDirection);
case 'name':
default:
return $query->orderBy('name', $this->sortDirection);
}
}
/**
* Get available operators for filter dropdown
*/
public function getOperatorsProperty()
{
return $this->remember('operators', function () {
return Operator::select('id', 'name')
->whereHas('parks')
->orderBy('name')
->get();
}, 3600);
}
/**
* Get available regions for filter dropdown
*/
public function getRegionsProperty()
{
return $this->remember('regions', function () {
return DB::table('locations')
->join('parks', 'locations.id', '=', 'parks.location_id')
->select('locations.state')
->distinct()
->whereNotNull('locations.state')
->orderBy('locations.state')
->pluck('state');
}, 3600);
}
/**
* Get available countries for filter dropdown
*/
public function getCountriesProperty()
{
return $this->remember('countries', function () {
return DB::table('locations')
->join('parks', 'locations.id', '=', 'parks.location_id')
->select('locations.country')
->distinct()
->whereNotNull('locations.country')
->orderBy('locations.country')
->pluck('country');
}, 3600);
}
/**
* Get available park types for filter dropdown
*/
public function getParkTypesProperty()
{
return $this->remember('park_types', function () {
return Park::select('park_type')
->distinct()
->whereNotNull('park_type')
->orderBy('park_type')
->pluck('park_type');
}, 3600);
}
/**
* Get filter summary for display
*/
public function getActiveFiltersProperty(): array
{
$filters = [];
if (!empty($this->search)) {
$filters[] = "Search: {$this->search}";
}
if (!empty($this->operatorId)) {
$operator = $this->operators->find($this->operatorId);
if ($operator) {
$filters[] = "Operator: {$operator->name}";
}
}
if (!empty($this->region)) {
$filters[] = "Region: {$this->region}";
}
if (!empty($this->country)) {
$filters[] = "Country: {$this->country}";
}
if (!empty($this->parkType)) {
$filters[] = "Type: {$this->parkType}";
}
if (!empty($this->openingYearFrom) || !empty($this->openingYearTo)) {
$yearRange = $this->openingYearFrom . ' - ' . $this->openingYearTo;
$filters[] = "Years: {$yearRange}";
}
if (!empty($this->minArea) || !empty($this->maxArea)) {
$areaRange = $this->minArea . ' - ' . $this->maxArea . ' acres';
$filters[] = "Area: {$areaRange}";
}
if (!empty($this->minRides)) {
$filters[] = "Min Rides: {$this->minRides}";
}
if (!empty($this->maxDistance) && $this->locationEnabled) {
$filters[] = "Within: {$this->maxDistance} km";
}
return $filters;
}
/**
* Render the component
*/
public function render()
{
$this->isLoading = false;
return view('livewire.parks-listing', [
'parks' => $this->parks,
'operators' => $this->operators,
'regions' => $this->regions,
'countries' => $this->countries,
'parkTypes' => $this->parkTypes,
'activeFilters' => $this->activeFilters,
]);
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
$locationKey = $this->userLocation ?
md5(json_encode($this->userLocation)) : 'no-location';
$filterKey = md5(serialize([
$this->search, $this->operatorId, $this->region, $this->country,
$this->parkType, $this->openingYearFrom, $this->openingYearTo,
$this->minArea, $this->maxArea, $this->minRides, $this->maxDistance,
$this->sortBy, $this->sortDirection
]));
return 'thrillwiki.' . class_basename(static::class) . '.' .
$locationKey . '.' . $filterKey . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,475 @@
<?php
namespace App\Livewire;
use App\Models\Park;
use App\Models\Operator;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
class ParksListingUniversal extends Component
{
use WithPagination;
// Universal Listing System Integration
public string $entityType = 'parks';
// Search and Filter Properties with URL binding
#[Url(as: 'q')]
public string $search = '';
#[Url(as: 'operator')]
public string $operatorId = '';
#[Url(as: 'region')]
public string $region = '';
#[Url(as: 'country')]
public string $country = '';
#[Url(as: 'type')]
public string $parkType = '';
#[Url(as: 'year_from')]
public string $openingYearFrom = '';
#[Url(as: 'year_to')]
public string $openingYearTo = '';
#[Url(as: 'min_area')]
public string $minArea = '';
#[Url(as: 'max_area')]
public string $maxArea = '';
#[Url(as: 'min_rides')]
public string $minRides = '';
#[Url(as: 'max_distance')]
public string $maxDistance = '';
#[Url(as: 'sort')]
public string $sortBy = 'name';
#[Url(as: 'dir')]
public string $sortDirection = 'asc';
// Location Properties (GPS Integration)
public ?array $userLocation = null;
public bool $locationEnabled = false;
public bool $locationLoading = false;
// UI State
public bool $showFilters = false;
/**
* Component initialization
*/
public function mount(): void
{
$this->resetPage();
}
/**
* Updated hook for reactive properties
*/
public function updated($property): void
{
if (in_array($property, [
'search', 'operatorId', 'region', 'country', 'parkType',
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
'minRides', 'maxDistance', 'sortBy', 'sortDirection'
])) {
$this->resetPage();
$this->invalidateCache('parks');
}
}
/**
* Enable location services (GPS Integration)
*/
public function enableLocation(): void
{
$this->locationLoading = true;
$this->dispatch('request-location');
}
/**
* Handle location received from JavaScript
*/
public function locationReceived(array $location): void
{
$this->userLocation = $location;
$this->locationEnabled = true;
$this->locationLoading = false;
$this->resetPage();
$this->invalidateCache('parks');
}
/**
* Handle location error
*/
public function locationError(string $error): void
{
$this->locationLoading = false;
$this->dispatch('location-error', message: $error);
}
/**
* Clear all filters
*/
public function clearFilters(): void
{
$this->reset([
'search', 'operatorId', 'region', 'country', 'parkType',
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
'minRides', 'maxDistance'
]);
$this->userLocation = null;
$this->locationEnabled = false;
$this->resetPage();
$this->invalidateCache('parks');
}
/**
* Toggle filters visibility
*/
public function toggleFilters(): void
{
$this->showFilters = !$this->showFilters;
}
/**
* Sort parks by given field
*/
public function sortBy(string $field): void
{
if ($this->sortBy === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $field;
$this->sortDirection = 'asc';
}
$this->resetPage();
$this->invalidateCache('parks');
}
/**
* Get parks with location-aware search and filtering (Django parity)
*/
public function getParksProperty()
{
return $this->remember('parks', function () {
return $this->buildParksQuery()
->paginate(12, ['*'], 'page', $this->getPage());
}, 1200); // 20-minute location-aware caching
}
/**
* Build the parks query with Django parity search and filtering
*/
protected function buildParksQuery(): Builder
{
$query = Park::query();
// Location-aware search functionality (Django parity)
if (!empty($this->search)) {
$query = $this->locationAwareSearch($query, $this->search, $this->userLocation);
}
// Apply advanced filters with geographic context
$query = $this->applyFilters($query, [
'operator_id' => $this->operatorId,
'region' => $this->region,
'country' => $this->country,
'park_type' => $this->parkType,
'opening_year_from' => $this->openingYearFrom,
'opening_year_to' => $this->openingYearTo,
'min_area' => $this->minArea,
'max_area' => $this->maxArea,
'min_rides' => $this->minRides,
'max_distance' => $this->maxDistance,
], $this->userLocation);
// Apply sorting with location-aware options
$query = $this->applySorting($query);
// Eager load relationships for performance
$query->with(['location', 'operator', 'photos', 'statistics'])
->withCount(['rides', 'reviews']);
return $query;
}
/**
* Location-aware search functionality matching Django implementation
*/
protected function locationAwareSearch(Builder $query, string $searchQuery, ?array $userLocation = null): Builder
{
$terms = explode(' ', trim($searchQuery));
foreach ($terms as $term) {
$term = trim($term);
if (empty($term)) continue;
$query->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}%"));
});
}
// Add distance-based ordering for location-aware results
if ($userLocation) {
$query->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');
}
return $query;
}
/**
* Apply advanced filters with geographic context
*/
protected function applyFilters(Builder $query, array $filters, ?array $userLocation = null): Builder
{
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']]);
});
}
/**
* Apply sorting with location-aware options
*/
protected function applySorting(Builder $query): Builder
{
switch ($this->sortBy) {
case 'distance':
if ($this->userLocation) {
// Distance sorting already applied in locationAwareSearch
return $query;
}
// Fallback to name if no location
return $query->orderBy('name', $this->sortDirection);
case 'rides_count':
return $query->orderBy('rides_count', $this->sortDirection);
case 'reviews_count':
return $query->orderBy('reviews_count', $this->sortDirection);
case 'opening_date':
return $query->orderBy('opening_date', $this->sortDirection);
case 'area_acres':
return $query->orderBy('area_acres', $this->sortDirection);
case 'name':
default:
return $query->orderBy('name', $this->sortDirection);
}
}
/**
* Get available operators for filter dropdown
*/
public function getOperatorsProperty()
{
return $this->remember('operators', function () {
return Operator::select('id', 'name')
->whereHas('parks')
->orderBy('name')
->get();
}, 3600);
}
/**
* Get available regions for filter dropdown
*/
public function getRegionsProperty()
{
return $this->remember('regions', function () {
return DB::table('locations')
->join('parks', 'locations.id', '=', 'parks.location_id')
->select('locations.state')
->distinct()
->whereNotNull('locations.state')
->orderBy('locations.state')
->pluck('state');
}, 3600);
}
/**
* Get available countries for filter dropdown
*/
public function getCountriesProperty()
{
return $this->remember('countries', function () {
return DB::table('locations')
->join('parks', 'locations.id', '=', 'parks.location_id')
->select('locations.country')
->distinct()
->whereNotNull('locations.country')
->orderBy('locations.country')
->pluck('country');
}, 3600);
}
/**
* Get available park types for filter dropdown
*/
public function getParkTypesProperty()
{
return $this->remember('park_types', function () {
return Park::select('park_type')
->distinct()
->whereNotNull('park_type')
->orderBy('park_type')
->pluck('park_type');
}, 3600);
}
/**
* Get filter summary for display
*/
public function getActiveFiltersProperty(): array
{
$filters = [];
if (!empty($this->search)) {
$filters[] = "Search: {$this->search}";
}
if (!empty($this->operatorId)) {
$operator = $this->operators->find($this->operatorId);
if ($operator) {
$filters[] = "Operator: {$operator->name}";
}
}
if (!empty($this->region)) {
$filters[] = "Region: {$this->region}";
}
if (!empty($this->country)) {
$filters[] = "Country: {$this->country}";
}
if (!empty($this->parkType)) {
$filters[] = "Type: {$this->parkType}";
}
if (!empty($this->openingYearFrom) || !empty($this->openingYearTo)) {
$yearRange = $this->openingYearFrom . ' - ' . $this->openingYearTo;
$filters[] = "Years: {$yearRange}";
}
if (!empty($this->minArea) || !empty($this->maxArea)) {
$areaRange = $this->minArea . ' - ' . $this->maxArea . ' acres';
$filters[] = "Area: {$areaRange}";
}
if (!empty($this->minRides)) {
$filters[] = "Min Rides: {$this->minRides}";
}
if (!empty($this->maxDistance) && $this->locationEnabled) {
$filters[] = "Within: {$this->maxDistance} km";
}
return $filters;
}
/**
* Render the component using Universal Listing System
*/
public function render()
{
return view('livewire.parks-listing-universal', [
'parks' => $this->parks,
'operators' => $this->operators,
'regions' => $this->regions,
'countries' => $this->countries,
'parkTypes' => $this->parkTypes,
'activeFilters' => $this->activeFilters,
]);
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
$locationKey = $this->userLocation ?
md5(json_encode($this->userLocation)) : 'no-location';
$filterKey = md5(serialize([
$this->search, $this->operatorId, $this->region, $this->country,
$this->parkType, $this->openingYearFrom, $this->openingYearTo,
$this->minArea, $this->maxArea, $this->minRides, $this->maxDistance,
$this->sortBy, $this->sortDirection
]));
return 'thrillwiki.' . class_basename(static::class) . '.' .
$locationKey . '.' . $filterKey . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
class ParksLocationSearch extends Component
{
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.parks-location-search');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
class ParksMapView extends Component
{
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.parks-map-view');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Support\Facades\Cache;
class RegionalParksListing extends Component
{
use WithPagination;
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Render the component
*/
public function render()
{
return view('livewire.regional-parks-listing');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,372 @@
<?php
namespace App\Livewire;
use App\Models\Ride;
use App\Models\Park;
use App\Models\Operator;
use App\Enums\RideCategory;
use App\Enums\RideStatus;
use Livewire\Component;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Cache;
class RidesFilters extends Component
{
// URL-bound filter properties for deep linking
#[Url(as: 'category')]
public ?string $selectedCategory = null;
#[Url(as: 'status')]
public ?string $selectedStatus = null;
#[Url(as: 'manufacturer')]
public ?int $selectedManufacturer = null;
#[Url(as: 'park')]
public ?int $selectedPark = null;
#[Url(as: 'year_min')]
public ?int $minOpeningYear = null;
#[Url(as: 'year_max')]
public ?int $maxOpeningYear = null;
#[Url(as: 'height_min')]
public ?int $minHeight = null;
#[Url(as: 'height_max')]
public ?int $maxHeight = null;
// Filter options (cached)
public array $categories = [];
public array $statuses = [];
public array $manufacturers = [];
public array $parks = [];
public array $yearRange = [];
public array $heightRange = [];
// UI state
public bool $showAdvancedFilters = false;
public int $activeFiltersCount = 0;
/**
* Component initialization
*/
public function mount(): void
{
$this->loadFilterOptions();
$this->calculateActiveFiltersCount();
}
/**
* Load filter options with caching
*/
protected function loadFilterOptions(): void
{
// Categories from enum
$this->categories = $this->remember(
'categories',
fn() => collect(RideCategory::cases())
->map(fn($case) => [
'value' => $case->value,
'label' => $case->name,
'count' => Ride::where('category', $case->value)->count()
])
->filter(fn($item) => $item['count'] > 0)
->values()
->toArray(),
3600 // 1-hour cache
);
// Statuses from enum
$this->statuses = $this->remember(
'statuses',
fn() => collect(RideStatus::cases())
->map(fn($case) => [
'value' => $case->value,
'label' => $case->name,
'count' => Ride::where('status', $case->value)->count()
])
->filter(fn($item) => $item['count'] > 0)
->values()
->toArray(),
3600
);
// Manufacturers (Operators that have manufactured rides)
$this->manufacturers = $this->remember(
'manufacturers',
fn() => Operator::select('id', 'name')
->whereHas('manufacturedRides')
->withCount('manufacturedRides')
->orderBy('name')
->get()
->map(fn($operator) => [
'value' => $operator->id,
'label' => $operator->name,
'count' => $operator->manufactured_rides_count
])
->toArray(),
3600
);
// Parks that have rides
$this->parks = $this->remember(
'parks',
fn() => Park::select('id', 'name')
->whereHas('rides')
->withCount('rides')
->orderBy('name')
->get()
->map(fn($park) => [
'value' => $park->id,
'label' => $park->name,
'count' => $park->rides_count
])
->toArray(),
3600
);
// Year range
$this->yearRange = $this->remember(
'year_range',
function() {
$years = Ride::whereNotNull('opening_year')
->selectRaw('MIN(opening_year) as min_year, MAX(opening_year) as max_year')
->first();
return [
'min' => $years->min_year ?? 1900,
'max' => $years->max_year ?? date('Y')
];
},
3600
);
// Height range
$this->heightRange = $this->remember(
'height_range',
function() {
$heights = Ride::whereNotNull('height_requirement')
->selectRaw('MIN(height_requirement) as min_height, MAX(height_requirement) as max_height')
->first();
return [
'min' => $heights->min_height ?? 0,
'max' => $heights->max_height ?? 200
];
},
3600
);
}
/**
* Calculate number of active filters
*/
protected function calculateActiveFiltersCount(): void
{
$this->activeFiltersCount = collect([
$this->selectedCategory,
$this->selectedStatus,
$this->selectedManufacturer,
$this->selectedPark,
$this->minOpeningYear,
$this->maxOpeningYear,
$this->minHeight,
$this->maxHeight
])->filter()->count();
}
/**
* Apply category filter
*/
public function setCategory(?string $category): void
{
$this->selectedCategory = $category === $this->selectedCategory ? null : $category;
$this->calculateActiveFiltersCount();
$this->dispatch('filters-updated', $this->getActiveFilters());
}
/**
* Apply status filter
*/
public function setStatus(?string $status): void
{
$this->selectedStatus = $status === $this->selectedStatus ? null : $status;
$this->calculateActiveFiltersCount();
$this->dispatch('filters-updated', $this->getActiveFilters());
}
/**
* Apply manufacturer filter
*/
public function setManufacturer(?int $manufacturerId): void
{
$this->selectedManufacturer = $manufacturerId === $this->selectedManufacturer ? null : $manufacturerId;
$this->calculateActiveFiltersCount();
$this->dispatch('filters-updated', $this->getActiveFilters());
}
/**
* Apply park filter
*/
public function setPark(?int $parkId): void
{
$this->selectedPark = $parkId === $this->selectedPark ? null : $parkId;
$this->calculateActiveFiltersCount();
$this->dispatch('filters-updated', $this->getActiveFilters());
}
/**
* Update year range filters
*/
public function updateYearRange(): void
{
$this->calculateActiveFiltersCount();
$this->dispatch('filters-updated', $this->getActiveFilters());
}
/**
* Update height range filters
*/
public function updateHeightRange(): void
{
$this->calculateActiveFiltersCount();
$this->dispatch('filters-updated', $this->getActiveFilters());
}
/**
* Toggle advanced filters visibility
*/
public function toggleAdvancedFilters(): void
{
$this->showAdvancedFilters = !$this->showAdvancedFilters;
}
/**
* Clear all filters
*/
public function clearAllFilters(): void
{
$this->selectedCategory = null;
$this->selectedStatus = null;
$this->selectedManufacturer = null;
$this->selectedPark = null;
$this->minOpeningYear = null;
$this->maxOpeningYear = null;
$this->minHeight = null;
$this->maxHeight = null;
$this->calculateActiveFiltersCount();
$this->dispatch('filters-updated', $this->getActiveFilters());
}
/**
* Get active filters for parent component
*/
public function getActiveFilters(): array
{
return array_filter([
'category' => $this->selectedCategory,
'status' => $this->selectedStatus,
'manufacturer_id' => $this->selectedManufacturer,
'park_id' => $this->selectedPark,
'min_opening_year' => $this->minOpeningYear,
'max_opening_year' => $this->maxOpeningYear,
'min_height' => $this->minHeight,
'max_height' => $this->maxHeight,
]);
}
/**
* Get filter summary for display
*/
public function getFilterSummary(): array
{
$summary = [];
if ($this->selectedCategory) {
$category = collect($this->categories)->firstWhere('value', $this->selectedCategory);
$summary[] = 'Category: ' . ($category['label'] ?? $this->selectedCategory);
}
if ($this->selectedStatus) {
$status = collect($this->statuses)->firstWhere('value', $this->selectedStatus);
$summary[] = 'Status: ' . ($status['label'] ?? $this->selectedStatus);
}
if ($this->selectedManufacturer) {
$manufacturer = collect($this->manufacturers)->firstWhere('value', $this->selectedManufacturer);
$summary[] = 'Manufacturer: ' . ($manufacturer['label'] ?? 'Unknown');
}
if ($this->selectedPark) {
$park = collect($this->parks)->firstWhere('value', $this->selectedPark);
$summary[] = 'Park: ' . ($park['label'] ?? 'Unknown');
}
if ($this->minOpeningYear || $this->maxOpeningYear) {
$yearText = 'Year: ';
if ($this->minOpeningYear && $this->maxOpeningYear) {
$yearText .= $this->minOpeningYear . '-' . $this->maxOpeningYear;
} elseif ($this->minOpeningYear) {
$yearText .= 'After ' . $this->minOpeningYear;
} else {
$yearText .= 'Before ' . $this->maxOpeningYear;
}
$summary[] = $yearText;
}
if ($this->minHeight || $this->maxHeight) {
$heightText = 'Height: ';
if ($this->minHeight && $this->maxHeight) {
$heightText .= $this->minHeight . '-' . $this->maxHeight . 'cm';
} elseif ($this->minHeight) {
$heightText .= 'Min ' . $this->minHeight . 'cm';
} else {
$heightText .= 'Max ' . $this->maxHeight . 'cm';
}
$summary[] = $heightText;
}
return $summary;
}
/**
* Render the component
*/
public function render()
{
return view('livewire.rides-filters');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,283 @@
<?php
namespace App\Livewire;
use App\Models\Ride;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\Url;
class RidesListing extends Component
{
use WithPagination;
// Search and filter properties with URL binding for deep linking
#[Url(as: 'q')]
public string $search = '';
#[Url(as: 'category')]
public string $category = '';
#[Url(as: 'status')]
public string $status = '';
#[Url(as: 'manufacturer')]
public string $manufacturerId = '';
#[Url(as: 'year_from')]
public string $openingYearFrom = '';
#[Url(as: 'year_to')]
public string $openingYearTo = '';
#[Url(as: 'min_height')]
public string $minHeight = '';
#[Url(as: 'max_height')]
public string $maxHeight = '';
#[Url(as: 'park')]
public string $parkId = '';
// Performance optimization
protected $queryString = [
'search' => ['except' => ''],
'category' => ['except' => ''],
'status' => ['except' => ''],
'manufacturerId' => ['except' => ''],
'openingYearFrom' => ['except' => ''],
'openingYearTo' => ['except' => ''],
'minHeight' => ['except' => ''],
'maxHeight' => ['except' => ''],
'parkId' => ['except' => ''],
'page' => ['except' => 1],
];
/**
* Component initialization
*/
public function mount(): void
{
// Initialize component state
}
/**
* Reset pagination when search/filters change
*/
public function updatedSearch(): void
{
$this->resetPage();
$this->invalidateCache('rides');
}
public function updatedCategory(): void
{
$this->resetPage();
$this->invalidateCache('rides');
}
public function updatedStatus(): void
{
$this->resetPage();
$this->invalidateCache('rides');
}
public function updatedManufacturerId(): void
{
$this->resetPage();
$this->invalidateCache('rides');
}
public function updatedOpeningYearFrom(): void
{
$this->resetPage();
$this->invalidateCache('rides');
}
public function updatedOpeningYearTo(): void
{
$this->resetPage();
$this->invalidateCache('rides');
}
public function updatedMinHeight(): void
{
$this->resetPage();
$this->invalidateCache('rides');
}
public function updatedMaxHeight(): void
{
$this->resetPage();
$this->invalidateCache('rides');
}
public function updatedParkId(): void
{
$this->resetPage();
$this->invalidateCache('rides');
}
/**
* Clear all filters
*/
public function clearFilters(): void
{
$this->reset([
'search',
'category',
'status',
'manufacturerId',
'openingYearFrom',
'openingYearTo',
'minHeight',
'maxHeight',
'parkId'
]);
$this->resetPage();
$this->invalidateCache('rides');
}
/**
* Get rides with Django parity search and filtering
*/
public function getRidesProperty()
{
$cacheKey = $this->getCacheKey('rides.' . md5(serialize([
'search' => $this->search,
'category' => $this->category,
'status' => $this->status,
'manufacturerId' => $this->manufacturerId,
'openingYearFrom' => $this->openingYearFrom,
'openingYearTo' => $this->openingYearTo,
'minHeight' => $this->minHeight,
'maxHeight' => $this->maxHeight,
'parkId' => $this->parkId,
'page' => $this->getPage(),
])));
return $this->remember($cacheKey, function () {
return $this->buildQuery()->paginate(12);
}, 300); // 5 minute cache
}
/**
* Build the query with Django parity search and filters
*/
protected function buildQuery()
{
$query = Ride::query()
->with(['park', 'manufacturer', 'designer', 'photos' => function ($q) {
$q->where('is_featured', true)->limit(1);
}]);
// Django parity multi-term search
if (!empty($this->search)) {
$terms = explode(' ', trim($this->search));
foreach ($terms as $term) {
$query->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}%"));
});
}
}
// Apply filters with Django parity
$query = $this->applyFilters($query);
return $query->orderBy('name');
}
/**
* Apply filters with Django parity
*/
protected function applyFilters($query)
{
return $query
->when($this->category, fn($q, $category) =>
$q->where('ride_type', $category))
->when($this->status, fn($q, $status) =>
$q->where('status', $status))
->when($this->manufacturerId, fn($q, $manufacturerId) =>
$q->where('manufacturer_id', $manufacturerId))
->when($this->openingYearFrom, fn($q, $year) =>
$q->where('opening_date', '>=', "{$year}-01-01"))
->when($this->openingYearTo, fn($q, $year) =>
$q->where('opening_date', '<=', "{$year}-12-31"))
->when($this->minHeight, fn($q, $height) =>
$q->where('height_requirement', '>=', $height))
->when($this->maxHeight, fn($q, $height) =>
$q->where('height_requirement', '<=', $height))
->when($this->parkId, fn($q, $parkId) =>
$q->where('park_id', $parkId));
}
/**
* Get available filter options for dropdowns
*/
public function getFilterOptionsProperty()
{
return $this->remember('filter_options', function () {
return [
'categories' => Ride::select('ride_type')
->distinct()
->whereNotNull('ride_type')
->orderBy('ride_type')
->pluck('ride_type', 'ride_type'),
'statuses' => Ride::select('status')
->distinct()
->whereNotNull('status')
->orderBy('status')
->pluck('status', 'status'),
'manufacturers' => \App\Models\Manufacturer::orderBy('name')
->pluck('name', 'id'),
'parks' => \App\Models\Park::orderBy('name')
->pluck('name', 'id'),
];
}, 3600); // 1 hour cache for filter options
}
/**
* Render the component
*/
public function render()
{
return view('livewire.rides-listing', [
'rides' => $this->rides,
'filterOptions' => $this->filterOptions,
]);
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace App\Livewire;
use App\Models\Ride;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\Url;
class RidesListingUniversal extends Component
{
use WithPagination;
// Universal Listing System integration
public string $entityType = 'rides';
// Search and filter properties with URL binding
#[Url(as: 'q')]
public string $search = '';
#[Url(as: 'categories')]
public array $categories = [];
#[Url(as: 'opening_year_from')]
public string $openingYearFrom = '';
#[Url(as: 'opening_year_to')]
public string $openingYearTo = '';
#[Url(as: 'sort')]
public string $sortBy = 'name';
#[Url(as: 'view')]
public string $viewMode = 'grid';
/**
* Get rides data for Universal Listing System
*/
public function getRidesProperty()
{
$cacheKey = 'rides_listing_' . md5(serialize([
'search' => $this->search,
'categories' => $this->categories,
'openingYearFrom' => $this->openingYearFrom,
'openingYearTo' => $this->openingYearTo,
'sortBy' => $this->sortBy,
'page' => $this->getPage(),
]));
return Cache::remember($cacheKey, 300, function () {
return $this->buildQuery()->paginate(12);
});
}
/**
* Build the optimized query
*/
protected function buildQuery()
{
$query = Ride::query()
->with(['park', 'manufacturer', 'designer', 'photos' => function ($q) {
$q->where('is_featured', true)->limit(1);
}]);
// Multi-term search with Django parity
if (!empty($this->search)) {
$terms = explode(' ', trim($this->search));
foreach ($terms as $term) {
$query->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}%"));
});
}
}
// Apply filters
if (!empty($this->categories)) {
$query->whereIn('ride_type', $this->categories);
}
if (!empty($this->openingYearFrom)) {
$query->where('opening_date', '>=', "{$this->openingYearFrom}-01-01");
}
if (!empty($this->openingYearTo)) {
$query->where('opening_date', '<=', "{$this->openingYearTo}-12-31");
}
// Apply sorting
switch ($this->sortBy) {
case 'opening_year':
$query->orderBy('opening_date', 'desc');
break;
case 'thrill_rating':
$query->orderBy('thrill_rating', 'desc');
break;
case 'height_meters':
$query->orderBy('height_meters', 'desc');
break;
default:
$query->orderBy('name');
}
return $query;
}
/**
* Reset pagination when filters change
*/
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedCategories(): void
{
$this->resetPage();
}
public function updatedOpeningYearFrom(): void
{
$this->resetPage();
}
public function updatedOpeningYearTo(): void
{
$this->resetPage();
}
public function updatedSortBy(): void
{
$this->resetPage();
}
/**
* Clear all filters
*/
public function clearFilters(): void
{
$this->reset(['search', 'categories', 'openingYearFrom', 'openingYearTo']);
$this->resetPage();
}
/**
* Render the component using Universal Listing System
*/
public function render()
{
return view('livewire.rides-listing-universal', [
'items' => $this->rides,
'entityType' => $this->entityType,
]);
}
}

View File

@@ -0,0 +1,221 @@
<?php
namespace App\Livewire;
use App\Models\Ride;
use App\Models\Park;
use App\Models\Operator;
use Livewire\Component;
use Livewire\Attributes\On;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Collection;
class RidesSearchSuggestions extends Component
{
public string $query = '';
public bool $showSuggestions = false;
public int $maxSuggestions = 8;
public array $suggestions = [];
/**
* Component initialization
*/
public function mount(string $query = ''): void
{
$this->query = $query;
if (!empty($query)) {
$this->updateSuggestions();
}
}
/**
* Listen for search query updates from parent components
*/
#[On('search-query-updated')]
public function handleSearchUpdate(string $query): void
{
$this->query = $query;
$this->updateSuggestions();
}
/**
* Update search suggestions based on current query
*/
public function updateSuggestions(): void
{
if (strlen($this->query) < 2) {
$this->suggestions = [];
$this->showSuggestions = false;
return;
}
$this->suggestions = $this->remember(
'suggestions.' . md5(strtolower($this->query)),
fn() => $this->buildSuggestions(),
300 // 5-minute cache for suggestions
);
$this->showSuggestions = !empty($this->suggestions);
}
/**
* Build search suggestions from multiple sources
*/
protected function buildSuggestions(): array
{
$query = strtolower(trim($this->query));
$suggestions = collect();
// Ride name suggestions
$rideSuggestions = Ride::select('name', 'slug', 'id')
->with(['park:id,name,slug'])
->where('name', 'ilike', "%{$query}%")
->limit(4)
->get()
->map(function ($ride) {
return [
'type' => 'ride',
'title' => $ride->name,
'subtitle' => $ride->park->name ?? 'Unknown Park',
'url' => route('rides.show', $ride->slug),
'icon' => 'ride',
'category' => 'Rides'
];
});
// Park name suggestions
$parkSuggestions = Park::select('name', 'slug', 'id')
->where('name', 'ilike', "%{$query}%")
->limit(3)
->get()
->map(function ($park) {
return [
'type' => 'park',
'title' => $park->name,
'subtitle' => 'Theme Park',
'url' => route('parks.show', $park->slug),
'icon' => 'park',
'category' => 'Parks'
];
});
// Manufacturer/Designer suggestions
$operatorSuggestions = Operator::select('name', 'slug', 'id')
->where('name', 'ilike', "%{$query}%")
->limit(2)
->get()
->map(function ($operator) {
return [
'type' => 'operator',
'title' => $operator->name,
'subtitle' => 'Manufacturer/Designer',
'url' => route('operators.show', $operator->slug),
'icon' => 'operator',
'category' => 'Companies'
];
});
// Combine and prioritize suggestions
$suggestions = $suggestions
->concat($rideSuggestions)
->concat($parkSuggestions)
->concat($operatorSuggestions)
->take($this->maxSuggestions);
return $suggestions->toArray();
}
/**
* Handle suggestion selection
*/
public function selectSuggestion(array $suggestion): void
{
$this->dispatch('suggestion-selected', $suggestion);
$this->hideSuggestions();
}
/**
* Hide suggestions dropdown
*/
public function hideSuggestions(): void
{
$this->showSuggestions = false;
}
/**
* Show suggestions dropdown
*/
public function showSuggestionsDropdown(): void
{
if (!empty($this->suggestions)) {
$this->showSuggestions = true;
}
}
/**
* Handle input focus
*/
public function onFocus(): void
{
$this->showSuggestionsDropdown();
}
/**
* Handle input blur with delay to allow clicks
*/
public function onBlur(): void
{
// Delay hiding to allow suggestion clicks
$this->dispatch('delayed-hide-suggestions');
}
/**
* Get icon class for suggestion type
*/
public function getIconClass(string $type): string
{
return match($type) {
'ride' => 'fas fa-roller-coaster',
'park' => 'fas fa-map-marker-alt',
'operator' => 'fas fa-industry',
default => 'fas fa-search'
};
}
/**
* Render the component
*/
public function render()
{
return view('livewire.rides-search-suggestions');
}
/**
* Get cache key for this component
*/
protected function getCacheKey(string $suffix = ''): string
{
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
}
/**
* Remember data with caching
*/
protected function remember(string $key, $callback, int $ttl = 3600)
{
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
}
/**
* Invalidate component cache
*/
protected function invalidateCache(string $key = null): void
{
if ($key) {
Cache::forget($this->getCacheKey($key));
} else {
// Clear all cache for this component
Cache::flush();
}
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Livewire;
use App\Models\Park;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\On;
use Livewire\Component;
use Livewire\WithPagination;
@@ -49,6 +50,17 @@ class SearchComponent extends Component
$this->filtersApplied = $this->hasActiveFilters();
}
#[On('suggestion-selected')]
public function handleSuggestionSelected($id, $text): void
{
$park = Park::find($id);
if ($park) {
$this->search = $text;
$this->filtersApplied = $this->hasActiveFilters();
redirect()->route('parks.show', $park);
}
}
public function updatedLocation(): void
{
$this->resetPage();

View File

@@ -2,26 +2,27 @@
namespace App\Models;
use App\Traits\HasSlugHistory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
class Designer extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
use HasSlugHistory;
protected $fillable = [
'name',
'slug',
'bio',
'description',
'website',
'founded_date',
'headquarters',
];
protected $casts = [
'founded_date' => 'date',
];
/**
* Boot the model.
*/
protected static function boot()
{
parent::boot();
@@ -32,11 +33,8 @@ class Designer extends Model
}
});
}
/**
* Get the rides designed by this designer.
*/
public function rides(): HasMany
public function rides()
{
return $this->hasMany(Ride::class);
}

View File

@@ -6,10 +6,11 @@ use App\Traits\HasSlugHistory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class Manufacturer extends Model
{
use HasFactory, HasSlugHistory;
use HasFactory, HasSlugHistory, SoftDeletes;
/**
* The attributes that are mass assignable.
@@ -24,15 +25,44 @@ class Manufacturer extends Model
'description',
'total_rides',
'total_roller_coasters',
'is_active',
'industry_presence_score',
'market_share_percentage',
'founded_year',
'specialization',
'product_portfolio',
'manufacturing_categories',
'global_installations',
'primary_market',
'is_major_manufacturer',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'total_rides' => 'integer',
'total_roller_coasters' => 'integer',
'is_active' => 'boolean',
'industry_presence_score' => 'integer',
'market_share_percentage' => 'decimal:2',
'founded_year' => 'integer',
'manufacturing_categories' => 'array',
'global_installations' => 'integer',
'is_major_manufacturer' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* Get the rides manufactured by this company.
* Note: This relationship will be properly set up when we implement the Rides system.
*/
public function rides(): HasMany
{
return $this->hasMany(Ride::class);
return $this->hasMany(Ride::class, 'manufacturer_id');
}
/**
@@ -42,7 +72,7 @@ class Manufacturer extends Model
{
$this->total_rides = $this->rides()->count();
$this->total_roller_coasters = $this->rides()
->where('type', 'roller_coaster')
->where('category', 'RC')
->count();
$this->save();
}
@@ -88,6 +118,22 @@ class Manufacturer extends Model
return $query->where('total_roller_coasters', '>', 0);
}
/**
* Scope a query to only include active manufacturers.
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Scope a query for optimized loading with statistics.
*/
public function scopeOptimized($query)
{
return $query->select(['id', 'name', 'slug', 'total_rides', 'total_roller_coasters', 'is_active']);
}
/**
* Get the route key for the model.
*/

View File

@@ -3,13 +3,17 @@
namespace App\Models;
use App\Traits\HasSlugHistory;
use App\Traits\HasLocation;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
class Operator extends Model
{
use HasFactory, HasSlugHistory;
use HasFactory, HasSlugHistory, HasLocation, SoftDeletes;
/**
* The attributes that are mass assignable.
@@ -20,14 +24,43 @@ class Operator extends Model
'name',
'slug',
'website',
'headquarters',
'description',
'industry_sector',
'founded_year',
'employee_count',
'annual_revenue',
'market_cap',
'stock_symbol',
'headquarters_location',
'geographic_presence',
'company_type',
'parent_company_id',
'is_public',
'is_active',
'total_parks',
'total_rides',
'total_rides_manufactured',
'total_rides_designed',
];
/**
* Get the parks operated by this company.
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'founded_year' => 'integer',
'employee_count' => 'integer',
'annual_revenue' => 'decimal:2',
'market_cap' => 'decimal:2',
'is_public' => 'boolean',
'is_active' => 'boolean',
'total_parks' => 'integer',
'total_rides_manufactured' => 'integer',
'total_rides_designed' => 'integer',
];
/**
* Get the parks operated by this operator.
*/
public function parks(): HasMany
{
@@ -35,21 +68,60 @@ class Operator extends Model
}
/**
* Update park statistics.
* Get the rides manufactured by this operator.
*/
public function manufactured_rides(): HasMany
{
return $this->hasMany(Ride::class, 'manufacturer_id');
}
/**
* Get the rides designed by this operator.
*/
public function designed_rides(): HasMany
{
return $this->hasMany(Ride::class, 'designer_id');
}
/**
* Get the parent company if this is a subsidiary.
*/
public function parent_company(): BelongsTo
{
return $this->belongsTo(Operator::class, 'parent_company_id');
}
/**
* Get the subsidiary companies.
*/
public function subsidiaries(): HasMany
{
return $this->hasMany(Operator::class, 'parent_company_id');
}
/**
* Update comprehensive statistics.
*/
public function updateStatistics(): void
{
$this->total_parks = $this->parks()->count();
$this->total_rides = $this->parks()->sum('ride_count');
$this->total_rides_manufactured = $this->manufactured_rides()->count();
$this->total_rides_designed = $this->designed_rides()->count();
$this->save();
}
/**
* Get the operator's name with total parks.
* Get the operator's display name with role indicators.
*/
public function getDisplayNameAttribute(): string
{
return "{$this->name} ({$this->total_parks} parks)";
$roles = [];
if ($this->total_parks > 0) $roles[] = 'Operator';
if ($this->total_rides_manufactured > 0) $roles[] = 'Manufacturer';
if ($this->total_rides_designed > 0) $roles[] = 'Designer';
$roleText = empty($roles) ? '' : ' (' . implode(', ', $roles) . ')';
return $this->name . $roleText;
}
/**
@@ -69,6 +141,98 @@ class Operator extends Model
return $website;
}
/**
* Get company size category based on employee count.
*/
public function getCompanySizeCategoryAttribute(): string
{
if (!$this->employee_count) return 'unknown';
return match (true) {
$this->employee_count <= 100 => 'small',
$this->employee_count <= 1000 => 'medium',
$this->employee_count <= 10000 => 'large',
default => 'enterprise'
};
}
/**
* Get geographic presence level.
*/
public function getGeographicPresenceLevelAttribute(): string
{
$countries = $this->parks()
->join('locations', 'parks.location_id', '=', 'locations.id')
->distinct('locations.country')
->count('locations.country');
return match (true) {
$countries <= 1 => 'regional',
$countries <= 3 => 'national',
default => 'international'
};
}
/**
* Get market influence score.
*/
public function getMarketInfluenceScoreAttribute(): float
{
$score = 0;
// Parks operated (40% weight)
$score += min($this->total_parks * 10, 40);
// Rides manufactured (30% weight)
$score += min($this->total_rides_manufactured * 2, 30);
// Revenue influence (20% weight)
if ($this->annual_revenue) {
$score += min(($this->annual_revenue / 1000000000) * 10, 20);
}
// Geographic presence (10% weight)
$countries = $this->parks()
->join('locations', 'parks.location_id', '=', 'locations.id')
->distinct('locations.country')
->count('locations.country');
$score += min($countries * 2, 10);
return round($score, 1);
}
/**
* Scope a query to only include active operators.
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Scope a query to only include park operators.
*/
public function scopeParkOperators($query)
{
return $query->whereHas('parks');
}
/**
* Scope a query to only include ride manufacturers.
*/
public function scopeManufacturers($query)
{
return $query->whereHas('manufactured_rides');
}
/**
* Scope a query to only include ride designers.
*/
public function scopeDesigners($query)
{
return $query->whereHas('designed_rides');
}
/**
* Scope a query to only include major operators (with multiple parks).
*/
@@ -77,6 +241,75 @@ class Operator extends Model
return $query->where('total_parks', '>=', $minParks);
}
/**
* Scope a query to filter by industry sector.
*/
public function scopeIndustrySector($query, string $sector)
{
return $query->where('industry_sector', $sector);
}
/**
* Scope a query to filter by company size.
*/
public function scopeCompanySize($query, string $size)
{
$ranges = [
'small' => [1, 100],
'medium' => [101, 1000],
'large' => [1001, 10000],
'enterprise' => [10001, PHP_INT_MAX]
];
if (isset($ranges[$size])) {
return $query->whereBetween('employee_count', $ranges[$size]);
}
return $query;
}
/**
* Scope a query to filter by founded year range.
*/
public function scopeFoundedBetween($query, int $startYear, int $endYear)
{
return $query->whereBetween('founded_year', [$startYear, $endYear]);
}
/**
* Scope a query to filter by revenue range.
*/
public function scopeRevenueBetween($query, float $minRevenue, float $maxRevenue)
{
return $query->whereBetween('annual_revenue', [$minRevenue, $maxRevenue]);
}
/**
* Scope a query to filter by dual roles.
*/
public function scopeDualRole($query, array $roles)
{
return $query->where(function ($q) use ($roles) {
if (in_array('park_operator', $roles)) {
$q->orWhereHas('parks');
}
if (in_array('ride_manufacturer', $roles)) {
$q->orWhereHas('manufactured_rides');
}
if (in_array('ride_designer', $roles)) {
$q->orWhereHas('designed_rides');
}
});
}
/**
* Scope a query for optimized loading with counts.
*/
public function scopeWithCounts($query)
{
return $query->withCount(['parks', 'manufactured_rides', 'designed_rides']);
}
/**
* Get the route key for the model.
*/

View File

@@ -4,8 +4,10 @@ namespace App\Models;
use App\Enums\ParkStatus;
use App\Traits\HasLocation;
use App\Models\Operator;
use App\Traits\HasSlugHistory;
use App\Traits\HasParkStatistics;
use App\Traits\TrackedModel;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -89,6 +91,15 @@ class Park extends Model
return $this->belongsTo(Operator::class);
}
/**
* Get the owner that owns the park.
* @deprecated Use operator() relationship instead until Company model is implemented
*/
public function owner(): BelongsTo
{
return $this->belongsTo(Operator::class, 'owner_id');
}
/**
* Get the areas in the park.
*/

102
app/Models/ReviewImage.php Normal file
View File

@@ -0,0 +1,102 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* ReviewImage Model
*
* Generated by ThrillWiki Model Generator
* Includes ThrillWiki optimization patterns and performance enhancements
*/
class ReviewImage extends Model
{
use HasFactory;
use HasSoftDeletes;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'review_images';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'description',
'is_active',
// Add more fillable attributes as needed
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'is_active' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
// Add more casts as needed
];
/**
* The attributes that should be hidden for arrays.
*
* @var array<int, string>
*/
protected $hidden = [
// Add hidden attributes if needed
];
// Query Scopes
/**
* Scope a query to only include active records.
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Scope for optimized queries with common relationships.
*/
public function scopeOptimized($query)
{
return $query->with($this->getOptimizedRelations());
}
// ThrillWiki Methods
/**
* Get optimized relations for this model.
*/
public function getOptimizedRelations(): array
{
return [
// Define common relationships to eager load
];
}
/**
* Get cache key for this model instance.
*/
public function getCacheKey(string $suffix = ''): string
{
$key = strtolower(class_basename($this)) . '.' . $this->id;
return $suffix ? $key . '.' . $suffix : $key;
}
}

102
app/Models/ReviewReport.php Normal file
View File

@@ -0,0 +1,102 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* ReviewReport Model
*
* Generated by ThrillWiki Model Generator
* Includes ThrillWiki optimization patterns and performance enhancements
*/
class ReviewReport extends Model
{
use HasFactory;
use HasSoftDeletes;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'review_reports';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'description',
'is_active',
// Add more fillable attributes as needed
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'is_active' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
// Add more casts as needed
];
/**
* The attributes that should be hidden for arrays.
*
* @var array<int, string>
*/
protected $hidden = [
// Add hidden attributes if needed
];
// Query Scopes
/**
* Scope a query to only include active records.
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Scope for optimized queries with common relationships.
*/
public function scopeOptimized($query)
{
return $query->with($this->getOptimizedRelations());
}
// ThrillWiki Methods
/**
* Get optimized relations for this model.
*/
public function getOptimizedRelations(): array
{
return [
// Define common relationships to eager load
];
}
/**
* Get cache key for this model instance.
*/
public function getCacheKey(string $suffix = ''): string
{
$key = strtolower(class_basename($this)) . '.' . $this->id;
return $suffix ? $key . '.' . $suffix : $key;
}
}

View File

@@ -4,26 +4,34 @@ namespace App\Models;
use App\Enums\RideCategory;
use App\Enums\RideStatus;
use App\Traits\HasSlugHistory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth;
class Ride extends Model
{
use SoftDeletes, HasSlugHistory;
protected $fillable = [
'name',
'slug',
'description',
'status',
'category',
'opening_date',
'closing_date',
'park_id',
'park_area_id',
'manufacturer_id',
'designer_id',
'ride_model_id',
'category',
'status',
'post_closing_status',
'status_since',
'opening_date',
'closing_date',
'min_height_in',
'max_height_in',
'capacity_per_hour',
@@ -35,13 +43,14 @@ class Ride extends Model
'category' => RideCategory::class,
'opening_date' => 'date',
'closing_date' => 'date',
'status_since' => 'date',
'min_height_in' => 'integer',
'max_height_in' => 'integer',
'capacity_per_hour' => 'integer',
'ride_duration_seconds' => 'integer',
];
// Base Relationships
// Core Relationships (Django Parity)
public function park(): BelongsTo
{
return $this->belongsTo(Park::class);
@@ -54,7 +63,7 @@ class Ride extends Model
public function manufacturer(): BelongsTo
{
return $this->belongsTo(Manufacturer::class);
return $this->belongsTo(Manufacturer::class, 'manufacturer_id');
}
public function designer(): BelongsTo
@@ -67,23 +76,51 @@ class Ride extends Model
return $this->belongsTo(RideModel::class);
}
// Extended Relationships
public function coasterStats(): HasOne
{
return $this->hasOne(RollerCoasterStats::class);
}
// Photo Relationships (Polymorphic)
public function photos(): MorphMany
{
return $this->morphMany(Photo::class, 'photosable');
}
// Review Relationships
public function reviews(): HasMany
public function reviews(): MorphMany
{
return $this->hasMany(Review::class);
return $this->morphMany(Review::class, 'reviewable');
}
public function approvedReviews(): HasMany
public function approvedReviews(): MorphMany
{
return $this->reviews()->approved();
return $this->reviews()->where('status', 'approved');
}
// Review Methods
// Query Scopes
public function scopeActive($query)
{
return $query->where('status', 'operating');
}
public function scopeByCategory($query, $category)
{
return $query->where('category', $category);
}
public function scopeInPark($query, $parkId)
{
return $query->where('park_id', $parkId);
}
public function scopeByManufacturer($query, $manufacturerId)
{
return $query->where('manufacturer_id', $manufacturerId);
}
// Attributes & Helper Methods
public function getAverageRatingAttribute(): ?float
{
return $this->approvedReviews()->avg('rating');
@@ -94,6 +131,32 @@ class Ride extends Model
return $this->approvedReviews()->count();
}
public function getDisplayNameAttribute(): string
{
return $this->name . ' at ' . $this->park->name;
}
public function getIsOperatingAttribute(): bool
{
return $this->status === RideStatus::OPERATING;
}
public function getHeightRequirementTextAttribute(): ?string
{
if (!$this->min_height_in) {
return null;
}
$text = "Must be at least {$this->min_height_in}\" tall";
if ($this->max_height_in) {
$text .= " and no taller than {$this->max_height_in}\" tall";
}
return $text;
}
// Review Management Methods
public function canBeReviewedBy(?int $userId): bool
{
if (!$userId) {
@@ -112,7 +175,32 @@ class Ride extends Model
'rating' => $data['rating'],
'title' => $data['title'] ?? null,
'content' => $data['content'],
'status' => ReviewStatus::PENDING,
'status' => 'pending',
]);
}
// Cache Management (Future: will use HasCaching trait)
public function getCacheKey(string $suffix = ''): string
{
return "ride:{$this->id}" . ($suffix ? ":{$suffix}" : '');
}
public function clearRelatedCache(): void
{
cache()->forget($this->getCacheKey('reviews'));
cache()->forget($this->getCacheKey('statistics'));
cache()->forget($this->getCacheKey('photos'));
}
// Statistics Management (Future: will use HasStatistics trait)
public function updateStatistics(): void
{
// Placeholder for future HasStatistics trait integration
// For now, manually manage statistics
$totalReviews = $this->reviews()->count();
$averageRating = $this->reviews()->avg('rating');
// Update any related statistics tracking
$this->clearRelatedCache();
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Policies;
use App\Models\Designer;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class DesignerPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('view designers');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Designer $designer): bool
{
return $user->hasPermissionTo('view designers');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('create designers');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Designer $designer): bool
{
return $user->hasPermissionTo('edit designers');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Designer $designer): bool
{
return $user->hasPermissionTo('delete designers');
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Designer $designer): bool
{
return $user->hasPermissionTo('restore designers');
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Designer $designer): bool
{
return $user->hasPermissionTo('force delete designers');
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->colors([
'primary' => Color::Amber,
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Pages\Dashboard::class,
])
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
])
->navigationGroups([
'Company Management',
'Attractions',
'Parks',
'User Management',
'System',
])
->brandName('ThrillWiki Admin')
->sidebarCollapsibleOnDesktop();
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class ModerationPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->id('moderation')
->path('moderation')
->login()
->colors([
'primary' => Color::Blue,
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Pages\Dashboard::class,
])
->widgets([
Widgets\AccountWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
])
->navigationGroups([
'Content Moderation',
'User Management',
'Reviews',
'Reports',
])
->brandName('ThrillWiki Moderation')
->sidebarCollapsibleOnDesktop()
->maxContentWidth('full');
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to your application's "home" route.
*
* Typically, users are redirected here after authentication.
*/
public const HOME = '/dashboard';
/**
* Define your route model bindings, pattern filters, and other route configuration.
*/
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Livewire\Volt\Volt;
class VoltServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
Volt::mount([
config('livewire.view_path', resource_path('views/livewire')),
resource_path('views/pages'),
]);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Traits;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Model;
trait TrackedModel {
protected static function bootTrackedModel() {
static::created(function (Model $model) {
static::logChange($model, 'created');
});
static::updated(function (Model $model) {
static::logChange($model, 'updated');
});
static::deleted(function (Model $model) {
static::logChange($model, 'deleted');
});
}
protected static function logChange(Model $model, string $action) {
$changes = $action === 'updated'
? [
'old' => $model->getOriginal(),
'new' => $model->getDirty()
]
: $model->getAttributes();
DB::table('model_history')->insert([
'model_type' => get_class($model),
'model_id' => $model->getKey(),
'user_id' => Auth::check() ? Auth::id() : null,
'action' => $action,
'changes' => json_encode($changes),
'created_at' => now(),
'updated_at' => now(),
]);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class AppLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.app');
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class GuestLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.guest');
}
}

View File

@@ -2,4 +2,6 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
App\Providers\VoltServiceProvider::class,
];

View File

@@ -1,29 +1,25 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": [
"laravel",
"framework"
],
"license": "MIT",
"require": {
"php": "^8.2",
"intervention/image": "^3.11",
"laravel/framework": "^11.31",
"laravel/tinker": "^2.9",
"livewire/livewire": "^3.5"
"php": "^8.1",
"filament/filament": "^3.2",
"filament/notifications": "^3.2",
"laravel/framework": "^11.0",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.8",
"livewire/livewire": "^3.4",
"livewire/volt": "^1.7.0",
"owen-it/laravel-auditing": "^13.5",
"spatie/laravel-permission": "^6.3"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.1",
"laravel/pint": "^1.13",
"laravel/sail": "^1.26",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.1",
"pestphp/pest": "^3.7",
"pestphp/pest-plugin-laravel": "^3.1"
"fakerphp/faker": "^1.9.1",
"laravel/breeze": "^2.3",
"laravel/pint": "^1.0",
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^8.0",
"phpunit/phpunit": "^10.1",
"spatie/laravel-ignition": "^2.0"
},
"autoload": {
"psr-4": {
@@ -43,19 +39,14 @@
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"@php artisan filament:upgrade"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
"@php artisan key:generate --ansi"
]
},
"extra": {

4575
composer.lock generated

File diff suppressed because it is too large Load Diff

202
config/permission.php Normal file
View File

@@ -0,0 +1,202 @@
<?php
return [
'models' => [
/*
* When using the "HasPermissions" trait from this package, we need to know which
* Eloquent model should be used to retrieve your permissions. Of course, it
* is often just the "Permission" model but you may use whatever you like.
*
* The model you want to use as a Permission model needs to implement the
* `Spatie\Permission\Contracts\Permission` contract.
*/
'permission' => Spatie\Permission\Models\Permission::class,
/*
* When using the "HasRoles" trait from this package, we need to know which
* Eloquent model should be used to retrieve your roles. Of course, it
* is often just the "Role" model but you may use whatever you like.
*
* The model you want to use as a Role model needs to implement the
* `Spatie\Permission\Contracts\Role` contract.
*/
'role' => Spatie\Permission\Models\Role::class,
],
'table_names' => [
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'roles' => 'roles',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your permissions. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'permissions' => 'permissions',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your models permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_permissions' => 'model_has_permissions',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your models roles. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_roles' => 'model_has_roles',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'role_has_permissions' => 'role_has_permissions',
],
'column_names' => [
/*
* Change this if you want to name the related pivots other than defaults
*/
'role_pivot_key' => null, // default 'role_id',
'permission_pivot_key' => null, // default 'permission_id',
/*
* Change this if you want to name the related model primary key other than
* `model_id`.
*
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/
'model_morph_key' => 'model_id',
/*
* Change this if you want to use the teams feature and your related model's
* foreign key is other than `team_id`.
*/
'team_foreign_key' => 'team_id',
],
/*
* When set to true, the method for checking permissions will be registered on the gate.
* Set this to false if you want to implement custom logic for checking permissions.
*/
'register_permission_check_method' => true,
/*
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
*/
'register_octane_reset_listener' => false,
/*
* Events will fire when a role or permission is assigned/unassigned:
* \Spatie\Permission\Events\RoleAttached
* \Spatie\Permission\Events\RoleDetached
* \Spatie\Permission\Events\PermissionAttached
* \Spatie\Permission\Events\PermissionDetached
*
* To enable, set to true, and then create listeners to watch these events.
*/
'events_enabled' => false,
/*
* Teams Feature.
* When set to true the package implements teams using the 'team_foreign_key'.
* If you want the migrations to register the 'team_foreign_key', you must
* set this to true before doing the migration.
* If you already did the migration then you must make a new migration to also
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
* (view the latest version of this package's migration file)
*/
'teams' => false,
/*
* The class to use to resolve the permissions team id
*/
'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
/*
* Passport Client Credentials Grant
* When set to true the package will use Passports Client to check permissions
*/
'use_passport_client_credentials' => false,
/*
* When set to true, the required permission names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
/*
* When set to true, the required role names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_role_in_exception' => false,
/*
* By default wildcard permission lookups are disabled.
* See documentation to understand supported syntax.
*/
'enable_wildcard_permission' => false,
/*
* The class to use for interpreting wildcard permissions.
* If you need to modify delimiters, override the class and specify its name here.
*/
// 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class,
/* Cache-specific settings */
'cache' => [
/*
* By default all permissions are cached for 24 hours to speed up performance.
* When permissions or roles are updated the cache is flushed automatically.
*/
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
/*
* The cache key used to store all permissions.
*/
'key' => 'spatie.permission.cache',
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
* file. Using 'default' here means to use the `default` set in cache.php.
*/
'store' => 'default',
],
];

View File

@@ -0,0 +1,645 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Universal Listing Entity Configurations
|--------------------------------------------------------------------------
|
| This file contains the configuration for each entity type that can be
| displayed using the universal listing component. Each entity defines
| its display fields, filters, badges, and other presentation options.
|
*/
'entities' => [
'operators' => [
'title' => 'Operators',
'description' => 'Discover theme park operators, ride manufacturers, and designers',
'searchPlaceholder' => 'Search operators, manufacturers, designers...',
'emptyStateMessage' => 'No operators found',
'emptyStateDescription' => 'Try adjusting your search or filters.',
'viewModes' => ['grid', 'list', 'portfolio'],
'colorScheme' => [
'primary' => 'blue',
'secondary' => 'green',
'accent' => 'purple'
],
'cardFields' => [
'title' => 'name',
'subtitle' => 'location.city_country',
'description' => 'description',
'score' => 'market_influence_score',
'scoreLabel' => 'Market Influence',
'metrics' => [
[
'field' => 'founded_year',
'label' => 'Founded',
'format' => null
],
[
'field' => 'industry_sector',
'label' => 'Sector',
'format' => null
],
[
'field' => 'employee_count',
'label' => 'Employees',
'format' => '%s'
],
[
'field' => 'geographic_presence_level',
'label' => 'Presence',
'format' => null
]
]
],
'badges' => [
'fields' => [
[
'field' => 'parks_count',
'prefix' => 'Operator: ',
'suffix' => ' parks',
'color' => 'blue'
],
[
'field' => 'manufactured_rides_count',
'prefix' => 'Manufacturer: ',
'suffix' => ' rides',
'color' => 'green'
],
[
'field' => 'designed_rides_count',
'prefix' => 'Designer: ',
'suffix' => ' rides',
'color' => 'purple'
]
]
],
'filters' => [
'quickFilters' => [
[
'key' => 'roleFilter',
'value' => 'park_operator',
'label' => 'Operators',
'active' => false,
'count' => null
],
[
'key' => 'roleFilter',
'value' => 'ride_manufacturer',
'label' => 'Manufacturers',
'active' => false,
'count' => null
],
[
'key' => 'roleFilter',
'value' => 'ride_designer',
'label' => 'Designers',
'active' => false,
'count' => null
]
],
'sections' => [
[
'title' => 'Operator Roles',
'type' => 'checkboxes',
'model' => 'roleFilter',
'options' => [
['value' => 'park_operator', 'label' => 'Park Operators', 'count' => null],
['value' => 'ride_manufacturer', 'label' => 'Manufacturers', 'count' => null],
['value' => 'ride_designer', 'label' => 'Designers', 'count' => null]
]
],
[
'title' => 'Company Size',
'type' => 'select',
'model' => 'companySize',
'placeholder' => 'All Sizes',
'options' => [
['value' => 'small', 'label' => 'Small (1-100)'],
['value' => 'medium', 'label' => 'Medium (101-1000)'],
['value' => 'large', 'label' => 'Large (1001-10000)'],
['value' => 'enterprise', 'label' => 'Enterprise (10000+)']
]
],
[
'title' => 'Industry Sector',
'type' => 'select',
'model' => 'industrySector',
'placeholder' => 'All Sectors',
'options' => []
],
[
'title' => 'Founded Year',
'type' => 'range',
'fromModel' => 'foundedYearFrom',
'toModel' => 'foundedYearTo',
'fromLabel' => 'From Year',
'toLabel' => 'To Year',
'fromPlaceholder' => '1900',
'toPlaceholder' => date('Y')
]
]
],
'sortOptions' => [
['value' => 'name', 'label' => 'Name'],
['value' => 'founded_year', 'label' => 'Founded Year'],
['value' => 'parks_count', 'label' => 'Parks Count'],
['value' => 'rides_count', 'label' => 'Rides Count'],
['value' => 'market_influence', 'label' => 'Market Influence']
]
],
'rides' => [
'title' => 'Rides',
'description' => 'Explore thrilling rides from theme parks around the world',
'searchPlaceholder' => 'Search rides, parks, manufacturers...',
'emptyStateMessage' => 'No rides found',
'emptyStateDescription' => 'Try adjusting your search or filters.',
'viewModes' => ['grid', 'list'],
'colorScheme' => [
'primary' => 'red',
'secondary' => 'orange',
'accent' => 'yellow'
],
'cardFields' => [
'title' => 'name',
'subtitle' => 'park.name',
'description' => 'description',
'score' => 'thrill_rating',
'scoreLabel' => 'Thrill Rating',
'metrics' => [
[
'field' => 'opening_year',
'label' => 'Opened',
'format' => null
],
[
'field' => 'category',
'label' => 'Category',
'format' => null
],
[
'field' => 'manufacturer.name',
'label' => 'Manufacturer',
'format' => null
],
[
'field' => 'height_meters',
'label' => 'Height',
'format' => '%sm'
]
]
],
'badges' => [
'fields' => [
[
'field' => 'category',
'prefix' => '',
'suffix' => '',
'color' => 'red'
],
[
'field' => 'status',
'prefix' => '',
'suffix' => '',
'color' => 'green'
]
]
],
'filters' => [
'quickFilters' => [
[
'key' => 'category',
'value' => 'roller_coaster',
'label' => 'Roller Coasters',
'active' => false,
'count' => null
],
[
'key' => 'category',
'value' => 'dark_ride',
'label' => 'Dark Rides',
'active' => false,
'count' => null
],
[
'key' => 'category',
'value' => 'flat_ride',
'label' => 'Flat Rides',
'active' => false,
'count' => null
]
],
'sections' => [
[
'title' => 'Ride Category',
'type' => 'checkboxes',
'model' => 'categories',
'options' => [
['value' => 'roller_coaster', 'label' => 'Roller Coasters', 'count' => null],
['value' => 'dark_ride', 'label' => 'Dark Rides', 'count' => null],
['value' => 'flat_ride', 'label' => 'Flat Rides', 'count' => null],
['value' => 'water_ride', 'label' => 'Water Rides', 'count' => null]
]
],
[
'title' => 'Opening Year',
'type' => 'range',
'fromModel' => 'openingYearFrom',
'toModel' => 'openingYearTo',
'fromLabel' => 'From Year',
'toLabel' => 'To Year',
'fromPlaceholder' => '1900',
'toPlaceholder' => date('Y')
]
]
],
'sortOptions' => [
['value' => 'name', 'label' => 'Name'],
['value' => 'opening_year', 'label' => 'Opening Year'],
['value' => 'thrill_rating', 'label' => 'Thrill Rating'],
['value' => 'height_meters', 'label' => 'Height']
]
],
'parks' => [
'title' => 'Parks',
'description' => 'Discover amazing theme parks from around the world',
'searchPlaceholder' => 'Search parks, locations, operators...',
'emptyStateMessage' => 'No parks found',
'emptyStateDescription' => 'Try adjusting your search or filters.',
'viewModes' => ['grid', 'list', 'portfolio'],
'colorScheme' => [
'primary' => 'green',
'secondary' => 'blue',
'accent' => 'teal'
],
'cardFields' => [
'title' => 'name',
'subtitle' => 'location.city_country',
'description' => 'description',
'score' => 'overall_rating',
'scoreLabel' => 'Rating',
'metrics' => [
[
'field' => 'opening_year',
'label' => 'Opened',
'format' => null
],
[
'field' => 'rides_count',
'label' => 'Rides',
'format' => '%s'
],
[
'field' => 'operator.name',
'label' => 'Operator',
'format' => null
],
[
'field' => 'area_hectares',
'label' => 'Area',
'format' => '%s ha'
]
]
],
'badges' => [
'fields' => [
[
'field' => 'park_type',
'prefix' => '',
'suffix' => '',
'color' => 'green'
],
[
'field' => 'status',
'prefix' => '',
'suffix' => '',
'color' => 'blue'
]
]
],
'filters' => [
'quickFilters' => [
[
'key' => 'parkType',
'value' => 'theme_park',
'label' => 'Theme Parks',
'active' => false,
'count' => null
],
[
'key' => 'parkType',
'value' => 'amusement_park',
'label' => 'Amusement Parks',
'active' => false,
'count' => null
],
[
'key' => 'parkType',
'value' => 'water_park',
'label' => 'Water Parks',
'active' => false,
'count' => null
]
],
'sections' => [
[
'title' => 'Park Type',
'type' => 'checkboxes',
'model' => 'parkTypes',
'options' => [
['value' => 'theme_park', 'label' => 'Theme Parks', 'count' => null],
['value' => 'amusement_park', 'label' => 'Amusement Parks', 'count' => null],
['value' => 'water_park', 'label' => 'Water Parks', 'count' => null],
['value' => 'family_entertainment', 'label' => 'Family Entertainment', 'count' => null]
]
],
[
'title' => 'Opening Year',
'type' => 'range',
'fromModel' => 'openingYearFrom',
'toModel' => 'openingYearTo',
'fromLabel' => 'From Year',
'toLabel' => 'To Year',
'fromPlaceholder' => '1900',
'toPlaceholder' => date('Y')
]
]
],
'sortOptions' => [
['value' => 'name', 'label' => 'Name'],
['value' => 'opening_year', 'label' => 'Opening Year'],
['value' => 'overall_rating', 'label' => 'Rating'],
['value' => 'rides_count', 'label' => 'Rides Count']
]
],
'designers' => [
'title' => 'Designers',
'description' => 'Explore creative minds behind amazing ride experiences',
'searchPlaceholder' => 'Search designers, specialties, projects...',
'emptyStateMessage' => 'No designers found',
'emptyStateDescription' => 'Try adjusting your search or filters.',
'viewModes' => ['grid', 'list', 'portfolio'],
'colorScheme' => [
'primary' => 'purple',
'secondary' => 'pink',
'accent' => 'indigo'
],
'cardFields' => [
'title' => 'name',
'subtitle' => 'location.city_country',
'description' => 'description',
'score' => 'innovation_score',
'scoreLabel' => 'Innovation Score',
'metrics' => [
[
'field' => 'founded_year',
'label' => 'Founded',
'format' => null
],
[
'field' => 'designed_rides_count',
'label' => 'Designs',
'format' => '%s'
],
[
'field' => 'specialty',
'label' => 'Specialty',
'format' => null
],
[
'field' => 'active_years',
'label' => 'Active Years',
'format' => '%s'
]
]
],
'badges' => [
'fields' => [
[
'field' => 'specialty',
'prefix' => '',
'suffix' => '',
'color' => 'purple'
],
[
'field' => 'status',
'prefix' => '',
'suffix' => '',
'color' => 'pink'
]
]
],
'filters' => [
'quickFilters' => [
[
'key' => 'specialty',
'value' => 'roller_coaster',
'label' => 'Coaster Designers',
'active' => false,
'count' => null
],
[
'key' => 'specialty',
'value' => 'dark_ride',
'label' => 'Dark Ride Designers',
'active' => false,
'count' => null
],
[
'key' => 'specialty',
'value' => 'themed_experience',
'label' => 'Experience Designers',
'active' => false,
'count' => null
]
],
'sections' => [
[
'title' => 'Design Specialty',
'type' => 'checkboxes',
'model' => 'specialties',
'options' => [
['value' => 'roller_coaster', 'label' => 'Roller Coasters', 'count' => null],
['value' => 'dark_ride', 'label' => 'Dark Rides', 'count' => null],
['value' => 'themed_experience', 'label' => 'Themed Experiences', 'count' => null],
['value' => 'water_attraction', 'label' => 'Water Attractions', 'count' => null]
]
],
[
'title' => 'Founded Year',
'type' => 'range',
'fromModel' => 'foundedYearFrom',
'toModel' => 'foundedYearTo',
'fromLabel' => 'From Year',
'toLabel' => 'To Year',
'fromPlaceholder' => '1900',
'toPlaceholder' => date('Y')
]
]
],
'sortOptions' => [
['value' => 'name', 'label' => 'Name'],
['value' => 'founded_year', 'label' => 'Founded Year'],
['value' => 'innovation_score', 'label' => 'Innovation Score'],
['value' => 'designed_rides_count', 'label' => 'Designs Count']
]
],
'manufacturers' => [
'title' => 'Manufacturers',
'description' => 'Explore ride manufacturers with product portfolios and industry presence',
'searchPlaceholder' => 'Search manufacturers, products, technologies...',
'emptyStateMessage' => 'No manufacturers found',
'emptyStateDescription' => 'Try adjusting your search or filters.',
'viewModes' => ['grid', 'list', 'portfolio'],
'colorScheme' => [
'primary' => 'orange',
'secondary' => 'amber',
'accent' => 'red'
],
'cardFields' => [
'title' => 'name',
'subtitle' => 'headquarters',
'description' => 'description',
'score' => 'industry_presence_score',
'scoreLabel' => 'Industry Presence',
'metrics' => [
[
'field' => 'total_rides',
'label' => 'Total Rides',
'format' => '%s rides'
],
[
'field' => 'total_roller_coasters',
'label' => 'Roller Coasters',
'format' => '%s coasters'
],
[
'field' => 'founded_year',
'label' => 'Founded',
'format' => null
],
[
'field' => 'market_share',
'label' => 'Market Share',
'format' => '%s%%'
]
]
],
'badges' => [
[
'field' => 'is_active',
'value' => true,
'label' => 'Active',
'color' => 'green'
],
[
'field' => 'specialization',
'value' => 'roller_coasters',
'label' => 'Coaster Specialist',
'color' => 'red'
],
[
'field' => 'specialization',
'value' => 'family_rides',
'label' => 'Family Rides',
'color' => 'blue'
],
[
'field' => 'specialization',
'value' => 'thrill_rides',
'label' => 'Thrill Rides',
'color' => 'purple'
],
[
'field' => 'innovation_leader',
'value' => true,
'label' => 'Innovation Leader',
'color' => 'yellow'
]
],
'filters' => [
[
'type' => 'select',
'field' => 'specialization',
'label' => 'Specialization',
'options' => [
'roller_coasters' => 'Roller Coasters',
'family_rides' => 'Family Rides',
'thrill_rides' => 'Thrill Rides',
'water_rides' => 'Water Rides',
'dark_rides' => 'Dark Rides',
'transportation' => 'Transportation'
]
],
[
'type' => 'range',
'field' => 'total_rides',
'label' => 'Total Rides',
'min' => 0,
'max' => 1000,
'step' => 10
],
[
'type' => 'range',
'field' => 'industry_presence_score',
'label' => 'Industry Presence Score',
'min' => 0,
'max' => 100,
'step' => 5
],
[
'type' => 'range',
'field' => 'founded_year',
'label' => 'Founded Year',
'min' => 1800,
'max' => 2025,
'step' => 5
],
[
'type' => 'checkbox',
'field' => 'is_active',
'label' => 'Active Manufacturers'
],
[
'type' => 'checkbox',
'field' => 'innovation_leader',
'label' => 'Innovation Leaders'
]
],
'statistics' => [
[
'label' => 'Total Manufacturers',
'field' => 'count',
'format' => '%s manufacturers'
],
[
'label' => 'Active Manufacturers',
'field' => 'active_count',
'format' => '%s active'
],
[
'label' => 'Total Rides Manufactured',
'field' => 'total_rides_sum',
'format' => '%s rides'
],
[
'label' => 'Average Industry Presence',
'field' => 'avg_industry_presence',
'format' => '%.1f/100'
]
],
'customSlots' => [
'header' => 'manufacturers-header',
'filters' => 'manufacturers-filters',
'statistics' => 'manufacturers-statistics',
'emptyState' => 'manufacturers-empty'
]
]
]
];

View File

@@ -0,0 +1,61 @@
<?php
namespace Database\Factories;
use App\Models\Manufacturer;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Manufacturer>
*/
class ManufacturerFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Manufacturer::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$name = $this->faker->unique()->words(2, true);
return [
'name' => $name,
'slug' => Str::slug($name),
'description' => $this->faker->paragraphs(2, true),
'is_active' => $this->faker->boolean(90), // 90% chance of being active
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
'updated_at' => function (array $attributes) {
return $this->faker->dateTimeBetween($attributes['created_at'], 'now');
},
];
}
/**
* Indicate that the model is active.
*/
public function active(): static
{
return $this->state(fn (array $attributes) => [
'is_active' => true,
]);
}
/**
* Indicate that the model is inactive.
*/
public function inactive(): static
{
return $this->state(fn (array $attributes) => [
'is_active' => false,
]);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Database\Factories;
use App\Models\Ride;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Ride>
*/
class RideFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Ride::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$name = $this->faker->unique()->words(2, true);
return [
'name' => $name,
'slug' => Str::slug($name),
'description' => $this->faker->paragraphs(2, true),
'is_active' => $this->faker->boolean(90), // 90% chance of being active
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
'updated_at' => function (array $attributes) {
return $this->faker->dateTimeBetween($attributes['created_at'], 'now');
},
];
}
/**
* Indicate that the model is active.
*/
public function active(): static
{
return $this->state(fn (array $attributes) => [
'is_active' => true,
]);
}
/**
* Indicate that the model is inactive.
*/
public function inactive(): static
{
return $this->state(fn (array $attributes) => [
'is_active' => false,
]);
}
}

View File

@@ -29,10 +29,17 @@ return new class extends Migration
$table->string('slug')->unique();
$table->string('website')->nullable();
$table->string('headquarters')->nullable();
$table->text('description')->nullable();
$table->integer('total_rides')->default(0);
$table->integer('total_roller_coasters')->default(0);
$table->text('description')->default('');
$table->unsignedInteger('total_rides')->default(0);
$table->unsignedInteger('total_roller_coasters')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
// Indexes
$table->index('is_active');
$table->index('total_rides');
$table->index(['name', 'is_active']);
});
// Create slug history table for tracking slug changes

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('designers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('website')->nullable();
$table->string('headquarters')->nullable();
$table->text('description')->default('');
$table->unsignedInteger('total_rides')->default(0);
$table->unsignedInteger('total_roller_coasters')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
// Indexes
$table->index('is_active');
$table->index('total_rides');
$table->index(['name', 'is_active']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('designers');
}
};

View File

@@ -39,6 +39,7 @@ return new class extends Migration
$table->decimal('average_rating', 3, 2)->nullable();
$table->timestamps();
$table->softDeletes();
// Indexes
$table->unique(['park_id', 'slug']);

View File

@@ -0,0 +1,140 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // permission id
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
}
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
};

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('review_images', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->boolean('is_active')->default(true);
// Add common ThrillWiki fields
$table->string('slug')->unique();
// Add indexes for performance
$table->index(['is_active']);
$table->index(['name']);
$table->index(['slug']);
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('review_images');
}
};

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('review_reports', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->boolean('is_active')->default(true);
// Add common ThrillWiki fields
$table->string('slug')->unique();
// Add indexes for performance
$table->index(['is_active']);
$table->index(['name']);
$table->index(['slug']);
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('review_reports');
}
};

View File

@@ -0,0 +1,62 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('manufacturers', function (Blueprint $table) {
// Industry presence and market analysis fields
$table->integer('industry_presence_score')->default(0)->after('total_roller_coasters');
$table->decimal('market_share_percentage', 5, 2)->default(0.00)->after('industry_presence_score');
$table->integer('founded_year')->nullable()->after('market_share_percentage');
$table->string('specialization')->nullable()->after('founded_year');
$table->text('product_portfolio')->nullable()->after('specialization');
$table->json('manufacturing_categories')->nullable()->after('product_portfolio');
$table->integer('global_installations')->default(0)->after('manufacturing_categories');
$table->string('primary_market')->nullable()->after('global_installations');
$table->boolean('is_major_manufacturer')->default(false)->after('primary_market');
// Add indexes for performance
$table->index('industry_presence_score');
$table->index('market_share_percentage');
$table->index('founded_year');
$table->index('specialization');
$table->index('is_major_manufacturer');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('manufacturers', function (Blueprint $table) {
// Drop indexes first
$table->dropIndex(['industry_presence_score']);
$table->dropIndex(['market_share_percentage']);
$table->dropIndex(['founded_year']);
$table->dropIndex(['specialization']);
$table->dropIndex(['is_major_manufacturer']);
// Drop columns
$table->dropColumn([
'industry_presence_score',
'market_share_percentage',
'founded_year',
'specialization',
'product_portfolio',
'manufacturing_categories',
'global_installations',
'primary_market',
'is_major_manufacturer'
]);
});
}
};

View File

@@ -0,0 +1,46 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class DesignerPermissionsSeeder extends Seeder
{
public function run(): void
{
// Create designer permissions
$permissions = [
'view designers',
'create designers',
'edit designers',
'delete designers',
'restore designers',
'force delete designers',
];
foreach ($permissions as $permission) {
Permission::create(['name' => $permission]);
}
// Assign permissions to admin role
$adminRole = Role::firstOrCreate(['name' => 'admin']);
$adminRole->givePermissionTo($permissions);
// Assign permissions to moderator role
$moderatorRole = Role::firstOrCreate(['name' => 'moderator']);
$moderatorRole->givePermissionTo([
'view designers',
'edit designers',
]);
// Assign permissions to editor role
$editorRole = Role::firstOrCreate(['name' => 'editor']);
$editorRole->givePermissionTo([
'view designers',
'create designers',
'edit designers',
]);
}
}

View File

@@ -0,0 +1,40 @@
# Handoffs System Introduction
The Handoffs System complements our Memory Bank by providing chronological documentation of development progress and enabling clean context transitions between LLM sessions.
## System Components
1. Sequential Handoff Documents
- Capture daily development progress
- Document specific work completed
- Flag work-in-progress items
- Note discoveries and solutions
2. Milestone Documents
- Consolidate multiple handoffs
- Summarize completed project phases
- Document lessons learned
- Store reusable patterns
## Usage Guidelines
### When to Create Handoffs
- After completing significant project segments
- When context becomes ~30% irrelevant
- After 10+ conversation exchanges
- During extended debugging sessions
### When to Create Milestones
- After major feature completion
- When 3-5 handoffs accumulate
- At stable/deployable states
- After solving critical problems
## Integration with Memory Bank
The Handoffs System complements our Memory Bank by:
1. Preserving chronological development history
2. Maintaining detailed context without loss
3. Enabling selective context loading
4. Providing clear project timeline
5. Facilitating clean context switches

View File

@@ -0,0 +1,29 @@
# Handoff Document Processing Instructions
Please read through all handoff documents chronologically. After reading, provide a summary that includes:
1. Current Project State
- Implemented features
- Work in progress
- Known issues
- Current priorities
2. Key Technical Insights
- Architectural decisions
- Implementation patterns
- Problem solutions
- Performance considerations
3. Development Context
- Current phase objectives
- Dependencies and requirements
- Integration points
- Testing considerations
4. Next Steps
- Immediate tasks
- Upcoming milestones
- Required preparations
- Potential challenges
After processing, respond with "HANDOFF PROCESSED" followed by your summary to confirm you've integrated the context.

View File

@@ -0,0 +1,29 @@
# Milestone Document Processing Instructions
Please process all milestone summary documents (0-prefixed files in milestone directories) to build comprehensive project context. After reading, provide a high-level overview that includes:
1. Project Evolution
- Completed project phases
- Major features implemented
- Architectural changes
- Technical debt addressed
2. System Architecture
- Core components
- Integration points
- Data flows
- Performance characteristics
3. Lessons Learned
- Technical insights
- Implementation patterns
- Avoided pitfalls
- Performance optimizations
4. Project Health
- Code quality metrics
- Test coverage
- Documentation status
- Technical debt status
After processing, respond with "MILESTONE CONTEXT INTEGRATED" followed by your summary to confirm you've absorbed the project context.

View File

@@ -0,0 +1,83 @@
# Documentation System Enhancement Handoff
## Current Project State
[2025-02-26 20:07]
### Implementation Status
- Memory Bank system active and operational
- Basic project structure established
- Core features in development phase
- Documentation system enhanced
### Work in Progress
1. Filament Admin Interface
- Core admin resources pending
- Permission system planning
- Moderation tools design
2. History Tracking System
- Model history implementation pending
- Audit logging structure planned
- User activity tracking design
3. Email Service Foundation
- Basic notification system planned
- Template management pending
- Infrastructure setup needed
## Technical Details
### Documentation Enhancement
1. Implemented Handoffs System
- Created directory structure
- Set up instruction templates
- Established creation guidelines
- Defined milestone criteria
2. Memory Bank Integration
- Complementary system roles defined
- Clear handoff triggers established
- Milestone consolidation process documented
- Context preservation mechanisms in place
### Key Decisions
- Dual documentation system approach
- Clear triggers for handoff creation
- Milestone consolidation criteria
- Content quality standards
## Next Steps
### Immediate Tasks
1. Begin using handoff system for daily progress
2. Create first milestone after 3-5 handoffs
3. Document Filament admin implementation
4. Track history system development
### Upcoming Work
1. Complete Filament admin setup
2. Implement history tracking
3. Establish email service foundation
4. Integrate analytics system
## Notes and Recommendations
- Use handoffs for significant progress points
- Create milestones after major features
- Maintain detail in technical documentation
- Focus on actionable information
## Open Issues
1. Admin Interface
- Permission mapping strategy
- Bulk action implementation
- Resource organization
2. History System
- Audit log storage approach
- Change tracking granularity
- Performance considerations
3. Documentation
- Handoff creation workflow
- Quality metrics definition
- System effectiveness measurement

View File

@@ -0,0 +1,108 @@
# Filament Admin Interface Implementation Plan
## Overview
[2025-02-26 20:14]
### Goals
1. Set up Filament PHP admin interface
2. Match Django admin capabilities
3. Implement permission system
4. Create moderation tools
### Technical Requirements
1. Feature Parity
- Match Django admin functionality
- Maintain consistent permission structure
- Support equivalent bulk actions
- Replicate list display and filters
2. Resource Mapping
- Map Django admin models to Filament resources
- Match fieldset structures
- Implement inline form relationships
- Support all Django admin actions
3. Permissions
- Integrate with Laravel permissions
- Match Django's permission model
- Support resource-level access control
- Implement audit trails
## Implementation Strategy
### Phase 1: Core Setup
1. Install Filament PHP
2. Configure basic admin panel
3. Set up resource structure
4. Implement authentication integration
### Phase 2: Resource Implementation
1. Create base resource templates
2. Map model relationships
3. Configure form layouts
4. Set up list views
### Phase 3: Permissions
1. Design permission structure
2. Implement role system
3. Configure access controls
4. Set up audit logging
### Phase 4: Moderation Tools
1. Create moderation panel
2. Implement review workflows
3. Set up notification system
4. Configure action logging
## Technical Details
### Django Admin Reference
- Location: //Users/talor/thrillwiki_django_no_react/
- Study admin configuration
- Map model relationships
- Document customizations
### Laravel Implementation
1. File Structure:
```
app/Filament/Resources/
├── UserResource.php
├── ParkResource.php
├── RideResource.php
└── ReviewResource.php
```
2. Key Components:
- Resource classes
- Form builders
- Table configurations
- Action handlers
## Dependencies
1. Required Packages
- filament/filament
- laravel/permissions
- filament/notifications
2. Configuration Files
- config/filament.php
- config/permissions.php
## Next Steps
1. Immediate Actions
- Install Filament package
- Create base resource structure
- Configure authentication
2. Planning Needed
- Resource organization strategy
- Permission hierarchy design
- Moderation workflow structure
## Notes and Considerations
- Follow Laravel/Filament naming conventions
- Maintain clear separation of concerns
- Document any deviations from Django
- Consider performance optimizations

Some files were not shown because too many files have changed in this diff Show More