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.
This commit is contained in:
pacnpal
2025-06-19 22:34:10 -04:00
parent 86263db9d9
commit cc33781245
148 changed files with 14026 additions and 2482 deletions

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();
}
}

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!');
}
}

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 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,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

@@ -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,29 @@ class Manufacturer extends Model
'description',
'total_rides',
'total_roller_coasters',
'is_active',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'total_rides' => 'integer',
'total_roller_coasters' => 'integer',
'is_active' => '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 +57,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 +103,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

@@ -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

@@ -53,9 +53,6 @@ class AdminPanelProvider extends PanelProvider
->authMiddleware([
Authenticate::class,
])
->resources([
config('filament.resources.namespace') => app_path('Filament/Resources'),
])
->navigationGroups([
'Company Management',
'Attractions',

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,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');
}
}