diff --git a/AI/Config/config.php b/AI/Config/config.php deleted file mode 100644 index 573f94d..0000000 --- a/AI/Config/config.php +++ /dev/null @@ -1,9 +0,0 @@ - env('AI_DEFAULT_PROVIDER'), - 'timeout' => (int) env('AI_TIMEOUT', 60), - 'max_output_tokens' => (int) env('AI_MAX_OUTPUT_TOKENS', 2048), - 'temperature' => (float) env('AI_TEMPERATURE', 0.3), - 'retain_executions_days' => (int) env('AI_RETAIN_EXECUTIONS_DAYS', 90), -]; diff --git a/AI/Console/PruneAIExecutions.php b/AI/Console/PruneAIExecutions.php deleted file mode 100644 index 5993517..0000000 --- a/AI/Console/PruneAIExecutions.php +++ /dev/null @@ -1,22 +0,0 @@ -option('days') ?: config('ai.retain_executions_days', 90)); - $count = AIExecution::where('created_at', '<', now()->subDays($days))->delete(); - $this->info("Deleted {$count} AI executions."); - - return self::SUCCESS; - } -} diff --git a/AI/Console/TestAIProvider.php b/AI/Console/TestAIProvider.php deleted file mode 100644 index 7b192bd..0000000 --- a/AI/Console/TestAIProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -testProvider((int) $this->argument('provider')); - $this->{$result['ok'] ? 'info' : 'error'}($result['message']); - - return $result['ok'] ? self::SUCCESS : self::FAILURE; - } -} diff --git a/AI/Contracts/AIManager.php b/AI/Contracts/AIManager.php deleted file mode 100644 index d7cfe3a..0000000 --- a/AI/Contracts/AIManager.php +++ /dev/null @@ -1,13 +0,0 @@ -id(); - $table->string('name'); - $table->string('driver', 40)->default('openai_compatible'); - $table->string('base_url')->nullable(); - $table->text('api_key')->nullable(); - $table->string('default_model')->nullable(); - $table->json('models')->nullable(); - $table->json('options')->nullable(); - $table->boolean('is_active')->default(true)->index(); - $table->boolean('is_default')->default(false)->index(); - $table->timestamp('last_tested_at')->nullable(); - $table->string('last_test_status', 20)->nullable(); - $table->text('last_test_message')->nullable(); - $table->timestamps(); - }); - Schema::create('ai_prompts', function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->string('slug')->unique(); - $table->string('purpose')->nullable()->index(); - $table->text('system_prompt')->nullable(); - $table->longText('user_template'); - $table->json('variables')->nullable(); - $table->unsignedInteger('version')->default(1); - $table->boolean('is_active')->default(true)->index(); - $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamps(); - }); - Schema::create('ai_executions', function (Blueprint $table) { - $table->id(); - $table->uuid('execution_id')->unique(); - $table->foreignId('ai_provider_id')->nullable()->constrained('ai_providers')->nullOnDelete(); - $table->foreignId('ai_prompt_id')->nullable()->constrained('ai_prompts')->nullOnDelete(); - $table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); - $table->string('model')->nullable(); - $table->string('purpose')->nullable()->index(); - $table->longText('input')->nullable(); - $table->longText('output')->nullable(); - $table->json('context')->nullable(); - $table->json('usage')->nullable(); - $table->string('status', 20)->default('pending')->index(); - $table->text('error')->nullable(); - $table->unsignedInteger('duration_ms')->nullable(); - $table->timestamp('completed_at')->nullable(); - $table->timestamps(); - }); - Schema::create('ai_knowledge_sources', function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->string('source_kind', 40)->default('manual')->index(); - $table->nullableMorphs('source'); - $table->longText('content')->nullable(); - $table->string('checksum', 64)->nullable()->index(); - $table->json('metadata')->nullable(); - $table->string('status', 20)->default('pending')->index(); - $table->unsignedInteger('chunk_count')->default(0); - $table->timestamp('indexed_at')->nullable(); - $table->timestamps(); - }); - Schema::create('ai_knowledge_chunks', function (Blueprint $table) { - $table->id(); - $table->foreignId('ai_knowledge_source_id')->constrained('ai_knowledge_sources')->cascadeOnDelete(); - $table->unsignedInteger('position'); - $table->longText('content'); - $table->json('metadata')->nullable(); - $table->timestamps(); - $table->unique(['ai_knowledge_source_id', 'position']); - }); - } - - public function down(): void - { - Schema::dropIfExists('ai_knowledge_chunks'); - Schema::dropIfExists('ai_knowledge_sources'); - Schema::dropIfExists('ai_executions'); - Schema::dropIfExists('ai_prompts'); - Schema::dropIfExists('ai_providers'); - } -}; diff --git a/AI/Drivers/OpenAICompatibleDriver.php b/AI/Drivers/OpenAICompatibleDriver.php deleted file mode 100644 index 74a7d5a..0000000 --- a/AI/Drivers/OpenAICompatibleDriver.php +++ /dev/null @@ -1,36 +0,0 @@ -timeout((int) config('ai.timeout', 60)); - - return filled($provider->api_key) ? $client->withToken($provider->api_key) : $client; - } - - public function generate(AIProvider $provider, array $messages, array $options = []): array - { - $base = rtrim((string) ($provider->base_url ?: 'https://api.openai.com/v1'), '/'); - $payload = ['model' => $options['model'] ?? $provider->default_model, 'messages' => $messages, 'temperature' => $options['temperature'] ?? config('ai.temperature', 0.3), 'max_tokens' => $options['max_tokens'] ?? config('ai.max_output_tokens', 2048)]; - $response = $this->client($provider)->post($base.'/chat/completions', array_filter($payload, fn ($v) => $v !== null)); - $response->throw(); - $json = $response->json(); - - return ['text' => (string) data_get($json, 'choices.0.message.content', ''), 'usage' => (array) data_get($json, 'usage', []), 'raw' => $json]; - } - - public function test(AIProvider $provider): array - { - $result = $this->generate($provider, [['role' => 'user', 'content' => 'Reply with OK only.']], ['max_tokens' => 8, 'temperature' => 0]); - - return ['ok' => true, 'message' => trim($result['text']) ?: 'Provider responded successfully.']; - } -} diff --git a/AI/Http/Controllers/AIApiController.php b/AI/Http/Controllers/AIApiController.php deleted file mode 100644 index 77000a1..0000000 --- a/AI/Http/Controllers/AIApiController.php +++ /dev/null @@ -1,20 +0,0 @@ -validate(['variables' => 'nullable|array', 'provider_id' => 'nullable|integer', 'model' => 'nullable|string|max:150']); - $execution = $manager->execute($prompt, $data['variables'] ?? [], array_filter(['provider_id' => $data['provider_id'] ?? null, 'model' => $data['model'] ?? null])); - - return response()->json(['data' => ['execution_id' => $execution->execution_id, 'status' => $execution->status, 'output' => $execution->output, 'usage' => $execution->usage, 'error' => $execution->error]], $execution->status === 'completed' ? 200 : 422); - } -} diff --git a/AI/Http/Controllers/AIController.php b/AI/Http/Controllers/AIController.php deleted file mode 100644 index 96c1f5f..0000000 --- a/AI/Http/Controllers/AIController.php +++ /dev/null @@ -1,69 +0,0 @@ - AIProvider::latest()->get(), 'prompts' => AIPrompt::latest()->paginate(15, '*', 'prompts'), 'executions' => AIExecution::latest()->limit(15)->get(), 'sources' => AIKnowledgeSource::latest()->limit(15)->get()]); - } - - public function storeProvider(Request $request): RedirectResponse - { - $data = $request->validate(['name' => 'required|string|max:100', 'driver' => 'required|in:openai_compatible,openai,lm_studio,ollama', 'base_url' => 'nullable|url', 'api_key' => 'nullable|string', 'default_model' => 'required|string|max:150', 'is_default' => 'nullable|boolean']); - if ($request->boolean('is_default')) { - AIProvider::query()->update(['is_default' => false]); - } - $data['is_default'] = $request->boolean('is_default'); - AIProvider::create($data); - - return back()->with('status', 'AI provider created.'); - } - - public function testProvider(AIProvider $provider, AIManager $manager): RedirectResponse - { - $result = $manager->testProvider($provider->id); - - return back()->with($result['ok'] ? 'status' : 'error', $result['message']); - } - - public function storePrompt(Request $request): RedirectResponse - { - $data = $request->validate(['name' => 'required|string|max:120', 'slug' => 'nullable|alpha_dash|max:140', 'purpose' => 'nullable|string|max:100', 'system_prompt' => 'nullable|string', 'user_template' => 'required|string']); - $data['slug'] = $data['slug'] ?: Str::slug($data['name']); - $data['created_by'] = auth()->id(); - AIPrompt::create($data); - - return back()->with('status', 'Prompt created.'); - } - - public function execute(Request $request, AIPrompt $prompt, AIManager $manager): RedirectResponse - { - $data = $request->validate(['input' => 'required|string|max:50000', 'provider_id' => 'nullable|exists:ai_providers,id']); - $execution = $manager->execute($prompt, ['input' => $data['input']], ['provider_id' => $data['provider_id'] ?? null]); - - return back()->with($execution->status === 'completed' ? 'status' : 'error', $execution->status === 'completed' ? 'AI execution completed.' : $execution->error); - } - - public function storeSource(Request $request, KnowledgeIndexer $indexer): RedirectResponse - { - $data = $request->validate(['name' => 'required|string|max:150', 'content' => 'required|string|max:1000000']); - $source = AIKnowledgeSource::create(['name' => $data['name'], 'source_kind' => 'manual', 'content' => $data['content'], 'status' => 'pending']); - $indexer->index($source); - - return back()->with('status','Knowledge source indexed.'); - } -} diff --git a/AI/Models/AIExecution.php b/AI/Models/AIExecution.php deleted file mode 100644 index 62ea778..0000000 --- a/AI/Models/AIExecution.php +++ /dev/null @@ -1,12 +0,0 @@ - 'array', 'usage' => 'array', 'completed_at' => 'datetime']; -} diff --git a/AI/Models/AIKnowledgeChunk.php b/AI/Models/AIKnowledgeChunk.php deleted file mode 100644 index 632ed3c..0000000 --- a/AI/Models/AIKnowledgeChunk.php +++ /dev/null @@ -1,12 +0,0 @@ - 'array']; -} diff --git a/AI/Models/AIKnowledgeSource.php b/AI/Models/AIKnowledgeSource.php deleted file mode 100644 index f863a52..0000000 --- a/AI/Models/AIKnowledgeSource.php +++ /dev/null @@ -1,24 +0,0 @@ - 'array', 'indexed_at' => 'datetime']; - - public function source(): MorphTo - { - return $this->morphTo(); - } - - public function chunks(): HasMany - { - return $this->hasMany(AIKnowledgeChunk::class); - } -} diff --git a/AI/Models/AIPrompt.php b/AI/Models/AIPrompt.php deleted file mode 100644 index 5e1a23f..0000000 --- a/AI/Models/AIPrompt.php +++ /dev/null @@ -1,17 +0,0 @@ - 'array', 'is_active' => 'boolean']; - - public function render(array $variables): string - { - return preg_replace_callback('/\{\{\s*([\w.]+)\s*\}\}/', fn ($m) => (string) data_get($variables, $m[1], ''), $this->user_template) ?? $this->user_template; - } -} diff --git a/AI/Models/AIProvider.php b/AI/Models/AIProvider.php deleted file mode 100644 index f96e235..0000000 --- a/AI/Models/AIProvider.php +++ /dev/null @@ -1,14 +0,0 @@ - 'encrypted', 'models' => 'array', 'options' => 'array', 'is_active' => 'boolean', 'is_default' => 'boolean', 'last_tested_at' => 'datetime']; - - protected $hidden = ['api_key']; -} diff --git a/AI/Providers/AIServiceProvider.php b/AI/Providers/AIServiceProvider.php deleted file mode 100644 index e7fec1b..0000000 --- a/AI/Providers/AIServiceProvider.php +++ /dev/null @@ -1,28 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('AI', 'Config/config.php'), 'ai'); - $this->app->singleton(AIManagerContract::class, AIManager::class); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('AI', 'Database/Migrations')); - $this->loadViewsFrom(module_path('AI', 'Resources/views'), 'ai'); - if ($this->app->runningInConsole()) { - $this->commands([TestAIProvider::class, PruneAIExecutions::class]); - } - } -} diff --git a/AI/Providers/RouteServiceProvider.php b/AI/Providers/RouteServiceProvider.php deleted file mode 100644 index 4306ef6..0000000 --- a/AI/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('AI', 'Routes/web.php')); - Route::middleware('api')->prefix('api/v1')->group(module_path('AI', 'Routes/api.php')); - } -} diff --git a/AI/Resources/views/admin/index.blade.php b/AI/Resources/views/admin/index.blade.php deleted file mode 100644 index 3948fa0..0000000 --- a/AI/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,21 +0,0 @@ - -
-
AI PlatformConfigure hosted or local models, reusable prompts, execution history and knowledge sources.
-@if(session('status')){{ session('status') }}@endif -@if(session('error')){{ session('error') }}@endif -
-Providers{{ $providers->count() }} -Prompts{{ $prompts->total() }} -Executions{{ \Modules\AI\Models\AIExecution::count() }} -Knowledge sources{{ \Modules\AI\Models\AIKnowledgeSource::count() }} -
-
-Provider
@csrf - -Add provider -
@foreach($providers as $provider)
{{ $provider->name }}
{{ $provider->driver }} · {{ $provider->default_model }}
@csrfTest
@endforeach
-Reusable prompt
@csrf -Add prompt
-Knowledge source
@csrfIndex source
@foreach($sources as $source)
{{ $source->name }}
{{ $source->status }} · {{ $source->chunk_count }} chunks
@endforeach
-Recent executions
@forelse($executions as $execution)
{{ $execution->purpose ?: 'Prompt execution' }}{{ $execution->status }}
{{ $execution->model }} · {{ $execution->duration_ms }} ms · {{ $execution->created_at?->diffForHumans() }}
@emptyNo executions yet.@endforelse
-
diff --git a/AI/Routes/api.php b/AI/Routes/api.php deleted file mode 100644 index 775f54c..0000000 --- a/AI/Routes/api.php +++ /dev/null @@ -1,6 +0,0 @@ -middleware(['auth.api', 'ability:ai.execute'])->name('api.ai.execute'); diff --git a/AI/Routes/web.php b/AI/Routes/web.php deleted file mode 100644 index c2acf21..0000000 --- a/AI/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/ai')->name('administration.ai.')->group(function () { - Route::get('/', [AIController::class, 'index'])->name('index'); - Route::post('/providers', [AIController::class, 'storeProvider'])->middleware('permission:ai.manage')->name('providers.store'); - Route::post('/providers/{provider}/test', [AIController::class, 'testProvider'])->middleware('permission:ai.manage')->name('providers.test'); - Route::post('/prompts', [AIController::class, 'storePrompt'])->middleware('permission:ai.manage')->name('prompts.store'); - Route::post('/prompts/{prompt}/execute', [AIController::class, 'execute'])->middleware('permission:ai.execute')->name('prompts.execute'); - Route::post('/knowledge', [AIController::class, 'storeSource'])->middleware('permission:ai.manage')->name('knowledge.store'); -}); diff --git a/AI/Services/AIManager.php b/AI/Services/AIManager.php deleted file mode 100644 index 2f1c2c0..0000000 --- a/AI/Services/AIManager.php +++ /dev/null @@ -1,65 +0,0 @@ -where('is_active', true)->firstOrFail() : $prompt; - $provider = $this->provider($options['provider_id'] ?? null); - $input = $promptModel->render($variables); - $execution = AIExecution::create(['execution_id' => (string) Str::uuid(), 'ai_provider_id' => $provider->id, 'ai_prompt_id' => $promptModel->id, 'user_id' => auth()->id(), 'model' => $options['model'] ?? $provider->default_model, 'purpose' => $promptModel->purpose, 'input' => $input, 'context' => $variables, 'status' => 'running']); - $started = microtime(true); - try { - $messages = []; - if (filled($promptModel->system_prompt)) { - $messages[] = ['role' => 'system', 'content' => $promptModel->system_prompt]; - } - $messages[] = ['role' => 'user', 'content' => $input]; - $result = $this->driver($provider)->generate($provider, $messages, $options); - $execution->update(['output' => $result['text'], 'usage' => $result['usage'], 'status' => 'completed', 'duration_ms' => (int) ((microtime(true) - $started) * 1000), 'completed_at' => now()]); - } catch (\Throwable $e) { - report($e); - $execution->update(['status' => 'failed', 'error' => $e->getMessage(), 'duration_ms' => (int) ((microtime(true) - $started) * 1000), 'completed_at' => now()]); - } - - return $execution->refresh(); - } - - public function testProvider(int $providerId): array - { - $provider = AIProvider::findOrFail($providerId); - try { - $result = $this->driver($provider)->test($provider); - $provider->update(['last_tested_at' => now(), 'last_test_status' => 'passed', 'last_test_message' => $result['message']]); - - return $result; - } catch (\Throwable $e) { - $provider->update(['last_tested_at' => now(), 'last_test_status' => 'failed', 'last_test_message' => $e->getMessage()]); - - return ['ok' => false, 'message' => $e->getMessage()]; - } - } - - private function provider(?int $id): AIProvider - { - return AIProvider::query()->when($id, fn ($q) => $q->whereKey($id))->when(! $id, fn ($q) => $q->where('is_default', true))->where('is_active', true)->first() - ?? AIProvider::where('is_active', true)->firstOrFail(); - } - - private function driver(AIProvider $provider): AIProviderDriver - { - return match ($provider->driver) { - 'openai_compatible','openai','lm_studio','ollama' => app(OpenAICompatibleDriver::class), default => throw new \InvalidArgumentException("Unsupported AI driver [{$provider->driver}].") - }; - } -} diff --git a/AI/Services/KnowledgeIndexer.php b/AI/Services/KnowledgeIndexer.php deleted file mode 100644 index 9c21d83..0000000 --- a/AI/Services/KnowledgeIndexer.php +++ /dev/null @@ -1,39 +0,0 @@ -content); - $source->chunks()->delete(); - $chunks = $content === '' ? [] : $this->split($content, max(500, $chunkSize)); - foreach ($chunks as $position => $chunk) { - $source->chunks()->create(['position' => $position + 1, 'content' => $chunk]); - } - $source->update(['checksum' => hash('sha256', $content), 'status' => 'indexed', 'chunk_count' => count($chunks), 'indexed_at' => now()]); - - return $source->refresh(); - } - - private function split(string $content, int $size): array - { - $paragraphs = preg_split('/\R{2,}/', $content) ?: [$content]; - $chunks = []; - $buffer = ''; - foreach ($paragraphs as $paragraph) { - if (strlen($buffer) + strlen($paragraph) + 2 > $size && $buffer !== '') { - $chunks[] = trim($buffer); - $buffer = ''; - } $buffer .= $paragraph."\n\n"; - } - if (trim($buffer) !== '') { - $chunks[] = trim($buffer); - } - -return $chunks; - } -} diff --git a/AI/Tests/Feature/ModuleBootTest.php b/AI/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index ebc1cac..0000000 --- a/AI/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,16 +0,0 @@ -assertTrue(class_exists(AIServiceProvider::class)); - $this->assertTrue(app()->bound(AIManager::class)); - } -} diff --git a/AI/module.json b/AI/module.json deleted file mode 100644 index dda2c66..0000000 --- a/AI/module.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"AI","alias":"ai","version":"1.0.0","description":"Provider-neutral artificial intelligence services, reusable prompts, execution history and knowledge sources.","keywords":["ai","llm","prompts","knowledge","rag"],"priority":140,"providers":["Modules\\AI\\Providers\\AIServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Audit","Media","Documents","Automation"]} diff --git a/Administration/Config/config.php b/Administration/Config/config.php deleted file mode 100644 index bb128b8..0000000 --- a/Administration/Config/config.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Administration', - 'enabled' => true, -]; diff --git a/Administration/Http/Controllers/DashboardController.php b/Administration/Http/Controllers/DashboardController.php deleted file mode 100644 index fd399d5..0000000 --- a/Administration/Http/Controllers/DashboardController.php +++ /dev/null @@ -1,82 +0,0 @@ -backend()->entryView(), [ - 'userCount' => User::count(), - 'roleCount' => Schema::hasTable('roles') ? Role::count() : 0, - 'mediaCount' => Schema::hasTable('media') ? Media::count() : 0, - 'auditCount' => Schema::hasTable('audit_logs') ? AuditLog::count() : 0, - 'recentAudit' => $this->recentAudit(), - 'modules' => $this->modules(), - ]); - } - - private function recentAudit(): LengthAwarePaginator - { - if (! Schema::hasTable('audit_logs')) { - return new LengthAwarePaginator( - items: [], - total: 0, - perPage: 8, - currentPage: Paginator::resolveCurrentPage('audit_page'), - options: [ - 'path' => request()->url(), - 'pageName' => 'audit_page', - ], - ); - } - - return AuditLog::query() - ->latest('created_at') - ->paginate(8, ['*'], 'audit_page') - ->withQueryString(); - } - - private function modules(): LengthAwarePaginator - { - $statuses = json_decode((string) file_get_contents(base_path('modules_statuses.json')), true) ?: []; - $modules = []; - - foreach (glob(base_path('Modules/*/module.json')) ?: [] as $manifest) { - $data = json_decode((string) file_get_contents($manifest), true) ?: []; - $name = $data['name'] ?? basename(dirname($manifest)); - - $modules[] = [ - 'name' => $name, - 'version' => $data['version'] ?? '1.0.0', - 'enabled' => (bool) ($statuses[$name] ?? false), - ]; - } - - usort($modules, fn (array $a, array $b): int => strcasecmp($a['name'], $b['name'])); - - $pageName = 'modules_page'; - $perPage = 8; - $currentPage = Paginator::resolveCurrentPage($pageName); - - return (new LengthAwarePaginator( - items: array_slice($modules, ($currentPage - 1) * $perPage, $perPage), - total: count($modules), - perPage: $perPage, - currentPage: $currentPage, - options: [ - 'path' => request()->url(), - 'pageName' => $pageName, - ], - ))->withQueryString(); - } -} diff --git a/Administration/Http/Controllers/ModuleController.php b/Administration/Http/Controllers/ModuleController.php deleted file mode 100644 index e277d1c..0000000 --- a/Administration/Http/Controllers/ModuleController.php +++ /dev/null @@ -1,27 +0,0 @@ -map(function (string $manifest) use ($statuses): array { - $data = json_decode((string) file_get_contents($manifest), true) ?: []; - $name = $data['name'] ?? basename(dirname($manifest)); - - return [ - 'name' => $name, - 'description' => $data['description'] ?? 'Laravel application module.', - 'version' => $data['version'] ?? '1.0.0', - 'enabled' => (bool) ($statuses[$name] ?? false), - 'providers' => $data['providers'] ?? [], - ]; - })->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE)->values(); - - return view('administration::modules.index', compact('modules')); - } -} diff --git a/Administration/Providers/AdministrationServiceProvider.php b/Administration/Providers/AdministrationServiceProvider.php deleted file mode 100644 index 9cf7292..0000000 --- a/Administration/Providers/AdministrationServiceProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); - $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower()); - } -} diff --git a/Administration/Providers/RouteServiceProvider.php b/Administration/Providers/RouteServiceProvider.php deleted file mode 100644 index 2f44254..0000000 --- a/Administration/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,17 +0,0 @@ -routes(function (): void { - Route::middleware('web')->group(module_path('Administration', 'Routes/web.php')); - }); - } -} diff --git a/Administration/Resources/views/modules/index.blade.php b/Administration/Resources/views/modules/index.blade.php deleted file mode 100644 index 69bbd42..0000000 --- a/Administration/Resources/views/modules/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Modules

Installed platform modules and runtime status.

@foreach($modules as $module)@endforeach
ModuleVersionStatusProviders
{{ $module['name'] }}
{{ $module['description'] }}
{{ $module['version'] }}{{ $module['enabled'] ? 'Enabled' : 'Disabled' }}{{ implode(', ', $module['providers']) ?: '—' }}
diff --git a/Administration/Routes/web.php b/Administration/Routes/web.php deleted file mode 100644 index ab08cb2..0000000 --- a/Administration/Routes/web.php +++ /dev/null @@ -1,8 +0,0 @@ -prefix('administration')->name('administration.')->group(function (): void { - Route::get('/modules', [ModuleController::class, 'index'])->name('modules.index'); -}); diff --git a/Administration/Support/ModuleInfo.php b/Administration/Support/ModuleInfo.php deleted file mode 100644 index 51ef6a8..0000000 --- a/Administration/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBe('Administration'); -}); - -it('uses the primary dashboard for administration overview content', function (): void { - $dashboard = Route::getRoutes()->getByName('dashboard'); - - expect($dashboard)->not->toBeNull() - ->and($dashboard->getActionName())->toBe(DashboardController::class) - ->and(view()->exists('stylist-theme-admin::dashboard'))->toBeTrue() - ->and(Route::has('administration.dashboard'))->toBeFalse(); -}); diff --git a/Administration/composer.json b/Administration/composer.json deleted file mode 100644 index 3a6aff0..0000000 --- a/Administration/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "absdev/administration-module", - "description": "Administration foundation module for the ABSDEV Laravel application platform.", - "type": "laravel-module", - "autoload": { - "psr-4": { - "Modules\\Administration\\": "" - } - } -} diff --git a/Administration/module.json b/Administration/module.json deleted file mode 100644 index 74cfa82..0000000 --- a/Administration/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Administration", - "alias": "administration", - "version": "1.0.0", - "description": "Administration foundation module for the ABSDEV Laravel application platform.", - "keywords": [ - "laravel", - "modules", - "administration" - ], - "priority": 10, - "providers": [ - "Modules\\Administration\\Providers\\AdministrationServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core" - ] -} diff --git a/Analytics/Config/config.php b/Analytics/Config/config.php deleted file mode 100644 index b2d0b76..0000000 --- a/Analytics/Config/config.php +++ /dev/null @@ -1,10 +0,0 @@ - (bool) env('ANALYTICS_ENABLED', true), - 'respect_dnt' => (bool) env('ANALYTICS_RESPECT_DNT', true), - 'anonymize_ip' => (bool) env('ANALYTICS_ANONYMIZE_IP', true), - 'retention_days' => (int) env('ANALYTICS_RETENTION_DAYS', 365), - 'exclude_paths' => array_values(array_filter(array_map('trim', explode(',', (string) env('ANALYTICS_EXCLUDE_PATHS', 'administration,api'))))), - 'queue' => env('ANALYTICS_QUEUE', 'default'), -]; diff --git a/Analytics/Console/AggregateAnalytics.php b/Analytics/Console/AggregateAnalytics.php deleted file mode 100644 index ae896e5..0000000 --- a/Analytics/Console/AggregateAnalytics.php +++ /dev/null @@ -1,20 +0,0 @@ -info('Metrics written: '.$aggregator->aggregate($this->option('date'))); - - return self::SUCCESS; - } -} diff --git a/Analytics/Console/PruneAnalytics.php b/Analytics/Console/PruneAnalytics.php deleted file mode 100644 index ead8b95..0000000 --- a/Analytics/Console/PruneAnalytics.php +++ /dev/null @@ -1,21 +0,0 @@ -subDays(config('analytics.retention_days', 365)))->delete(); - $this->info("Deleted {$count} events."); - - return self::SUCCESS; - } -} diff --git a/Analytics/Console/RunScheduledReports.php b/Analytics/Console/RunScheduledReports.php deleted file mode 100644 index e169620..0000000 --- a/Analytics/Console/RunScheduledReports.php +++ /dev/null @@ -1,21 +0,0 @@ -whereNotNull('next_run_at')->where('next_run_at', '<=', now())->update(['last_run_at' => now(), 'next_run_at' => now()->addDay()]); - $this->info("Processed {$count} scheduled reports."); - - return self::SUCCESS; - } -} diff --git a/Analytics/Database/Migrations/2026_07_24_150000_create_analytics_tables.php b/Analytics/Database/Migrations/2026_07_24_150000_create_analytics_tables.php deleted file mode 100644 index 4cea18d..0000000 --- a/Analytics/Database/Migrations/2026_07_24_150000_create_analytics_tables.php +++ /dev/null @@ -1,80 +0,0 @@ -id(); - $table->uuid('event_id')->unique(); - $table->foreignId('site_id')->nullable()->constrained('sites')->nullOnDelete(); - $table->string('event_type', 40)->index(); - $table->string('path', 2048)->nullable(); - $table->string('title')->nullable(); - $table->string('referrer', 2048)->nullable(); - $table->string('visitor_hash', 64)->nullable()->index(); - $table->string('session_hash', 64)->nullable()->index(); - $table->string('ip_hash', 64)->nullable(); - $table->string('country_code', 2)->nullable()->index(); - $table->string('device_type', 20)->nullable()->index(); - $table->string('browser', 40)->nullable(); - $table->string('operating_system', 40)->nullable(); - $table->nullableMorphs('subject'); - $table->json('properties')->nullable(); - $table->timestamp('occurred_at')->index(); - $table->timestamps(); - $table->index(['site_id', 'event_type', 'occurred_at']); - }); - Schema::create('analytics_daily_metrics', function (Blueprint $table) { - $table->id(); - $table->foreignId('site_id')->nullable()->constrained('sites')->nullOnDelete(); - $table->date('metric_date')->index(); - $table->string('metric', 60); - $table->string('dimension', 120)->default('all'); - $table->unsignedBigInteger('value')->default(0); - $table->json('metadata')->nullable(); - $table->timestamps(); - $table->unique(['site_id', 'metric_date', 'metric', 'dimension'], 'analytics_daily_unique'); - }); - Schema::create('analytics_reports', function (Blueprint $table) { - $table->id(); - $table->foreignId('site_id')->nullable()->constrained('sites')->nullOnDelete(); - $table->string('name'); - $table->string('report_type', 40)->default('overview'); - $table->json('filters')->nullable(); - $table->json('columns')->nullable(); - $table->string('schedule')->nullable(); - $table->string('format', 10)->default('csv'); - $table->json('recipients')->nullable(); - $table->boolean('is_active')->default(true)->index(); - $table->timestamp('last_run_at')->nullable(); - $table->timestamp('next_run_at')->nullable()->index(); - $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamps(); - }); - Schema::create('analytics_report_runs', function (Blueprint $table) { - $table->id(); - $table->foreignId('analytics_report_id')->constrained('analytics_reports')->cascadeOnDelete(); - $table->string('status', 20)->default('pending')->index(); - $table->string('disk')->nullable(); - $table->string('path')->nullable(); - $table->unsignedBigInteger('row_count')->default(0); - $table->text('error')->nullable(); - $table->timestamp('started_at')->nullable(); - $table->timestamp('completed_at')->nullable(); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('analytics_report_runs'); - Schema::dropIfExists('analytics_reports'); - Schema::dropIfExists('analytics_daily_metrics'); - Schema::dropIfExists('analytics_events'); - } -}; diff --git a/Analytics/Http/Controllers/AnalyticsController.php b/Analytics/Http/Controllers/AnalyticsController.php deleted file mode 100644 index 4552d84..0000000 --- a/Analytics/Http/Controllers/AnalyticsController.php +++ /dev/null @@ -1,43 +0,0 @@ -integer('days', 30))); - $from = now()->subDays($days - 1)->startOfDay(); - $events = AnalyticsEvent::query()->where('occurred_at', '>=', $from); - - return view('analytics::index', [ - 'days' => $days, 'pageViews' => $events->clone()->where('event_type', 'page_view')->count(), - 'visitors' => $events->clone()->whereNotNull('visitor_hash')->distinct('visitor_hash')->count('visitor_hash'), - 'conversions' => $events->clone()->where('event_type', 'conversion')->count(), - 'searches' => $events->clone()->where('event_type', 'search')->count(), - 'topPages' => $events->clone()->where('event_type', 'page_view')->selectRaw('path, count(*) as total')->groupBy('path')->orderByDesc('total')->limit(10)->get(), - 'recentEvents' => $events->clone()->latest('occurred_at')->limit(20)->get(), 'reports' => AnalyticsReport::latest()->limit(20)->get(), - ]); - } - - public function storeReport(Request $request): RedirectResponse - { - AnalyticsReport::create($request->validate(['name' => 'required|string|max:255', 'report_type' => 'required|in:overview,content,search,conversion,commerce', 'schedule' => 'nullable|string|max:50', 'format' => 'required|in:csv,json', 'is_active' => 'nullable|boolean']) + ['created_by' => $request->user()->id, 'is_active' => $request->boolean('is_active', true)]); - - return back()->with('status', 'Report created.'); - } - - public function destroyReport(AnalyticsReport $report): RedirectResponse - { - $report->delete(); - - return back()->with('status','Report deleted.'); - } -} diff --git a/Analytics/Http/Controllers/TrackingController.php b/Analytics/Http/Controllers/TrackingController.php deleted file mode 100644 index 0523987..0000000 --- a/Analytics/Http/Controllers/TrackingController.php +++ /dev/null @@ -1,25 +0,0 @@ -validate([ - 'event_type' => ['required', Rule::in(['page_view', 'search', 'conversion', 'content_view', 'form_submit', 'commerce'])], - 'path' => ['nullable', 'string', 'max:2048'], 'title' => ['nullable', 'string', 'max:255'], 'referrer' => ['nullable', 'string', 'max:2048'], - 'visitor_id' => ['nullable', 'string', 'max:255'], 'session_id' => ['nullable', 'string', 'max:255'], - 'subject_type' => ['nullable', 'string', 'max:255'], 'subject_id' => ['nullable', 'integer'], 'properties' => ['nullable', 'array'], - ]); - $event = $tracker->track($request, $data); - - return response()->json(['recorded' => (bool) $event], $event ? 201 : 202); - } -} diff --git a/Analytics/Models/AnalyticsEvent.php b/Analytics/Models/AnalyticsEvent.php deleted file mode 100644 index 389e863..0000000 --- a/Analytics/Models/AnalyticsEvent.php +++ /dev/null @@ -1,12 +0,0 @@ - 'array', 'occurred_at' => 'datetime']; -} diff --git a/Analytics/Models/AnalyticsReport.php b/Analytics/Models/AnalyticsReport.php deleted file mode 100644 index 3d8a81c..0000000 --- a/Analytics/Models/AnalyticsReport.php +++ /dev/null @@ -1,12 +0,0 @@ - 'array', 'columns' => 'array', 'recipients' => 'array', 'is_active' => 'boolean', 'last_run_at' => 'datetime', 'next_run_at' => 'datetime']; -} diff --git a/Analytics/Models/AnalyticsReportRun.php b/Analytics/Models/AnalyticsReportRun.php deleted file mode 100644 index 53930be..0000000 --- a/Analytics/Models/AnalyticsReportRun.php +++ /dev/null @@ -1,12 +0,0 @@ - 'datetime', 'completed_at' => 'datetime']; -} diff --git a/Analytics/Models/DailyMetric.php b/Analytics/Models/DailyMetric.php deleted file mode 100644 index 745d269..0000000 --- a/Analytics/Models/DailyMetric.php +++ /dev/null @@ -1,14 +0,0 @@ - 'date', 'metadata' => 'array']; -} diff --git a/Analytics/Providers/AnalyticsServiceProvider.php b/Analytics/Providers/AnalyticsServiceProvider.php deleted file mode 100644 index ce1b3d5..0000000 --- a/Analytics/Providers/AnalyticsServiceProvider.php +++ /dev/null @@ -1,32 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Analytics', 'Config/config.php'), 'analytics'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Analytics', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Analytics', 'Resources/views'), 'analytics'); - if ($this->app->runningInConsole()) { - $this->commands([AggregateAnalytics::class, PruneAnalytics::class, RunScheduledReports::class]); - $this->callAfterResolving(Schedule::class, function (Schedule $schedule): void { - $schedule->command('analytics:aggregate')->hourly()->withoutOverlapping(); - $schedule->command('analytics:reports')->everyFifteenMinutes()->withoutOverlapping(); - $schedule->command('analytics:prune')->dailyAt('02:30')->withoutOverlapping(); - }); - } - } -} diff --git a/Analytics/Providers/RouteServiceProvider.php b/Analytics/Providers/RouteServiceProvider.php deleted file mode 100644 index 9d62eec..0000000 --- a/Analytics/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Analytics', 'Routes/web.php')); - Route::middleware('api')->prefix('api/v1')->group(module_path('Analytics', 'Routes/api.php')); - } -} diff --git a/Analytics/Resources/views/index.blade.php b/Analytics/Resources/views/index.blade.php deleted file mode 100644 index 66e7cd8..0000000 --- a/Analytics/Resources/views/index.blade.php +++ /dev/null @@ -1,14 +0,0 @@ - -
-
AnalyticsVisitor, content, search and conversion performance.
- @if(session('status')){{ session('status') }}@endif -
- @foreach([['Page views',$pageViews],['Visitors',$visitors],['Conversions',$conversions],['Searches',$searches]] as [$label,$value])
{{ $label }}
{{ number_format($value) }}
@endforeach -
-
-
Top pages
@forelse($topPages as $page)
{{ $page->path ?: '/' }}{{ number_format($page->total) }}
@empty

No page views recorded.

@endforelse
-
Recent events
@forelse($recentEvents as $event)
{{ str($event->event_type)->replace('_',' ')->title() }}{{ $event->occurred_at?->diffForHumans() }}

{{ $event->path ?: data_get($event->properties,'query','—') }}

@empty

No events recorded.

@endforelse
-
- @can('analytics.manage')
Scheduled reports
@csrf
Create report
@forelse($reports as $report)
{{ $report->name }}

{{ ucfirst($report->report_type) }} · {{ strtoupper($report->format) }}

@csrf @method('DELETE')Delete
@empty

No reports configured.

@endforelse
@endcan -
-
diff --git a/Analytics/Routes/api.php b/Analytics/Routes/api.php deleted file mode 100644 index 4ae8da7..0000000 --- a/Analytics/Routes/api.php +++ /dev/null @@ -1,6 +0,0 @@ -middleware('throttle:60,1')->name('api.analytics.events.store'); diff --git a/Analytics/Routes/web.php b/Analytics/Routes/web.php deleted file mode 100644 index 5a5437e..0000000 --- a/Analytics/Routes/web.php +++ /dev/null @@ -1,10 +0,0 @@ -prefix('administration/analytics')->name('administration.analytics.')->group(function () { - Route::get('/', [AnalyticsController::class, 'index'])->name('index'); - Route::post('/reports', [AnalyticsController::class, 'storeReport'])->middleware('permission:analytics.manage')->name('reports.store'); - Route::delete('/reports/{report}', [AnalyticsController::class, 'destroyReport'])->middleware('permission:analytics.manage')->name('reports.destroy'); -}); diff --git a/Analytics/Services/AnalyticsAggregator.php b/Analytics/Services/AnalyticsAggregator.php deleted file mode 100644 index 0f08c0d..0000000 --- a/Analytics/Services/AnalyticsAggregator.php +++ /dev/null @@ -1,32 +0,0 @@ -subHour()->toDateString(); - $written = 0; - $base = AnalyticsEvent::query()->whereDate('occurred_at', $date); - foreach ($base->clone()->select('site_id')->distinct()->pluck('site_id') as $siteId) { - $query = $base->clone()->where('site_id', $siteId); - $metrics = [ - ['page_views', 'all', $query->clone()->where('event_type', 'page_view')->count()], - ['visitors', 'all', $query->clone()->whereNotNull('visitor_hash')->distinct('visitor_hash')->count('visitor_hash')], - ['sessions', 'all', $query->clone()->whereNotNull('session_hash')->distinct('session_hash')->count('session_hash')], - ['conversions', 'all', $query->clone()->where('event_type', 'conversion')->count()], - ['searches', 'all', $query->clone()->where('event_type', 'search')->count()], - ]; - foreach ($metrics as [$metric,$dimension,$value]) { - DailyMetric::updateOrCreate(['site_id' => $siteId, 'metric_date' => $date, 'metric' => $metric, 'dimension' => $dimension], ['value' => $value]); - $written++; - } - } - - return $written; - } -} diff --git a/Analytics/Services/AnalyticsTracker.php b/Analytics/Services/AnalyticsTracker.php deleted file mode 100644 index 4c37c77..0000000 --- a/Analytics/Services/AnalyticsTracker.php +++ /dev/null @@ -1,61 +0,0 @@ -header('DNT') === '1') { - return null; - } - - $path = ltrim((string) ($payload['path'] ?? ''), '/'); - - foreach (config('analytics.exclude_paths', []) as $excluded) { - if ($excluded !== '' && str_starts_with($path, trim($excluded, '/'))) { - return null; - } - } - - $ip = (string) $request->ip(); - $visitorIdentifier = (string) ($payload['visitor_id'] ?? $request->cookie('analytics_visitor', 'guest')); - $sessionIdentifier = (string) ($payload['session_id'] ?? $this->sessionIdentifier($request, $visitorIdentifier)); - - return AnalyticsEvent::create([ - 'event_id' => (string) Str::uuid(), - 'site_id' => app()->bound('currentSite') ? app('currentSite')?->id : null, - 'event_type' => $payload['event_type'] ?? 'page_view', - 'path' => '/'.$path, - 'title' => $payload['title'] ?? null, - 'referrer' => $payload['referrer'] ?? $request->headers->get('referer'), - 'visitor_hash' => hash('sha256', $visitorIdentifier), - 'session_hash' => hash('sha256', $sessionIdentifier), - 'ip_hash' => config('analytics.anonymize_ip') ? hash('sha256', $ip.config('app.key')) : $ip, - 'device_type' => $payload['device_type'] ?? null, - 'browser' => $payload['browser'] ?? null, - 'operating_system' => $payload['operating_system'] ?? null, - 'subject_type' => $payload['subject_type'] ?? null, - 'subject_id' => $payload['subject_id'] ?? null, - 'properties' => $payload['properties'] ?? [], - 'occurred_at' => now(), - ]); - } - - private function sessionIdentifier(Request $request, string $visitorIdentifier): string - { - if ($request->hasSession()) { - return $request->session()->getId(); - } - - return (string) ($request->header('X-Analytics-Session') ?: $visitorIdentifier ?: 'stateless'); - } -} diff --git a/Analytics/Tests/Feature/AnalyticsModuleTest.php b/Analytics/Tests/Feature/AnalyticsModuleTest.php deleted file mode 100644 index ce99fab..0000000 --- a/Analytics/Tests/Feature/AnalyticsModuleTest.php +++ /dev/null @@ -1,17 +0,0 @@ -postJson('/api/v1/analytics/events', ['event_type' => 'page_view', 'path' => '/about'])->assertCreated()->assertJson(['recorded' => true]); - $this->assertDatabaseHas('analytics_events', ['event_type' => 'page_view', 'path' => '/about']); - } -} diff --git a/Analytics/module.json b/Analytics/module.json deleted file mode 100644 index 7afb408..0000000 --- a/Analytics/module.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"Analytics","alias":"analytics","version":"1.0.0","description":"Privacy-conscious visitor, content, search, conversion and report analytics.","keywords":["analytics","reports","metrics","visitors","conversions"],"priority":150,"providers":["Modules\\Analytics\\Providers\\AnalyticsServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Sites","Search","Forms","Newsletter","Commerce"]} diff --git a/Api/Config/config.php b/Api/Config/config.php deleted file mode 100644 index b11bfae..0000000 --- a/Api/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'api/v1', 'token_prefix' => 'cms_', 'pagination' => 20, 'webhook_timeout' => 10, 'openapi' => ['title' => env('APP_NAME', 'Content Manager').' API', 'version' => '1.0.0']]; diff --git a/Api/Database/Migrations/2026_07_24_090000_create_api_tokens_table.php b/Api/Database/Migrations/2026_07_24_090000_create_api_tokens_table.php deleted file mode 100644 index bb0d7b7..0000000 --- a/Api/Database/Migrations/2026_07_24_090000_create_api_tokens_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $t->foreignId('user_id')->constrained()->cascadeOnDelete(); - $t->string('name'); - $t->string('token', 64)->unique(); - $t->json('abilities')->nullable(); - $t->timestamp('last_used_at')->nullable(); - $t->timestamp('expires_at')->nullable(); - $t->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('api_tokens'); - } -}; diff --git a/Api/Database/Migrations/2026_07_24_090100_create_webhooks_tables.php b/Api/Database/Migrations/2026_07_24_090100_create_webhooks_tables.php deleted file mode 100644 index 113840c..0000000 --- a/Api/Database/Migrations/2026_07_24_090100_create_webhooks_tables.php +++ /dev/null @@ -1,43 +0,0 @@ -id(); - $t->foreignId('site_id')->nullable()->constrained('sites')->nullOnDelete(); - $t->string('name'); - $t->string('url'); - $t->string('secret'); - $t->json('events'); - $t->boolean('is_active')->default(true); - $t->unsignedSmallInteger('failure_count')->default(0); - $t->timestamp('last_triggered_at')->nullable(); - $t->timestamps(); - }); - Schema::create('webhook_deliveries', function (Blueprint $t): void { - $t->id(); - $t->foreignId('webhook_id')->constrained()->cascadeOnDelete(); - $t->uuid('uuid')->unique(); - $t->string('event'); - $t->json('payload'); - $t->unsignedSmallInteger('response_status')->nullable(); - $t->text('response_body')->nullable(); - $t->unsignedSmallInteger('attempts')->default(0); - $t->timestamp('delivered_at')->nullable(); - $t->timestamp('failed_at')->nullable(); - $t->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('webhook_deliveries'); - Schema::dropIfExists('webhooks'); - } -}; diff --git a/Api/Http/Controllers/ApiController.php b/Api/Http/Controllers/ApiController.php deleted file mode 100644 index fbe22bf..0000000 --- a/Api/Http/Controllers/ApiController.php +++ /dev/null @@ -1,49 +0,0 @@ - ApiToken::with('user')->latest()->paginate(20, '*', 'tokens'), 'webhooks' => Webhook::latest()->paginate(20, '*', 'webhooks')]); - } - - public function storeToken(Request $r): RedirectResponse - { - $d = $r->validate(['name' => 'required|string|max:255', 'abilities' => 'nullable|string', 'expires_at' => 'nullable|date|after:now']); - $abilities = array_values(array_filter(array_map('trim', explode(',', $d['abilities'] ?: '*')))); - [,$plain] = ApiToken::issue($r->user(), $d['name'], $abilities, isset($d['expires_at']) ? new \DateTimeImmutable($d['expires_at']) : null); - - return back()->with('api_token', $plain)->with('status', 'API token created.'); - } - - public function destroyToken(ApiToken $token): RedirectResponse - { - $token->delete(); - - return back()->with('status', 'API token revoked.'); - } - - public function storeWebhook(Request $r): RedirectResponse - { - $d = $r->validate(['name' => 'required|string|max:255', 'url' => 'required|url|max:2048', 'events' => 'required|string', 'site_id' => 'nullable|integer|exists:sites,id']); - Webhook::create(['name' => $d['name'], 'url' => $d['url'], 'events' => array_values(array_filter(array_map('trim', explode(',', $d['events'])))), 'site_id' => $d['site_id'] ?? null, 'secret' => Str::random(48), 'is_active' => true]); - - return back()->with('status', 'Webhook created.'); - } - - public function destroyWebhook(Webhook $webhook): RedirectResponse - { - $webhook->delete(); - - return back()->with('status','Webhook deleted.'); - } -} diff --git a/Api/Http/Controllers/ContentController.php b/Api/Http/Controllers/ContentController.php deleted file mode 100644 index 5be30a0..0000000 --- a/Api/Http/Controllers/ContentController.php +++ /dev/null @@ -1,54 +0,0 @@ -many(Page::query()->where('status', 'published')); - } - - public function blog(): AnonymousResourceCollection - { - return $this->many(Blog::query()->published()); - } - - public function news(): AnonymousResourceCollection - { - return $this->many(News::query()->published()); - } - - public function page(Page $page): ContentResource - { - abort_unless($page->status === 'published', 404); - - return new ContentResource($page); - } - - public function blogPost(Blog $blog): ContentResource - { - abort_unless(Blog::query()->published()->whereKey($blog)->exists(), 404); - - return new ContentResource($blog); - } - - public function newsArticle(News $news): ContentResource - { - abort_unless(News::query()->published()->whereKey($news)->exists(), 404); - - return new ContentResource($news); - } - - private function many(Builder $q): AnonymousResourceCollection - { - return ContentResource::collection($q->latest('updated_at')->paginate((int) config('api.pagination', 20))); - } -} diff --git a/Api/Http/Controllers/OpenApiController.php b/Api/Http/Controllers/OpenApiController.php deleted file mode 100644 index f609f27..0000000 --- a/Api/Http/Controllers/OpenApiController.php +++ /dev/null @@ -1,13 +0,0 @@ -json(['openapi' => '3.1.0', 'info' => config('api.openapi'), 'servers' => [['url' => url('/'.config('api.prefix', 'api/v1'))]], 'components' => ['securitySchemes' => ['bearerAuth' => ['type' => 'http', 'scheme' => 'bearer']]], 'paths' => ['/pages' => ['get' => ['summary' => 'List published pages', 'responses' => ['200' => ['description' => 'OK']]]], '/blog' => ['get' => ['summary' => 'List published blog posts', 'responses' => ['200' => ['description' => 'OK']]]], '/news' => ['get' => ['summary' => 'List published news', 'responses' => ['200' => ['description' => 'OK']]]], '/user' => ['get' => ['summary' => 'Authenticated user', 'security' => [['bearerAuth' => []]], 'responses' => ['200' => ['description' => 'OK'], '401' => ['description' => 'Unauthenticated']]]]]]); - } -} diff --git a/Api/Http/Middleware/AuthenticateApiToken.php b/Api/Http/Middleware/AuthenticateApiToken.php deleted file mode 100644 index 236b1b5..0000000 --- a/Api/Http/Middleware/AuthenticateApiToken.php +++ /dev/null @@ -1,28 +0,0 @@ -bearerToken(); - if (! $plain) { - return response()->json(['message' => 'Unauthenticated.'], 401); - }$token = ApiToken::with('user')->where('token', hash('sha256', $plain))->first(); - if (! $token || ($token->expires_at && $token->expires_at->isPast()) || ! $token->user) { - return response()->json(['message' => 'Invalid or expired API token.'], 401); - }if ($ability && ! $token->allows($ability)) { - return response()->json(['message' => 'Insufficient token ability.'], 403); - }$token->update(['last_used_at' => now()]); - auth()->setUser($token->user); - $r->attributes->set('apiToken', $token); - - return $next($r); - } -} diff --git a/Api/Http/Resources/ContentResource.php b/Api/Http/Resources/ContentResource.php deleted file mode 100644 index 1ac7b0a..0000000 --- a/Api/Http/Resources/ContentResource.php +++ /dev/null @@ -1,14 +0,0 @@ - $this->id, 'type' => $this->resource->getTable(), 'title' => $this->title, 'slug' => $this->slug, 'excerpt' => $this->excerpt ?? null, 'content' => $this->content ?? null, 'status' => $this->status ?? null, 'featured_image' => $this->featured_image ?? null, 'published_at' => $this->published_at ?? null, 'updated_at' => $this->updated_at]; - } -} diff --git a/Api/Models/ApiToken.php b/Api/Models/ApiToken.php deleted file mode 100644 index 6de3874..0000000 --- a/Api/Models/ApiToken.php +++ /dev/null @@ -1,40 +0,0 @@ - 'array', 'last_used_at' => 'datetime', 'expires_at' => 'datetime']; - } - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - - public static function issue(User $u, string $name, array $abilities = ['*'], ?\DateTimeInterface $expires = null): array - { - $plain = config('api.token_prefix', 'cms_').Str::random(64); - $record = self::query()->create(['user_id' => $u->id, 'name' => $name, 'token' => hash('sha256', $plain), 'abilities' => $abilities, 'expires_at' => $expires]); - - return [$record, $plain]; - } - - public function allows(string $ability): bool - { - $a = $this->abilities ?: []; - - return in_array('*', $a, true) || in_array($ability, $a, true); - } -} diff --git a/Api/Models/Webhook.php b/Api/Models/Webhook.php deleted file mode 100644 index 28d08ef..0000000 --- a/Api/Models/Webhook.php +++ /dev/null @@ -1,28 +0,0 @@ - 'array', 'is_active' => 'boolean', 'last_triggered_at' => 'datetime']; - } - - public function deliveries(): HasMany - { - return $this->hasMany(WebhookDelivery::class); - } - - public function subscribesTo(string $event): bool - { - return in_array('*', $this->events ?: [], true) || in_array($event, $this->events ?: [], true); - } -} diff --git a/Api/Models/WebhookDelivery.php b/Api/Models/WebhookDelivery.php deleted file mode 100644 index a07b1a4..0000000 --- a/Api/Models/WebhookDelivery.php +++ /dev/null @@ -1,21 +0,0 @@ - 'array', 'delivered_at' => 'datetime', 'failed_at' => 'datetime']; - } - - public function webhook(): BelongsTo - { - return $this->belongsTo(Webhook::class); - } -} diff --git a/Api/Providers/ApiServiceProvider.php b/Api/Providers/ApiServiceProvider.php deleted file mode 100644 index 1f1122c..0000000 --- a/Api/Providers/ApiServiceProvider.php +++ /dev/null @@ -1,22 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Api', 'Config/config.php'), 'api'); - $this->app->singleton(WebhookDispatcher::class); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Api', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Api', 'Resources/views'), 'api'); - } -} diff --git a/Api/Providers/RouteServiceProvider.php b/Api/Providers/RouteServiceProvider.php deleted file mode 100644 index f6699cb..0000000 --- a/Api/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -prefix(config('api.prefix', 'api/v1'))->name('api.v1.')->group(module_path('Api', 'Routes/api.php')); - Route::middleware('web')->group(module_path('Api', 'Routes/web.php')); - } -} diff --git a/Api/Resources/views/admin/index.blade.php b/Api/Resources/views/admin/index.blade.php deleted file mode 100644 index b5d5117..0000000 --- a/Api/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

{{ __('API & Webhooks') }}

{{ __('Manage headless access, bearer tokens and webhook subscriptions.') }}

@if(session('status')){{ session('status') }}@endif @if(session('api_token')){{ __('Copy this token now') }}
{{ session('api_token') }}
@endif

{{ __('Create token') }}

@csrf{{ __('Create') }}

{{ __('Create webhook') }}

@csrf{{ __('Create') }}

{{ __('Tokens') }}

OpenAPI JSON
@forelse($tokens as $token)
{{ $token->name }} {{ implode(', ', $token->abilities ?? []) }}
@csrf @method('DELETE'){{ __('Revoke') }}
@empty

{{ __('No tokens.') }}

@endforelse{{ $tokens->links() }}

{{ __('Webhooks') }}

@forelse($webhooks as $webhook)
{{ $webhook->name }} {{ $webhook->url }}
@csrf @method('DELETE'){{ __('Delete') }}
@empty

{{ __('No webhooks.') }}

@endforelse{{ $webhooks->links() }}
diff --git a/Api/Routes/api.php b/Api/Routes/api.php deleted file mode 100644 index 63c733c..0000000 --- a/Api/Routes/api.php +++ /dev/null @@ -1,18 +0,0 @@ -name('openapi'); -Route::controller(ContentController::class)->group(function (): void { - Route::get('/pages', 'pages'); - Route::get('/pages/{page}', 'page'); - Route::get('/blog', 'blog'); - Route::get('/blog/{blog}', 'blogPost'); - Route::get('/news', 'news'); - Route::get('/news/{news}', 'newsArticle'); -}); -Route::middleware(AuthenticateApiToken::class)->get('/user', fn (Request $r) => $r->user()); diff --git a/Api/Routes/web.php b/Api/Routes/web.php deleted file mode 100644 index 1b2d91a..0000000 --- a/Api/Routes/web.php +++ /dev/null @@ -1,12 +0,0 @@ -prefix('administration/api')->name('administration.api.')->controller(ApiController::class)->group(function (): void { - Route::get('/', 'index')->name('index'); - Route::post('/tokens', 'storeToken')->name('tokens.store'); - Route::delete('/tokens/{token}', 'destroyToken')->name('tokens.destroy'); - Route::post('/webhooks', 'storeWebhook')->name('webhooks.store'); - Route::delete('/webhooks/{webhook}', 'destroyWebhook')->name('webhooks.destroy'); -}); diff --git a/Api/Services/WebhookDispatcher.php b/Api/Services/WebhookDispatcher.php deleted file mode 100644 index 05da44d..0000000 --- a/Api/Services/WebhookDispatcher.php +++ /dev/null @@ -1,28 +0,0 @@ -get()->filter(fn (Webhook $h) => ($h->site_id === null || $h->site_id === $siteId) && $h->subscribesTo($event))->each(function (Webhook $h) use ($event, $payload): void { - $uuid = (string) Str::uuid(); - $json = json_encode($payload, JSON_UNESCAPED_SLASHES) ?: '{}'; - $delivery = WebhookDelivery::create(['webhook_id' => $h->id, 'uuid' => $uuid, 'event' => $event, 'payload' => $payload, 'attempts' => 1]); - try { - $res = Http::timeout((int) config('api.webhook_timeout', 10))->withHeaders(['X-CMS-Event' => $event, 'X-CMS-Delivery' => $uuid, 'X-CMS-Signature' => 'sha256='.hash_hmac('sha256', $json, $h->getRawOriginal('secret'))])->withBody($json, 'application/json')->post($h->url); - $delivery->update(['response_status' => $res->status(), 'response_body' => Str::limit($res->body(), 10000), 'delivered_at' => $res->successful() ? now() : null, 'failed_at' => $res->successful() ? null : now()]); - $h->update(['last_triggered_at' => now(), 'failure_count' => $res->successful() ? 0 : $h->failure_count + 1]); - } catch (\Throwable $e) { - $delivery->update(['response_body' => $e->getMessage(), 'failed_at' => now()]); - $h->increment('failure_count'); - } - }); - } -} diff --git a/Api/Tests/Feature/ApiModuleBootTest.php b/Api/Tests/Feature/ApiModuleBootTest.php deleted file mode 100644 index f3b4f0a..0000000 --- a/Api/Tests/Feature/ApiModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ - expect(app()->providerIsLoaded(ApiServiceProvider::class))->toBeTrue()); -it('hashes issued api tokens', function (): void { - $u = User::factory()->create(); - [$t,$plain] = ApiToken::issue($u, 'Test', ['content.read']); - expect($t->getRawOriginal('token'))->toBe(hash('sha256', $plain))->not->toBe($plain); -}); -it('publishes openapi documentation', fn () => $this->getJson('/api/v1/openapi.json')->assertOk()->assertJsonPath('openapi', '3.1.0')); diff --git a/Api/module.json b/Api/module.json deleted file mode 100644 index e9574cd..0000000 --- a/Api/module.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"Api","alias":"api","version":"1.0.0","description":"REST APIs, bearer tokens, webhooks and OpenAPI.","keywords":["api","headless","webhooks"],"priority":90,"providers":["Modules\\Api\\Providers\\ApiServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Sites"]} diff --git a/Audit/Config/config.php b/Audit/Config/config.php deleted file mode 100644 index b43f43a..0000000 --- a/Audit/Config/config.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Audit', - 'enabled' => true, -]; diff --git a/Audit/Database/Migrations/2026_07_23_000200_create_audit_logs_table.php b/Audit/Database/Migrations/2026_07_23_000200_create_audit_logs_table.php deleted file mode 100644 index 81771b0..0000000 --- a/Audit/Database/Migrations/2026_07_23_000200_create_audit_logs_table.php +++ /dev/null @@ -1,28 +0,0 @@ -id(); - $table->nullableMorphs('actor'); - $table->string('event')->index(); - $table->nullableMorphs('auditable'); - $table->json('old_values')->nullable(); - $table->json('new_values')->nullable(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->timestamp('created_at')->useCurrent(); - }); - } - - public function down(): void - { - Schema::dropIfExists('audit_logs'); - } -}; diff --git a/Audit/Http/Controllers/AuditController.php b/Audit/Http/Controllers/AuditController.php deleted file mode 100644 index f28a59b..0000000 --- a/Audit/Http/Controllers/AuditController.php +++ /dev/null @@ -1,14 +0,0 @@ - AuditLog::query()->latest('created_at')->paginate(50)]); - } -} diff --git a/Audit/Models/AuditLog.php b/Audit/Models/AuditLog.php deleted file mode 100644 index 6dce2ae..0000000 --- a/Audit/Models/AuditLog.php +++ /dev/null @@ -1,17 +0,0 @@ - 'array', 'new_values' => 'array', 'created_at' => 'datetime']; - } -} diff --git a/Audit/Providers/AuditServiceProvider.php b/Audit/Providers/AuditServiceProvider.php deleted file mode 100644 index 939ae99..0000000 --- a/Audit/Providers/AuditServiceProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); - $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower()); - } -} diff --git a/Audit/Providers/RouteServiceProvider.php b/Audit/Providers/RouteServiceProvider.php deleted file mode 100644 index ce59b66..0000000 --- a/Audit/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,17 +0,0 @@ -routes(function (): void { - Route::middleware('web')->group(module_path('Audit', 'Routes/web.php')); - }); - } -} diff --git a/Audit/Resources/views/index.blade.php b/Audit/Resources/views/index.blade.php deleted file mode 100644 index 7bbc774..0000000 --- a/Audit/Resources/views/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Audit log

Recorded platform activity.

@forelse($logs as $log)@empty@endforelse
EventActorIPDate
{{ $log->event }}{{ $log->actor_type ? class_basename($log->actor_type).' #'.$log->actor_id : 'System' }}{{ $log->ip_address ?: '—' }}{{ optional($log->created_at)->format('d M Y H:i') }}
No audit events recorded.
{{ $logs->links() }}
diff --git a/Audit/Routes/web.php b/Audit/Routes/web.php deleted file mode 100644 index f4c314e..0000000 --- a/Audit/Routes/web.php +++ /dev/null @@ -1,6 +0,0 @@ -get('administration/audit', [AuditController::class, 'index'])->name('administration.audit.index'); diff --git a/Audit/Support/ModuleInfo.php b/Audit/Support/ModuleInfo.php deleted file mode 100644 index 96f4251..0000000 --- a/Audit/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBe('Audit'); -}); diff --git a/Audit/composer.json b/Audit/composer.json deleted file mode 100644 index b14ddd2..0000000 --- a/Audit/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "absdev/audit-module", - "description": "Audit foundation module for the ABSDEV Laravel application platform.", - "type": "laravel-module", - "autoload": { - "psr-4": { - "Modules\\Audit\\": "" - } - } -} diff --git a/Audit/module.json b/Audit/module.json deleted file mode 100644 index 6a2042e..0000000 --- a/Audit/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Audit", - "alias": "audit", - "version": "1.0.0", - "description": "Audit foundation module for the ABSDEV Laravel application platform.", - "keywords": [ - "laravel", - "modules", - "audit" - ], - "priority": 10, - "providers": [ - "Modules\\Audit\\Providers\\AuditServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core" - ] -} diff --git a/Authors/Config/config.php b/Authors/Config/config.php deleted file mode 100644 index 9255ff8..0000000 --- a/Authors/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Authors']; diff --git a/Authors/Database/Migrations/2026_07_24_030300_create_authors_table.php b/Authors/Database/Migrations/2026_07_24_030300_create_authors_table.php deleted file mode 100644 index fb9ddce..0000000 --- a/Authors/Database/Migrations/2026_07_24_030300_create_authors_table.php +++ /dev/null @@ -1,31 +0,0 @@ -id(); - $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); - $table->string('name'); - $table->string('slug')->unique(); - $table->string('job_title')->nullable(); - $table->text('biography')->nullable(); - $table->string('avatar_path')->nullable(); - $table->json('social_links')->nullable(); - $table->string('meta_title')->nullable(); - $table->text('meta_description')->nullable(); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('authors'); - } -}; diff --git a/Authors/Http/Controllers/AuthorController.php b/Authors/Http/Controllers/AuthorController.php deleted file mode 100644 index a25fa53..0000000 --- a/Authors/Http/Controllers/AuthorController.php +++ /dev/null @@ -1,57 +0,0 @@ - Author::with('user')->latest()->paginate(25)]); - } - - public function create(): View - { - return view('authors::admin.form', ['users' => User::orderBy('name')->get()]); - } - - public function store(Request $r): RedirectResponse - { - Author::create($this->data($r)); - - return redirect()->route('administration.authors.index')->with('status', 'Author created.'); - } - - public function edit(Author $author): View - { - return view('authors::admin.form', ['record' => $author, 'users' => User::orderBy('name')->get()]); - } - - public function update(Request $r, Author $author): RedirectResponse - { - $author->update($this->data($r)); - - return redirect()->route('administration.authors.index')->with('status', 'Author updated.'); - } - - public function destroy(Author $author): RedirectResponse - { - $author->delete(); - - return back()->with('status', 'Author deleted.'); - } - - private function data(Request $r): array - { - $d = $r->validate(['user_id' => ['nullable', 'exists:users,id'], 'name' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255'], 'job_title' => ['nullable', 'string', 'max:255'], 'biography' => ['nullable', 'string'], 'avatar_path' => ['nullable', 'string', 'max:255'], 'meta_title' => ['nullable', 'string', 'max:255'], 'meta_description' => ['nullable', 'string'], 'is_active' => ['sometimes', 'boolean']]); - $d['is_active'] = $r->boolean('is_active'); - - return $d; - } -} diff --git a/Authors/Models/Author.php b/Authors/Models/Author.php deleted file mode 100644 index 7fa531e..0000000 --- a/Authors/Models/Author.php +++ /dev/null @@ -1,22 +0,0 @@ - 'array', 'is_active' => 'boolean']; - } - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } -} diff --git a/Authors/Providers/AuthorsServiceProvider.php b/Authors/Providers/AuthorsServiceProvider.php deleted file mode 100644 index 1df491a..0000000 --- a/Authors/Providers/AuthorsServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Authors', 'Config/config.php'), 'authors'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Authors', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Authors', 'Resources/views'), 'authors'); - } -} diff --git a/Authors/Providers/RouteServiceProvider.php b/Authors/Providers/RouteServiceProvider.php deleted file mode 100644 index f3e7ff9..0000000 --- a/Authors/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Authors', 'Routes/web.php')); - } -} diff --git a/Authors/Resources/views/admin/form.blade.php b/Authors/Resources/views/admin/form.blade.php deleted file mode 100644 index c224794..0000000 --- a/Authors/Resources/views/admin/form.blade.php +++ /dev/null @@ -1 +0,0 @@ -
@csrf @isset($record) @method('PUT') @endisset @foreach($users as $user)@endforeach{{ old('biography',$record->biography ?? '') }}Save
diff --git a/Authors/Resources/views/admin/index.blade.php b/Authors/Resources/views/admin/index.blade.php deleted file mode 100644 index 5db3b07..0000000 --- a/Authors/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Authors

Manage publishing authors.

Create
@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $record)@empty@endforelse
Name / titleStatusActions
{{ $record->title ?? $record->name }}{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}Edit
@csrf @method('DELETE')Delete
No records yet.
{{ $records->links() }}
diff --git a/Authors/Routes/web.php b/Authors/Routes/web.php deleted file mode 100644 index 9209fa8..0000000 --- a/Authors/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/authors')->name('administration.authors.')->group(function (): void { - Route::get('/', [AuthorController::class, 'index'])->name('index'); - Route::get('/create', [AuthorController::class, 'create'])->name('create'); - Route::post('/', [AuthorController::class, 'store'])->name('store'); - Route::get('/{author}/edit', [AuthorController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{author}', [AuthorController::class, 'update'])->name('update'); - Route::delete('/{author}', [AuthorController::class, 'destroy'])->name('destroy'); -}); diff --git a/Authors/Tests/Feature/ModuleBootTest.php b/Authors/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 256a4e6..0000000 --- a/Authors/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('Authors', config('authors.name')); - } -} diff --git a/Authors/module.json b/Authors/module.json deleted file mode 100644 index c972181..0000000 --- a/Authors/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Authors", - "alias": "authors", - "version": "1.0.0", - "description": "Authors publishing module.", - "keywords": ["publishing", "authors"], - "priority": 30, - "providers": ["Modules\\Authors\\Providers\\AuthorsServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Automation/Config/config.php b/Automation/Config/config.php deleted file mode 100644 index 0d56d36..0000000 --- a/Automation/Config/config.php +++ /dev/null @@ -1,8 +0,0 @@ - env('AUTOMATION_QUEUE', 'default'), - 'max_steps' => (int) env('AUTOMATION_MAX_STEPS', 50), - 'webhook_timeout' => (int) env('AUTOMATION_WEBHOOK_TIMEOUT', 10), - 'retain_runs_days' => (int) env('AUTOMATION_RETAIN_RUNS_DAYS', 90), -]; diff --git a/Automation/Console/DispatchScheduledAutomations.php b/Automation/Console/DispatchScheduledAutomations.php deleted file mode 100644 index b80d5fd..0000000 --- a/Automation/Console/DispatchScheduledAutomations.php +++ /dev/null @@ -1,30 +0,0 @@ -where('status', 'active')->where('trigger_type', 'schedule')->whereNotNull('next_run_at')->where('next_run_at', '<=', now())->chunkById(100, function ($items) use ($runner, &$count) { - foreach ($items as $automation) { - $runner->dispatch($automation, ['scheduled_at' => now()->toIso8601String()], 'schedule'); - $interval = max(1, (int) ($automation->trigger_config['interval_minutes'] ?? 60)); - $automation->update(['next_run_at' => now()->addMinutes($interval)]); - $count++; - } - }); - $this->info("Dispatched {$count} scheduled automation(s)."); - - return self::SUCCESS; - } -} diff --git a/Automation/Contracts/ActionRegistry.php b/Automation/Contracts/ActionRegistry.php deleted file mode 100644 index 49c599c..0000000 --- a/Automation/Contracts/ActionRegistry.php +++ /dev/null @@ -1,12 +0,0 @@ -id(); - $table->string('name'); - $table->text('description')->nullable(); - $table->string('trigger_type')->index(); - $table->json('trigger_config')->nullable(); - $table->string('status', 20)->default('draft')->index(); - $table->boolean('stop_on_failure')->default(true); - $table->unsignedInteger('run_count')->default(0); - $table->timestamp('last_ran_at')->nullable(); - $table->timestamp('next_run_at')->nullable()->index(); - $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamps(); - }); - Schema::create('automation_steps', function (Blueprint $table) { - $table->id(); - $table->foreignId('automation_id')->constrained('automations')->cascadeOnDelete(); - $table->unsignedSmallInteger('position')->default(1); - $table->string('action_type'); - $table->string('name')->nullable(); - $table->json('configuration')->nullable(); - $table->boolean('is_enabled')->default(true); - $table->timestamps(); - $table->unique(['automation_id', 'position']); - }); - Schema::create('automation_runs', function (Blueprint $table) { - $table->id(); - $table->foreignId('automation_id')->constrained('automations')->cascadeOnDelete(); - $table->uuid('execution_id')->unique(); - $table->string('trigger_type'); - $table->json('payload')->nullable(); - $table->string('status', 20)->default('queued')->index(); - $table->unsignedSmallInteger('current_step')->default(0); - $table->text('error')->nullable(); - $table->timestamp('started_at')->nullable(); - $table->timestamp('finished_at')->nullable(); - $table->timestamps(); - }); - Schema::create('automation_run_logs', function (Blueprint $table) { - $table->id(); - $table->foreignId('automation_run_id')->constrained('automation_runs')->cascadeOnDelete(); - $table->foreignId('automation_step_id')->nullable()->constrained('automation_steps')->nullOnDelete(); - $table->string('level', 20)->default('info'); - $table->string('message'); - $table->json('context')->nullable(); - $table->timestamp('created_at')->useCurrent(); - }); - } - - public function down(): void - { - Schema::dropIfExists('automation_run_logs'); - Schema::dropIfExists('automation_runs'); - Schema::dropIfExists('automation_steps'); - Schema::dropIfExists('automations'); - } -}; diff --git a/Automation/Http/Controllers/AutomationApiController.php b/Automation/Http/Controllers/AutomationApiController.php deleted file mode 100644 index 72701c4..0000000 --- a/Automation/Http/Controllers/AutomationApiController.php +++ /dev/null @@ -1,24 +0,0 @@ -trigger_type === 'webhook', 404); - $secret = (string) ($automation->trigger_config['secret'] ?? ''); - if ($secret !== '') { - abort_unless(hash_equals($secret, (string) $request->header('X-Automation-Secret')), 401, 'Invalid automation secret.'); - } - $run = $runner->dispatch($automation, $request->all(), 'webhook'); - - return response()->json(['data' => ['execution_id' => $run->execution_id, 'status' => $run->status]], 202); - } -} diff --git a/Automation/Http/Controllers/AutomationController.php b/Automation/Http/Controllers/AutomationController.php deleted file mode 100644 index 38524c9..0000000 --- a/Automation/Http/Controllers/AutomationController.php +++ /dev/null @@ -1,63 +0,0 @@ - Automation::withCount(['steps', 'runs'])->latest()->paginate(20), 'recentRuns' => AutomationRun::with('automation')->latest()->limit(15)->get(), 'triggers' => $triggers->all(), 'actions' => $actions->all()]); - } - - public function store(Request $request, TriggerRegistry $triggers): RedirectResponse - { - $data = $request->validate(['name' => ['required', 'string', 'max:255'], 'description' => ['nullable', 'string'], 'trigger_type' => ['required', 'string'], 'status' => ['required', 'in:draft,active,paused'], 'next_run_at' => ['nullable', 'date'], 'trigger_config' => ['nullable', 'json']]); - abort_unless($triggers->has($data['trigger_type']), 422, 'Unknown trigger type.'); - $data['trigger_config'] = filled($data['trigger_config'] ?? null) ? json_decode($data['trigger_config'], true, 512, JSON_THROW_ON_ERROR) : []; - $data['stop_on_failure'] = $request->boolean('stop_on_failure'); - $data['created_by'] = $request->user()->id; - Automation::create($data); - - return back()->with('status', 'Automation created.'); - } - - public function addStep(Request $request, Automation $automation, ActionRegistry $actions): RedirectResponse - { - $data = $request->validate(['action_type' => ['required', 'string'], 'name' => ['nullable', 'string', 'max:255'], 'configuration' => ['nullable', 'json']]); - abort_unless(array_key_exists($data['action_type'], $actions->all()), 422, 'Unknown action type.'); - $automation->steps()->create(['position' => (int) $automation->steps()->max('position') + 1, 'action_type' => $data['action_type'], 'name' => $data['name'] ?? null, 'configuration' => filled($data['configuration'] ?? null) ? json_decode($data['configuration'], true, 512, JSON_THROW_ON_ERROR) : [], 'is_enabled' => true]); - - return back()->with('status', 'Automation step added.'); - } - - public function run(Automation $automation, AutomationRunner $runner): RedirectResponse - { - $run = $runner->dispatch($automation, ['initiated_by' => request()->user()->id], 'manual'); - - return back()->with('status', "Automation queued as {$run->execution_id}."); - } - - public function toggle(Automation $automation): RedirectResponse - { - $automation->update(['status' => $automation->status === 'active' ? 'paused' : 'active']); - - return back()->with('status', 'Automation status updated.'); - } - - public function destroy(Automation $automation): RedirectResponse - { - $automation->delete(); - - return back()->with('status','Automation deleted.'); - } -} diff --git a/Automation/Jobs/ExecuteAutomation.php b/Automation/Jobs/ExecuteAutomation.php deleted file mode 100644 index b84d34d..0000000 --- a/Automation/Jobs/ExecuteAutomation.php +++ /dev/null @@ -1,46 +0,0 @@ -findOrFail($this->runId); - $automation = $run->automation; - $run->update(['status' => 'running', 'started_at' => now()]); - try { - foreach ($automation->steps->where('is_enabled', true) as $step) { - $run->update(['current_step' => $step->position]); - try { - $result = $actions->execute($step, $run->payload ?? []); - $run->logs()->create(['automation_step_id' => $step->id, 'level' => 'info', 'message' => $step->name ?: $step->action_type.' completed', 'context' => $result, 'created_at' => now()]); - } catch (\Throwable $e) { - $run->logs()->create(['automation_step_id' => $step->id, 'level' => 'error', 'message' => $e->getMessage(), 'created_at' => now()]); - if ($automation->stop_on_failure) { - throw $e; - } - } - } - $run->update(['status' => 'completed', 'finished_at' => now()]); - } catch (\Throwable $e) { - $run->update(['status' => 'failed', 'error' => $e->getMessage(), 'finished_at' => now()]); - throw $e; - } finally { - $automation->increment('run_count'); - $automation->update(['last_ran_at' => now()]); - } - } -} diff --git a/Automation/Models/Automation.php b/Automation/Models/Automation.php deleted file mode 100644 index 0a060ed..0000000 --- a/Automation/Models/Automation.php +++ /dev/null @@ -1,28 +0,0 @@ - 'array', 'stop_on_failure' => 'boolean', 'last_ran_at' => 'datetime', 'next_run_at' => 'datetime']; - - public function steps(): HasMany - { - return $this->hasMany(AutomationStep::class)->orderBy('position'); - } - - public function runs(): HasMany - { - return $this->hasMany(AutomationRun::class); - } - - public function isActive(): bool - { - return $this->status === 'active'; - } -} diff --git a/Automation/Models/AutomationRun.php b/Automation/Models/AutomationRun.php deleted file mode 100644 index 38b5fb5..0000000 --- a/Automation/Models/AutomationRun.php +++ /dev/null @@ -1,24 +0,0 @@ - 'array', 'started_at' => 'datetime', 'finished_at' => 'datetime']; - - public function automation(): BelongsTo - { - return $this->belongsTo(Automation::class); - } - - public function logs(): HasMany - { - return $this->hasMany(AutomationRunLog::class); - } -} diff --git a/Automation/Models/AutomationRunLog.php b/Automation/Models/AutomationRunLog.php deleted file mode 100644 index 3d93839..0000000 --- a/Automation/Models/AutomationRunLog.php +++ /dev/null @@ -1,14 +0,0 @@ - 'array', 'created_at' => 'datetime']; -} diff --git a/Automation/Models/AutomationStep.php b/Automation/Models/AutomationStep.php deleted file mode 100644 index 2627289..0000000 --- a/Automation/Models/AutomationStep.php +++ /dev/null @@ -1,18 +0,0 @@ - 'array', 'is_enabled' => 'boolean']; - - public function automation(): BelongsTo - { - return $this->belongsTo(Automation::class); - } -} diff --git a/Automation/Providers/AutomationServiceProvider.php b/Automation/Providers/AutomationServiceProvider.php deleted file mode 100644 index b4c3b7c..0000000 --- a/Automation/Providers/AutomationServiceProvider.php +++ /dev/null @@ -1,30 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Automation', 'Config/config.php'), 'automation'); - $this->app->singleton(ActionRegistry::class, AutomationActionRegistry::class); - $this->app->singleton(TriggerRegistry::class, AutomationTriggerRegistry::class); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Automation', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Automation', 'Resources/views'), 'automation'); - if ($this->app->runningInConsole()) { - $this->commands([DispatchScheduledAutomations::class]); - } - } -} diff --git a/Automation/Providers/RouteServiceProvider.php b/Automation/Providers/RouteServiceProvider.php deleted file mode 100644 index f03573a..0000000 --- a/Automation/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Automation', 'Routes/web.php')); - Route::middleware('api')->prefix('api/v1')->group(module_path('Automation', 'Routes/api.php')); - } -} diff --git a/Automation/Resources/views/admin/index.blade.php b/Automation/Resources/views/admin/index.blade.php deleted file mode 100644 index 1033167..0000000 --- a/Automation/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,26 +0,0 @@ - -
-
Automation PlatformCreate event-driven, scheduled and webhook automations.
-@if(session('status')){{ session('status') }}@endif -
-Automations{{ $automations->total() }} -Active{{ \Modules\Automation\Models\Automation::where('status','active')->count() }} -Completed runs{{ \Modules\Automation\Models\AutomationRun::where('status','completed')->count() }} -Failed runs{{ \Modules\Automation\Models\AutomationRun::where('status','failed')->count() }} -
-@can('automation.manage') -Create automation
@csrf -@foreach($triggers as $key=>$trigger)@endforeach - - -
Create automation
-
-@endcan -
-@forelse($automations as $automation)
{{ $automation->name }}{{ $automation->trigger_type }} · {{ $automation->steps_count }} steps · {{ $automation->runs_count }} runs
{{ ucfirst($automation->status) }}
-@if($automation->description){{ $automation->description }}@endif -
@can('automation.execute')
@csrfRun
@endcan @can('automation.manage')
@csrf @method('PATCH'){{ $automation->status==='active'?'Pause':'Activate' }}
@csrf @method('DELETE')Delete
@endcan
-@can('automation.manage')
@csrf@foreach($actions as $key=>$action)@endforeach
Add step
@endcan -
@emptyNo automations have been created.@endforelse -{{ $automations->links() }}
Recent runs
@forelse($recentRuns as $run)
{{ $run->automation?->name }}{{ $run->status }}
{{ $run->execution_id }}
@emptyNo runs yet.@endforelse
-
diff --git a/Automation/Routes/api.php b/Automation/Routes/api.php deleted file mode 100644 index f90901d..0000000 --- a/Automation/Routes/api.php +++ /dev/null @@ -1,6 +0,0 @@ -middleware('throttle:60,1')->name('api.automations.trigger'); diff --git a/Automation/Routes/web.php b/Automation/Routes/web.php deleted file mode 100644 index 342d7c0..0000000 --- a/Automation/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -name('administration.automation.')->middleware(['auth', 'permission:automation.view'])->group(function () { - Route::get('/', [AutomationController::class, 'index'])->name('index'); - Route::post('/', [AutomationController::class, 'store'])->middleware('permission:automation.manage')->name('store'); - Route::post('/{automation}/steps', [AutomationController::class, 'addStep'])->middleware('permission:automation.manage')->name('steps.store'); - Route::post('/{automation}/run', [AutomationController::class, 'run'])->middleware('permission:automation.execute')->name('run'); - Route::patch('/{automation}/toggle', [AutomationController::class, 'toggle'])->middleware('permission:automation.manage')->name('toggle'); - Route::delete('/{automation}', [AutomationController::class, 'destroy'])->middleware('permission:automation.manage')->name('destroy'); -}); diff --git a/Automation/Services/AutomationActionRegistry.php b/Automation/Services/AutomationActionRegistry.php deleted file mode 100644 index b8e7f2c..0000000 --- a/Automation/Services/AutomationActionRegistry.php +++ /dev/null @@ -1,79 +0,0 @@ - ['label' => 'Write log', 'fields' => ['message']], - 'webhook' => ['label' => 'Send webhook', 'fields' => ['url', 'method']], - 'email' => ['label' => 'Send email', 'fields' => ['to', 'subject', 'body']], - 'delay' => ['label' => 'Delay', 'fields' => ['seconds']], - ]; - } - - public function execute(AutomationStep $step, array $payload): array - { - $config = $step->configuration ?? []; - - return match ($step->action_type) { - 'log' => $this->log($config, $payload), - 'webhook' => $this->webhook($config, $payload), - 'email' => $this->email($config, $payload), - 'delay' => $this->delay($config), - default => throw new \InvalidArgumentException("Unknown automation action [{$step->action_type}]."), - }; - } - - private function log(array $config, array $payload): array - { - $message = (string) ($config['message'] ?? 'Automation executed'); - Log::info($message, $payload); - - return ['message' => $message]; - } - - private function webhook(array $config, array $payload): array - { - $url = (string) ($config['url'] ?? ''); - if ($url === '') { - throw new \InvalidArgumentException('Webhook URL is required.'); - } - $method = strtolower((string) ($config['method'] ?? 'post')); - $response = Http::timeout(config('automation.webhook_timeout', 10))->send($method, $url, ['json' => $payload]); - $response->throw(); - - return ['status' => $response->status()]; - } - - private function email(array $config, array $payload): array - { - $to = (string) ($config['to'] ?? ''); - if ($to === '') { - throw new \InvalidArgumentException('Email recipient is required.'); - } - $subject = (string) ($config['subject'] ?? 'Automation notification'); - $body = (string) ($config['body'] ?? json_encode($payload, JSON_PRETTY_PRINT)); - Mail::raw($body, fn ($message) => $message->to($to)->subject($subject)); - - return ['to' => $to]; - } - - private function delay(array $config): array - { - $seconds = max(0, min(30, (int) ($config['seconds'] ?? 1))); - if ($seconds > 0) { - sleep($seconds); - } - -return ['seconds' => $seconds]; - } -} diff --git a/Automation/Services/AutomationRunner.php b/Automation/Services/AutomationRunner.php deleted file mode 100644 index 254ba8a..0000000 --- a/Automation/Services/AutomationRunner.php +++ /dev/null @@ -1,46 +0,0 @@ -isActive(), 422, 'Automation is not active.'); - $run = $automation->runs()->create(['execution_id' => (string) Str::uuid(), 'trigger_type' => $triggerType ?? $automation->trigger_type, 'payload' => $payload, 'status' => 'queued']); - ExecuteAutomation::dispatch($run->id)->onQueue(config('automation.queue', 'default')); - - return $run; - } - - public function dispatchMatching(string $triggerType, array $payload = [], array $constraints = []): int - { - $query = Automation::query()->where('status', 'active')->where('trigger_type', $triggerType); - $count = 0; - foreach ($query->get() as $automation) { - if ($this->matches($automation, $constraints)) { - $this->dispatch($automation, $payload, $triggerType); - $count++; - } - } - - return $count; - } - - private function matches(Automation $automation, array $constraints): bool - { - $config = $automation->trigger_config ?? []; - foreach ($constraints as $key => $value) { - if (isset($config[$key]) && (string) $config[$key] !== (string) $value) { - return false; - } - } - - return true; - } -} diff --git a/Automation/Services/AutomationTriggerRegistry.php b/Automation/Services/AutomationTriggerRegistry.php deleted file mode 100644 index c0716f4..0000000 --- a/Automation/Services/AutomationTriggerRegistry.php +++ /dev/null @@ -1,23 +0,0 @@ - ['label' => 'Manual', 'description' => 'Run from the administration area or API.'], - 'schedule' => ['label' => 'Schedule', 'description' => 'Run when next_run_at is due.'], - 'webhook' => ['label' => 'Incoming webhook', 'description' => 'Run from a signed API request.'], - 'domain_event' => ['label' => 'Domain event', 'description' => 'Run when an application event is dispatched.'], - ]; - } - - public function has(string $type): bool - { - return array_key_exists($type, $this->all()); - } -} diff --git a/Automation/Tests/Feature/ModuleBootTest.php b/Automation/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 984262f..0000000 --- a/Automation/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,16 +0,0 @@ -assertTrue(class_exists(AutomationServiceProvider::class)); - $this->assertTrue(app()->bound(ActionRegistry::class)); - } -} diff --git a/Automation/module.json b/Automation/module.json deleted file mode 100644 index c39c7ae..0000000 --- a/Automation/module.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"Automation","alias":"automation","version":"1.0.0","description":"Event-driven and scheduled automations with reusable triggers, actions, runs and execution logs.","keywords":["automation","events","triggers","actions","scheduler"],"priority":130,"providers":["Modules\\Automation\\Providers\\AutomationServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Audit","Api"]} diff --git a/Banners/Config/config.php b/Banners/Config/config.php deleted file mode 100644 index b2a20d0..0000000 --- a/Banners/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Banners']; diff --git a/Banners/Http/Controllers/BannerController.php b/Banners/Http/Controllers/BannerController.php deleted file mode 100644 index a66e3f0..0000000 --- a/Banners/Http/Controllers/BannerController.php +++ /dev/null @@ -1,60 +0,0 @@ - Banner::query()->latest()->paginate(20)]); - } - - public function create(): View - { - return view('banners::admin.form'); - } - - public function store(Request $request): RedirectResponse - { - $data = $request->validate(['title' => ['required', 'string', 'max:255'], - 'content' => ['nullable', 'string'], - 'link_url' => ['nullable', 'string'], - 'position' => ['required', 'string', 'max:255'], - 'enabled' => ['sometimes', 'boolean']]); - $data['enabled'] = $request->boolean('enabled'); - Banner::create($data); - - return redirect()->route('administration.banners.index')->with('status', 'Banner created.'); - } - - public function edit(Banner $banner): View - { - return view('banners::admin.form', ['record' => $banner]); - } - - public function update(Request $request, Banner $banner): RedirectResponse - { - $data = $request->validate(['title' => ['required', 'string', 'max:255'], - 'content' => ['nullable', 'string'], - 'link_url' => ['nullable', 'string'], - 'position' => ['required', 'string', 'max:255'], - 'enabled' => ['sometimes', 'boolean']]); - $data['enabled'] = $request->boolean('enabled'); - $banner->update($data); - - return redirect()->route('administration.banners.index')->with('status', 'Banner updated.'); - } - - public function destroy(Banner $banner): RedirectResponse - { - $banner->delete(); - - return back()->with('status', 'Banner deleted.'); - } -} diff --git a/Banners/Models/Banner.php b/Banners/Models/Banner.php deleted file mode 100644 index 7ba1279..0000000 --- a/Banners/Models/Banner.php +++ /dev/null @@ -1,23 +0,0 @@ - 'boolean', - ]; - } -} diff --git a/Banners/Providers/BannersServiceProvider.php b/Banners/Providers/BannersServiceProvider.php deleted file mode 100644 index eda98f7..0000000 --- a/Banners/Providers/BannersServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Banners', 'Config/config.php'), 'banners'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Banners', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Banners', 'Resources/views'), 'banners'); - } -} diff --git a/Banners/Providers/RouteServiceProvider.php b/Banners/Providers/RouteServiceProvider.php deleted file mode 100644 index c948511..0000000 --- a/Banners/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -routes(fn () => Route::middleware('web')->group(module_path('Banners', 'Routes/web.php'))); - } -} diff --git a/Banners/Resources/views/admin/form.blade.php b/Banners/Resources/views/admin/form.blade.php deleted file mode 100644 index b78be52..0000000 --- a/Banners/Resources/views/admin/form.blade.php +++ /dev/null @@ -1,9 +0,0 @@ - -

{{ isset($record) ? 'Edit' : 'Create' }} Banner

@csrf @isset($record) @method('PUT') @endisset -
@error('title')

{{ $message }}

@enderror
-
@error('content')

{{ $message }}

@enderror
-
@error('link_url')

{{ $message }}

@enderror
-
@error('position')

{{ $message }}

@enderror
- -
Cancel
-
diff --git a/Banners/Resources/views/admin/index.blade.php b/Banners/Resources/views/admin/index.blade.php deleted file mode 100644 index 4ac0c2d..0000000 --- a/Banners/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,5 +0,0 @@ - -

Banners

Manage banners content.

Add new
-@if(session('status'))
{{ session('status') }}
@endif -
@forelse($records as $record)@empty@endforelse
TitleContentLink UrlActions
{{ is_array($record->title) ? json_encode($record->title) : Str::limit((string) $record->title, 70) }}{{ is_array($record->content) ? json_encode($record->content) : Str::limit((string) $record->content, 70) }}{{ is_array($record->link_url) ? json_encode($record->link_url) : Str::limit((string) $record->link_url, 70) }}Edit
@csrf @method('DELETE')
No records yet.
{{ $records->links() }}
-
diff --git a/Banners/Routes/web.php b/Banners/Routes/web.php deleted file mode 100644 index 3651a6c..0000000 --- a/Banners/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/banners')->name('administration.banners.')->group(function (): void { - Route::get('/', [BannerController::class, 'index'])->name('index'); - Route::get('/create', [BannerController::class, 'create'])->name('create'); - Route::post('/', [BannerController::class, 'store'])->name('store'); - Route::get('/{banner}/edit', [BannerController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{banner}', [BannerController::class, 'update'])->name('update'); - Route::delete('/{banner}', [BannerController::class, 'destroy'])->name('destroy'); -}); diff --git a/Banners/Support/ModuleInfo.php b/Banners/Support/ModuleInfo.php deleted file mode 100644 index 09e1bda..0000000 --- a/Banners/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBeTrue(); -}); diff --git a/Banners/module.json b/Banners/module.json deleted file mode 100644 index 238aec6..0000000 --- a/Banners/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Banners", - "alias": "banners", - "version": "1.0.0", - "description": "Banners CMS module.", - "keywords": [ - "cms", - "banners" - ], - "priority": 26, - "providers": [ - "Modules\\Banners\\Providers\\BannersServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core", - "Administration" - ] -} diff --git a/Blog/Config/config.php b/Blog/Config/config.php deleted file mode 100644 index 97da7a5..0000000 --- a/Blog/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Blog']; diff --git a/Blog/Database/Migrations/2026_07_24_03041_create_blog_posts_table.php b/Blog/Database/Migrations/2026_07_24_03041_create_blog_posts_table.php deleted file mode 100644 index 30684a4..0000000 --- a/Blog/Database/Migrations/2026_07_24_03041_create_blog_posts_table.php +++ /dev/null @@ -1,40 +0,0 @@ -id(); - $table->foreignId('author_id')->nullable()->constrained('authors')->nullOnDelete(); - $table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete(); - $table->string('title'); - $table->string('slug')->unique(); - $table->text('excerpt')->nullable(); - $table->longText('content')->nullable(); - $table->string('featured_image')->nullable(); - $table->string('status')->default('draft')->index(); - $table->boolean('is_featured')->default(false); - $table->timestamp('published_at')->nullable()->index(); - $table->timestamp('expires_at')->nullable(); - $table->string('meta_title')->nullable(); - $table->text('meta_description')->nullable(); - $table->timestamps(); - }); - Schema::create('blog_post_tag', function (Blueprint $table): void { - $table->foreignId('blog_post_id')->constrained('blog_posts')->cascadeOnDelete(); - $table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete(); - $table->primary(['blog_post_id', 'tag_id']); - }); - } - - public function down(): void - { - Schema::dropIfExists('blog_post_tag'); - Schema::dropIfExists('blog_posts'); - } -}; diff --git a/Blog/Http/Controllers/BlogController.php b/Blog/Http/Controllers/BlogController.php deleted file mode 100644 index 0e96878..0000000 --- a/Blog/Http/Controllers/BlogController.php +++ /dev/null @@ -1,66 +0,0 @@ - Blog::with(['author', 'category'])->latest()->paginate(25)]); - } - - public function create(): View - { - return view('blog::admin.form', $this->lookups()); - } - - public function store(Request $r): RedirectResponse - { - $record = Blog::create($this->data($r)); - $record->tags()->sync($r->input('tag_ids', [])); - - return redirect()->route('administration.blog.index')->with('status', 'Blog created.'); - } - - public function edit(Blog $blog_post): View - { - return view('blog::admin.form', array_merge($this->lookups(), ['record' => $blog_post])); - } - - public function update(Request $r, Blog $blog_post): RedirectResponse - { - $blog_post->update($this->data($r)); - $blog_post->tags()->sync($r->input('tag_ids', [])); - - return redirect()->route('administration.blog.index')->with('status', 'Blog updated.'); - } - - public function destroy(Blog $blog_post): RedirectResponse - { - $blog_post->delete(); - - return back()->with('status', 'Blog deleted.'); - } - - private function lookups(): array - { - return ['authors' => Author::where('is_active', true)->orderBy('name')->get(), 'categories' => Category::where('is_active', true)->orderBy('name')->get(), 'tags' => Tag::orderBy('name')->get()]; - } - - private function data(Request $r): array - { - $d = $r->validate(['author_id' => ['nullable', 'exists:authors,id'], 'category_id' => ['nullable', 'exists:categories,id'], 'title' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255'], 'excerpt' => ['nullable', 'string'], 'content' => ['nullable', 'string'], 'featured_image' => ['nullable', 'string', 'max:255'], 'status' => ['required', 'in:draft,review,published,archived'], 'is_featured' => ['sometimes', 'boolean'], 'published_at' => ['nullable', 'date'], 'expires_at' => ['nullable', 'date', 'after:published_at'], 'meta_title' => ['nullable', 'string', 'max:255'], 'meta_description' => ['nullable', 'string']]); - $d['is_featured'] = $r->boolean('is_featured'); - - return $d; - } -} diff --git a/Blog/Http/Controllers/PublicBlogController.php b/Blog/Http/Controllers/PublicBlogController.php deleted file mode 100644 index 6fb2096..0000000 --- a/Blog/Http/Controllers/PublicBlogController.php +++ /dev/null @@ -1,20 +0,0 @@ - Blog::published()->with(['author', 'category'])->latest('published_at')->paginate(12)]); - } - - public function show(string $slug): View - { - return view('blog::public.show', ['record' => Blog::published()->with(['author', 'category', 'tags'])->where('slug', $slug)->firstOrFail()]); - } -} diff --git a/Blog/Models/Blog.php b/Blog/Models/Blog.php deleted file mode 100644 index 6f58ea0..0000000 --- a/Blog/Models/Blog.php +++ /dev/null @@ -1,48 +0,0 @@ - 'boolean', 'published_at' => 'datetime', 'expires_at' => 'datetime']; - } - - public function author(): BelongsTo - { - return $this->belongsTo(Author::class); - } - - public function category(): BelongsTo - { - return $this->belongsTo(Category::class); - } - - public function tags(): BelongsToMany - { - return $this->belongsToMany(Tag::class, 'blog_post_tag'); - } - - public function scopePublished(Builder $query): Builder - { - return $query->where('status', 'published')->where(fn (Builder $q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()))->where(fn (Builder $q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())); - } -} diff --git a/Blog/Providers/BlogServiceProvider.php b/Blog/Providers/BlogServiceProvider.php deleted file mode 100644 index 74e8bcf..0000000 --- a/Blog/Providers/BlogServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Blog', 'Config/config.php'), 'blog'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Blog', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Blog', 'Resources/views'), 'blog'); - } -} diff --git a/Blog/Providers/RouteServiceProvider.php b/Blog/Providers/RouteServiceProvider.php deleted file mode 100644 index 6c7b0c0..0000000 --- a/Blog/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Blog', 'Routes/web.php')); - } -} diff --git a/Blog/Resources/views/admin/form.blade.php b/Blog/Resources/views/admin/form.blade.php deleted file mode 100644 index cf54f2b..0000000 --- a/Blog/Resources/views/admin/form.blade.php +++ /dev/null @@ -1 +0,0 @@ -
@csrf @isset($record) @method('PUT') @endisset
@foreach($authors as $author)@endforeach@foreach($categories as $category)@endforeach@foreach(['draft','review','published','archived'] as $status)@endforeach
{{ old('excerpt',$record->excerpt ?? '') }}{{ old('content',$record->content ?? '') }}
@foreach($tags as $tag)@endforeach
{{ old('meta_description',$record->meta_description ?? '') }}Save
diff --git a/Blog/Resources/views/admin/index.blade.php b/Blog/Resources/views/admin/index.blade.php deleted file mode 100644 index 9ec9eda..0000000 --- a/Blog/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Blog

Manage publishing blog.

Create
@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $record)@empty@endforelse
Name / titleStatusActions
{{ $record->title ?? $record->name }}{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}Edit
@csrf @method('DELETE')Delete
No records yet.
{{ $records->links() }}
diff --git a/Blog/Resources/views/public/index.blade.php b/Blog/Resources/views/public/index.blade.php deleted file mode 100644 index b616fa9..0000000 --- a/Blog/Resources/views/public/index.blade.php +++ /dev/null @@ -1,31 +0,0 @@ -@extends('stylist-theme-saasland::layouts.app') - -@php - $title = 'Blog'; - $description = 'Ideas, guides and updates from our team.'; - $eyebrow = 'Insights'; - $breadcrumbs = ['Blog' => null]; -@endphp - -@section('content') -
-
-
- @forelse($records as $record) -
-
-
- -

{{ $record->title }}

- @if($record->excerpt)

{{ $record->excerpt }}

@endif - Read article → -
-
- @empty -
No blog posts have been published yet.
- @endforelse -
-
{{ $records->links() }}
-
-
-@endsection diff --git a/Blog/Resources/views/public/show.blade.php b/Blog/Resources/views/public/show.blade.php deleted file mode 100644 index c710f44..0000000 --- a/Blog/Resources/views/public/show.blade.php +++ /dev/null @@ -1,23 +0,0 @@ -@extends('stylist-theme-saasland::layouts.app') - -@php - $title = $record->title; - $description = $record->excerpt; - $eyebrow = 'Blog'; - $breadcrumbs = ['Blog' => route('blog.public.index'), $record->title => null]; -@endphp - -@section('content') -
-
-
- - @if($record->excerpt)

{{ $record->excerpt }}

@endif -
{!! nl2br(e($record->content)) !!}
-
-
-
-@endsection diff --git a/Blog/Routes/web.php b/Blog/Routes/web.php deleted file mode 100644 index 01f9b3d..0000000 --- a/Blog/Routes/web.php +++ /dev/null @@ -1,16 +0,0 @@ -prefix('administration/blog')->name('administration.blog.')->group(function (): void { - Route::get('/', [BlogController::class, 'index'])->name('index'); - Route::get('/create', [BlogController::class, 'create'])->name('create'); - Route::post('/', [BlogController::class, 'store'])->name('store'); - Route::get('/{blog_post}/edit', [BlogController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{blog_post}', [BlogController::class, 'update'])->name('update'); - Route::delete('/{blog_post}', [BlogController::class, 'destroy'])->name('destroy'); -}); -Route::get('/blog', [PublicBlogController::class, 'index'])->name('blog.public.index'); -Route::get('/blog/{slug}', [PublicBlogController::class, 'show'])->name('blog.public.show'); diff --git a/Blog/module.json b/Blog/module.json deleted file mode 100644 index e72c145..0000000 --- a/Blog/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Blog", - "alias": "blog", - "version": "1.0.0", - "description": "Blog publishing module.", - "keywords": ["publishing", "blog"], - "priority": 30, - "providers": ["Modules\\Blog\\Providers\\BlogServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Builder/Components/ContentComponent.php b/Builder/Components/ContentComponent.php deleted file mode 100644 index 63dc588..0000000 --- a/Builder/Components/ContentComponent.php +++ /dev/null @@ -1,38 +0,0 @@ - ['type' => 'textarea', 'required' => true]]; - } - - public function defaults(): array - { - return ['content' => '

Add your content here.

']; - } - - public function render(array $properties, array $context = []): string - { - return (string) ($properties['content'] ?? ''); - } -} diff --git a/Builder/Components/HeadingComponent.php b/Builder/Components/HeadingComponent.php deleted file mode 100644 index e619242..0000000 --- a/Builder/Components/HeadingComponent.php +++ /dev/null @@ -1,40 +0,0 @@ - ['type' => 'text', 'required' => true], 'level' => ['type' => 'select', 'options' => [1, 2, 3, 4, 5, 6]]]; - } - - public function defaults(): array - { - return ['text' => 'Heading', 'level' => 2]; - } - - public function render(array $properties, array $context = []): string - { - $level = max(1, min(6, (int) ($properties['level'] ?? 2))); - - return ''.e($properties['text'] ?? '').''; - } -} diff --git a/Builder/Components/HeroComponent.php b/Builder/Components/HeroComponent.php deleted file mode 100644 index f017dea..0000000 --- a/Builder/Components/HeroComponent.php +++ /dev/null @@ -1,38 +0,0 @@ - ['type' => 'text', 'required' => true], 'content' => ['type' => 'textarea'], 'button_label' => ['type' => 'text'], 'button_url' => ['type' => 'text']]; - } - - public function defaults(): array - { - return ['heading' => 'A compelling headline', 'content' => 'Add supporting content here.', 'button_label' => 'Learn more', 'button_url' => '#']; - } - - public function render(array $properties, array $context = []): string - { - return view('builder::components.hero', ['properties' => $properties])->render(); - } -} diff --git a/Builder/Config/config.php b/Builder/Config/config.php deleted file mode 100644 index b3a8362..0000000 --- a/Builder/Config/config.php +++ /dev/null @@ -1,8 +0,0 @@ - (int) env('BUILDER_MAX_DOCUMENT_SIZE_KB', 512), - 'autosave_seconds' => (int) env('BUILDER_AUTOSAVE_SECONDS', 30), - 'preview_route' => env('BUILDER_PREVIEW_ROUTE', 'builder.preview'), - 'allowed_types' => ['page', 'form', 'dashboard', 'report', 'menu', 'email', 'query', 'workflow'], -]; diff --git a/Builder/Contracts/BuilderComponent.php b/Builder/Contracts/BuilderComponent.php deleted file mode 100644 index e16fad7..0000000 --- a/Builder/Contracts/BuilderComponent.php +++ /dev/null @@ -1,18 +0,0 @@ -id(); - $table->foreignId('site_id')->nullable()->constrained('sites')->nullOnDelete(); - $table->string('type', 30)->index(); - $table->string('name'); - $table->string('slug')->nullable(); - $table->string('status', 20)->default('draft')->index(); - $table->unsignedInteger('version')->default(1); - $table->json('document')->nullable(); - $table->json('settings')->nullable(); - $table->nullableMorphs('buildable'); - $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); - $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamp('published_at')->nullable(); - $table->timestamps(); - $table->unique(['site_id', 'type', 'slug'], 'builder_document_site_type_slug_unique'); - }); - Schema::create('builder_document_versions', function (Blueprint $table) { - $table->id(); - $table->foreignId('builder_document_id')->constrained('builder_documents')->cascadeOnDelete(); - $table->unsignedInteger('version'); - $table->json('document'); - $table->json('settings')->nullable(); - $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamps(); - $table->unique(['builder_document_id', 'version']); - }); - Schema::create('builder_templates', function (Blueprint $table) { - $table->id(); - $table->string('type', 30)->index(); - $table->string('name'); - $table->string('slug')->unique(); - $table->text('description')->nullable(); - $table->json('document'); - $table->json('settings')->nullable(); - $table->boolean('is_system')->default(false); - $table->boolean('is_active')->default(true)->index(); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('builder_templates'); - Schema::dropIfExists('builder_document_versions'); - Schema::dropIfExists('builder_documents'); - } -}; diff --git a/Builder/Http/Controllers/BuilderApiController.php b/Builder/Http/Controllers/BuilderApiController.php deleted file mode 100644 index 57296aa..0000000 --- a/Builder/Http/Controllers/BuilderApiController.php +++ /dev/null @@ -1,27 +0,0 @@ -json(['data' => $registry->components(), 'widgets' => $registry->widgets(), 'fields' => $registry->fields()]); - } - - public function show(BuilderDocument $document): JsonResponse - { - return response()->json(['data' => $document]); - } - - public function preview(BuilderDocument $document, BuilderRenderer $renderer): JsonResponse - { - return response()->json(['html' => $renderer->render($document->document ?? [])]); - } -} diff --git a/Builder/Http/Controllers/BuilderController.php b/Builder/Http/Controllers/BuilderController.php deleted file mode 100644 index 70443cc..0000000 --- a/Builder/Http/Controllers/BuilderController.php +++ /dev/null @@ -1,61 +0,0 @@ -string('type')->toString(); - $documents = BuilderDocument::query()->when($type, fn ($q) => $q->where('type', $type))->latest()->paginate(20)->withQueryString(); - - return view('builder::index', compact('documents', 'type')); - } - - public function create(BuilderRegistry $registry): View - { - return view('builder::editor', ['document' => new BuilderDocument(['type' => request('type', 'page'), 'document' => ['nodes' => []]]), 'components' => $registry->components()]); - } - - public function edit(BuilderDocument $document, BuilderRegistry $registry): View - { - return view('builder::editor', ['document' => $document, 'components' => $registry->components()]); - } - - public function store(Request $request, BuilderDocumentService $service): RedirectResponse - { - $document = $service->save(new BuilderDocument, $this->validated($request), $request->user()?->id); - - return redirect()->route('administration.builder.edit', $document)->with('status', 'Builder document created.'); - } - - public function update(Request $request, BuilderDocument $document, BuilderDocumentService $service): RedirectResponse - { - $service->save($document, $this->validated($request), $request->user()?->id); - - return back()->with('status', 'Builder document saved.'); - } - - public function destroy(BuilderDocument $document): RedirectResponse - { - $document->delete(); - - return redirect()->route('administration.builder.index')->with('status', 'Builder document deleted.'); - } - - private function validated(Request $request): array - { - $data = $request->validate(['type' => ['required', 'in:'.implode(',', config('builder.allowed_types'))], 'name' => ['required', 'string', 'max:255'], 'slug' => ['nullable', 'string', 'max:255'], 'status' => ['required', 'in:draft,published,archived'], 'document_json' => ['required', 'json'], 'settings_json' => ['nullable', 'json']]); - - return ['type' => $data['type'], 'name' => $data['name'], 'slug' => $data['slug'] ?: Str::slug($data['name']), 'status' => $data['status'], 'document' => json_decode($data['document_json'], true, 512, JSON_THROW_ON_ERROR), 'settings' => json_decode($data['settings_json'] ?: '{}', true, 512, JSON_THROW_ON_ERROR), 'published_at' => $data['status'] === 'published' ? now() : null]; - } -} diff --git a/Builder/Models/BuilderDocument.php b/Builder/Models/BuilderDocument.php deleted file mode 100644 index edb53ad..0000000 --- a/Builder/Models/BuilderDocument.php +++ /dev/null @@ -1,18 +0,0 @@ - 'array', 'settings' => 'array', 'published_at' => 'datetime']; - - public function versions(): HasMany - { - return $this->hasMany(BuilderDocumentVersion::class); - } -} diff --git a/Builder/Models/BuilderDocumentVersion.php b/Builder/Models/BuilderDocumentVersion.php deleted file mode 100644 index 30d6a78..0000000 --- a/Builder/Models/BuilderDocumentVersion.php +++ /dev/null @@ -1,12 +0,0 @@ - 'array', 'settings' => 'array']; -} diff --git a/Builder/Models/BuilderTemplate.php b/Builder/Models/BuilderTemplate.php deleted file mode 100644 index 68520d6..0000000 --- a/Builder/Models/BuilderTemplate.php +++ /dev/null @@ -1,12 +0,0 @@ - 'array', 'settings' => 'array', 'is_system' => 'boolean', 'is_active' => 'boolean']; -} diff --git a/Builder/Providers/BuilderServiceProvider.php b/Builder/Providers/BuilderServiceProvider.php deleted file mode 100644 index d1d9c21..0000000 --- a/Builder/Providers/BuilderServiceProvider.php +++ /dev/null @@ -1,24 +0,0 @@ -app->singleton(BuilderRegistry::class); - $this->app->alias(BuilderRegistry::class, 'builder.registry'); - $this->app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Builder', 'Config/config.php'), 'builder'); - } - - public function boot(BuilderRegistry $registry): void - { - $this->loadMigrationsFrom(module_path('Builder', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Builder', 'Resources/views'), 'builder'); - $registry->registerDefaults(); - } -} diff --git a/Builder/Providers/RouteServiceProvider.php b/Builder/Providers/RouteServiceProvider.php deleted file mode 100644 index e63c673..0000000 --- a/Builder/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Builder', 'Routes/web.php')); - Route::middleware('api')->prefix('api/v1')->group(module_path('Builder', 'Routes/api.php')); - } -} diff --git a/Builder/Resources/views/components/hero.blade.php b/Builder/Resources/views/components/hero.blade.php deleted file mode 100644 index 34083c8..0000000 --- a/Builder/Resources/views/components/hero.blade.php +++ /dev/null @@ -1 +0,0 @@ -

{{ $properties['heading'] ?? '' }}

{{ $properties['content'] ?? '' }}

@if(!empty($properties['button_label'])){{ $properties['button_label'] }}@endif
diff --git a/Builder/Resources/views/editor.blade.php b/Builder/Resources/views/editor.blade.php deleted file mode 100644 index 82c8070..0000000 --- a/Builder/Resources/views/editor.blade.php +++ /dev/null @@ -1,10 +0,0 @@ - -
-
{{ $document->exists ? 'Edit' : 'Create' }} Builder DocumentUse the component palette and JSON document canvas. Visual drag-and-drop clients can use the component API.
-
@csrf @if($document->exists) @method('PUT') @endif -
@foreach(config('builder.allowed_types') as $type)@endforeach
- -
{{ old('document_json',json_encode($document->document ?: ['nodes'=>[]],JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)) }}{{ old('settings_json',json_encode($document->settings ?: new stdClass(),JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)) }}
-
SaveCancel
- -
diff --git a/Builder/Resources/views/index.blade.php b/Builder/Resources/views/index.blade.php deleted file mode 100644 index c146805..0000000 --- a/Builder/Resources/views/index.blade.php +++ /dev/null @@ -1,7 +0,0 @@ - -
-
Low-Code BuilderPages, forms, dashboards, reports, menus, emails, queries and workflows.
@can('builder.manage')Create@endcan
-
@foreach(config('builder.allowed_types') as $builderType)
{{ str($builderType)->headline() }}
@endforeach
-
@forelse($documents as $document)@empty@endforelse
NameTypeStatusVersion
{{ $document->name }}{{ str($document->type)->headline() }}{{ ucfirst($document->status) }}{{ $document->version }}Edit
No builder documents yet.
- {{ $documents->links() }} -
diff --git a/Builder/Routes/api.php b/Builder/Routes/api.php deleted file mode 100644 index 41b80ee..0000000 --- a/Builder/Routes/api.php +++ /dev/null @@ -1,11 +0,0 @@ -prefix('builder')->name('api.builder.')->group(function () { - Route::get('/components', [BuilderApiController::class, 'components'])->name('components'); - Route::get('/documents/{document}', [BuilderApiController::class, 'show'])->name('show'); - Route::get('/documents/{document}/preview', [BuilderApiController::class, 'preview'])->name('preview'); -}); diff --git a/Builder/Routes/web.php b/Builder/Routes/web.php deleted file mode 100644 index ed43359..0000000 --- a/Builder/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/builder')->name('administration.builder.')->group(function () { - Route::get('/', [BuilderController::class, 'index'])->name('index'); - Route::get('/create', [BuilderController::class, 'create'])->middleware('permission:builder.manage')->name('create'); - Route::post('/', [BuilderController::class, 'store'])->middleware('permission:builder.manage')->name('store'); - Route::get('/{document}/edit', [BuilderController::class, 'edit'])->middleware('permission:builder.manage')->name('edit'); - Route::put('/{document}', [BuilderController::class, 'update'])->middleware('permission:builder.manage')->name('update'); - Route::delete('/{document}', [BuilderController::class, 'destroy'])->middleware('permission:builder.manage')->name('destroy'); -}); diff --git a/Builder/Services/BuilderDocumentService.php b/Builder/Services/BuilderDocumentService.php deleted file mode 100644 index 29f2368..0000000 --- a/Builder/Services/BuilderDocumentService.php +++ /dev/null @@ -1,28 +0,0 @@ -exists && $document->isDirty()) { - $document->versions()->create(['version' => $document->version, 'document' => $document->getOriginal('document') ?: [], 'settings' => $document->getOriginal('settings') ?: [], 'created_by' => $userId]); - } - $document->fill($payload); - $document->updated_by = $userId; - if (! $document->exists) { - $document->created_by = $userId; - } else { - $document->version++; - } - $document->save(); - - return $document->refresh(); - }); - } -} diff --git a/Builder/Services/BuilderRegistry.php b/Builder/Services/BuilderRegistry.php deleted file mode 100644 index 0e63193..0000000 --- a/Builder/Services/BuilderRegistry.php +++ /dev/null @@ -1,56 +0,0 @@ -registerComponent(new $component); - } - foreach (['metric', 'chart', 'table', 'activity', 'calendar'] as $widget) { - $this->widgets[$widget] = ['type' => $widget, 'label' => str($widget)->headline()->toString()]; - } - foreach (['text', 'textarea', 'email', 'number', 'select', 'checkbox', 'date', 'file'] as $field) { - $this->fields[$field] = ['type' => $field, 'label' => str($field)->headline()->toString()]; - } - } - - public function registerComponent(BuilderComponent $component): void - { - $this->components[$component->type()] = $component; - } - - public function component(string $type): BuilderComponent - { - return $this->components[$type] ?? throw new InvalidArgumentException("Unknown builder component [$type]."); - } - - public function components(): array - { - return array_map(fn (BuilderComponent $c) => ['type' => $c->type(), 'label' => $c->label(), 'category' => $c->category(), 'schema' => $c->schema(), 'defaults' => $c->defaults()], array_values($this->components)); - } - - public function widgets(): array - { - return array_values($this->widgets); - } - - public function fields(): array - { - return array_values($this->fields); - } -} diff --git a/Builder/Services/BuilderRenderer.php b/Builder/Services/BuilderRenderer.php deleted file mode 100644 index 2b8a647..0000000 --- a/Builder/Services/BuilderRenderer.php +++ /dev/null @@ -1,19 +0,0 @@ -map(function (array $node) use ($context) { - if (($node['kind'] ?? 'component') === 'container') { - return '
'.$this->render($node, $context).'
'; - } - - return $this->registry->component((string) ($node['type'] ?? 'content'))->render($node['properties'] ?? [], $context); - })->implode("\n"); - } -} diff --git a/Builder/Tests/Feature/BuilderModuleTest.php b/Builder/Tests/Feature/BuilderModuleTest.php deleted file mode 100644 index 0844567..0000000 --- a/Builder/Tests/Feature/BuilderModuleTest.php +++ /dev/null @@ -1,16 +0,0 @@ -components(); - $this->assertNotEmpty($components); - $this->assertContains('hero', array_column($components, 'type')); - } -} diff --git a/Builder/module.json b/Builder/module.json deleted file mode 100644 index a17c5b0..0000000 --- a/Builder/module.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"Builder","alias":"builder","version":"1.0.0","description":"Low-code visual builders for pages, forms, dashboards, reports, menus, emails, queries and workflows.","keywords":["builder","low-code","visual","components","layouts"],"priority":160,"providers":["Modules\\Builder\\Providers\\BuilderServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Pages","Forms","Menus","Workflow","Analytics","Sites"]} diff --git a/CRM/Config/config.php b/CRM/Config/config.php deleted file mode 100644 index c94cac3..0000000 --- a/CRM/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'CRM']; diff --git a/CRM/Database/Migrations/2026_07_24_04000_create_crm_tables.php b/CRM/Database/Migrations/2026_07_24_04000_create_crm_tables.php deleted file mode 100644 index cf3bc64..0000000 --- a/CRM/Database/Migrations/2026_07_24_04000_create_crm_tables.php +++ /dev/null @@ -1,79 +0,0 @@ -id(); - $t->string('name'); - $t->string('website')->nullable(); - $t->string('phone')->nullable(); - $t->string('email')->nullable(); - $t->string('industry')->nullable(); - $t->text('address')->nullable(); - $t->text('notes')->nullable(); - $t->boolean('is_active')->default(true); - $t->timestamps(); - }); - Schema::create('crm_contacts', function (Blueprint $t): void { - $t->id(); - $t->foreignId('company_id')->nullable()->constrained('crm_companies')->nullOnDelete(); - $t->string('first_name'); - $t->string('last_name'); - $t->string('email')->nullable()->index(); - $t->string('phone')->nullable(); - $t->string('job_title')->nullable(); - $t->text('notes')->nullable(); - $t->boolean('is_active')->default(true); - $t->timestamps(); - }); - Schema::create('crm_leads', function (Blueprint $t): void { - $t->id(); - $t->foreignId('contact_id')->nullable()->constrained('crm_contacts')->nullOnDelete(); - $t->string('title'); - $t->string('source')->nullable(); - $t->string('status')->default('new')->index(); - $t->decimal('estimated_value', 12, 2)->nullable(); - $t->text('notes')->nullable(); - $t->timestamp('next_action_at')->nullable(); - $t->timestamps(); - }); - Schema::create('crm_opportunities', function (Blueprint $t): void { - $t->id(); - $t->foreignId('company_id')->nullable()->constrained('crm_companies')->nullOnDelete(); - $t->foreignId('contact_id')->nullable()->constrained('crm_contacts')->nullOnDelete(); - $t->string('name'); - $t->string('stage')->default('qualification')->index(); - $t->decimal('value', 12, 2)->default(0); - $t->unsignedTinyInteger('probability')->default(0); - $t->date('expected_close_at')->nullable(); - $t->text('notes')->nullable(); - $t->timestamps(); - }); - Schema::create('crm_activities', function (Blueprint $t): void { - $t->id(); - $t->nullableMorphs('subject'); - $t->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); - $t->string('type'); - $t->string('title'); - $t->text('description')->nullable(); - $t->timestamp('due_at')->nullable(); - $t->timestamp('completed_at')->nullable(); - $t->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('crm_activities'); - Schema::dropIfExists('crm_opportunities'); - Schema::dropIfExists('crm_leads'); - Schema::dropIfExists('crm_contacts'); - Schema::dropIfExists('crm_companies'); - } -}; diff --git a/CRM/Http/Controllers/CRMController.php b/CRM/Http/Controllers/CRMController.php deleted file mode 100644 index 0d85cd6..0000000 --- a/CRM/Http/Controllers/CRMController.php +++ /dev/null @@ -1,55 +0,0 @@ - Company::withCount('contacts')->latest()->paginate(20), 'contacts' => Contact::with('company')->latest()->limit(10)->get(), 'leads' => Lead::with('contact')->latest()->limit(10)->get(), 'opportunities' => Opportunity::with('company')->latest()->limit(10)->get(), 'pipelineValue' => Opportunity::whereNotIn('stage', ['won', 'lost'])->sum('value')]); - } - - public function storeCompany(Request $r): RedirectResponse - { - Company::create($r->validate(['name' => ['required', 'string', 'max:255'], 'website' => ['nullable', 'url', 'max:255'], 'phone' => ['nullable', 'string', 'max:80'], 'email' => ['nullable', 'email', 'max:255'], 'industry' => ['nullable', 'string', 'max:120'], 'address' => ['nullable', 'string'], 'notes' => ['nullable', 'string']]) + ['is_active' => $r->boolean('is_active', true)]); - - return back()->with('status', 'Company created.'); - } - - public function storeContact(Request $r): RedirectResponse - { - Contact::create($r->validate(['company_id' => ['nullable', 'exists:crm_companies,id'], 'first_name' => ['required', 'string', 'max:120'], 'last_name' => ['required', 'string', 'max:120'], 'email' => ['nullable', 'email', 'max:255'], 'phone' => ['nullable', 'string', 'max:80'], 'job_title' => ['nullable', 'string', 'max:120'], 'notes' => ['nullable', 'string']]) + ['is_active' => $r->boolean('is_active', true)]); - - return back()->with('status', 'Contact created.'); - } - - public function storeLead(Request $r): RedirectResponse - { - Lead::create($r->validate(['contact_id' => ['nullable', 'exists:crm_contacts,id'], 'title' => ['required', 'string', 'max:255'], 'source' => ['nullable', 'string', 'max:120'], 'status' => ['required', 'in:new,qualified,proposal,won,lost'], 'estimated_value' => ['nullable', 'numeric', 'min:0'], 'notes' => ['nullable', 'string'], 'next_action_at' => ['nullable', 'date']])); - - return back()->with('status', 'Lead created.'); - } - - public function storeOpportunity(Request $r): RedirectResponse - { - Opportunity::create($r->validate(['company_id' => ['nullable', 'exists:crm_companies,id'], 'contact_id' => ['nullable', 'exists:crm_contacts,id'], 'name' => ['required', 'string', 'max:255'], 'stage' => ['required', 'in:qualification,proposal,negotiation,won,lost'], 'value' => ['required', 'numeric', 'min:0'], 'probability' => ['required', 'integer', 'between:0,100'], 'expected_close_at' => ['nullable', 'date'], 'notes' => ['nullable', 'string']])); - - return back()->with('status', 'Opportunity created.'); - } - - public function destroyCompany(Company $company): RedirectResponse - { - $company->delete(); - - return back()->with('status', 'Company deleted.'); - } -} diff --git a/CRM/Models/Activity.php b/CRM/Models/Activity.php deleted file mode 100644 index 1a2e8b6..0000000 --- a/CRM/Models/Activity.php +++ /dev/null @@ -1,23 +0,0 @@ - 'datetime', 'completed_at' => 'datetime']; - } - - public function subject(): MorphTo - { - return $this->morphTo(); - } -} diff --git a/CRM/Models/Company.php b/CRM/Models/Company.php deleted file mode 100644 index 76bcf10..0000000 --- a/CRM/Models/Company.php +++ /dev/null @@ -1,23 +0,0 @@ - 'boolean']; - } - - public function contacts(): HasMany - { - return $this->hasMany(Contact::class); - } -} diff --git a/CRM/Models/Contact.php b/CRM/Models/Contact.php deleted file mode 100644 index e1f08cb..0000000 --- a/CRM/Models/Contact.php +++ /dev/null @@ -1,28 +0,0 @@ - 'boolean']; - } - - public function company(): BelongsTo - { - return $this->belongsTo(Company::class); - } - - public function getNameAttribute(): string - { - return trim($this->first_name.' '.$this->last_name); - } -} diff --git a/CRM/Models/Lead.php b/CRM/Models/Lead.php deleted file mode 100644 index 571b5dd..0000000 --- a/CRM/Models/Lead.php +++ /dev/null @@ -1,23 +0,0 @@ - 'decimal:2', 'next_action_at' => 'datetime']; - } - - public function contact(): BelongsTo - { - return $this->belongsTo(Contact::class); - } -} diff --git a/CRM/Models/Opportunity.php b/CRM/Models/Opportunity.php deleted file mode 100644 index 6cd7884..0000000 --- a/CRM/Models/Opportunity.php +++ /dev/null @@ -1,28 +0,0 @@ - 'decimal:2', 'expected_close_at' => 'date']; - } - - public function company(): BelongsTo - { - return $this->belongsTo(Company::class); - } - - public function contact(): BelongsTo - { - return $this->belongsTo(Contact::class); - } -} diff --git a/CRM/Providers/CRMServiceProvider.php b/CRM/Providers/CRMServiceProvider.php deleted file mode 100644 index ba46a79..0000000 --- a/CRM/Providers/CRMServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('CRM', 'Config/config.php'), 'crm'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('CRM', 'Database/Migrations')); - $this->loadViewsFrom(module_path('CRM', 'Resources/views'), 'crm'); - } -} diff --git a/CRM/Providers/RouteServiceProvider.php b/CRM/Providers/RouteServiceProvider.php deleted file mode 100644 index 94e56b2..0000000 --- a/CRM/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('CRM', 'Routes/web.php')); - } -} diff --git a/CRM/Resources/views/admin/index.blade.php b/CRM/Resources/views/admin/index.blade.php deleted file mode 100644 index 4a4868f..0000000 --- a/CRM/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

CRM

Companies, contacts, leads and sales opportunities.

@if(session('status'))
{{ session('status') }}
@endif
Companies
{{ $companies->total() }}
Contacts
{{ \Modules\CRM\Models\Contact::count() }}
Open leads
{{ \Modules\CRM\Models\Lead::whereNotIn('status',['won','lost'])->count() }}
Pipeline
£{{ number_format($pipelineValue,2) }}
@csrf

Add company

Create company
@csrf

Add contact

@foreach($companies as $company)@endforeach
Create contact
@forelse($companies as $company)@empty@endforelse
CompanyIndustryContacts
{{ $company->name }}{{ $company->industry ?: '—' }}{{ $company->contacts_count }}
@csrf @method('DELETE')Delete
No companies yet.
{{ $companies->links() }}
diff --git a/CRM/Routes/web.php b/CRM/Routes/web.php deleted file mode 100644 index 5333122..0000000 --- a/CRM/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/crm')->name('administration.crm.')->group(function (): void { - Route::get('/', [CRMController::class, 'index'])->name('index'); - Route::post('/companies', [CRMController::class, 'storeCompany'])->name('companies.store'); - Route::delete('/companies/{company}', [CRMController::class, 'destroyCompany'])->name('companies.destroy'); - Route::post('/contacts', [CRMController::class, 'storeContact'])->name('contacts.store'); - Route::post('/leads', [CRMController::class, 'storeLead'])->name('leads.store'); - Route::post('/opportunities', [CRMController::class, 'storeOpportunity'])->name('opportunities.store'); -}); diff --git a/CRM/Tests/Feature/ModuleBootTest.php b/CRM/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 3bbcf6f..0000000 --- a/CRM/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('CRM', config('crm.name')); - } -} diff --git a/CRM/module.json b/CRM/module.json deleted file mode 100644 index 8d2e7b2..0000000 --- a/CRM/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "CRM", - "alias": "crm", - "version": "1.0.0", - "description": "Customer relationship management for companies, contacts, leads, opportunities and activities.", - "keywords": ["business", "crm"], - "priority": 40, - "providers": ["Modules\\CRM\\Providers\\CRMServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Careers/Config/config.php b/Careers/Config/config.php deleted file mode 100644 index 46a8b16..0000000 --- a/Careers/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Careers']; diff --git a/Careers/Database/Migrations/2026_07_24_04300_create_careers_tables.php b/Careers/Database/Migrations/2026_07_24_04300_create_careers_tables.php deleted file mode 100644 index c8627af..0000000 --- a/Careers/Database/Migrations/2026_07_24_04300_create_careers_tables.php +++ /dev/null @@ -1,43 +0,0 @@ -id(); - $t->string('title'); - $t->string('slug')->unique(); - $t->string('department')->nullable(); - $t->string('location')->nullable(); - $t->string('employment_type')->default('full-time'); - $t->longText('description')->nullable(); - $t->string('status')->default('draft')->index(); - $t->timestamp('published_at')->nullable(); - $t->timestamp('closes_at')->nullable(); - $t->timestamps(); - }); - Schema::create('career_applications', function (Blueprint $t): void { - $t->id(); - $t->foreignId('job_id')->constrained('career_jobs')->cascadeOnDelete(); - $t->string('name'); - $t->string('email')->index(); - $t->string('phone')->nullable(); - $t->string('cv_path')->nullable(); - $t->text('cover_letter')->nullable(); - $t->string('status')->default('new')->index(); - $t->text('notes')->nullable(); - $t->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('career_applications'); - Schema::dropIfExists('career_jobs'); - } -}; diff --git a/Careers/Http/Controllers/CareerController.php b/Careers/Http/Controllers/CareerController.php deleted file mode 100644 index d9c2bad..0000000 --- a/Careers/Http/Controllers/CareerController.php +++ /dev/null @@ -1,32 +0,0 @@ - Job::withCount('applications')->latest()->paginate(20), 'applications' => Application::latest()->limit(10)->get()]); - } - - public function store(Request $r): RedirectResponse - { - Job::create($r->validate(['title' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255', 'unique:career_jobs,slug'], 'department' => ['nullable', 'string', 'max:120'], 'location' => ['nullable', 'string', 'max:120'], 'employment_type' => ['required', 'in:full-time,part-time,contract,temporary,internship'], 'description' => ['nullable', 'string'], 'status' => ['required', 'in:draft,published,closed'], 'published_at' => ['nullable', 'date'], 'closes_at' => ['nullable', 'date', 'after:published_at']])); - - return back()->with('status', 'Job created.'); - } - - public function destroy(Job $job): RedirectResponse - { - $job->delete(); - - return back()->with('status', 'Job deleted.'); - } -} diff --git a/Careers/Models/Application.php b/Careers/Models/Application.php deleted file mode 100644 index e85d73e..0000000 --- a/Careers/Models/Application.php +++ /dev/null @@ -1,12 +0,0 @@ - 'datetime', 'closes_at' => 'datetime']; - } - - public function applications(): HasMany - { - return $this->hasMany(Application::class); - } -} diff --git a/Careers/Providers/CareersServiceProvider.php b/Careers/Providers/CareersServiceProvider.php deleted file mode 100644 index 5f4cfff..0000000 --- a/Careers/Providers/CareersServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Careers', 'Config/config.php'), 'careers'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Careers', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Careers', 'Resources/views'), 'careers'); - } -} diff --git a/Careers/Providers/RouteServiceProvider.php b/Careers/Providers/RouteServiceProvider.php deleted file mode 100644 index 4e15858..0000000 --- a/Careers/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Careers', 'Routes/web.php')); - } -} diff --git a/Careers/Resources/views/admin/index.blade.php b/Careers/Resources/views/admin/index.blade.php deleted file mode 100644 index bac71a2..0000000 --- a/Careers/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Careers

Manage vacancies and candidate applications.

@if(session('status'))
{{ session('status') }}
@endif
@csrf

Create vacancy

Create vacancy
@forelse($jobs as $job)@empty@endforelse
VacancyDepartmentStatusApplications
{{ $job->title }}{{ $job->department ?: '—' }}{{ ucfirst($job->status) }}{{ $job->applications_count }}
@csrf @method('DELETE')Delete
No vacancies yet.
{{ $jobs->links() }}
diff --git a/Careers/Routes/web.php b/Careers/Routes/web.php deleted file mode 100644 index ed2f45f..0000000 --- a/Careers/Routes/web.php +++ /dev/null @@ -1,10 +0,0 @@ -prefix('administration/careers')->name('administration.careers.')->group(function (): void { - Route::get('/', [CareerController::class, 'index'])->name('index'); - Route::post('/', [CareerController::class, 'store'])->name('store'); - Route::delete('/{job}', [CareerController::class, 'destroy'])->name('destroy'); -}); diff --git a/Careers/Tests/Feature/ModuleBootTest.php b/Careers/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 28b7a14..0000000 --- a/Careers/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('Careers', config('careers.name')); - } -} diff --git a/Careers/module.json b/Careers/module.json deleted file mode 100644 index cda83cf..0000000 --- a/Careers/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Careers", - "alias": "careers", - "version": "1.0.0", - "description": "Job vacancies and candidate application workflow.", - "keywords": ["business", "careers"], - "priority": 43, - "providers": ["Modules\\Careers\\Providers\\CareersServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Categories/Config/config.php b/Categories/Config/config.php deleted file mode 100644 index f310716..0000000 --- a/Categories/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Categories']; diff --git a/Categories/Database/Migrations/2026_07_24_030100_create_categories_table.php b/Categories/Database/Migrations/2026_07_24_030100_create_categories_table.php deleted file mode 100644 index 9796bc5..0000000 --- a/Categories/Database/Migrations/2026_07_24_030100_create_categories_table.php +++ /dev/null @@ -1,28 +0,0 @@ -id(); - $table->foreignId('parent_id')->nullable()->constrained('categories')->nullOnDelete(); - $table->string('name'); - $table->string('slug')->unique(); - $table->text('description')->nullable(); - $table->string('type')->default('content')->index(); - $table->unsignedInteger('sort_order')->default(0); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('categories'); - } -}; diff --git a/Categories/Http/Controllers/CategoryController.php b/Categories/Http/Controllers/CategoryController.php deleted file mode 100644 index feca75e..0000000 --- a/Categories/Http/Controllers/CategoryController.php +++ /dev/null @@ -1,56 +0,0 @@ - Category::with('parent')->orderBy('type')->orderBy('sort_order')->paginate(30)]); - } - - public function create(): View - { - return view('categories::admin.form', ['parents' => Category::orderBy('name')->get()]); - } - - public function store(Request $request): RedirectResponse - { - Category::create($this->validated($request)); - - return redirect()->route('administration.categories.index')->with('status', 'Category created.'); - } - - public function edit(Category $category): View - { - return view('categories::admin.form', ['record' => $category, 'parents' => Category::whereKeyNot($category->getKey())->orderBy('name')->get()]); - } - - public function update(Request $request, Category $category): RedirectResponse - { - $category->update($this->validated($request)); - - return redirect()->route('administration.categories.index')->with('status', 'Category updated.'); - } - - public function destroy(Category $category): RedirectResponse - { - $category->delete(); - - return back()->with('status', 'Category deleted.'); - } - - private function validated(Request $request): array - { - $data = $request->validate(['parent_id' => ['nullable', 'exists:categories,id'], 'name' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255'], 'description' => ['nullable', 'string'], 'type' => ['required', 'string', 'max:50'], 'sort_order' => ['nullable', 'integer', 'min:0'], 'is_active' => ['sometimes', 'boolean']]); - $data['is_active'] = $request->boolean('is_active'); - - return $data; - } -} diff --git a/Categories/Models/Category.php b/Categories/Models/Category.php deleted file mode 100644 index 8835411..0000000 --- a/Categories/Models/Category.php +++ /dev/null @@ -1,27 +0,0 @@ - 'boolean', 'sort_order' => 'integer']; - } - - public function parent(): BelongsTo - { - return $this->belongsTo(self::class, 'parent_id'); - } - - public function children(): HasMany - { - return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order'); - } -} diff --git a/Categories/Providers/CategoriesServiceProvider.php b/Categories/Providers/CategoriesServiceProvider.php deleted file mode 100644 index 8a20b37..0000000 --- a/Categories/Providers/CategoriesServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Categories', 'Config/config.php'), 'categories'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Categories', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Categories', 'Resources/views'), 'categories'); - } -} diff --git a/Categories/Providers/RouteServiceProvider.php b/Categories/Providers/RouteServiceProvider.php deleted file mode 100644 index 1423c92..0000000 --- a/Categories/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Categories', 'Routes/web.php')); - } -} diff --git a/Categories/Resources/views/admin/form.blade.php b/Categories/Resources/views/admin/form.blade.php deleted file mode 100644 index 3ec530b..0000000 --- a/Categories/Resources/views/admin/form.blade.php +++ /dev/null @@ -1 +0,0 @@ -
@csrf @isset($record) @method('PUT') @endisset @foreach($parents as $parent)@endforeach{{ old('description',$record->description ?? '') }}Save
diff --git a/Categories/Resources/views/admin/index.blade.php b/Categories/Resources/views/admin/index.blade.php deleted file mode 100644 index 8b15801..0000000 --- a/Categories/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Categories

Manage publishing categories.

Create
@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $record)@empty@endforelse
Name / titleStatusActions
{{ $record->title ?? $record->name }}{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}Edit
@csrf @method('DELETE')Delete
No records yet.
{{ $records->links() }}
diff --git a/Categories/Routes/web.php b/Categories/Routes/web.php deleted file mode 100644 index 6ec5ed1..0000000 --- a/Categories/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/categories')->name('administration.categories.')->group(function (): void { - Route::get('/', [CategoryController::class, 'index'])->name('index'); - Route::get('/create', [CategoryController::class, 'create'])->name('create'); - Route::post('/', [CategoryController::class, 'store'])->name('store'); - Route::get('/{category}/edit', [CategoryController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{category}', [CategoryController::class, 'update'])->name('update'); - Route::delete('/{category}', [CategoryController::class, 'destroy'])->name('destroy'); -}); diff --git a/Categories/Tests/Feature/ModuleBootTest.php b/Categories/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 479e0ae..0000000 --- a/Categories/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('Categories', config('categories.name')); - } -} diff --git a/Categories/module.json b/Categories/module.json deleted file mode 100644 index bde3d86..0000000 --- a/Categories/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Categories", - "alias": "categories", - "version": "1.0.0", - "description": "Categories publishing module.", - "keywords": ["publishing", "categories"], - "priority": 30, - "providers": ["Modules\\Categories\\Providers\\CategoriesServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/CloudDevOps/Config/config.php b/CloudDevOps/Config/config.php deleted file mode 100644 index 8278777..0000000 --- a/CloudDevOps/Config/config.php +++ /dev/null @@ -1,9 +0,0 @@ - env('CLOUD_DEVOPS_ENABLED', true), - 'allow_execution' => env('CLOUD_DEVOPS_ALLOW_EXECUTION', false), - 'default_timeout' => (int) env('CLOUD_DEVOPS_TIMEOUT', 900), - 'retention_days' => (int) env('CLOUD_DEVOPS_RETENTION_DAYS', 90), - 'allowed_drivers' => ['local', 'ssh', 'docker', 'kubernetes'], -]; diff --git a/CloudDevOps/Console/Commands/CloudDevOpsHealthCommand.php b/CloudDevOps/Console/Commands/CloudDevOpsHealthCommand.php deleted file mode 100644 index 565e25b..0000000 --- a/CloudDevOps/Console/Commands/CloudDevOpsHealthCommand.php +++ /dev/null @@ -1,22 +0,0 @@ -each(fn ($check) => $runner->run($check)); - $this->info('Health checks completed.'); - - return self::SUCCESS; - } -} diff --git a/CloudDevOps/Console/Commands/CloudDevOpsPruneCommand.php b/CloudDevOps/Console/Commands/CloudDevOpsPruneCommand.php deleted file mode 100644 index 4309b43..0000000 --- a/CloudDevOps/Console/Commands/CloudDevOpsPruneCommand.php +++ /dev/null @@ -1,22 +0,0 @@ -option('days') ?: config('clouddevops.retention_days', 90)); - $count = DeploymentRelease::where('created_at', '<', now()->subDays($days))->whereIn('status', ['succeeded', 'failed', 'cancelled'])->delete(); - $this->info("Pruned {$count} releases."); - - return self::SUCCESS; - } -} diff --git a/CloudDevOps/Database/Migrations/2026_07_24_180000_create_cloud_devops_tables.php b/CloudDevOps/Database/Migrations/2026_07_24_180000_create_cloud_devops_tables.php deleted file mode 100644 index 9b9d98e..0000000 --- a/CloudDevOps/Database/Migrations/2026_07_24_180000_create_cloud_devops_tables.php +++ /dev/null @@ -1,122 +0,0 @@ -id(); - $table->string('name'); - $table->string('slug')->unique(); - $table->string('type')->default('production'); - $table->string('status')->default('active'); - $table->boolean('is_protected')->default(false); - $table->json('variables')->nullable(); - $table->timestamps(); - }); - - Schema::create('deployment_targets', function (Blueprint $table): void { - $table->id(); - $table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete(); - $table->string('name'); - $table->string('driver')->default('ssh'); - $table->string('host')->nullable(); - $table->unsignedInteger('port')->nullable(); - $table->string('path')->nullable(); - $table->json('configuration')->nullable(); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - - Schema::create('deployment_pipelines', function (Blueprint $table): void { - $table->id(); - $table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete(); - $table->string('name'); - $table->string('trigger')->default('manual'); - $table->json('steps'); - $table->boolean('requires_approval')->default(false); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - - Schema::create('deployment_releases', function (Blueprint $table): void { - $table->id(); - $table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete(); - $table->foreignId('pipeline_id')->nullable()->constrained('deployment_pipelines')->nullOnDelete(); - $table->string('version'); - $table->string('commit_hash')->nullable(); - $table->string('status')->default('pending'); - $table->foreignId('requested_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamp('started_at')->nullable(); - $table->timestamp('completed_at')->nullable(); - $table->text('failure_message')->nullable(); - $table->json('metadata')->nullable(); - $table->timestamps(); - $table->index(['environment_id', 'status']); - }); - - Schema::create('deployment_steps', function (Blueprint $table): void { - $table->id(); - $table->foreignId('release_id')->constrained('deployment_releases')->cascadeOnDelete(); - $table->string('name'); - $table->unsignedInteger('position')->default(0); - $table->string('status')->default('pending'); - $table->text('output')->nullable(); - $table->timestamp('started_at')->nullable(); - $table->timestamp('completed_at')->nullable(); - $table->timestamps(); - }); - - Schema::create('deployment_health_checks', function (Blueprint $table): void { - $table->id(); - $table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete(); - $table->string('name'); - $table->string('type')->default('http'); - $table->string('endpoint')->nullable(); - $table->unsignedInteger('expected_status')->default(200); - $table->string('status')->default('unknown'); - $table->unsignedInteger('response_time_ms')->nullable(); - $table->timestamp('checked_at')->nullable(); - $table->text('message')->nullable(); - $table->timestamps(); - }); - - Schema::create('deployment_rollbacks', function (Blueprint $table): void { - $table->id(); - $table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete(); - $table->foreignId('from_release_id')->constrained('deployment_releases')->cascadeOnDelete(); - $table->foreignId('to_release_id')->nullable()->constrained('deployment_releases')->nullOnDelete(); - $table->string('status')->default('pending'); - $table->foreignId('requested_by')->nullable()->constrained('users')->nullOnDelete(); - $table->text('reason')->nullable(); - $table->timestamps(); - }); - - Schema::create('deployment_secrets', function (Blueprint $table): void { - $table->id(); - $table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete(); - $table->string('key'); - $table->string('provider')->default('database'); - $table->string('reference')->nullable(); - $table->timestamp('rotated_at')->nullable(); - $table->timestamps(); - $table->unique(['environment_id', 'key']); - }); - } - - public function down(): void - { - Schema::dropIfExists('deployment_secrets'); - Schema::dropIfExists('deployment_rollbacks'); - Schema::dropIfExists('deployment_health_checks'); - Schema::dropIfExists('deployment_steps'); - Schema::dropIfExists('deployment_releases'); - Schema::dropIfExists('deployment_pipelines'); - Schema::dropIfExists('deployment_targets'); - Schema::dropIfExists('deployment_environments'); - } -}; diff --git a/CloudDevOps/Http/Controllers/CloudDevOpsApiController.php b/CloudDevOps/Http/Controllers/CloudDevOpsApiController.php deleted file mode 100644 index 54897f1..0000000 --- a/CloudDevOps/Http/Controllers/CloudDevOpsApiController.php +++ /dev/null @@ -1,21 +0,0 @@ -json(['data' => DeploymentEnvironment::withCount(['targets', 'pipelines', 'releases'])->get()]); - } - - public function releases(): JsonResponse - { - return response()->json(['data' => DeploymentRelease::with(['environment', 'steps'])->latest()->paginate()]); - } -} diff --git a/CloudDevOps/Http/Controllers/CloudDevOpsController.php b/CloudDevOps/Http/Controllers/CloudDevOpsController.php deleted file mode 100644 index dcc212b..0000000 --- a/CloudDevOps/Http/Controllers/CloudDevOpsController.php +++ /dev/null @@ -1,50 +0,0 @@ - DeploymentEnvironment::withCount(['targets', 'pipelines', 'releases'])->latest()->get(), - 'releases' => DeploymentRelease::with('environment')->latest()->limit(10)->get(), - 'checks' => DeploymentHealthCheck::with('environment')->latest('checked_at')->limit(10)->get(), - ]); - } - - public function storeEnvironment(Request $request): RedirectResponse - { - $data = $request->validate(['name' => 'required|string|max:120', 'slug' => 'required|alpha_dash|max:120|unique:deployment_environments,slug', 'type' => 'required|in:development,staging,production']); - DeploymentEnvironment::create($data); - - return back()->with('status', 'Environment created.'); - } - - public function deploy(Request $request, DeploymentEnvironment $environment, DeploymentManager $manager): RedirectResponse - { - $data = $request->validate(['pipeline_id' => 'required|integer', 'version' => 'nullable|string|max:120', 'commit_hash' => 'nullable|string|max:120']); - $pipeline = DeploymentPipeline::where('environment_id', $environment->id)->findOrFail($data['pipeline_id']); - $manager->createRelease($environment, $pipeline, $data + ['requested_by' => $request->user()?->id]); - - return back()->with('status', 'Release created.'); - } - - public function runCheck(DeploymentHealthCheck $check, HealthCheckRunner $runner): RedirectResponse - { - $runner->run($check); - - return back()->with('status', 'Health check completed.'); - } -} diff --git a/CloudDevOps/Models/DeploymentEnvironment.php b/CloudDevOps/Models/DeploymentEnvironment.php deleted file mode 100644 index 55bc24b..0000000 --- a/CloudDevOps/Models/DeploymentEnvironment.php +++ /dev/null @@ -1,36 +0,0 @@ - 'encrypted:array', 'is_protected' => 'boolean']; - } - - public function targets(): HasMany - { - return $this->hasMany(DeploymentTarget::class, 'environment_id'); - } - - public function pipelines(): HasMany - { - return $this->hasMany(DeploymentPipeline::class, 'environment_id'); - } - - public function releases(): HasMany - { - return $this->hasMany(DeploymentRelease::class, 'environment_id'); - } - - public function healthChecks(): HasMany - { - return $this->hasMany(DeploymentHealthCheck::class, 'environment_id'); - } -} diff --git a/CloudDevOps/Models/DeploymentHealthCheck.php b/CloudDevOps/Models/DeploymentHealthCheck.php deleted file mode 100644 index 2ff53fb..0000000 --- a/CloudDevOps/Models/DeploymentHealthCheck.php +++ /dev/null @@ -1,15 +0,0 @@ - 'datetime']; - } -} diff --git a/CloudDevOps/Models/DeploymentPipeline.php b/CloudDevOps/Models/DeploymentPipeline.php deleted file mode 100644 index 1aa3ad2..0000000 --- a/CloudDevOps/Models/DeploymentPipeline.php +++ /dev/null @@ -1,15 +0,0 @@ - 'array', 'requires_approval' => 'boolean', 'is_active' => 'boolean']; - } -} diff --git a/CloudDevOps/Models/DeploymentRelease.php b/CloudDevOps/Models/DeploymentRelease.php deleted file mode 100644 index 5eca011..0000000 --- a/CloudDevOps/Models/DeploymentRelease.php +++ /dev/null @@ -1,27 +0,0 @@ - 'array', 'started_at' => 'datetime', 'completed_at' => 'datetime']; - } - - public function environment(): BelongsTo - { - return $this->belongsTo(DeploymentEnvironment::class, 'environment_id'); - } - - public function steps(): HasMany - { - return $this->hasMany(DeploymentStep::class, 'release_id')->orderBy('position'); - } -} diff --git a/CloudDevOps/Models/DeploymentRollback.php b/CloudDevOps/Models/DeploymentRollback.php deleted file mode 100644 index 8c4cb66..0000000 --- a/CloudDevOps/Models/DeploymentRollback.php +++ /dev/null @@ -1,10 +0,0 @@ - 'datetime']; - } -} diff --git a/CloudDevOps/Models/DeploymentStep.php b/CloudDevOps/Models/DeploymentStep.php deleted file mode 100644 index fc2e05b..0000000 --- a/CloudDevOps/Models/DeploymentStep.php +++ /dev/null @@ -1,15 +0,0 @@ - 'datetime', 'completed_at' => 'datetime']; - } -} diff --git a/CloudDevOps/Models/DeploymentTarget.php b/CloudDevOps/Models/DeploymentTarget.php deleted file mode 100644 index d5de432..0000000 --- a/CloudDevOps/Models/DeploymentTarget.php +++ /dev/null @@ -1,15 +0,0 @@ - 'encrypted:array', 'is_active' => 'boolean']; - } -} diff --git a/CloudDevOps/Providers/CloudDevOpsServiceProvider.php b/CloudDevOps/Providers/CloudDevOpsServiceProvider.php deleted file mode 100644 index 763eab7..0000000 --- a/CloudDevOps/Providers/CloudDevOpsServiceProvider.php +++ /dev/null @@ -1,25 +0,0 @@ -mergeConfigFrom(module_path('CloudDevOps', 'Config/config.php'), 'clouddevops'); - $this->app->register(RouteServiceProvider::class); - } - - public function boot(): void - { - $this->loadViewsFrom(module_path('CloudDevOps', 'Resources/views'), 'clouddevops'); - $this->loadMigrationsFrom(module_path('CloudDevOps', 'Database/Migrations')); - if ($this->app->runningInConsole()) { - $this->commands([CloudDevOpsHealthCommand::class, CloudDevOpsPruneCommand::class]); - } - } -} diff --git a/CloudDevOps/Providers/RouteServiceProvider.php b/CloudDevOps/Providers/RouteServiceProvider.php deleted file mode 100644 index 1b62500..0000000 --- a/CloudDevOps/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('CloudDevOps', 'Routes/web.php')); - Route::middleware('api')->prefix('api')->group(module_path('CloudDevOps', 'Routes/api.php')); - } -} diff --git a/CloudDevOps/Resources/views/index.blade.php b/CloudDevOps/Resources/views/index.blade.php deleted file mode 100644 index cefb0ce..0000000 --- a/CloudDevOps/Resources/views/index.blade.php +++ /dev/null @@ -1,44 +0,0 @@ - -
-
- Cloud & DevOps - Manage environments, deployment releases, pipelines and service health. -
- - @if (session('status')) - {{ session('status') }} - @endif - -
-

Environments

{{ $environments->count() }}

-

Recent releases

{{ $releases->count() }}

-

Healthy checks

{{ $checks->where('status', 'healthy')->count() }}

-
- - @can('cloud-devops.manage') -
- @csrf - - - -
Create environment
- - @endcan - -
- Environments -
- @forelse ($environments as $environment) -
-

{{ $environment->name }}

{{ ucfirst($environment->type) }} · {{ ucfirst($environment->status) }}

@if ($environment->is_protected)Protected@endif
-
Targets

{{ $environment->targets_count }}

Pipelines

{{ $environment->pipelines_count }}

Releases

{{ $environment->releases_count }}

-
- @empty -
No deployment environments configured.
- @endforelse -
-
- -
Recent releases
@forelse ($releases as $release)@empty@endforelse
VersionEnvironmentStatusCreated
{{ $release->version }}{{ $release->environment?->name }}{{ ucfirst(str_replace('_', ' ', $release->status)) }}{{ $release->created_at?->diffForHumans() }}
No releases yet.
-
-
diff --git a/CloudDevOps/Routes/api.php b/CloudDevOps/Routes/api.php deleted file mode 100644 index 89d17bf..0000000 --- a/CloudDevOps/Routes/api.php +++ /dev/null @@ -1,9 +0,0 @@ -prefix('v1/cloud-devops')->group(function (): void { - Route::get('/environments', [CloudDevOpsApiController::class, 'environments']); - Route::get('/releases', [CloudDevOpsApiController::class, 'releases']); -}); diff --git a/CloudDevOps/Routes/web.php b/CloudDevOps/Routes/web.php deleted file mode 100644 index fb5ebf6..0000000 --- a/CloudDevOps/Routes/web.php +++ /dev/null @@ -1,14 +0,0 @@ -prefix('administration/cloud-devops') - ->name('administration.cloud-devops.') - ->group(function (): void { - Route::get('/', [CloudDevOpsController::class, 'index'])->name('index'); - Route::post('/environments', [CloudDevOpsController::class, 'storeEnvironment'])->middleware('permission:cloud-devops.manage')->name('environments.store'); - Route::post('/environments/{environment}/deploy', [CloudDevOpsController::class, 'deploy'])->middleware('permission:cloud-devops.deploy')->name('deploy'); - Route::post('/health-checks/{check}/run', [CloudDevOpsController::class, 'runCheck'])->middleware('permission:cloud-devops.manage')->name('checks.run'); - }); diff --git a/CloudDevOps/Services/DeploymentManager.php b/CloudDevOps/Services/DeploymentManager.php deleted file mode 100644 index 1b9c665..0000000 --- a/CloudDevOps/Services/DeploymentManager.php +++ /dev/null @@ -1,40 +0,0 @@ -releases()->create([ - 'pipeline_id' => $pipeline->getKey(), - 'version' => $attributes['version'] ?? now()->format('YmdHis').'-'.Str::lower(Str::random(6)), - 'commit_hash' => $attributes['commit_hash'] ?? null, - 'status' => $pipeline->requires_approval ? 'awaiting_approval' : 'pending', - 'requested_by' => $attributes['requested_by'] ?? null, - 'metadata' => $attributes['metadata'] ?? [], - ]); - - foreach ($pipeline->steps as $position => $step) { - $release->steps()->create(['name' => $step['name'] ?? 'Step '.($position + 1), 'position' => $position]); - } - - return $release->load('steps'); - }); - } - - public function execute(DeploymentRelease $release): DeploymentRelease - { - abort_unless(config('clouddevops.allow_execution'), 403, 'Deployment execution is disabled.'); - $release->update(['status' => 'running', 'started_at' => now()]); - - return $release->refresh(); - } -} diff --git a/CloudDevOps/Services/HealthCheckRunner.php b/CloudDevOps/Services/HealthCheckRunner.php deleted file mode 100644 index 3c9f32a..0000000 --- a/CloudDevOps/Services/HealthCheckRunner.php +++ /dev/null @@ -1,24 +0,0 @@ -get((string) $check->endpoint); - $status = $response->status() === $check->expected_status ? 'healthy' : 'unhealthy'; - $message = 'HTTP '.$response->status(); - } catch (\Throwable $e) { - $status = 'unhealthy'; - $message = $e->getMessage(); - } $check->update(['status' => $status, 'response_time_ms' => (int) ((microtime(true) - $started) * 1000), 'checked_at' => now(), 'message' => $message]); - - return $check->refresh(); - } -} diff --git a/CloudDevOps/Tests/Feature/CloudDevOpsModuleTest.php b/CloudDevOps/Tests/Feature/CloudDevOpsModuleTest.php deleted file mode 100644 index 5425d71..0000000 --- a/CloudDevOps/Tests/Feature/CloudDevOpsModuleTest.php +++ /dev/null @@ -1,18 +0,0 @@ - 'Production', 'slug' => 'production', 'type' => 'production']); - $this->assertDatabaseHas('deployment_environments', ['id' => $environment->id, 'slug' => 'production']); - } -} diff --git a/CloudDevOps/module.json b/CloudDevOps/module.json deleted file mode 100644 index 5a14f70..0000000 --- a/CloudDevOps/module.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "CloudDevOps", - "alias": "clouddevops", - "description": "Cloud deployment and DevOps operations for Content Manager.", - "keywords": ["cloud", "devops", "deployments", "pipelines"], - "priority": 18, - "providers": ["Modules\\CloudDevOps\\Providers\\CloudDevOpsServiceProvider"], - "files": [] -} diff --git a/Comments/Config/config.php b/Comments/Config/config.php deleted file mode 100644 index e205f05..0000000 --- a/Comments/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Comments']; diff --git a/Comments/Database/Migrations/2026_07_24_030600_create_comments_table.php b/Comments/Database/Migrations/2026_07_24_030600_create_comments_table.php deleted file mode 100644 index 89de713..0000000 --- a/Comments/Database/Migrations/2026_07_24_030600_create_comments_table.php +++ /dev/null @@ -1,31 +0,0 @@ -id(); - $table->nullableMorphs('commentable'); - $table->foreignId('parent_id')->nullable()->constrained('comments')->cascadeOnDelete(); - $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); - $table->string('author_name'); - $table->string('author_email'); - $table->text('body'); - $table->string('status')->default('pending')->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('moderation_notes')->nullable(); - $table->timestamp('approved_at')->nullable(); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('comments'); - } -}; diff --git a/Comments/Http/Controllers/CommentController.php b/Comments/Http/Controllers/CommentController.php deleted file mode 100644 index c8948af..0000000 --- a/Comments/Http/Controllers/CommentController.php +++ /dev/null @@ -1,38 +0,0 @@ -filled('status')) { - $q->where('status', $r->string('status')); - } - -return view('comments::admin.index', ['records' => $q->paginate(30)]); - } - - public function update(Request $r, Comment $comment): RedirectResponse - { - $data = $r->validate(['status' => ['required', 'in:pending,approved,spam,rejected'], 'moderation_notes' => ['nullable', 'string']]); - $data['approved_at'] = $data['status'] === 'approved' ? now() : null; - $comment->update($data); - - return back()->with('status', 'Comment moderated.'); - } - - public function destroy(Comment $comment): RedirectResponse - { - $comment->delete(); - - return back()->with('status', 'Comment deleted.'); - } -} diff --git a/Comments/Models/Comment.php b/Comments/Models/Comment.php deleted file mode 100644 index 0da0238..0000000 --- a/Comments/Models/Comment.php +++ /dev/null @@ -1,21 +0,0 @@ - 'datetime']; - } - - public function commentable(): MorphTo - { - return $this->morphTo(); - } -} diff --git a/Comments/Providers/CommentsServiceProvider.php b/Comments/Providers/CommentsServiceProvider.php deleted file mode 100644 index d82e931..0000000 --- a/Comments/Providers/CommentsServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Comments', 'Config/config.php'), 'comments'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Comments', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Comments', 'Resources/views'), 'comments'); - } -} diff --git a/Comments/Providers/RouteServiceProvider.php b/Comments/Providers/RouteServiceProvider.php deleted file mode 100644 index b83240b..0000000 --- a/Comments/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Comments', 'Routes/web.php')); - } -} diff --git a/Comments/Resources/views/admin/index.blade.php b/Comments/Resources/views/admin/index.blade.php deleted file mode 100644 index a9e6652..0000000 --- a/Comments/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Comment moderation

@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $comment)
{{ $comment->author_name }}{{ $comment->status }}

{{ $comment->body }}

@csrf @method('PUT')Update
@empty

No comments awaiting moderation.

@endforelse
{{ $records->links() }}
diff --git a/Comments/Routes/web.php b/Comments/Routes/web.php deleted file mode 100644 index d40cb18..0000000 --- a/Comments/Routes/web.php +++ /dev/null @@ -1,10 +0,0 @@ -prefix('administration/comments')->name('administration.comments.')->group(function (): void { - Route::get('/', [CommentController::class, 'index'])->name('index'); - Route::put('/{comment}', [CommentController::class, 'update'])->name('update'); - Route::delete('/{comment}', [CommentController::class, 'destroy'])->name('destroy'); -}); diff --git a/Comments/Tests/Feature/ModuleBootTest.php b/Comments/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index bf969d3..0000000 --- a/Comments/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('Comments', config('comments.name')); - } -} diff --git a/Comments/module.json b/Comments/module.json deleted file mode 100644 index ab9f85e..0000000 --- a/Comments/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Comments", - "alias": "comments", - "version": "1.0.0", - "description": "Comments publishing module.", - "keywords": ["publishing", "comments"], - "priority": 30, - "providers": ["Modules\\Comments\\Providers\\CommentsServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Commerce/Config/config.php b/Commerce/Config/config.php deleted file mode 100644 index 7eb951b..0000000 --- a/Commerce/Config/config.php +++ /dev/null @@ -1,8 +0,0 @@ - env('COMMERCE_CURRENCY', 'GBP'), - 'currency_symbol' => env('COMMERCE_CURRENCY_SYMBOL', '£'), - 'prices_include_tax' => (bool) env('COMMERCE_PRICES_INCLUDE_TAX', true), - 'low_stock_threshold' => (int) env('COMMERCE_LOW_STOCK_THRESHOLD', 5), -]; diff --git a/Commerce/Contracts/InventoryProvider.php b/Commerce/Contracts/InventoryProvider.php deleted file mode 100644 index d2aaa5c..0000000 --- a/Commerce/Contracts/InventoryProvider.php +++ /dev/null @@ -1,12 +0,0 @@ -id(); - $table->foreignId('parent_id')->nullable()->constrained('commerce_categories')->nullOnDelete(); - $table->string('name'); - $table->string('slug')->unique(); - $table->text('description')->nullable(); - $table->boolean('is_active')->default(true); - $table->unsignedInteger('sort_order')->default(0); - $table->timestamps(); - }); - Schema::create('commerce_products', function (Blueprint $table) { - $table->id(); - $table->foreignId('category_id')->nullable()->constrained('commerce_categories')->nullOnDelete(); - $table->string('type')->default('physical'); - $table->string('name'); - $table->string('slug')->unique(); - $table->string('sku')->nullable()->unique(); - $table->text('summary')->nullable(); - $table->longText('description')->nullable(); - $table->decimal('price', 12, 2)->default(0); - $table->decimal('compare_at_price', 12, 2)->nullable(); - $table->decimal('cost_price', 12, 2)->nullable(); - $table->string('currency', 3)->default('GBP'); - $table->boolean('track_inventory')->default(true); - $table->integer('stock_quantity')->default(0); - $table->integer('low_stock_threshold')->default(5); - $table->boolean('is_featured')->default(false); - $table->string('status')->default('draft'); - $table->timestamp('published_at')->nullable(); - $table->json('metadata')->nullable(); - $table->timestamps(); - $table->softDeletes(); - $table->index(['status', 'published_at']); - }); - Schema::create('commerce_product_variants', function (Blueprint $table) { - $table->id(); - $table->foreignId('product_id')->constrained('commerce_products')->cascadeOnDelete(); - $table->string('name'); - $table->string('sku')->unique(); - $table->decimal('price', 12, 2)->nullable(); - $table->integer('stock_quantity')->default(0); - $table->json('options')->nullable(); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - Schema::create('commerce_customers', function (Blueprint $table) { - $table->id(); - $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); - $table->string('email')->index(); - $table->string('first_name'); - $table->string('last_name'); - $table->string('company')->nullable(); - $table->string('phone')->nullable(); - $table->string('tax_number')->nullable(); - $table->json('billing_address')->nullable(); - $table->json('shipping_address')->nullable(); - $table->string('status')->default('active'); - $table->timestamps(); - }); - Schema::create('commerce_orders', function (Blueprint $table) { - $table->id(); - $table->string('number')->unique(); - $table->foreignId('customer_id')->nullable()->constrained('commerce_customers')->nullOnDelete(); - $table->string('email'); - $table->string('status')->default('pending'); - $table->string('payment_status')->default('unpaid'); - $table->string('fulfilment_status')->default('unfulfilled'); - $table->string('currency', 3)->default('GBP'); - $table->decimal('subtotal', 12, 2)->default(0); - $table->decimal('discount_total', 12, 2)->default(0); - $table->decimal('shipping_total', 12, 2)->default(0); - $table->decimal('tax_total', 12, 2)->default(0); - $table->decimal('grand_total', 12, 2)->default(0); - $table->json('billing_address')->nullable(); - $table->json('shipping_address')->nullable(); - $table->text('notes')->nullable(); - $table->timestamp('placed_at')->nullable(); - $table->timestamps(); - $table->index(['status', 'payment_status']); - }); - Schema::create('commerce_order_items', function (Blueprint $table) { - $table->id(); - $table->foreignId('order_id')->constrained('commerce_orders')->cascadeOnDelete(); - $table->foreignId('product_id')->nullable()->constrained('commerce_products')->nullOnDelete(); - $table->foreignId('variant_id')->nullable()->constrained('commerce_product_variants')->nullOnDelete(); - $table->string('name'); - $table->string('sku')->nullable(); - $table->unsignedInteger('quantity'); - $table->decimal('unit_price', 12, 2); - $table->decimal('tax_total', 12, 2)->default(0); - $table->decimal('line_total', 12, 2); - $table->json('metadata')->nullable(); - $table->timestamps(); - }); - Schema::create('commerce_inventory_movements', function (Blueprint $table) { - $table->id(); - $table->foreignId('product_id')->constrained('commerce_products')->cascadeOnDelete(); - $table->foreignId('variant_id')->nullable()->constrained('commerce_product_variants')->cascadeOnDelete(); - $table->integer('quantity'); - $table->string('reason'); - $table->string('reference_type')->nullable(); - $table->unsignedBigInteger('reference_id')->nullable(); - $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); - $table->text('notes')->nullable(); - $table->timestamps(); - $table->index(['reference_type', 'reference_id']); - }); - Schema::create('commerce_promotions', function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->string('code')->nullable()->unique(); - $table->string('type')->default('percentage'); - $table->decimal('value', 12, 2); - $table->decimal('minimum_order', 12, 2)->nullable(); - $table->unsignedInteger('usage_limit')->nullable(); - $table->unsignedInteger('times_used')->default(0); - $table->timestamp('starts_at')->nullable(); - $table->timestamp('ends_at')->nullable(); - $table->boolean('is_active')->default(true); - $table->json('conditions')->nullable(); - $table->timestamps(); - }); - Schema::create('commerce_shipping_methods', function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->string('code')->unique(); - $table->decimal('price', 12, 2)->default(0); - $table->decimal('free_over', 12, 2)->nullable(); - $table->boolean('is_active')->default(true); - $table->json('settings')->nullable(); - $table->timestamps(); - }); - Schema::create('commerce_tax_rates', function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->string('country_code', 2)->nullable(); - $table->decimal('rate', 7, 4)->default(0); - $table->boolean('is_default')->default(false); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - Schema::create('commerce_payment_transactions', function (Blueprint $table) { - $table->id(); - $table->foreignId('order_id')->constrained('commerce_orders')->cascadeOnDelete(); - $table->string('provider'); - $table->string('provider_reference')->nullable()->index(); - $table->string('type')->default('payment'); - $table->string('status')->default('pending'); - $table->decimal('amount', 12, 2); - $table->string('currency', 3); - $table->json('payload')->nullable(); - $table->timestamps(); - }); - } - - public function down(): void - { - foreach (['commerce_payment_transactions', 'commerce_tax_rates', 'commerce_shipping_methods', 'commerce_promotions', 'commerce_inventory_movements', 'commerce_order_items', 'commerce_orders', 'commerce_customers', 'commerce_product_variants', 'commerce_products', 'commerce_categories'] as $table) { - Schema::dropIfExists($table); - } - } -}; diff --git a/Commerce/Http/Controllers/CommerceController.php b/Commerce/Http/Controllers/CommerceController.php deleted file mode 100644 index bf7eb83..0000000 --- a/Commerce/Http/Controllers/CommerceController.php +++ /dev/null @@ -1,53 +0,0 @@ - Product::latest()->paginate(20), 'orders' => Order::latest()->limit(10)->get(), 'revenue' => Order::where('payment_status', 'paid')->sum('grand_total'), 'lowStock' => Product::where('track_inventory', true)->whereColumn('stock_quantity', '<=', 'low_stock_threshold')->count(), 'customers' => Customer::count(), 'promotions' => Promotion::where('is_active', true)->count()]); - } - - public function storeProduct(Request $request): RedirectResponse - { - $data = $request->validate(['name' => ['required', 'string', 'max:255'], 'slug' => ['nullable', 'string', 'max:255', 'unique:commerce_products,slug'], 'sku' => ['nullable', 'string', 'max:120', 'unique:commerce_products,sku'], 'type' => ['required', 'in:physical,digital,service,bundle'], 'summary' => ['nullable', 'string'], 'description' => ['nullable', 'string'], 'price' => ['required', 'numeric', 'min:0'], 'stock_quantity' => ['required', 'integer'], 'status' => ['required', 'in:draft,active,archived']]); - $data['slug'] = $data['slug'] ?: Str::slug($data['name']); - $data['currency'] = config('commerce.currency', 'GBP'); - $data['published_at'] = $data['status'] === 'active' ? now() : null; - Product::create($data); - - return back()->with('status', 'Product created.'); - } - - public function updateProduct(Request $request, Product $product): RedirectResponse - { - $product->update($request->validate(['name' => ['required', 'string', 'max:255'], 'price' => ['required', 'numeric', 'min:0'], 'stock_quantity' => ['required', 'integer'], 'status' => ['required', 'in:draft,active,archived']])); - - return back()->with('status', 'Product updated.'); - } - - public function destroyProduct(Product $product): RedirectResponse - { - $product->delete(); - - return back()->with('status', 'Product deleted.'); - } - - public function updateOrder(Request $request, Order $order): RedirectResponse - { - $order->update($request->validate(['status' => ['required', 'in:pending,processing,completed,cancelled,refunded'], 'payment_status' => ['required', 'in:unpaid,authorised,paid,partially_refunded,refunded,failed'], 'fulfilment_status' => ['required', 'in:unfulfilled,partial,fulfilled,returned']])); - - return back()->with('status', 'Order updated.'); - } -} diff --git a/Commerce/Http/Controllers/ProductApiController.php b/Commerce/Http/Controllers/ProductApiController.php deleted file mode 100644 index 07e871f..0000000 --- a/Commerce/Http/Controllers/ProductApiController.php +++ /dev/null @@ -1,20 +0,0 @@ -json(Product::query()->where('status', 'active')->where(fn ($q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()))->latest()->paginate(24)); - } - - public function show(string $product): JsonResponse - { - return response()->json(Product::where('status', 'active')->where(fn ($q) => $q->where('slug', $product)->orWhere('id', $product))->firstOrFail()); - } -} diff --git a/Commerce/Models/Category.php b/Commerce/Models/Category.php deleted file mode 100644 index 76dce14..0000000 --- a/Commerce/Models/Category.php +++ /dev/null @@ -1,17 +0,0 @@ - 'boolean']; - } -} diff --git a/Commerce/Models/Customer.php b/Commerce/Models/Customer.php deleted file mode 100644 index 2ed400c..0000000 --- a/Commerce/Models/Customer.php +++ /dev/null @@ -1,17 +0,0 @@ - 'array', 'shipping_address' => 'array']; - } -} diff --git a/Commerce/Models/InventoryMovement.php b/Commerce/Models/InventoryMovement.php deleted file mode 100644 index 68c3cb1..0000000 --- a/Commerce/Models/InventoryMovement.php +++ /dev/null @@ -1,17 +0,0 @@ - 'decimal:2', 'discount_total' => 'decimal:2', 'shipping_total' => 'decimal:2', 'tax_total' => 'decimal:2', 'grand_total' => 'decimal:2', 'billing_address' => 'array', 'shipping_address' => 'array', 'placed_at' => 'datetime']; - } -} diff --git a/Commerce/Models/OrderItem.php b/Commerce/Models/OrderItem.php deleted file mode 100644 index 06072b7..0000000 --- a/Commerce/Models/OrderItem.php +++ /dev/null @@ -1,17 +0,0 @@ - 'decimal:2', 'tax_total' => 'decimal:2', 'line_total' => 'decimal:2', 'metadata' => 'array']; - } -} diff --git a/Commerce/Models/Product.php b/Commerce/Models/Product.php deleted file mode 100644 index a58482f..0000000 --- a/Commerce/Models/Product.php +++ /dev/null @@ -1,20 +0,0 @@ - 'decimal:2', 'compare_at_price' => 'decimal:2', 'cost_price' => 'decimal:2', 'track_inventory' => 'boolean', 'is_featured' => 'boolean', 'published_at' => 'datetime', 'metadata' => 'array']; - } -} diff --git a/Commerce/Models/ProductVariant.php b/Commerce/Models/ProductVariant.php deleted file mode 100644 index 72482ce..0000000 --- a/Commerce/Models/ProductVariant.php +++ /dev/null @@ -1,17 +0,0 @@ - 'decimal:2', 'options' => 'array', 'is_active' => 'boolean']; - } -} diff --git a/Commerce/Models/Promotion.php b/Commerce/Models/Promotion.php deleted file mode 100644 index e16cb92..0000000 --- a/Commerce/Models/Promotion.php +++ /dev/null @@ -1,17 +0,0 @@ - 'decimal:2', 'minimum_order' => 'decimal:2', 'starts_at' => 'datetime', 'ends_at' => 'datetime', 'is_active' => 'boolean', 'conditions' => 'array']; - } -} diff --git a/Commerce/Models/Relations.php b/Commerce/Models/Relations.php deleted file mode 100644 index 7814843..0000000 --- a/Commerce/Models/Relations.php +++ /dev/null @@ -1,3 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Commerce', 'Config/config.php'), 'commerce'); - $this->app->bind(InventoryProvider::class, DatabaseInventoryProvider::class); - $this->app->bind(PaymentProvider::class, ManualPaymentProvider::class); - $this->app->bind(ShippingProvider::class, FlatRateShippingProvider::class); - $this->app->bind(TaxProvider::class, ConfiguredTaxProvider::class); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Commerce', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Commerce', 'Resources/views'), 'commerce'); - } -} diff --git a/Commerce/Providers/RouteServiceProvider.php b/Commerce/Providers/RouteServiceProvider.php deleted file mode 100644 index 1dcfdb7..0000000 --- a/Commerce/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Commerce', 'Routes/web.php')); - Route::prefix('api/v1')->middleware('api')->group(module_path('Commerce', 'Routes/api.php')); - } -} diff --git a/Commerce/Resources/views/admin/index.blade.php b/Commerce/Resources/views/admin/index.blade.php deleted file mode 100644 index 4960512..0000000 --- a/Commerce/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,5 +0,0 @@ -

Commerce

Catalogue, stock, customers, orders and promotions.

@if(session('status'))
{{ session('status') }}
@endif -
Products
{{ $products->total() }}
Orders
{{ \Modules\Commerce\Models\Order::count() }}
Revenue
{{ config('commerce.currency_symbol','£') }}{{ number_format($revenue,2) }}
Customers
{{ $customers }}
Low stock
{{ $lowStock }}
-
@csrf

Add product

Create product -
@forelse($products as $product)@empty@endforelse
ProductSKUPriceStockStatus
{{ $product->name }}{{ $product->sku ?: '—' }}{{ config('commerce.currency_symbol','£') }}{{ number_format($product->price,2) }}{{ $product->stock_quantity }}{{ ucfirst($product->status) }}
@csrf @method('DELETE')Delete
No products yet.
{{ $products->links() }} -

Recent orders

@forelse($orders as $order)@empty@endforelse
OrderCustomerTotalStatusPayment
{{ $order->number }}{{ $order->email }}{{ $order->currency }} {{ number_format($order->grand_total,2) }}{{ ucfirst($order->status) }}{{ ucfirst($order->payment_status) }}
No orders yet.
diff --git a/Commerce/Routes/api.php b/Commerce/Routes/api.php deleted file mode 100644 index 16ddbdc..0000000 --- a/Commerce/Routes/api.php +++ /dev/null @@ -1,7 +0,0 @@ -name('administration.commerce.')->middleware(['auth', 'permission:commerce.view'])->group(function () { - Route::get('/', [CommerceController::class, 'index'])->name('index'); - Route::post('/products', [CommerceController::class, 'storeProduct'])->middleware('permission:commerce.manage')->name('products.store'); - Route::put('/products/{product}', [CommerceController::class, 'updateProduct'])->middleware('permission:commerce.manage')->name('products.update'); - Route::delete('/products/{product}', [CommerceController::class, 'destroyProduct'])->middleware('permission:commerce.manage')->name('products.destroy'); - Route::put('/orders/{order}', [CommerceController::class, 'updateOrder'])->middleware('permission:commerce.manage')->name('orders.update'); -}); diff --git a/Commerce/Services/ConfiguredTaxProvider.php b/Commerce/Services/ConfiguredTaxProvider.php deleted file mode 100644 index ee0a4ad..0000000 --- a/Commerce/Services/ConfiguredTaxProvider.php +++ /dev/null @@ -1,13 +0,0 @@ -increment('stock_quantity', $quantity); - InventoryMovement::create(['product_id' => $product->id, 'quantity' => $quantity, 'reason' => $reason, 'created_by' => $userId]); - }); - } - - public function available(Product $product): int - { - return max(0, (int) $product->stock_quantity); - } -} diff --git a/Commerce/Services/FlatRateShippingProvider.php b/Commerce/Services/FlatRateShippingProvider.php deleted file mode 100644 index 30e08ed..0000000 --- a/Commerce/Services/FlatRateShippingProvider.php +++ /dev/null @@ -1,13 +0,0 @@ -= 50 ? 0.0 : 4.95; - } -} diff --git a/Commerce/Services/ManualPaymentProvider.php b/Commerce/Services/ManualPaymentProvider.php deleted file mode 100644 index 5d7ebed..0000000 --- a/Commerce/Services/ManualPaymentProvider.php +++ /dev/null @@ -1,19 +0,0 @@ - 'pending', 'provider' => 'manual', 'reference' => null]; - } - - public function refund(Order $order, float $amount): array - { - return ['status' => 'pending', 'provider' => 'manual', 'amount' => $amount]; - } -} diff --git a/Commerce/Tests/Feature/ModuleBootTest.php b/Commerce/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 2932611..0000000 --- a/Commerce/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,19 +0,0 @@ -assertTrue(app('router')->has('administration.commerce.index')); - } - - public function test_commerce_contracts_are_bound(): void - { - $this->assertInstanceOf(InventoryProvider::class, app(InventoryProvider::class)); - } -} diff --git a/Commerce/module.json b/Commerce/module.json deleted file mode 100644 index f5f48cb..0000000 --- a/Commerce/module.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"Commerce","alias":"commerce","version":"1.0.0","description":"Products, inventory, customers, orders, promotions, tax, shipping and payments.","keywords":["commerce","catalog","inventory","orders","customers"],"priority":110,"providers":["Modules\\Commerce\\Providers\\CommerceServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Media","Sites"]} diff --git a/Content/Config/config.php b/Content/Config/config.php deleted file mode 100644 index b0ba6d5..0000000 --- a/Content/Config/config.php +++ /dev/null @@ -1,5 +0,0 @@ - 'home', - 'regions' => ['header', 'body', 'footer'], -]; diff --git a/Content/Console/InstallSaaslandContent.php b/Content/Console/InstallSaaslandContent.php deleted file mode 100644 index ba501a6..0000000 --- a/Content/Console/InstallSaaslandContent.php +++ /dev/null @@ -1,244 +0,0 @@ - ! Schema::hasTable($table))); - - if ($missingTables !== []) { - $this->components->error('Content composition tables are missing: '.implode(', ', $missingTables).'.'); - $this->components->info('Run php artisan migrate before installing the Saasland composition.'); - return self::FAILURE; - } - - DB::transaction(function (): void { - $this->installSharedSettings(); - $this->installMenus(); - if ($this->option('force')) { - PageContent::query()->where('page_key', 'home')->delete(); - } - - $order = [ - 'site-header' => ['header', 10], 'hero' => ['body', 10], 'feature-grid' => ['body', 20], - 'product-feed' => ['body', 30], 'testimonial-feed' => ['body', 40], 'team-feed' => ['body', 50], - 'blog-feed' => ['body', 60], 'news-feed' => ['body', 70], 'faq-feed' => ['body', 80], - 'newsletter-form' => ['body', 90], 'call-to-action' => ['body', 100], 'site-footer' => ['footer', 10], - ]; - - foreach ($this->definitions() as $key => $definition) { - $type = ContentType::query()->updateOrCreate(['key' => $key], $definition['type']); - $document = ContentDocument::query()->firstOrCreate( - ['slug' => 'saasland-'.$key], - [ - 'content_type_id' => $type->id, - 'title' => $definition['title'], - 'data' => $definition['data'], - 'status' => 'published', - 'published_at' => now(), - ] - ); - - $currentData = is_array($document->data) ? $document->data : []; - $mergedData = $this->mergeMissingDefaults($currentData, $definition['data']); - - $document->forceFill([ - 'content_type_id' => $type->id, - 'data' => $mergedData, - ])->save(); - [$region, $sort] = $order[$key]; - PageContent::query()->updateOrCreate( - ['page_key' => 'home', 'region' => $region, 'sort_order' => $sort], - ['content_document_id' => $document->id, 'is_enabled' => true] - ); - } - }); - - $this->components->info('Saasland content composition installed with editable schemas.'); - return self::SUCCESS; - } - - private function installSharedSettings(): void - { - $settings = app(SettingsRepository::class); - $legacy = []; - - if (Schema::hasTable('stylist_theme_settings')) { - $stored = DB::table('stylist_theme_settings')->where('theme_key', 'saasland')->value('settings'); - $legacy = is_string($stored) ? (json_decode($stored, true) ?: []) : []; - } - - $defaults = [ - 'site_name' => $legacy['site_title'] ?? config('app.name'), - 'site_description' => $legacy['site_description'] ?? 'Modern software for modern companies.', - 'company_name' => $legacy['site_title'] ?? config('app.name'), - 'site_url' => url('/'), - 'logo_url' => $legacy['logo_url'] ?? '', - 'facebook_url' => $legacy['facebook_url'] ?? '', - 'x_url' => $legacy['x_url'] ?? '', - 'linkedin_url' => $legacy['linkedin_url'] ?? '', - 'dashboard_label' => 'Dashboard', - 'login_label' => 'Log in', - 'registration_label' => 'Get started', - ]; - - foreach ($defaults as $key => $value) { - if ($settings->get($key, null, 'site') === null) { - $settings->put($key, $value, 'site', 'string', true); - } - } - } - - private function installMenus(): void - { - if (! Schema::hasTable('menus')) { - return; - } - - $menus = [ - 'primary' => ['Primary navigation', [['enabled' => true, 'label' => 'Home', 'url' => '/'], ['enabled' => true, 'label' => 'Blog', 'url' => '/blog'], ['enabled' => true, 'label' => 'News', 'url' => '/news']]], - 'footer' => ['Footer — Explore', [['enabled' => true, 'label' => 'Home', 'url' => '/'], ['enabled' => true, 'label' => 'Blog', 'url' => '/blog'], ['enabled' => true, 'label' => 'News', 'url' => '/news']]], - 'footer_account' => ['Footer — Account', [['enabled' => true, 'label' => 'Dashboard', 'url' => '/dashboard'], ['enabled' => true, 'label' => 'Profile', 'url' => '/settings/profile']]], - 'footer_legal' => ['Footer — Legal', []], - ]; - - foreach ($menus as $location => [$name, $items]) { - Menu::query()->firstOrCreate(['location' => $location], ['name' => $name, 'items' => $items, 'enabled' => true]); - } - } - - private function mergeMissingDefaults(array $current, array $defaults): array - { - foreach ($defaults as $key => $defaultValue) { - if (! array_key_exists($key, $current)) { - $current[$key] = $defaultValue; - continue; - } - - if ( - is_array($defaultValue) - && is_array($current[$key]) - && ! array_is_list($defaultValue) - && ! array_is_list($current[$key]) - ) { - $current[$key] = $this->mergeMissingDefaults( - $current[$key], - $defaultValue - ); - } - } - - return $current; - } - - private function definitions(): array - { - $field = static fn (string $key, string $label, string $type = 'text', array $extra = []): array => array_merge(compact('key', 'label', 'type'), $extra); - $type = static fn (string $name, string $renderer, array $regions, array $schema): array => [ - 'name' => $name, 'renderer' => $renderer, 'allowed_regions' => $regions, - 'is_system' => true, 'is_active' => true, 'schema' => $schema, - ]; - $section = [$field('enabled', 'Show this section', 'boolean', ['default' => true])]; - $feed = array_merge($section, [ - $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), - $field('limit', 'Maximum items', 'number', ['min' => 1, 'max' => 50]), - $field('include_ids', 'Only show record IDs', 'csv', ['help' => 'Optional comma-separated IDs. Leave blank to use the latest records.']), - $field('exclude_ids', 'Exclude record IDs', 'csv', ['help' => 'Optional comma-separated IDs to hide from this section.']), - ]); - - return [ - 'site-header' => [ - 'title' => 'Main website header', - 'type' => $type('Site header', 'site-header', ['header'], [ - $field('enabled', 'Show header', 'boolean', ['default' => true]), - $field('menu_location', 'Navigation menu location', 'text', ['help' => 'Usually primary. Edit links from Administration → Menus.']), - $field('show_navigation', 'Show navigation', 'boolean', ['default' => true]), - $field('show_dashboard', 'Show dashboard link', 'boolean', ['default' => true]), - $field('show_user_name', 'Show signed-in user', 'boolean', ['default' => true]), - $field('show_login', 'Show login', 'boolean', ['default' => true]), - $field('show_registration', 'Show registration', 'boolean', ['default' => true]), - $field('show_mobile_menu', 'Show mobile menu', 'boolean', ['default' => true]), - ]), - 'data' => ['enabled' => true, 'menu_location' => 'primary', 'show_navigation' => true, 'show_dashboard' => true, 'show_user_name' => true, 'show_login' => true, 'show_registration' => true, 'show_mobile_menu' => true], - ], - 'hero' => [ - 'title' => 'Homepage hero', - 'type' => $type('Hero', 'hero', ['body'], [ - ...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), - $field('primary_label', 'Primary button label'), $field('primary_url', 'Primary button URL', 'url'), - $field('secondary_label', 'Secondary button label'), $field('secondary_url', 'Secondary button URL', 'url'), - $field('show_metrics', 'Show metrics panel', 'boolean', ['default' => true]), - $field('metrics', 'Metrics', 'repeater', ['fields' => [ - $field('enabled', 'Show item', 'boolean', ['default' => true]), $field('label', 'Label'), $field('value', 'Value'), $field('change', 'Change'), $field('height', 'Chart height (%)', 'number', ['min' => 0, 'max' => 100]), - ]]), - ]), - 'data' => ['enabled' => true, 'eyebrow' => 'A better way to run your business', 'heading' => 'Transform Your Business With Our SaaS Solution', 'description' => 'Streamline operations, boost productivity, and improve customer satisfaction with one connected platform built for modern teams.', 'primary_label' => 'Get started free', 'primary_url' => '/register', 'secondary_label' => 'Book demo', 'secondary_url' => '#contact', 'show_metrics' => true, 'metrics' => [ - ['enabled' => true, 'label' => 'Monthly revenue', 'value' => '£48,240', 'change' => '+18.4%', 'height' => 62], - ['enabled' => true, 'label' => 'Active users', 'value' => '12,842', 'change' => '+9.2%', 'height' => 78], - ['enabled' => true, 'label' => 'Conversion', 'value' => '7.8%', 'change' => '+2.1%', 'height' => 92], - ]], - ], - 'feature-grid' => [ - 'title' => 'Core features', - 'type' => $type('Feature grid', 'feature-grid', ['body'], [ - ...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), - $field('items', 'Features', 'repeater', ['fields' => [$field('enabled', 'Show item', 'boolean', ['default' => true]), $field('title', 'Title'), $field('description', 'Description', 'textarea')]]), - ]), - 'data' => ['enabled' => true, 'eyebrow' => 'Powerful features', 'heading' => 'Everything your team needs to grow', 'description' => 'Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.', 'items' => [ - ['enabled' => true, 'title' => 'Smart automation', 'description' => 'Remove repetitive work and delays.'], - ['enabled' => true, 'title' => 'Real-time analytics', 'description' => 'Turn activity into useful business insight.'], - ['enabled' => true, 'title' => 'Content operations', 'description' => 'Create, review and publish confidently.'], - ['enabled' => true, 'title' => 'Secure by design', 'description' => 'Protect users and data with dependable controls.'], - ['enabled' => true, 'title' => 'Flexible integrations', 'description' => 'Connect the tools your organisation already uses.'], - ['enabled' => true, 'title' => 'Modular architecture', 'description' => 'Activate only the capabilities each site needs.'], - ]], - ], - 'product-feed' => ['title' => 'Pricing plans', 'type' => $type('Product feed', 'product-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Simple pricing', 'heading' => 'Choose a plan that fits', 'description' => 'Plans are managed from Products & Pricing in the CMS.', 'limit' => 6, 'include_ids' => '', 'exclude_ids' => '']], - 'testimonial-feed' => ['title' => 'Customer stories', 'type' => $type('Testimonial feed', 'testimonial-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Customer stories', 'heading' => 'Trusted by ambitious teams', 'description' => '', 'limit' => 6, 'include_ids' => '', 'exclude_ids' => '']], - 'team-feed' => ['title' => 'Team', 'type' => $type('Team feed', 'team-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Our team', 'heading' => 'People behind the platform', 'description' => '', 'limit' => 8, 'include_ids' => '', 'exclude_ids' => '']], - 'blog-feed' => ['title' => 'Latest blog posts', 'type' => $type('Blog feed', 'blog-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Insights', 'heading' => 'Latest from the blog', 'description' => '', 'limit' => 3, 'include_ids' => '', 'exclude_ids' => '']], - 'news-feed' => ['title' => 'Latest news', 'type' => $type('News feed', 'news-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Newsroom', 'heading' => 'Latest news', 'description' => '', 'limit' => 3, 'include_ids' => '', 'exclude_ids' => '']], - 'faq-feed' => ['title' => 'Frequently asked questions', 'type' => $type('FAQ feed', 'faq-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'FAQ', 'heading' => 'Answers before you begin', 'description' => '', 'limit' => 12, 'include_ids' => '', 'exclude_ids' => '']], - 'newsletter-form' => [ - 'title' => 'Newsletter signup', - 'type' => $type('Newsletter form', 'newsletter-form', ['body'], [...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), $field('placeholder', 'Email placeholder'), $field('button_label', 'Button label')]), - 'data' => ['enabled' => true, 'eyebrow' => 'Newsletter', 'heading' => 'Useful updates, no noise', 'description' => 'Receive product news, publishing guidance, and practical platform updates.', 'placeholder' => 'you@example.com', 'button_label' => 'Subscribe'], - ], - 'call-to-action' => [ - 'title' => 'Homepage final call to action', - 'type' => $type('Call to action', 'call-to-action', ['body'], [...$section, $field('anchor', 'Section anchor'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), $field('button_label', 'Button label'), $field('button_url', 'Button URL', 'url')]), - 'data' => ['enabled' => true, 'anchor' => 'contact', 'heading' => 'Ready to transform your business?', 'description' => 'Start building a faster, more connected operation today.', 'button_label' => 'Get started free', 'button_url' => '/register'], - ], - 'site-footer' => [ - 'title' => 'Main website footer', - 'type' => $type('Site footer', 'site-footer', ['footer'], [ - $field('enabled', 'Show footer', 'boolean', ['default' => true]), - $field('explore_heading', 'Explore column heading'), - $field('explore_menu_location', 'Explore menu location'), - $field('account_heading', 'Account column heading'), - $field('account_menu_location', 'Account menu location'), - $field('legal_heading', 'Legal column heading'), - $field('legal_menu_location', 'Legal menu location'), - $field('show_back_to_top', 'Show back-to-top link', 'boolean', ['default' => true]), - $field('back_to_top_label', 'Back-to-top label'), - ]), - 'data' => ['enabled' => true, 'explore_heading' => 'Explore', 'explore_menu_location' => 'footer', 'account_heading' => 'Account', 'account_menu_location' => 'footer_account', 'legal_heading' => 'Legal', 'legal_menu_location' => 'footer_legal', 'show_back_to_top' => true, 'back_to_top_label' => 'Back to top ↑'], - ], - ]; - } -} diff --git a/Content/Database/Migrations/2026_07_27_000000_create_content_composition_tables.php b/Content/Database/Migrations/2026_07_27_000000_create_content_composition_tables.php deleted file mode 100644 index 2fcd905..0000000 --- a/Content/Database/Migrations/2026_07_27_000000_create_content_composition_tables.php +++ /dev/null @@ -1,62 +0,0 @@ -id(); - $table->string('name'); - $table->string('key')->unique(); - $table->text('description')->nullable(); - $table->json('schema')->nullable(); - $table->string('renderer'); - $table->json('allowed_regions')->nullable(); - $table->boolean('is_system')->default(false); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - - Schema::create('content_documents', function (Blueprint $table): void { - $table->id(); - $table->foreignId('content_type_id')->constrained('content_types')->cascadeOnDelete(); - $table->string('title'); - $table->string('slug')->unique(); - $table->json('data')->nullable(); - $table->string('status')->default('draft')->index(); - $table->timestamp('published_at')->nullable()->index(); - $table->timestamp('expires_at')->nullable(); - $table->string('language', 12)->nullable(); - $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); - $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamps(); - $table->softDeletes(); - }); - - Schema::create('page_content', function (Blueprint $table): void { - $table->id(); - $table->foreignId('page_id')->nullable()->constrained('pages')->cascadeOnDelete(); - $table->string('page_key')->nullable()->index(); - $table->foreignId('content_document_id')->constrained('content_documents')->cascadeOnDelete(); - $table->string('region')->index(); - $table->unsignedInteger('sort_order')->default(0); - $table->boolean('is_enabled')->default(true); - $table->json('visibility_rules')->nullable(); - $table->json('presentation')->nullable(); - $table->timestamp('starts_at')->nullable(); - $table->timestamp('ends_at')->nullable(); - $table->timestamps(); - $table->unique(['page_key', 'region', 'sort_order'], 'page_content_key_region_order_unique'); - }); - } - - public function down(): void - { - Schema::dropIfExists('page_content'); - Schema::dropIfExists('content_documents'); - Schema::dropIfExists('content_types'); - } -}; diff --git a/Content/Http/Controllers/ContentDocumentController.php b/Content/Http/Controllers/ContentDocumentController.php deleted file mode 100644 index af35ce9..0000000 --- a/Content/Http/Controllers/ContentDocumentController.php +++ /dev/null @@ -1,146 +0,0 @@ -with('type') - ->withCount('placements') - ->when($request->string('type')->isNotEmpty(), fn ($query) => $query->whereHas('type', fn ($type) => $type->where('key', $request->string('type')->toString()))) - ->when($request->string('status')->isNotEmpty(), fn ($query) => $query->where('status', $request->string('status')->toString())) - ->when($request->string('search')->isNotEmpty(), fn ($query) => $query->where('title', 'like', '%'.$request->string('search')->toString().'%')) - ->orderBy('title') - ->paginate(30) - ->withQueryString(); - - return view('content::admin.index', [ - 'documents' => $documents, - 'types' => ContentType::query()->where('is_active', true)->orderBy('name')->get(), - ]); - } - - public function create(Request $request): View - { - $types = ContentType::query()->where('is_active', true)->orderBy('name')->get(); - $selectedType = $types->firstWhere('id', (int) $request->integer('type')) ?? $types->first(); - - return view('content::admin.form', [ - 'document' => new ContentDocument(['status' => 'draft', 'data' => []]), - 'types' => $types, - 'selectedType' => $selectedType, - ]); - } - - public function store(Request $request): RedirectResponse - { - $validated = $this->validateDocument($request); - $type = ContentType::query()->where('is_active', true)->findOrFail($validated['content_type_id']); - - $document = ContentDocument::query()->create([ - 'content_type_id' => $type->id, - 'title' => $validated['title'], - 'slug' => $this->uniqueSlug($validated['slug'] ?: $validated['title']), - 'status' => $validated['status'], - 'published_at' => $validated['status'] === 'published' ? ($validated['published_at'] ?? now()) : null, - 'expires_at' => $validated['expires_at'] ?? null, - 'data' => json_decode($validated['data_json'], true, 512, JSON_THROW_ON_ERROR), - 'created_by' => $request->user()?->id, - 'updated_by' => $request->user()?->id, - ]); - - return redirect()->route('content.documents.edit', $document)->with('status', 'Content document created.'); - } - - public function edit(ContentDocument $document): View - { - $document->load('type', 'placements'); - - return view('content::admin.form', [ - 'document' => $document, - 'types' => ContentType::query()->where('is_active', true)->orderBy('name')->get(), - 'selectedType' => $document->type, - ]); - } - - public function update(Request $request, ContentDocument $document): RedirectResponse - { - $validated = $this->validateDocument($request, $document); - - $document->update([ - 'content_type_id' => $validated['content_type_id'], - 'title' => $validated['title'], - 'slug' => $this->uniqueSlug($validated['slug'] ?: $validated['title'], $document), - 'status' => $validated['status'], - 'published_at' => $validated['status'] === 'published' ? ($validated['published_at'] ?? $document->published_at ?? now()) : null, - 'expires_at' => $validated['expires_at'] ?? null, - 'data' => json_decode($validated['data_json'], true, 512, JSON_THROW_ON_ERROR), - 'updated_by' => $request->user()?->id, - ]); - - return back()->with('status', 'Content document updated.'); - } - - public function duplicate(Request $request, ContentDocument $document): RedirectResponse - { - $copy = $document->replicate(['slug', 'status', 'published_at', 'created_by', 'updated_by']); - $copy->title = $document->title.' copy'; - $copy->slug = $this->uniqueSlug($copy->title); - $copy->status = 'draft'; - $copy->published_at = null; - $copy->created_by = $request->user()?->id; - $copy->updated_by = $request->user()?->id; - $copy->save(); - - return redirect()->route('content.documents.edit', $copy)->with('status', 'Independent draft copy created.'); - } - - public function destroy(ContentDocument $document): RedirectResponse - { - if ($document->placements()->exists()) { - return back()->withErrors(['document' => 'Remove this document from every page before deleting it.']); - } - - $document->delete(); - - return redirect()->route('content.index')->with('status', 'Content document deleted.'); - } - - private function validateDocument(Request $request, ?ContentDocument $document = null): array - { - return $request->validate([ - 'content_type_id' => ['required', 'integer', Rule::exists('content_types', 'id')->where('is_active', true)], - 'title' => ['required', 'string', 'max:255'], - 'slug' => ['nullable', 'string', 'max:255'], - 'status' => ['required', Rule::in(['draft', 'published'])], - 'published_at' => ['nullable', 'date'], - 'expires_at' => ['nullable', 'date', 'after:published_at'], - 'data_json' => ['required', 'json'], - ]); - } - - private function uniqueSlug(string $value, ?ContentDocument $except = null): string - { - $base = Str::slug($value) ?: 'content-document'; - $slug = $base; - $suffix = 2; - - while (ContentDocument::withTrashed()->where('slug', $slug)->when($except, fn ($query) => $query->whereKeyNot($except->getKey()))->exists()) { - $slug = $base.'-'.$suffix++; - } - - return $slug; - } -} diff --git a/Content/Http/Controllers/PageCompositionController.php b/Content/Http/Controllers/PageCompositionController.php deleted file mode 100644 index d3c21ad..0000000 --- a/Content/Http/Controllers/PageCompositionController.php +++ /dev/null @@ -1,189 +0,0 @@ -with('document.type') - ->where('page_key', $pageKey) - ->orderByRaw("CASE region WHEN 'header' THEN 1 WHEN 'body' THEN 2 ELSE 3 END") - ->orderBy('sort_order') - ->get() - ->groupBy('region'); - - $documents = ContentDocument::query() - ->with('type') - ->withCount('placements') - ->orderBy('title') - ->get(); - - return view('content::admin.composition', compact('pageKey', 'placements', 'documents')); - } - - - public function editPage(Page $page): View - { - $placements = PageContent::query() - ->with('document.type') - ->where(fn ($query) => $query->where('page_id', $page->id)->orWhere('page_key', $page->slug)) - ->orderByRaw("CASE region WHEN 'header' THEN 1 WHEN 'body' THEN 2 ELSE 3 END") - ->orderBy('sort_order') - ->get(); - - PageContent::query() - ->whereNull('page_id') - ->where('page_key', $page->slug) - ->update(['page_id' => $page->id]); - - $documents = ContentDocument::query() - ->with('type') - ->withCount('placements') - ->orderBy('title') - ->get(); - - return view('content::admin.composition', [ - 'pageKey' => $page->slug, - 'page' => $page, - 'placements' => $placements->groupBy('region'), - 'documents' => $documents, - ]); - } - - public function preview(string $pageKey): View - { - abort_unless($pageKey === 'home', 404, 'This phase currently provides the Saasland homepage preview.'); - - return view('stylist-theme-saasland::home', ['pageKey' => $pageKey]); - } - - public function store(Request $request, string $pageKey): RedirectResponse - { - $validated = $request->validate([ - 'content_document_id' => ['required', 'integer', Rule::exists('content_documents', 'id')->whereNull('deleted_at')], - 'region' => ['required', Rule::in(self::REGIONS)], - ]); - - $document = ContentDocument::query()->with('type')->findOrFail($validated['content_document_id']); - abort_unless(in_array($validated['region'], $document->type?->allowed_regions ?? [], true), 422, 'This content type is not allowed in the selected region.'); - - $page = Page::query()->where('slug', $pageKey)->first(); - $nextOrder = ((int) PageContent::query()->where(fn ($query) => $query->where('page_key', $pageKey)->when($page, fn ($q) => $q->orWhere('page_id', $page->id)))->where('region', $validated['region'])->max('sort_order')) + 10; - PageContent::query()->create([ - 'page_id' => $page?->id, - 'page_key' => $pageKey, - 'content_document_id' => $document->id, - 'region' => $validated['region'], - 'sort_order' => $nextOrder, - 'is_enabled' => true, - ]); - - return back()->with('status', 'Document added to the '.$validated['region'].' region.'); - } - - public function update(Request $request, PageContent $placement): RedirectResponse - { - $validated = $request->validate([ - 'is_enabled' => ['nullable', 'boolean'], - 'starts_at' => ['nullable', 'date'], - 'ends_at' => ['nullable', 'date', 'after:starts_at'], - ]); - - $placement->update([ - 'is_enabled' => $request->boolean('is_enabled'), - 'starts_at' => $validated['starts_at'] ?? null, - 'ends_at' => $validated['ends_at'] ?? null, - ]); - - return back()->with('status', 'Page placement updated.'); - } - - public function destroy(PageContent $placement): RedirectResponse - { - $placement->delete(); - $this->normaliseRegion($placement->page_key, $placement->region); - - return back()->with('status', 'Document removed from this page. The reusable document was not deleted.'); - } - - public function duplicate(Request $request, PageContent $placement): RedirectResponse - { - $source = $placement->document()->firstOrFail(); - $copy = DB::transaction(function () use ($request, $placement, $source): ContentDocument { - $copy = $source->replicate(['slug', 'status', 'published_at', 'created_by', 'updated_by']); - $copy->title = $source->title.' copy'; - $copy->slug = $source->slug.'-copy-'.strtolower(str()->random(6)); - $copy->status = 'draft'; - $copy->published_at = null; - $copy->created_by = $request->user()?->id; - $copy->updated_by = $request->user()?->id; - $copy->save(); - - PageContent::query()->create([ - 'page_id' => $placement->page_id, - 'page_key' => $placement->page_key, - 'content_document_id' => $copy->id, - 'region' => $placement->region, - 'sort_order' => $placement->sort_order + 5, - 'is_enabled' => false, - 'visibility_rules' => $placement->visibility_rules, - 'presentation' => $placement->presentation, - ]); - - return $copy; - }); - - $this->normaliseRegion($placement->page_key, $placement->region); - - return redirect()->route('content.documents.edit', $copy)->with('status', 'Page-specific draft copy created and added after the original.'); - } - - public function reorder(Request $request, string $pageKey): JsonResponse|RedirectResponse - { - $validated = $request->validate([ - 'region' => ['required', Rule::in(self::REGIONS)], - 'placements' => ['required', 'array'], - 'placements.*' => ['required', 'integer'], - ]); - - $ids = array_values(array_unique(array_map('intval', $validated['placements']))); - $owned = PageContent::query()->where('page_key', $pageKey)->where('region', $validated['region'])->whereIn('id', $ids)->pluck('id')->all(); - abort_unless(count($owned) === count($ids), 422, 'One or more placements do not belong to this page region.'); - - DB::transaction(function () use ($ids): void { - PageContent::query()->whereIn('id', $ids)->increment('sort_order', 100000); - foreach ($ids as $index => $id) { - PageContent::query()->whereKey($id)->update(['sort_order' => ($index + 1) * 10]); - } - }); - - return $request->expectsJson() - ? response()->json(['message' => 'Order saved.']) - : back()->with('status', 'Page order updated.'); - } - - private function normaliseRegion(?string $pageKey, string $region): void - { - $ids = PageContent::query()->where('page_key', $pageKey)->where('region', $region)->orderBy('sort_order')->pluck('id'); - DB::transaction(function () use ($ids): void { - foreach ($ids as $index => $id) { - PageContent::query()->whereKey($id)->update(['sort_order' => ($index + 1) * 10]); - } - }); - } -} diff --git a/Content/Models/ContentDocument.php b/Content/Models/ContentDocument.php deleted file mode 100644 index edf93be..0000000 --- a/Content/Models/ContentDocument.php +++ /dev/null @@ -1,17 +0,0 @@ -'array','published_at'=>'datetime','expires_at'=>'datetime']; } - public function type(): BelongsTo { return $this->belongsTo(ContentType::class, 'content_type_id'); } - public function placements(): HasMany { return $this->hasMany(PageContent::class); } - public function scopePublished($query) { return $query->where('status','published')->where(fn($q) => $q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at','>',now())); } -} diff --git a/Content/Models/ContentType.php b/Content/Models/ContentType.php deleted file mode 100644 index 6b0ffd7..0000000 --- a/Content/Models/ContentType.php +++ /dev/null @@ -1,12 +0,0 @@ -'array','allowed_regions'=>'array','is_system'=>'boolean','is_active'=>'boolean']; } - public function documents(): HasMany { return $this->hasMany(ContentDocument::class); } -} diff --git a/Content/Models/PageContent.php b/Content/Models/PageContent.php deleted file mode 100644 index cb204f3..0000000 --- a/Content/Models/PageContent.php +++ /dev/null @@ -1,15 +0,0 @@ -'boolean','visibility_rules'=>'array','presentation'=>'array','starts_at'=>'datetime','ends_at'=>'datetime']; } - public function document(): BelongsTo { return $this->belongsTo(ContentDocument::class, 'content_document_id'); } - public function page(): BelongsTo { return $this->belongsTo(Page::class); } -} diff --git a/Content/Providers/ContentServiceProvider.php b/Content/Providers/ContentServiceProvider.php deleted file mode 100644 index 0ba0b27..0000000 --- a/Content/Providers/ContentServiceProvider.php +++ /dev/null @@ -1,22 +0,0 @@ -mergeConfigFrom(module_path('Content','Config/config.php'),'content'); - $this->app->singleton(PageRenderer::class); - } - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Content','Database/Migrations')); - $this->loadViewsFrom(module_path('Content','Resources/views'),'content'); - $this->loadRoutesFrom(module_path('Content','Routes/web.php')); - if ($this->app->runningInConsole()) $this->commands([InstallSaaslandContent::class]); - } -} diff --git a/Content/README.md b/Content/README.md deleted file mode 100644 index b1a800d..0000000 --- a/Content/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Content Composer - -The Content module provides reusable typed documents and ordered page regions. - -## Phase 2 features - -- Header, body and footer composition editor. -- Drag-and-drop ordering with server-side ownership validation. -- Add existing published or draft documents to a page. -- Remove a placement without deleting the reusable document. -- Create an independent page-specific draft copy. -- Shared-use warnings and deletion protection. -- Placement enable/disable and date scheduling. -- Content document creation, duplication, filtering and publishing controls. -- Authenticated Saasland homepage preview, including draft labels. -- Content Composer entry in the administration sidebar. - -## Setup - -```bash -php artisan migrate -php artisan content:install-saasland -``` - -Reset the seeded homepage composition when required: - -```bash -php artisan content:install-saasland --force -``` - -Administration: - -- `/admin/content` -- `/admin/content/pages/home` -- `/admin/content/pages/home/preview` - -The public theme remains backward-compatible: when no composition exists, the legacy Saasland homepage, header and footer are rendered. diff --git a/Content/Resources/views/admin/composition.blade.php b/Content/Resources/views/admin/composition.blade.php deleted file mode 100644 index 32a92ce..0000000 --- a/Content/Resources/views/admin/composition.blade.php +++ /dev/null @@ -1,147 +0,0 @@ - -
-
-
-
Pages/Content composer
-

{{ isset($page) ? $page->title : ucfirst($pageKey).' page' }}

-

Add sections, edit their content and drag them into the order visitors should see.

-
-
- @isset($page)Page settings@endisset - Create section - @if(isset($page) && $page->status === 'published')Open live page@endif -
-
- - @if(session('status'))
{{ session('status') }}
@endif - @if($errors->any())
    @foreach($errors->all() as $error)
  • {{ $error }}
  • @endforeach
@endif - -
-
- @foreach(['header', 'body', 'footer'] as $region) -
-
-

{{ ucfirst($region) }}

{{ $region === 'body' ? 'Ordered content sections' : 'Page-level '.$region.' document' }}

- {{ $placements->get($region, collect())->count() }} document{{ $placements->get($region, collect())->count() === 1 ? '' : 's' }} -
-
- @forelse($placements->get($region, collect()) as $placement) -
-
- -
-
-

{{ $placement->document?->title }}

- {{ $placement->document?->type?->name }} - @if(($placement->document?->placements()->count() ?? 0) > 1)Shared@endif - @unless($placement->is_enabled)Disabled@endunless -
-

Order {{ $placement->sort_order }} · {{ $placement->document?->status === 'published' ? 'Published' : 'Draft — hidden from public rendering' }}

-
- Edit -
- -
- Placement settings -
-
- @csrf @method('PUT') - - -
- - Save -
- -
-
-
@csrfCreate page-specific copy
-
@csrf @method('DELETE')Remove from page
-
-
-
- @empty -
No documents assigned to this region.
- @endforelse -
-
- @endforeach -
- - -
-
- -@push('scripts') - -@endpush -
diff --git a/Content/Resources/views/admin/form.blade.php b/Content/Resources/views/admin/form.blade.php deleted file mode 100644 index 4a12768..0000000 --- a/Content/Resources/views/admin/form.blade.php +++ /dev/null @@ -1,636 +0,0 @@ - -
-
-
- - Content documents - -

- {{ $document->exists ? $document->title : 'Create content document' }} -

-

- Edit every frontend field below. JSON is retained only as an emergency advanced tool. -

-
- - @if ($document->exists) -
-
- @csrf - Create independent copy -
- -
- @csrf - @method('DELETE') - Delete -
-
- @endif -
- - @if (session('status')) -
- {{ session('status') }} -
- @endif - - @if ($errors->any()) -
-
    - @foreach ($errors->all() as $error) -
  • {{ $error }}
  • - @endforeach -
-
- @endif - - @if ($document->exists && $document->placements->count() > 1) -
- Shared document. - Changes affect all {{ $document->placements->count() }} placements. -
- @endif - -
- @csrf - @if ($document->exists) - @method('PUT') - @endif - -
- -

Document

- - - - @foreach ($types as $type) - - @endforeach - -
- - -
-
-

Frontend content

-

- Switches take effect when this document is saved. -

-
- -
- -
- -
- Advanced JSON (emergency use only) -

- Normal editing should be performed with the controls above. -

- -
-
-
- - -
-
- - -
diff --git a/Content/Resources/views/admin/index.blade.php b/Content/Resources/views/admin/index.blade.php deleted file mode 100644 index c219cb4..0000000 --- a/Content/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,53 +0,0 @@ - -
-
-
-

Content documents

-

Create and reuse typed content across page regions.

-
-
- Compose homepage - Create document -
-
- - @if(session('status')) -
{{ session('status') }}
- @endif - @error('document')
{{ $message }}
@enderror - -
- - - - @foreach($types as $type)@endforeach - - - - - - -
FilterReset
- - -
- - - - @forelse($documents as $document) - - - - - - - - @empty - - @endforelse - -
DocumentTypeStatusUsesActions
{{ $document->title }}
{{ $document->slug }}
{{ $document->type?->name }}{{ ucfirst($document->status) }}{{ $document->placements_count }}Edit
No content documents match the current filters.
-
- {{ $documents->links() }} -
-
diff --git a/Content/Resources/views/documents/unsupported.blade.php b/Content/Resources/views/documents/unsupported.blade.php deleted file mode 100644 index b80545d..0000000 --- a/Content/Resources/views/documents/unsupported.blade.php +++ /dev/null @@ -1,3 +0,0 @@ -@if(app()->hasDebugModeEnabled()) - -@endif diff --git a/Content/Routes/web.php b/Content/Routes/web.php deleted file mode 100644 index 910c7f6..0000000 --- a/Content/Routes/web.php +++ /dev/null @@ -1,24 +0,0 @@ -prefix('admin/content')->name('content.')->group(function (): void { - Route::get('/', [ContentDocumentController::class, 'index'])->name('index'); - Route::get('/documents/create', [ContentDocumentController::class, 'create'])->name('documents.create'); - Route::post('/documents', [ContentDocumentController::class, 'store'])->name('documents.store'); - Route::get('/documents/{document}/edit', [ContentDocumentController::class, 'edit'])->name('documents.edit'); - Route::put('/documents/{document}', [ContentDocumentController::class, 'update'])->name('documents.update'); - Route::post('/documents/{document}/duplicate', [ContentDocumentController::class, 'duplicate'])->name('documents.duplicate'); - Route::delete('/documents/{document}', [ContentDocumentController::class, 'destroy'])->name('documents.destroy'); - - Route::get('/page/{page}/compose', [PageCompositionController::class, 'editPage'])->name('pages.page.edit'); - Route::get('/pages/{pageKey}', [PageCompositionController::class, 'edit'])->name('pages.edit'); - Route::get('/pages/{pageKey}/preview', [PageCompositionController::class, 'preview'])->name('pages.preview'); - Route::post('/pages/{pageKey}/placements', [PageCompositionController::class, 'store'])->name('placements.store'); - Route::put('/placements/{placement}', [PageCompositionController::class, 'update'])->name('placements.update'); - Route::delete('/placements/{placement}', [PageCompositionController::class, 'destroy'])->name('placements.destroy'); - Route::post('/placements/{placement}/duplicate', [PageCompositionController::class, 'duplicate'])->name('placements.duplicate'); - Route::post('/pages/{pageKey}/reorder', [PageCompositionController::class, 'reorder'])->name('placements.reorder'); -}); diff --git a/Content/Services/PageRenderer.php b/Content/Services/PageRenderer.php deleted file mode 100644 index d02c898..0000000 --- a/Content/Services/PageRenderer.php +++ /dev/null @@ -1,83 +0,0 @@ -where('page_key', $pageKey)->where('is_enabled', true)->exists(); - } catch (Throwable) { - return false; - } - } - - public function renderRegion(string $pageKey, string $region): string - { - if (! $this->hasComposition($pageKey)) { - return ''; - } - - $preview = Route::currentRouteNamed('content.pages.preview'); - $placements = PageContent::query() - ->with('document.type') - ->where('page_key', $pageKey) - ->where('region', $region) - ->where('is_enabled', true) - ->where(fn ($query) => $query->whereNull('starts_at')->orWhere('starts_at', '<=', now())) - ->where(fn ($query) => $query->whereNull('ends_at')->orWhere('ends_at', '>', now())) - ->orderBy('sort_order') - ->get(); - - return $placements->map(function (PageContent $placement) use ($preview): string { - $document = $placement->document; - if (! $document || ! $document->type || ! $this->isRenderable($document, $preview)) { - return ''; - } - - $view = 'stylist-theme-saasland::documents.'.$document->type->renderer; - if (! View::exists($view)) { - $view = 'content::documents.unsupported'; - } - - $rendered = View::make($view, [ - 'document' => $document, - 'data' => $document->data ?? [], - 'placement' => $placement, - 'isCompositionPreview' => $preview, - ])->render(); - - if ($preview && $document->status !== 'published') { - return '
Draft preview
'.$rendered.'
'; - } - - return $rendered; - })->implode("\n"); - } - - private function isRenderable(ContentDocument $document, bool $preview): bool - { - if ($preview) { - return true; - } - - if ($document->status !== 'published') { - return false; - } - - if ($document->published_at?->isFuture()) { - return false; - } - - return ! $document->expires_at?->isPast(); - } -} diff --git a/Content/module.json b/Content/module.json deleted file mode 100644 index e34671c..0000000 --- a/Content/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Content", - "alias": "content", - "version": "1.0.0", - "description": "Composable typed content documents and page regions.", - "keywords": ["cms", "content", "regions", "composition"], - "priority": 22, - "providers": ["Modules\\Content\\Providers\\ContentServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Pages", "Stylist"] -} diff --git a/Content/views/admin/form.blade.php b/Content/views/admin/form.blade.php deleted file mode 100644 index 47ae90d..0000000 --- a/Content/views/admin/form.blade.php +++ /dev/null @@ -1,399 +0,0 @@ - -
-
-
- - Content documents - -

- {{ $document->exists ? $document->title : 'Create content document' }} -

-

- Edit every frontend field below. JSON is retained only as an emergency advanced tool. -

-
- - @if ($document->exists) -
-
- @csrf - Create independent copy -
- -
- @csrf - @method('DELETE') - Delete -
-
- @endif -
- - @if (session('status')) -
- {{ session('status') }} -
- @endif - - @if ($errors->any()) -
-
    - @foreach ($errors->all() as $error) -
  • {{ $error }}
  • - @endforeach -
-
- @endif - - @if ($document->exists && $document->placements->count() > 1) -
- Shared document. - Changes affect all {{ $document->placements->count() }} placements. -
- @endif - -
- @csrf - @if ($document->exists) - @method('PUT') - @endif - -
- -

Document

- - - - @foreach ($types as $type) - - @endforeach - -
- - -
-
-

Frontend content

-

- Switches take effect when this document is saved. -

-
- -
- -
- -
- Advanced JSON (emergency use only) -

- Normal editing should be performed with the controls above. -

- -
-
-
- - -
-
- - -
diff --git a/Core/Config/config.php b/Core/Config/config.php deleted file mode 100644 index c00cc12..0000000 --- a/Core/Config/config.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Core', - 'enabled' => true, -]; diff --git a/Core/Contracts/ModuleRegistry.php b/Core/Contracts/ModuleRegistry.php deleted file mode 100644 index e05ca63..0000000 --- a/Core/Contracts/ModuleRegistry.php +++ /dev/null @@ -1,11 +0,0 @@ - */ - public function all(): array; - - public function enabled(string $name): bool; -} diff --git a/Core/Providers/CoreServiceProvider.php b/Core/Providers/CoreServiceProvider.php deleted file mode 100644 index cee3091..0000000 --- a/Core/Providers/CoreServiceProvider.php +++ /dev/null @@ -1,24 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->app->singleton(ModuleRegistry::class, LaravelModuleRegistry::class); - $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); - $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower()); - } -} diff --git a/Core/Providers/RouteServiceProvider.php b/Core/Providers/RouteServiceProvider.php deleted file mode 100644 index d400e95..0000000 --- a/Core/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,17 +0,0 @@ -routes(function (): void { - Route::middleware('web')->group(module_path('Core', 'Routes/web.php')); - }); - } -} diff --git a/Core/Routes/web.php b/Core/Routes/web.php deleted file mode 100644 index 4c3fffc..0000000 --- a/Core/Routes/web.php +++ /dev/null @@ -1,3 +0,0 @@ - $name, - 'alias' => (string) ($data['alias'] ?? strtolower($name)), - 'version' => (string) ($data['version'] ?? '0.0.0'), - 'enabled' => (bool) ($statuses[$name] ?? false), - ]; - } - usort($modules, fn (array $a, array $b): int => $a['name'] <=> $b['name']); - - return $modules; - } - - public function enabled(string $name): bool - { - foreach ($this->all() as $module) { - if (strcasecmp($module['name'], $name) === 0) { - return $module['enabled']; - } - } - - return false; - } -} diff --git a/Core/Support/ModuleInfo.php b/Core/Support/ModuleInfo.php deleted file mode 100644 index 5710024..0000000 --- a/Core/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBe('Core'); -}); diff --git a/Core/composer.json b/Core/composer.json deleted file mode 100644 index ca9d3b5..0000000 --- a/Core/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "absdev/core-module", - "description": "Core foundation module for the ABSDEV Laravel application platform.", - "type": "laravel-module", - "autoload": { - "psr-4": { - "Modules\\Core\\": "" - } - } -} diff --git a/Core/module.json b/Core/module.json deleted file mode 100644 index 0affe62..0000000 --- a/Core/module.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "Core", - "alias": "core", - "version": "1.0.0", - "description": "Core foundation module for the ABSDEV Laravel application platform.", - "keywords": [ - "laravel", - "modules", - "core" - ], - "priority": 100, - "providers": [ - "Modules\\Core\\Providers\\CoreServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [] -} diff --git a/Developer/Config/config.php b/Developer/Config/config.php deleted file mode 100644 index f3bc1fc..0000000 --- a/Developer/Config/config.php +++ /dev/null @@ -1,8 +0,0 @@ - 'Modules', - 'modules_path' => base_path('Modules'), - 'themes_path' => resource_path('themes'), - 'overwrite' => false, -]; diff --git a/Developer/Console/Commands/InspectModulesCommand.php b/Developer/Console/Commands/InspectModulesCommand.php deleted file mode 100644 index 11f3078..0000000 --- a/Developer/Console/Commands/InspectModulesCommand.php +++ /dev/null @@ -1,44 +0,0 @@ -argument('name'); - $rows = []; - foreach ($files as $file) { - $manifest = json_decode((string) file_get_contents($file), true); - if (! is_array($manifest)) { - $this->error("Invalid JSON: {$file}"); - - return self::FAILURE; - } - if ($name && strcasecmp((string) ($manifest['name'] ?? ''), (string) $name) !== 0) { - continue; - } - $rows[] = [ - 'name' => $manifest['name'] ?? basename(dirname($file)), - 'version' => $manifest['version'] ?? 'unknown', - 'requires' => implode(', ', $manifest['requires'] ?? []), - 'providers' => count($manifest['providers'] ?? []), - ]; - } - if ($this->option('json')) { - $this->line(json_encode($rows, JSON_PRETTY_PRINT)); - - return self::SUCCESS; - } - $this->table(['Module', 'Version', 'Requires', 'Providers'], $rows); - - return self::SUCCESS; - } -} diff --git a/Developer/Console/Commands/MakeCrudCommand.php b/Developer/Console/Commands/MakeCrudCommand.php deleted file mode 100644 index d615a48..0000000 --- a/Developer/Console/Commands/MakeCrudCommand.php +++ /dev/null @@ -1,37 +0,0 @@ -option('module') ?: $this->choice('Select the target module', $this->modules()); - try { - $files = $generator->generate((string) $module, $this->argument('model'), (bool) $this->option('force')); - } catch (Throwable $e) { - $this->error($e->getMessage()); - - return self::FAILURE; - } - foreach ($files as $file) { - $this->line("created {$file}"); - } - $this->warn('Register the generated controller routes in the target module Routes/web.php file.'); - - return self::SUCCESS; - } - - private function modules(): array - { - return collect(glob(base_path('Modules/*/module.json')) ?: [])->map(fn (string $file) => basename(dirname($file)))->sort()->values()->all(); - } -} diff --git a/Developer/Console/Commands/MakeModuleCommand.php b/Developer/Console/Commands/MakeModuleCommand.php deleted file mode 100644 index 34ece37..0000000 --- a/Developer/Console/Commands/MakeModuleCommand.php +++ /dev/null @@ -1,32 +0,0 @@ -generate($this->argument('name'), (bool) $this->option('force')); - } catch (Throwable $e) { - $this->error($e->getMessage()); - - return self::FAILURE; - } - foreach ($files as $file) { - $this->line("created {$file}"); - } - $this->newLine(); - $this->info('Enable the module in modules_statuses.json, then run composer dump-autoload.'); - - return self::SUCCESS; - } -} diff --git a/Developer/Console/Commands/MakeThemeCommand.php b/Developer/Console/Commands/MakeThemeCommand.php deleted file mode 100644 index 2457559..0000000 --- a/Developer/Console/Commands/MakeThemeCommand.php +++ /dev/null @@ -1,30 +0,0 @@ -generate($this->argument('name'), (bool) $this->option('force')); - } catch (Throwable $e) { - $this->error($e->getMessage()); - - return self::FAILURE; - } - foreach ($files as $file) { - $this->line("created {$file}"); - } - - return self::SUCCESS; - } -} diff --git a/Developer/Http/Controllers/DeveloperController.php b/Developer/Http/Controllers/DeveloperController.php deleted file mode 100644 index d921e99..0000000 --- a/Developer/Http/Controllers/DeveloperController.php +++ /dev/null @@ -1,27 +0,0 @@ -map(function (string $file): array { - $manifest = json_decode((string) file_get_contents($file), true) ?: []; - - return [ - 'name' => $manifest['name'] ?? basename(dirname($file)), - 'version' => $manifest['version'] ?? 'unknown', - 'description' => $manifest['description'] ?? '', - 'requires' => $manifest['requires'] ?? [], - ]; - }) - ->sortBy('name') - ->values(); - - return view('developer::admin.index', compact('modules')); - } -} diff --git a/Developer/Providers/DeveloperServiceProvider.php b/Developer/Providers/DeveloperServiceProvider.php deleted file mode 100644 index a5f09a9..0000000 --- a/Developer/Providers/DeveloperServiceProvider.php +++ /dev/null @@ -1,32 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Developer', 'Config/config.php'), 'developer'); - } - - public function boot(): void - { - $this->loadViewsFrom(module_path('Developer', 'Resources/views'), 'developer'); - - if ($this->app->runningInConsole()) { - $this->commands([ - MakeModuleCommand::class, - MakeCrudCommand::class, - MakeThemeCommand::class, - InspectModulesCommand::class, - ]); - } - } -} diff --git a/Developer/Providers/RouteServiceProvider.php b/Developer/Providers/RouteServiceProvider.php deleted file mode 100644 index 50cbdde..0000000 --- a/Developer/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,14 +0,0 @@ -group(module_path('Developer', 'Routes/web.php')); - } -} diff --git a/Developer/Resources/views/admin/index.blade.php b/Developer/Resources/views/admin/index.blade.php deleted file mode 100644 index ca9fe9f..0000000 --- a/Developer/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,26 +0,0 @@ - -
-
-

{{ __('Developer Tools') }}

-

{{ __('Generate modules, CRUD resources and themes using the platform conventions.') }}

-
- -
-

{{ __('Module Generator') }}

php artisan module:make Products
-

{{ __('CRUD Generator') }}

php artisan crud:make Product --module=Products
-

{{ __('Theme Generator') }}

php artisan theme:make Corporate
-
- -
-
{{ __('Installed modules') }}
-
- @foreach ($modules as $module) -
-
{{ $module['name'] }}
{{ $module['description'] }}
-
v{{ $module['version'] }}
{{ implode(', ', $module['requires']) }}
-
- @endforeach -
-
-
-
diff --git a/Developer/Routes/web.php b/Developer/Routes/web.php deleted file mode 100644 index 5a8712a..0000000 --- a/Developer/Routes/web.php +++ /dev/null @@ -1,11 +0,0 @@ -prefix('administration/developer') - ->name('administration.developer.') - ->group(function (): void { - Route::get('/', DeveloperController::class)->name('index'); - }); diff --git a/Developer/Support/CrudGenerator.php b/Developer/Support/CrudGenerator.php deleted file mode 100644 index 90db77c..0000000 --- a/Developer/Support/CrudGenerator.php +++ /dev/null @@ -1,69 +0,0 @@ -studly($module); - $model = $this->studly($model); - $table = str($model)->plural()->snake()->toString(); - $route = str($model)->plural()->kebab()->toString(); - $root = rtrim(config('developer.modules_path', base_path('Modules')), '/').'/'.$module; - $timestamp = now()->format('Y_m_d_His'); - - $files = [ - "$root/Models/{$model}.php" => $this->model($module, $model), - "$root/Http/Controllers/{$model}Controller.php" => $this->controller($module, $model), - "$root/Http/Requests/Store{$model}Request.php" => $this->request($module, $model), - "$root/Database/Migrations/{$timestamp}_create_{$table}_table.php" => $this->migration($table), - "$root/Resources/views/admin/{$route}/index.blade.php" => $this->indexView($model, $route), - "$root/Resources/views/admin/{$route}/form.blade.php" => $this->formView($model), - "$root/Tests/Feature/{$model}CrudTest.php" => $this->crudTest($module, $model), - ]; - - foreach ($files as $path => $contents) { - $this->write($path, $contents, $force); - } - - return array_keys($files); - } - - private function model(string $module, string $model): string - { - return " 'boolean'];\n }\n}\n"; - } - - private function controller(string $module, string $model): string - { - $plural = str($model)->plural()->kebab(); - - return "snake()."::admin.{$plural}.index', ['records' => {$model}::query()->latest()->paginate()]);\n }\n\n public function store(Store{$model}Request \$request): RedirectResponse\n {\n {$model}::query()->create(\$request->validated());\n return back()->with('status', '{$model} created.');\n }\n}\n"; - } - - private function request(string $module, string $model): string - { - return "user() !== null; }\n public function rules(): array { return ['name' => ['required', 'string', 'max:255'], 'is_active' => ['sometimes', 'boolean']]; }\n}\n"; - } - - private function migration(string $table): string - { - return "id(); \$table->string('name'); \$table->boolean('is_active')->default(true); \$table->timestamps(); }); }\n public function down(): void { Schema::dropIfExists('{$table}'); }\n};\n"; - } - - private function indexView(string $model, string $route): string - { - return "\n

{$model}

@foreach (\$records as \$record)
{{ \$record->name }}
@endforeach{{ \$records->links() }}
\n
\n"; - } - - private function formView(string $model): string - { - return "
@csrf{{ __('Save {$model}') }}\n"; - } - - private function crudTest(string $module, string $model): string - { - return "create(['name' => 'Example']))->toBeInstanceOf({$model}::class); });\n"; - } -} diff --git a/Developer/Support/Generator.php b/Developer/Support/Generator.php deleted file mode 100644 index a8c8bf8..0000000 --- a/Developer/Support/Generator.php +++ /dev/null @@ -1,43 +0,0 @@ -files->isDirectory($path)) { - $this->files->makeDirectory($path, 0755, true); - } - } - - protected function write(string $path, string $contents, bool $force = false): void - { - if ($this->files->exists($path) && ! $force) { - throw new RuntimeException("File already exists: {$path}"); - } - - $this->createDirectory(dirname($path)); - $this->files->put($path, $contents); - } - - protected function studly(string $value): string - { - return str($value)->studly()->toString(); - } - - protected function snake(string $value): string - { - return str($value)->snake()->toString(); - } - - protected function kebab(string $value): string - { - return str($value)->kebab()->toString(); - } -} diff --git a/Developer/Support/ModuleGenerator.php b/Developer/Support/ModuleGenerator.php deleted file mode 100644 index a9668e3..0000000 --- a/Developer/Support/ModuleGenerator.php +++ /dev/null @@ -1,55 +0,0 @@ -studly($name); - $alias = $this->kebab($name); - $root = rtrim(config('developer.modules_path', base_path('Modules')), '/').'/'.$name; - $provider = "Modules\\{$name}\\Providers\\{$name}ServiceProvider"; - - $files = [ - "$root/module.json" => json_encode([ - 'name' => $name, - 'alias' => $alias, - 'version' => '1.0.0', - 'description' => "{$name} module.", - 'keywords' => [$alias], - 'priority' => 100, - 'providers' => [$provider], - 'aliases' => (object) [], - 'files' => [], - 'requires' => ['Core', 'Administration'], - ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL, - "$root/Config/config.php" => " '{$name}'];\n", - "$root/Providers/{$name}ServiceProvider.php" => $this->provider($name), - "$root/Providers/RouteServiceProvider.php" => $this->routeProvider($name), - "$root/Routes/web.php" => "prefix('administration/{$alias}')->name('administration.{$alias}.')->group(function (): void {\n // Register module routes here.\n});\n", - "$root/Tests/Feature/{$name}ModuleBootTest.php" => $this->test($name), - ]; - - foreach ($files as $path => $contents) { - $this->write($path, $contents, $force); - } - - return array_keys($files); - } - - private function provider(string $name): string - { - return "app->register(RouteServiceProvider::class);\n \$this->mergeConfigFrom(module_path('{$name}', 'Config/config.php'), '".str($name)->snake()."');\n }\n\n public function boot(): void\n {\n \$this->loadMigrationsFrom(module_path('{$name}', 'Database/Migrations'));\n \$this->loadViewsFrom(module_path('{$name}', 'Resources/views'), '".str($name)->snake()."');\n }\n}\n"; - } - - private function routeProvider(string $name): string - { - return "group(module_path('{$name}', 'Routes/web.php'));\n }\n}\n"; - } - - private function test(string $name): string - { - return "providerIsLoaded(Modules\\{$name}\\Providers\\{$name}ServiceProvider::class))->toBeTrue();\n});\n"; - } -} diff --git a/Developer/Support/ThemeGenerator.php b/Developer/Support/ThemeGenerator.php deleted file mode 100644 index dd1de18..0000000 --- a/Developer/Support/ThemeGenerator.php +++ /dev/null @@ -1,24 +0,0 @@ -kebab($name); - $root = rtrim(config('developer.themes_path', resource_path('themes')), '/').'/'.$name; - $files = [ - "$root/theme.json" => json_encode(['name' => str($name)->headline()->toString(), 'slug' => $name, 'version' => '1.0.0', 'parent' => 'common'], JSON_PRETTY_PRINT).PHP_EOL, - "$root/views/layouts/app.blade.php" => "getLocale()) }}\">{{ \$title ?? config('app.name') }}@vite(['resources/css/app.css', 'resources/js/app.js']){{ \$slot }}\n", - "$root/views/home.blade.php" => "

{{ config('app.name') }}

\n", - "$root/assets/css/theme.css" => ":root { --theme-radius: 0.5rem; }\n", - "$root/README.md" => '# '.str($name)->headline()." Theme\n\nGenerated by `php artisan theme:make {$name}`.\n", - ]; - foreach ($files as $path => $contents) { - $this->write($path, $contents, $force); - } - - return array_keys($files); - } -} diff --git a/Developer/Tests/Feature/DeveloperModuleBootTest.php b/Developer/Tests/Feature/DeveloperModuleBootTest.php deleted file mode 100644 index 80f181c..0000000 --- a/Developer/Tests/Feature/DeveloperModuleBootTest.php +++ /dev/null @@ -1,11 +0,0 @@ -providerIsLoaded(DeveloperServiceProvider::class))->toBeTrue(); -}); - -it('registers developer commands', function (): void { - $this->artisan('module:inspect')->assertSuccessful(); -}); diff --git a/Developer/module.json b/Developer/module.json deleted file mode 100644 index 7df5745..0000000 --- a/Developer/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Developer", - "alias": "developer", - "version": "1.0.0", - "description": "Developer generators, module inspection and scaffolding tools.", - "keywords": ["developer", "generator", "scaffold"], - "priority": 80, - "providers": ["Modules\\Developer\\Providers\\DeveloperServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Documents/Config/config.php b/Documents/Config/config.php deleted file mode 100644 index f780704..0000000 --- a/Documents/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Documents']; diff --git a/Documents/Database/Migrations/2026_07_24_000500_create_documents_table.php b/Documents/Database/Migrations/2026_07_24_000500_create_documents_table.php deleted file mode 100644 index 87aaf16..0000000 --- a/Documents/Database/Migrations/2026_07_24_000500_create_documents_table.php +++ /dev/null @@ -1,26 +0,0 @@ -id(); - $table->string('title'); - $table->text('description')->nullable(); - $table->string('file_path')->nullable(); - $table->boolean('is_public')->default(true); - $table->timestamps(); - $table->softDeletes(); - }); - } - - public function down(): void - { - Schema::dropIfExists('documents'); - } -}; diff --git a/Documents/Http/Controllers/DocumentController.php b/Documents/Http/Controllers/DocumentController.php deleted file mode 100644 index ba1fc4e..0000000 --- a/Documents/Http/Controllers/DocumentController.php +++ /dev/null @@ -1,58 +0,0 @@ - Document::query()->latest()->paginate(20)]); - } - - public function create(): View - { - return view('documents::admin.form'); - } - - public function store(Request $request): RedirectResponse - { - $data = $request->validate(['title' => ['required', 'string', 'max:255'], - 'description' => ['nullable', 'string'], - 'file_path' => ['nullable', 'string'], - 'is_public' => ['sometimes', 'boolean']]); - $data['is_public'] = $request->boolean('is_public'); - Document::create($data); - - return redirect()->route('administration.documents.index')->with('status', 'Document created.'); - } - - public function edit(Document $document): View - { - return view('documents::admin.form', ['record' => $document]); - } - - public function update(Request $request, Document $document): RedirectResponse - { - $data = $request->validate(['title' => ['required', 'string', 'max:255'], - 'description' => ['nullable', 'string'], - 'file_path' => ['nullable', 'string'], - 'is_public' => ['sometimes', 'boolean']]); - $data['is_public'] = $request->boolean('is_public'); - $document->update($data); - - return redirect()->route('administration.documents.index')->with('status', 'Document updated.'); - } - - public function destroy(Document $document): RedirectResponse - { - $document->delete(); - - return back()->with('status', 'Document deleted.'); - } -} diff --git a/Documents/Models/Document.php b/Documents/Models/Document.php deleted file mode 100644 index 2d9d2cf..0000000 --- a/Documents/Models/Document.php +++ /dev/null @@ -1,23 +0,0 @@ - 'boolean', - ]; - } -} diff --git a/Documents/Providers/DocumentsServiceProvider.php b/Documents/Providers/DocumentsServiceProvider.php deleted file mode 100644 index 5e19ae9..0000000 --- a/Documents/Providers/DocumentsServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Documents', 'Config/config.php'), 'documents'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Documents', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Documents', 'Resources/views'), 'documents'); - } -} diff --git a/Documents/Providers/RouteServiceProvider.php b/Documents/Providers/RouteServiceProvider.php deleted file mode 100644 index 7c25a1d..0000000 --- a/Documents/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -routes(fn () => Route::middleware('web')->group(module_path('Documents', 'Routes/web.php'))); - } -} diff --git a/Documents/Resources/views/admin/form.blade.php b/Documents/Resources/views/admin/form.blade.php deleted file mode 100644 index d94c9d1..0000000 --- a/Documents/Resources/views/admin/form.blade.php +++ /dev/null @@ -1,8 +0,0 @@ - -

{{ isset($record) ? 'Edit' : 'Create' }} Document

@csrf @isset($record) @method('PUT') @endisset -
@error('title')

{{ $message }}

@enderror
-
@error('description')

{{ $message }}

@enderror
-
@error('file_path')

{{ $message }}

@enderror
- -
Cancel
-
diff --git a/Documents/Resources/views/admin/index.blade.php b/Documents/Resources/views/admin/index.blade.php deleted file mode 100644 index f79986e..0000000 --- a/Documents/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,5 +0,0 @@ - -

Documents

Manage documents content.

Add new
-@if(session('status'))
{{ session('status') }}
@endif -
@forelse($records as $record)@empty@endforelse
TitleDescriptionFile PathActions
{{ is_array($record->title) ? json_encode($record->title) : Str::limit((string) $record->title, 70) }}{{ is_array($record->description) ? json_encode($record->description) : Str::limit((string) $record->description, 70) }}{{ is_array($record->file_path) ? json_encode($record->file_path) : Str::limit((string) $record->file_path, 70) }}Edit
@csrf @method('DELETE')
No records yet.
{{ $records->links() }}
-
diff --git a/Documents/Routes/web.php b/Documents/Routes/web.php deleted file mode 100644 index 675d903..0000000 --- a/Documents/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/documents')->name('administration.documents.')->group(function (): void { - Route::get('/', [DocumentController::class, 'index'])->name('index'); - Route::get('/create', [DocumentController::class, 'create'])->name('create'); - Route::post('/', [DocumentController::class, 'store'])->name('store'); - Route::get('/{document}/edit', [DocumentController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{document}', [DocumentController::class, 'update'])->name('update'); - Route::delete('/{document}', [DocumentController::class, 'destroy'])->name('destroy'); -}); diff --git a/Documents/Support/ModuleInfo.php b/Documents/Support/ModuleInfo.php deleted file mode 100644 index c815ec2..0000000 --- a/Documents/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBeTrue(); -}); diff --git a/Documents/module.json b/Documents/module.json deleted file mode 100644 index 318fef2..0000000 --- a/Documents/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Documents", - "alias": "documents", - "version": "1.0.0", - "description": "Documents CMS module.", - "keywords": [ - "cms", - "documents" - ], - "priority": 25, - "providers": [ - "Modules\\Documents\\Providers\\DocumentsServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core", - "Administration" - ] -} diff --git a/Enterprise/Config/config.php b/Enterprise/Config/config.php deleted file mode 100644 index 2dd4c3a..0000000 --- a/Enterprise/Config/config.php +++ /dev/null @@ -1,11 +0,0 @@ - env('ENTERPRISE_BACKUP_DISK', 'local'), - 'backup_path' => env('ENTERPRISE_BACKUP_PATH', 'backups'), - 'retention_days' => (int) env('ENTERPRISE_BACKUP_RETENTION_DAYS', 30), - 'health' => [ - 'minimum_free_disk_mb' => (int) env('ENTERPRISE_MIN_FREE_DISK_MB', 512), - 'maximum_failed_jobs' => (int) env('ENTERPRISE_MAX_FAILED_JOBS', 0), - ], -]; diff --git a/Enterprise/Console/BackupCreateCommand.php b/Enterprise/Console/BackupCreateCommand.php deleted file mode 100644 index 3492f0c..0000000 --- a/Enterprise/Console/BackupCreateCommand.php +++ /dev/null @@ -1,21 +0,0 @@ -create(); - $this->info('Backup created: '.$backup->path); - - return self::SUCCESS; - } -} diff --git a/Enterprise/Console/BackupPruneCommand.php b/Enterprise/Console/BackupPruneCommand.php deleted file mode 100644 index bd7fbf4..0000000 --- a/Enterprise/Console/BackupPruneCommand.php +++ /dev/null @@ -1,20 +0,0 @@ -info($service->prune().' backup(s) pruned.'); - - return self::SUCCESS; - } -} diff --git a/Enterprise/Console/HealthCheckCommand.php b/Enterprise/Console/HealthCheckCommand.php deleted file mode 100644 index 40d1b07..0000000 --- a/Enterprise/Console/HealthCheckCommand.php +++ /dev/null @@ -1,28 +0,0 @@ -run(); - HealthRun::create(['status' => $result['status'], 'results' => $result, 'ran_at' => now()]); - if ($this->option('json')) { - $this->line(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - } else { - $this->table(['Check', 'Status', 'Message'], collect($result['checks'])->map(fn (array $check) => [$check['name'], $check['status'], $check['message']])->all()); - $this->line('Overall: '.$result['status']); - } - - return $result['status'] === 'failed' ? self::FAILURE : self::SUCCESS; - } -} diff --git a/Enterprise/Database/Migrations/2026_07_24_100000_create_enterprise_health_runs_table.php b/Enterprise/Database/Migrations/2026_07_24_100000_create_enterprise_health_runs_table.php deleted file mode 100644 index 1413a3f..0000000 --- a/Enterprise/Database/Migrations/2026_07_24_100000_create_enterprise_health_runs_table.php +++ /dev/null @@ -1,25 +0,0 @@ -id(); - $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); - $table->string('status', 20); - $table->json('results'); - $table->timestamp('ran_at'); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('enterprise_health_runs'); - } -}; diff --git a/Enterprise/Database/Migrations/2026_07_24_100100_create_enterprise_backups_table.php b/Enterprise/Database/Migrations/2026_07_24_100100_create_enterprise_backups_table.php deleted file mode 100644 index 4477ca8..0000000 --- a/Enterprise/Database/Migrations/2026_07_24_100100_create_enterprise_backups_table.php +++ /dev/null @@ -1,28 +0,0 @@ -id(); - $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); - $table->string('disk'); - $table->string('path'); - $table->unsignedBigInteger('size')->default(0); - $table->string('status', 20)->default('completed'); - $table->text('message')->nullable(); - $table->timestamp('completed_at')->nullable(); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('enterprise_backups'); - } -}; diff --git a/Enterprise/Http/Controllers/EnterpriseController.php b/Enterprise/Http/Controllers/EnterpriseController.php deleted file mode 100644 index de0182f..0000000 --- a/Enterprise/Http/Controllers/EnterpriseController.php +++ /dev/null @@ -1,82 +0,0 @@ - $health->run(), - 'latestRun' => HealthRun::latest('ran_at')->first(), - 'backups' => Backup::latest()->limit(10)->get(), - 'failedJobs' => DB::getSchemaBuilder()->hasTable('failed_jobs') ? DB::table('failed_jobs')->count() : 0, - 'queueConnection' => config('queue.default'), - 'cacheStore' => config('cache.default'), - 'schedulerConfigured' => file_exists(base_path('routes/console.php')), - 'phpVersion' => PHP_VERSION, - 'laravelVersion' => app()->version(), - ]); - } - - public function runHealth(SystemHealthService $service): RedirectResponse - { - $result = $service->run(); - HealthRun::create(['user_id' => auth()->id(), 'status' => $result['status'], 'results' => $result, 'ran_at' => now()]); - - return back()->with('status', 'Health checks completed.'); - } - - public function clearCache(): RedirectResponse - { - Artisan::call('optimize:clear'); - Cache::flush(); - - return back()->with('status', 'Application and cache stores cleared.'); - } - - public function runScheduler(): RedirectResponse - { - Artisan::call('schedule:run'); - - return back()->with('status', trim(Artisan::output()) ?: 'Scheduler executed.'); - } - - public function retryFailedJobs(): RedirectResponse - { - Artisan::call('queue:retry', ['id' => 'all']); - - return back()->with('status', trim(Artisan::output()) ?: 'Failed jobs queued for retry.'); - } - - public function createBackup(BackupService $service): RedirectResponse - { - try { - $service->create(auth()->id()); - - return back()->with('status', 'Backup created.'); - } catch (Throwable $e) { - report($e); - - return back()->withErrors(['backup' => $e->getMessage()]); - } - } - - public function deleteBackup(Backup $backup, BackupService $service): RedirectResponse - { - $service->delete($backup); - - return back()->with('status', 'Backup deleted.'); - } -} diff --git a/Enterprise/Models/Backup.php b/Enterprise/Models/Backup.php deleted file mode 100644 index 14ecef9..0000000 --- a/Enterprise/Models/Backup.php +++ /dev/null @@ -1,24 +0,0 @@ - 'datetime']; - } - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } -} diff --git a/Enterprise/Models/HealthRun.php b/Enterprise/Models/HealthRun.php deleted file mode 100644 index 34824c2..0000000 --- a/Enterprise/Models/HealthRun.php +++ /dev/null @@ -1,24 +0,0 @@ - 'array', 'ran_at' => 'datetime']; - } - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } -} diff --git a/Enterprise/Providers/EnterpriseServiceProvider.php b/Enterprise/Providers/EnterpriseServiceProvider.php deleted file mode 100644 index a532d0a..0000000 --- a/Enterprise/Providers/EnterpriseServiceProvider.php +++ /dev/null @@ -1,30 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Enterprise', 'Config/config.php'), 'enterprise'); - $this->app->singleton(SystemHealthService::class); - $this->app->singleton(BackupService::class); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Enterprise', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Enterprise', 'Resources/views'), 'enterprise'); - if ($this->app->runningInConsole()) { - $this->commands([HealthCheckCommand::class, BackupCreateCommand::class, BackupPruneCommand::class]); - } - } -} diff --git a/Enterprise/Providers/RouteServiceProvider.php b/Enterprise/Providers/RouteServiceProvider.php deleted file mode 100644 index c1150b7..0000000 --- a/Enterprise/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,14 +0,0 @@ -group(module_path('Enterprise', 'Routes/web.php')); - } -} diff --git a/Enterprise/Resources/views/admin/index.blade.php b/Enterprise/Resources/views/admin/index.blade.php deleted file mode 100644 index d337ff9..0000000 --- a/Enterprise/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,24 +0,0 @@ - -
-

{{ __('Enterprise Services') }}

{{ __('Health, queues, scheduler, cache, backups and production diagnostics.') }}

- @if(session('status')){{ session('status') }}@endif - @if($errors->any()){{ $errors->first() }}@endif - -
-
{{ __('Overall health') }}
{{ $health['status'] }}
-
{{ __('Queue') }}
{{ $queueConnection }}
{{ $failedJobs }} {{ __('failed') }}
-
{{ __('Cache') }}
{{ $cacheStore }}
-
{{ __('Runtime') }}
PHP {{ $phpVersion }}
Laravel {{ $laravelVersion }}
-
- - -

{{ __('Health checks') }}

@csrf{{ __('Run checks') }}
-
@foreach($health['checks'] as $check)
{{ $check['name'] }}{{ $check['status'] }}{{ $check['message'] }}
@endforeach
-
- -
-

{{ __('Operations') }}

@csrf{{ __('Clear cache') }}
@csrf{{ __('Run scheduler') }}
@csrf{{ __('Retry failed jobs') }}
-

{{ __('Backups') }}

@csrf{{ __('Create backup') }}
@forelse($backups as $backup)
{{ $backup->path }}
{{ number_format($backup->size / 1024 / 1024, 2) }} MB · {{ $backup->created_at->diffForHumans() }}
@csrf @method('DELETE'){{ __('Delete') }}
@empty

{{ __('No backups created.') }}

@endforelse
-
-
-
diff --git a/Enterprise/Routes/web.php b/Enterprise/Routes/web.php deleted file mode 100644 index 69e772f..0000000 --- a/Enterprise/Routes/web.php +++ /dev/null @@ -1,18 +0,0 @@ -prefix('administration/enterprise') - ->name('administration.enterprise.') - ->controller(EnterpriseController::class) - ->group(function (): void { - Route::get('/', 'index')->name('index'); - Route::post('/health', 'runHealth')->middleware('permission:enterprise.manage')->name('health.run'); - Route::post('/cache/clear', 'clearCache')->middleware('permission:enterprise.manage')->name('cache.clear'); - Route::post('/scheduler/run', 'runScheduler')->middleware('permission:enterprise.manage')->name('scheduler.run'); - Route::post('/queue/retry', 'retryFailedJobs')->middleware('permission:enterprise.manage')->name('queue.retry'); - Route::post('/backups', 'createBackup')->middleware('permission:enterprise.manage')->name('backups.store'); - Route::delete('/backups/{backup}', 'deleteBackup')->middleware('permission:enterprise.manage')->name('backups.destroy'); - }); diff --git a/Enterprise/Services/BackupService.php b/Enterprise/Services/BackupService.php deleted file mode 100644 index 94463da..0000000 --- a/Enterprise/Services/BackupService.php +++ /dev/null @@ -1,76 +0,0 @@ -format('Ymd-His').'.zip'; - $relativePath = $directory.'/'.$name; - $temporary = storage_path('app/'.$name); - $zip = new ZipArchive; - if ($zip->open($temporary, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { - throw new RuntimeException('Unable to create backup archive.'); - } - - $manifest = json_encode([ - 'application' => config('app.name'), - 'environment' => app()->environment(), - 'created_at' => now()->toIso8601String(), - 'php' => PHP_VERSION, - 'laravel' => app()->version(), - ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - $zip->addFromString('manifest.json', $manifest ?: '{}'); - $zip->addFile(base_path('composer.json'), 'composer.json'); - if (File::exists(base_path('modules_statuses.json'))) { - $zip->addFile(base_path('modules_statuses.json'), 'modules_statuses.json'); - } - if (config('database.default') === 'sqlite') { - $database = database_path('database.sqlite'); - if (File::exists($database)) { - $zip->addFile($database, 'database/database.sqlite'); - } - } - $zip->close(); - - Storage::disk($disk)->put($relativePath, File::get($temporary)); - File::delete($temporary); - - return Backup::create([ - 'user_id' => $userId, - 'disk' => $disk, - 'path' => $relativePath, - 'size' => Storage::disk($disk)->size($relativePath), - 'status' => 'completed', - 'completed_at' => now(), - ]); - } - - public function delete(Backup $backup): void - { - Storage::disk($backup->disk)->delete($backup->path); - $backup->delete(); - } - - public function prune(): int - { - $cutoff = now()->subDays((int) config('enterprise.retention_days', 30)); - $backups = Backup::query()->where('created_at', '<', $cutoff)->get(); - $backups->each(fn (Backup $backup) => $this->delete($backup)); - - return $backups->count(); - } -} diff --git a/Enterprise/Services/SystemHealthService.php b/Enterprise/Services/SystemHealthService.php deleted file mode 100644 index b08b319..0000000 --- a/Enterprise/Services/SystemHealthService.php +++ /dev/null @@ -1,94 +0,0 @@ -database(), - $this->cache(), - $this->storage(), - $this->queue(), - $this->environment(), - ]; - - return [ - 'status' => collect($checks)->contains(fn (array $check): bool => $check['status'] === 'failed') ? 'failed' : (collect($checks)->contains(fn (array $check): bool => $check['status'] === 'warning') ? 'warning' : 'healthy'), - 'checks' => $checks, - 'generated_at' => now()->toIso8601String(), - ]; - } - - private function database(): array - { - try { - DB::connection()->getPdo(); - - return $this->result('Database', 'healthy', DB::connection()->getDriverName()); - } catch (Throwable $e) { - return $this->result('Database', 'failed', $e->getMessage()); - } - } - - private function cache(): array - { - try { - $key = 'enterprise-health-'.bin2hex(random_bytes(6)); - Cache::put($key, 'ok', 10); - $healthy = Cache::get($key) === 'ok'; - Cache::forget($key); - - return $this->result('Cache', $healthy ? 'healthy' : 'failed', config('cache.default')); - } catch (Throwable $e) { - return $this->result('Cache', 'failed', $e->getMessage()); - } - } - - private function storage(): array - { - try { - Storage::disk('local')->put('enterprise-health.txt', now()->toIso8601String()); - Storage::disk('local')->delete('enterprise-health.txt'); - $freeMb = (int) floor(disk_free_space(storage_path()) / 1024 / 1024); - $minimum = (int) config('enterprise.health.minimum_free_disk_mb', 512); - - return $this->result('Storage', $freeMb < $minimum ? 'warning' : 'healthy', $freeMb.' MB free'); - } catch (Throwable $e) { - return $this->result('Storage', 'failed', $e->getMessage()); - } - } - - private function queue(): array - { - $failed = Schema::hasTable('failed_jobs') ? DB::table('failed_jobs')->count() : 0; - $maximum = (int) config('enterprise.health.maximum_failed_jobs', 0); - - return $this->result('Queue', $failed > $maximum ? 'warning' : 'healthy', $failed.' failed jobs'); - } - - private function environment(): array - { - $warnings = []; - if (app()->environment('production') && config('app.debug')) { - $warnings[] = 'APP_DEBUG is enabled'; - } - if (! app()->environment('production')) { - $warnings[] = 'Not running in production'; - } - - return $this->result('Environment', $warnings ? 'warning' : 'healthy', $warnings ? implode('; ', $warnings) : app()->environment()); - } - - private function result(string $name, string $status, string $message): array - { - return compact('name', 'status', 'message'); - } -} diff --git a/Enterprise/Tests/Feature/EnterpriseModuleBootTest.php b/Enterprise/Tests/Feature/EnterpriseModuleBootTest.php deleted file mode 100644 index fe1d1d1..0000000 --- a/Enterprise/Tests/Feature/EnterpriseModuleBootTest.php +++ /dev/null @@ -1,10 +0,0 @@ - expect(app()->providerIsLoaded(EnterpriseServiceProvider::class))->toBeTrue()); -it('runs platform health checks', function (): void { - $result = app(SystemHealthService::class)->run(); - expect($result)->toHaveKeys(['status', 'checks', 'generated_at'])->and($result['checks'])->not->toBeEmpty(); -}); diff --git a/Enterprise/module.json b/Enterprise/module.json deleted file mode 100644 index fdce53e..0000000 --- a/Enterprise/module.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"Enterprise","alias":"enterprise","version":"1.0.0","description":"Health, diagnostics, cache, queues, scheduler, backups and storage operations.","keywords":["health","diagnostics","queue","scheduler","backup"],"priority":100,"providers":["Modules\\Enterprise\\Providers\\EnterpriseServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration"]} diff --git a/Events/Config/config.php b/Events/Config/config.php deleted file mode 100644 index 6e87093..0000000 --- a/Events/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Events']; diff --git a/Events/Database/Migrations/2026_07_24_04200_create_events_tables.php b/Events/Database/Migrations/2026_07_24_04200_create_events_tables.php deleted file mode 100644 index a974f12..0000000 --- a/Events/Database/Migrations/2026_07_24_04200_create_events_tables.php +++ /dev/null @@ -1,51 +0,0 @@ -id(); - $t->string('name'); - $t->text('address')->nullable(); - $t->unsignedInteger('capacity')->nullable(); - $t->timestamps(); - }); - Schema::create('events', function (Blueprint $t): void { - $t->id(); - $t->foreignId('venue_id')->nullable()->constrained('event_venues')->nullOnDelete(); - $t->string('title'); - $t->string('slug')->unique(); - $t->text('summary')->nullable(); - $t->longText('description')->nullable(); - $t->timestamp('starts_at')->index(); - $t->timestamp('ends_at')->nullable(); - $t->string('status')->default('draft')->index(); - $t->unsignedInteger('capacity')->nullable(); - $t->decimal('price', 10, 2)->default(0); - $t->boolean('registration_open')->default(false); - $t->timestamps(); - }); - Schema::create('event_registrations', function (Blueprint $t): void { - $t->id(); - $t->foreignId('event_id')->constrained()->cascadeOnDelete(); - $t->string('name'); - $t->string('email')->index(); - $t->unsignedInteger('quantity')->default(1); - $t->string('status')->default('confirmed')->index(); - $t->text('notes')->nullable(); - $t->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('event_registrations'); - Schema::dropIfExists('events'); - Schema::dropIfExists('event_venues'); - } -}; diff --git a/Events/Http/Controllers/EventController.php b/Events/Http/Controllers/EventController.php deleted file mode 100644 index 5668979..0000000 --- a/Events/Http/Controllers/EventController.php +++ /dev/null @@ -1,32 +0,0 @@ - Event::with(['venue'])->withCount('registrations')->orderBy('starts_at')->paginate(20), 'venues' => Venue::orderBy('name')->get()]); - } - - public function store(Request $r): RedirectResponse - { - Event::create($r->validate(['venue_id' => ['nullable', 'exists:event_venues,id'], 'title' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255', 'unique:events,slug'], 'summary' => ['nullable', 'string'], 'description' => ['nullable', 'string'], 'starts_at' => ['required', 'date'], 'ends_at' => ['nullable', 'date', 'after:starts_at'], 'status' => ['required', 'in:draft,published,cancelled,completed'], 'capacity' => ['nullable', 'integer', 'min:1'], 'price' => ['required', 'numeric', 'min:0']]) + ['registration_open' => $r->boolean('registration_open')]); - - return back()->with('status', 'Event created.'); - } - - public function destroy(Event $event): RedirectResponse - { - $event->delete(); - - return back()->with('status', 'Event deleted.'); - } -} diff --git a/Events/Models/Event.php b/Events/Models/Event.php deleted file mode 100644 index cb1d6d4..0000000 --- a/Events/Models/Event.php +++ /dev/null @@ -1,27 +0,0 @@ - 'datetime', 'ends_at' => 'datetime', 'price' => 'decimal:2', 'registration_open' => 'boolean']; - } - - public function venue(): BelongsTo - { - return $this->belongsTo(Venue::class); - } - - public function registrations(): HasMany - { - return $this->hasMany(Registration::class); - } -} diff --git a/Events/Models/Registration.php b/Events/Models/Registration.php deleted file mode 100644 index 1ea8d7f..0000000 --- a/Events/Models/Registration.php +++ /dev/null @@ -1,12 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Events', 'Config/config.php'), 'events'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Events', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Events', 'Resources/views'), 'events'); - } -} diff --git a/Events/Providers/RouteServiceProvider.php b/Events/Providers/RouteServiceProvider.php deleted file mode 100644 index ca21554..0000000 --- a/Events/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Events', 'Routes/web.php')); - } -} diff --git a/Events/Resources/views/admin/index.blade.php b/Events/Resources/views/admin/index.blade.php deleted file mode 100644 index 8cab5c2..0000000 --- a/Events/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Events

Manage events, venues and registrations.

@if(session('status'))
{{ session('status') }}
@endif
@csrf

Create event

Create event
@forelse($events as $event)@empty@endforelse
EventStartsStatusRegistrations
{{ $event->title }}{{ $event->starts_at->format('j M Y H:i') }}{{ ucfirst($event->status) }}{{ $event->registrations_count }}
@csrf @method('DELETE')Delete
No events yet.
{{ $events->links() }}
diff --git a/Events/Routes/web.php b/Events/Routes/web.php deleted file mode 100644 index 7f27dfa..0000000 --- a/Events/Routes/web.php +++ /dev/null @@ -1,10 +0,0 @@ -prefix('administration/events')->name('administration.events.')->group(function (): void { - Route::get('/', [EventController::class, 'index'])->name('index'); - Route::post('/', [EventController::class, 'store'])->name('store'); - Route::delete('/{event}', [EventController::class, 'destroy'])->name('destroy'); -}); diff --git a/Events/Tests/Feature/ModuleBootTest.php b/Events/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 27cd6b4..0000000 --- a/Events/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('Events', config('events.name')); - } -} diff --git a/Events/module.json b/Events/module.json deleted file mode 100644 index 7990472..0000000 --- a/Events/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Events", - "alias": "events", - "version": "1.0.0", - "description": "Events, venues and registration management.", - "keywords": ["business", "events"], - "priority": 42, - "providers": ["Modules\\Events\\Providers\\EventsServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Faq/Config/config.php b/Faq/Config/config.php deleted file mode 100644 index 47a7171..0000000 --- a/Faq/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Faq']; diff --git a/Faq/Database/Migrations/2026_07_24_000700_create_faqs_table.php b/Faq/Database/Migrations/2026_07_24_000700_create_faqs_table.php deleted file mode 100644 index 504d804..0000000 --- a/Faq/Database/Migrations/2026_07_24_000700_create_faqs_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->string('question'); - $table->longText('answer'); - $table->string('category')->nullable(); - $table->unsignedInteger('sort_order')->default(0); - $table->boolean('enabled')->default(true); - $table->timestamps(); - $table->softDeletes(); - }); - } - - public function down(): void - { - Schema::dropIfExists('faqs'); - } -}; diff --git a/Faq/Http/Controllers/FaqController.php b/Faq/Http/Controllers/FaqController.php deleted file mode 100644 index b38040d..0000000 --- a/Faq/Http/Controllers/FaqController.php +++ /dev/null @@ -1,60 +0,0 @@ - Faq::query()->latest()->paginate(20)]); - } - - public function create(): View - { - return view('faq::admin.form'); - } - - public function store(Request $request): RedirectResponse - { - $data = $request->validate(['question' => ['required', 'string', 'max:255'], - 'answer' => ['required', 'string'], - 'category' => ['nullable', 'string'], - 'sort_order' => ['nullable', 'integer', 'min:0'], - 'enabled' => ['sometimes', 'boolean']]); - $data['enabled'] = $request->boolean('enabled'); - Faq::create($data); - - return redirect()->route('administration.faq.index')->with('status', 'Faq created.'); - } - - public function edit(Faq $faq): View - { - return view('faq::admin.form', ['record' => $faq]); - } - - public function update(Request $request, Faq $faq): RedirectResponse - { - $data = $request->validate(['question' => ['required', 'string', 'max:255'], - 'answer' => ['required', 'string'], - 'category' => ['nullable', 'string'], - 'sort_order' => ['nullable', 'integer', 'min:0'], - 'enabled' => ['sometimes', 'boolean']]); - $data['enabled'] = $request->boolean('enabled'); - $faq->update($data); - - return redirect()->route('administration.faq.index')->with('status', 'Faq updated.'); - } - - public function destroy(Faq $faq): RedirectResponse - { - $faq->delete(); - - return back()->with('status', 'Faq deleted.'); - } -} diff --git a/Faq/Models/Faq.php b/Faq/Models/Faq.php deleted file mode 100644 index b2011f3..0000000 --- a/Faq/Models/Faq.php +++ /dev/null @@ -1,24 +0,0 @@ - 'integer', - 'enabled' => 'boolean', - ]; - } -} diff --git a/Faq/Providers/FaqServiceProvider.php b/Faq/Providers/FaqServiceProvider.php deleted file mode 100644 index 43f9071..0000000 --- a/Faq/Providers/FaqServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Faq', 'Config/config.php'), 'faq'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Faq', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Faq', 'Resources/views'), 'faq'); - } -} diff --git a/Faq/Providers/RouteServiceProvider.php b/Faq/Providers/RouteServiceProvider.php deleted file mode 100644 index 8c5175d..0000000 --- a/Faq/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -routes(fn () => Route::middleware('web')->group(module_path('Faq', 'Routes/web.php'))); - } -} diff --git a/Faq/Resources/views/admin/form.blade.php b/Faq/Resources/views/admin/form.blade.php deleted file mode 100644 index 9998104..0000000 --- a/Faq/Resources/views/admin/form.blade.php +++ /dev/null @@ -1,9 +0,0 @@ - -

{{ isset($record) ? 'Edit' : 'Create' }} Faq

@csrf @isset($record) @method('PUT') @endisset -
@error('question')

{{ $message }}

@enderror
-
@error('answer')

{{ $message }}

@enderror
-
@error('category')

{{ $message }}

@enderror
-
@error('sort_order')

{{ $message }}

@enderror
- -
Cancel
-
diff --git a/Faq/Resources/views/admin/index.blade.php b/Faq/Resources/views/admin/index.blade.php deleted file mode 100644 index 5bceb8c..0000000 --- a/Faq/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,5 +0,0 @@ - -

Faq

Manage faq content.

Add new
-@if(session('status'))
{{ session('status') }}
@endif -
@forelse($records as $record)@empty@endforelse
QuestionAnswerCategoryActions
{{ is_array($record->question) ? json_encode($record->question) : Str::limit((string) $record->question, 70) }}{{ is_array($record->answer) ? json_encode($record->answer) : Str::limit((string) $record->answer, 70) }}{{ is_array($record->category) ? json_encode($record->category) : Str::limit((string) $record->category, 70) }}Edit
@csrf @method('DELETE')
No records yet.
{{ $records->links() }}
-
diff --git a/Faq/Routes/web.php b/Faq/Routes/web.php deleted file mode 100644 index 031dc8b..0000000 --- a/Faq/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/faq')->name('administration.faq.')->group(function (): void { - Route::get('/', [FaqController::class, 'index'])->name('index'); - Route::get('/create', [FaqController::class, 'create'])->name('create'); - Route::post('/', [FaqController::class, 'store'])->name('store'); - Route::get('/{faq}/edit', [FaqController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{faq}', [FaqController::class, 'update'])->name('update'); - Route::delete('/{faq}', [FaqController::class, 'destroy'])->name('destroy'); -}); diff --git a/Faq/Support/ModuleInfo.php b/Faq/Support/ModuleInfo.php deleted file mode 100644 index 34b932c..0000000 --- a/Faq/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBeTrue(); -}); diff --git a/Faq/module.json b/Faq/module.json deleted file mode 100644 index 1996222..0000000 --- a/Faq/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Faq", - "alias": "faq", - "version": "1.0.0", - "description": "Faq CMS module.", - "keywords": [ - "cms", - "faq" - ], - "priority": 27, - "providers": [ - "Modules\\Faq\\Providers\\FaqServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core", - "Administration" - ] -} diff --git a/Feeds/Config/config.php b/Feeds/Config/config.php deleted file mode 100644 index 166f718..0000000 --- a/Feeds/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Feeds']; diff --git a/Feeds/Http/Controllers/FeedController.php b/Feeds/Http/Controllers/FeedController.php deleted file mode 100644 index 8f2fab8..0000000 --- a/Feeds/Http/Controllers/FeedController.php +++ /dev/null @@ -1,21 +0,0 @@ -view('feeds::rss', ['title' => config('app.name').' Blog', 'records' => Blog::published()->latest('published_at')->limit(50)->get(), 'routeName' => 'blog.public.show'])->header('Content-Type', 'application/rss+xml; charset=UTF-8'); - } - - public function news(): Response - { - return response()->view('feeds::rss', ['title' => config('app.name').' News', 'records' => News::published()->latest('published_at')->limit(50)->get(), 'routeName' => 'news.public.show'])->header('Content-Type', 'application/rss+xml; charset=UTF-8'); - } -} diff --git a/Feeds/Providers/FeedsServiceProvider.php b/Feeds/Providers/FeedsServiceProvider.php deleted file mode 100644 index c1200c7..0000000 --- a/Feeds/Providers/FeedsServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Feeds', 'Config/config.php'), 'feeds'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Feeds', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Feeds', 'Resources/views'), 'feeds'); - } -} diff --git a/Feeds/Providers/RouteServiceProvider.php b/Feeds/Providers/RouteServiceProvider.php deleted file mode 100644 index 89c193a..0000000 --- a/Feeds/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Feeds', 'Routes/web.php')); - } -} diff --git a/Feeds/Resources/views/rss.blade.php b/Feeds/Resources/views/rss.blade.php deleted file mode 100644 index e516351..0000000 --- a/Feeds/Resources/views/rss.blade.php +++ /dev/null @@ -1 +0,0 @@ -{!! '' !!}{{ $title }}{{ url('/') }}{{ $title }}@foreach($records as $record){{ $record->title }}{{ route($routeName, $record->slug) }}{{ route($routeName, $record->slug) }}excerpt }}]]>{{ optional($record->published_at)->toRssString() }}@endforeach diff --git a/Feeds/Routes/web.php b/Feeds/Routes/web.php deleted file mode 100644 index b9ba81f..0000000 --- a/Feeds/Routes/web.php +++ /dev/null @@ -1,7 +0,0 @@ -name('feeds.blog'); -Route::get('/feeds/news.xml', [FeedController::class, 'news'])->name('feeds.news'); diff --git a/Feeds/Tests/Feature/ModuleBootTest.php b/Feeds/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 7b90716..0000000 --- a/Feeds/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('Feeds', config('feeds.name')); - } -} diff --git a/Feeds/module.json b/Feeds/module.json deleted file mode 100644 index 9569047..0000000 --- a/Feeds/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Feeds", - "alias": "feeds", - "version": "1.0.0", - "description": "Feeds publishing module.", - "keywords": ["publishing", "feeds"], - "priority": 30, - "providers": ["Modules\\Feeds\\Providers\\FeedsServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Forms/Config/config.php b/Forms/Config/config.php deleted file mode 100644 index ba58e72..0000000 --- a/Forms/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Forms']; diff --git a/Forms/Database/Migrations/2026_07_24_000400_create_forms_table.php b/Forms/Database/Migrations/2026_07_24_000400_create_forms_table.php deleted file mode 100644 index aee029b..0000000 --- a/Forms/Database/Migrations/2026_07_24_000400_create_forms_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->string('name'); - $table->string('slug'); - $table->text('description')->nullable(); - $table->json('schema')->nullable(); - $table->boolean('enabled')->default(true); - $table->timestamps(); - $table->softDeletes(); - }); - } - - public function down(): void - { - Schema::dropIfExists('forms'); - } -}; diff --git a/Forms/Http/Controllers/FormController.php b/Forms/Http/Controllers/FormController.php deleted file mode 100644 index eacb1b8..0000000 --- a/Forms/Http/Controllers/FormController.php +++ /dev/null @@ -1,66 +0,0 @@ - Form::query()->latest()->paginate(20)]); - } - - public function create(): View - { - return view('forms::admin.form'); - } - - public function store(Request $request): RedirectResponse - { - $data = $request->validate(['name' => ['required', 'string', 'max:255'], - 'slug' => ['required', 'string', 'max:255'], - 'description' => ['nullable', 'string'], - 'schema' => ['nullable', 'array'], - 'enabled' => ['sometimes', 'boolean']]); - if ($request->filled('schema_json')) { - $data['schema'] = json_decode($request->string('schema_json')->toString(), true, 512, JSON_THROW_ON_ERROR); - } - $data['enabled'] = $request->boolean('enabled'); - Form::create($data); - - return redirect()->route('administration.forms.index')->with('status', 'Form created.'); - } - - public function edit(Form $form): View - { - return view('forms::admin.form', ['record' => $form]); - } - - public function update(Request $request, Form $form): RedirectResponse - { - $data = $request->validate(['name' => ['required', 'string', 'max:255'], - 'slug' => ['required', 'string', 'max:255'], - 'description' => ['nullable', 'string'], - 'schema' => ['nullable', 'array'], - 'enabled' => ['sometimes', 'boolean']]); - if ($request->filled('schema_json')) { - $data['schema'] = json_decode($request->string('schema_json')->toString(), true, 512, JSON_THROW_ON_ERROR); - } - $data['enabled'] = $request->boolean('enabled'); - $form->update($data); - - return redirect()->route('administration.forms.index')->with('status', 'Form updated.'); - } - - public function destroy(Form $form): RedirectResponse - { - $form->delete(); - - return back()->with('status', 'Form deleted.'); - } -} diff --git a/Forms/Models/Form.php b/Forms/Models/Form.php deleted file mode 100644 index d72aec6..0000000 --- a/Forms/Models/Form.php +++ /dev/null @@ -1,24 +0,0 @@ - 'array', - 'enabled' => 'boolean', - ]; - } -} diff --git a/Forms/Providers/FormsServiceProvider.php b/Forms/Providers/FormsServiceProvider.php deleted file mode 100644 index 53ce6d1..0000000 --- a/Forms/Providers/FormsServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Forms', 'Config/config.php'), 'forms'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Forms', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Forms', 'Resources/views'), 'forms'); - } -} diff --git a/Forms/Providers/RouteServiceProvider.php b/Forms/Providers/RouteServiceProvider.php deleted file mode 100644 index 1d81fce..0000000 --- a/Forms/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -routes(fn () => Route::middleware('web')->group(module_path('Forms', 'Routes/web.php'))); - } -} diff --git a/Forms/Resources/views/admin/form.blade.php b/Forms/Resources/views/admin/form.blade.php deleted file mode 100644 index 12e5a85..0000000 --- a/Forms/Resources/views/admin/form.blade.php +++ /dev/null @@ -1,32 +0,0 @@ - -
-

{{ isset($record) ? 'Edit' : 'Create' }} Form

-
@csrf @isset($record) - @method('PUT') - @endisset -
@error('name') -

{{ $message }}

@enderror
-
@error('slug') -

{{ $message }}

@enderror
-
@error('description') -

{{ $message }}

@enderror
-
-
- -
- - Cancel
-
-
-
diff --git a/Forms/Resources/views/admin/index.blade.php b/Forms/Resources/views/admin/index.blade.php deleted file mode 100644 index a8dd83d..0000000 --- a/Forms/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,43 +0,0 @@ - -
-
-

Forms

-

Manage forms content.

- Add new -
- @if(session('status')) -
{{ session('status') }}
- @endif -
- - - - - - - - - - @forelse($records as $record) - - - - - - - @empty - - - - @endforelse -
NameSlugDescriptionActions
{{ is_array($record->name) ? json_encode($record->name) : Str::limit((string) $record->name, 70) }}{{ is_array($record->slug) ? json_encode($record->slug) : Str::limit((string) $record->slug, 70) }}{{ is_array($record->description) ? json_encode($record->description) : Str::limit((string) $record->description, 70) }}Edit -
@csrf @method('DELETE') - -
-
No records yet.
-
{{ $records->links() }}
-
diff --git a/Forms/Routes/web.php b/Forms/Routes/web.php deleted file mode 100644 index 4da162d..0000000 --- a/Forms/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/forms')->name('administration.forms.')->group(function (): void { - Route::get('/', [FormController::class, 'index'])->name('index'); - Route::get('/create', [FormController::class, 'create'])->name('create'); - Route::post('/', [FormController::class, 'store'])->name('store'); - Route::get('/{form}/edit', [FormController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{form}', [FormController::class, 'update'])->name('update'); - Route::delete('/{form}', [FormController::class, 'destroy'])->name('destroy'); -}); diff --git a/Forms/Support/ModuleInfo.php b/Forms/Support/ModuleInfo.php deleted file mode 100644 index 916091c..0000000 --- a/Forms/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBeTrue(); -}); diff --git a/Forms/module.json b/Forms/module.json deleted file mode 100644 index b4450a2..0000000 --- a/Forms/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Forms", - "alias": "forms", - "version": "1.0.0", - "description": "Forms CMS module.", - "keywords": [ - "cms", - "forms" - ], - "priority": 24, - "providers": [ - "Modules\\Forms\\Providers\\FormsServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core", - "Administration" - ] -} diff --git a/Licensing/Config/.gitkeep b/Licensing/Config/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Config/config.php b/Licensing/Config/config.php deleted file mode 100644 index 3b94eda..0000000 --- a/Licensing/Config/config.php +++ /dev/null @@ -1,8 +0,0 @@ - 'Licensing', - 'features' => - [ - ], -]; diff --git a/Licensing/Console/.gitkeep b/Licensing/Console/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Console/SonarDeskLicenseDeliver.php b/Licensing/Console/SonarDeskLicenseDeliver.php deleted file mode 100644 index 67e5050..0000000 --- a/Licensing/Console/SonarDeskLicenseDeliver.php +++ /dev/null @@ -1,20 +0,0 @@ -option('limit'), 100)); - $count = $delivery->deliverPending($limit); - $this->info("Delivered {$count} licence email(s)."); - return self::SUCCESS; - } -} diff --git a/Licensing/Console/SonarDeskLicenseIssue.php b/Licensing/Console/SonarDeskLicenseIssue.php deleted file mode 100755 index 12469cb..0000000 --- a/Licensing/Console/SonarDeskLicenseIssue.php +++ /dev/null @@ -1,29 +0,0 @@ -argument('product'))->firstOrFail(); - [$l,$key] = $issuer->issue($p, $this->option('name') ?: null, $this->option('email') ?: null); - if (! $key) { - $this->warn('Licence already existed; plaintext key is intentionally not recoverable.'); - - return self::FAILURE; - } $this->line('Licence ID: '.$l->id); - $this->line('Licence key: '.$key); - $this->warn('This is the only time the plaintext key is shown.'); - - return self::SUCCESS; - } -} diff --git a/Licensing/Console/SonarDeskLicenseRevoke.php b/Licensing/Console/SonarDeskLicenseRevoke.php deleted file mode 100755 index ad34c16..0000000 --- a/Licensing/Console/SonarDeskLicenseRevoke.php +++ /dev/null @@ -1,22 +0,0 @@ -argument('id')); - $l->update(['revoked_at' => now(), 'revoke_reason' => $this->option('reason')]); - $this->info('Licence revoked.'); - - return self::SUCCESS; - } -} diff --git a/Licensing/Console/SonarDeskProductCreate.php b/Licensing/Console/SonarDeskProductCreate.php deleted file mode 100755 index 8622a85..0000000 --- a/Licensing/Console/SonarDeskProductCreate.php +++ /dev/null @@ -1,21 +0,0 @@ - $this->argument('code')], ['name' => $this->option('name'), 'edition' => $this->option('edition'), 'activation_limit' => (int) $this->option('activations'), 'max_major_version' => $this->option('max-major') !== '' ? (int) $this->option('max-major') : null, 'payment_provider' => $this->option('provider') ?: null, 'external_product_id' => $this->option('product-id') ?: null, 'external_price_id' => $this->option('price-id') ?: null, 'active' => true]); - $this->info("Product {$p->code} ready."); - - return self::SUCCESS; - } -} diff --git a/Licensing/Database/Migrations/.gitkeep b/Licensing/Database/Migrations/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Database/Migrations/2026_09_08_200000_create_sonardesk_licensing_tables.php b/Licensing/Database/Migrations/2026_09_08_200000_create_sonardesk_licensing_tables.php deleted file mode 100755 index babc7a3..0000000 --- a/Licensing/Database/Migrations/2026_09_08_200000_create_sonardesk_licensing_tables.php +++ /dev/null @@ -1,92 +0,0 @@ -id(); - $table->string('code')->unique(); - $table->string('name'); - $table->string('edition')->default('Professional'); - $table->unsignedInteger('activation_limit')->default(1); - $table->unsignedInteger('max_major_version')->nullable(); - $table->string('payment_provider')->nullable(); - $table->string('external_product_id')->nullable(); - $table->string('external_price_id')->nullable(); - $table->boolean('active')->default(true); - $table->timestamps(); - $table->unique(['payment_provider', 'external_price_id']); - }); - - Schema::create('licenses', function (Blueprint $table) { - $table->uuid('id')->primary(); - $table->foreignId('license_product_id')->constrained('license_products'); - $table->string('license_key_hash', 64)->unique(); - $table->string('license_key_last4', 4); - $table->string('customer_name')->nullable(); - $table->string('customer_email')->nullable()->index(); - $table->string('edition'); - $table->unsignedInteger('activation_limit'); - $table->unsignedInteger('max_major_version')->nullable(); - $table->timestamp('expires_at')->nullable(); - $table->timestamp('revoked_at')->nullable(); - $table->string('revoke_reason')->nullable(); - $table->string('payment_provider')->nullable(); - $table->string('external_order_id')->nullable(); - $table->string('external_customer_id')->nullable(); - $table->timestamps(); - $table->unique(['payment_provider', 'external_order_id']); - }); - - Schema::create('license_delivery_outbox', function (Blueprint $table) { - $table->id(); - $table->uuid('license_id')->unique(); - $table->text('encrypted_license_key'); - $table->string('recipient_email')->nullable(); - $table->timestamp('delivered_at')->nullable(); - $table->timestamps(); - $table->foreign('license_id')->references('id')->on('licenses')->cascadeOnDelete(); - }); - - Schema::create('license_activations', function (Blueprint $table) { - $table->id(); - $table->uuid('license_id'); - $table->uuid('installation_id'); - $table->string('platform', 32)->nullable(); - $table->string('app_version', 32)->nullable(); - $table->timestamp('activated_at'); - $table->timestamp('last_seen_at')->nullable(); - $table->timestamp('deactivated_at')->nullable(); - $table->foreign('license_id')->references('id')->on('licenses')->cascadeOnDelete(); - $table->unique(['license_id', 'installation_id']); - $table->index(['license_id', 'deactivated_at']); - }); - - Schema::create('license_webhook_events', function (Blueprint $table) { - $table->id(); - $table->string('provider'); - $table->string('external_event_id'); - $table->string('event_type'); - $table->string('payload_sha256', 64); - $table->json('payload'); - $table->timestamp('processed_at')->nullable(); - $table->text('processing_error')->nullable(); - $table->timestamps(); - $table->unique(['provider', 'external_event_id']); - }); - } - - public function down(): void - { - Schema::dropIfExists('license_webhook_events'); - Schema::dropIfExists('license_activations'); - Schema::dropIfExists('license_delivery_outbox'); - Schema::dropIfExists('licenses'); - Schema::dropIfExists('license_products'); - } -}; diff --git a/Licensing/Database/Migrations/2026_09_08_210000_add_sonardesk_portal_fields.php b/Licensing/Database/Migrations/2026_09_08_210000_add_sonardesk_portal_fields.php deleted file mode 100755 index b302e12..0000000 --- a/Licensing/Database/Migrations/2026_09_08_210000_add_sonardesk_portal_fields.php +++ /dev/null @@ -1,30 +0,0 @@ - $table->boolean('is_admin')->default(false)->index()); - } - - if (Schema::hasTable('license_activations') && ! Schema::hasColumn('license_activations', 'installation_name')) { - Schema::table('license_activations', fn (Blueprint $table) => $table->string('installation_name')->nullable()->after('installation_id')); - } - } - - public function down(): void - { - if (Schema::hasTable('license_activations') && Schema::hasColumn('license_activations', 'installation_name')) { - Schema::table('license_activations', fn (Blueprint $table) => $table->dropColumn('installation_name')); - } - - if (Schema::hasTable('users') && Schema::hasColumn('users', 'is_admin')) { - Schema::table('users', fn (Blueprint $table) => $table->dropColumn('is_admin')); - } - } -}; diff --git a/Licensing/Database/Seeders/.gitkeep b/Licensing/Database/Seeders/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Database/Seeders/LicensingDatabaseSeeder.php b/Licensing/Database/Seeders/LicensingDatabaseSeeder.php deleted file mode 100644 index fbb0b58..0000000 --- a/Licensing/Database/Seeders/LicensingDatabaseSeeder.php +++ /dev/null @@ -1,21 +0,0 @@ -call("OthersTableSeeder"); - } -} diff --git a/Licensing/Database/factories/.gitkeep b/Licensing/Database/factories/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Helpers/.gitkeep b/Licensing/Helpers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Helpers/Helper.php b/Licensing/Helpers/Helper.php deleted file mode 100644 index 7607c60..0000000 --- a/Licensing/Helpers/Helper.php +++ /dev/null @@ -1,12 +0,0 @@ - License::query()->count(), - 'activeCount' => License::query()->whereNull('revoked_at')->where(function ($q): void { - $q->whereNull('expires_at')->orWhere('expires_at', '>', now()); - })->count(), - 'productCount' => LicenseProduct::query()->count(), - 'recentLicenses' => License::query()->with('product')->latest()->limit(10)->get(), - ]); - } - - public function licenses(): View - { - return view('licensing::admin.licenses', [ - 'licenses' => License::query()->with('product')->latest()->paginate(25), - ]); - } - - public function show(License $license): View - { - return view('licensing::admin.show', [ - 'license' => $license->load(['product', 'activations']), - ]); - } -} diff --git a/Licensing/Http/Controllers/Api/LicenseActivationController.php b/Licensing/Http/Controllers/Api/LicenseActivationController.php deleted file mode 100755 index 7580871..0000000 --- a/Licensing/Http/Controllers/Api/LicenseActivationController.php +++ /dev/null @@ -1,164 +0,0 @@ -validate([ - 'licenseKey' => ['required', 'string', 'max:128'], - 'installationID' => ['required', 'uuid'], - 'appVersion' => ['required', 'string', 'max:32'], - 'platform' => ['required', 'string', 'max:32'], - ]); - - return DB::transaction(function () use ($data, $signer) { - $license = License::where('license_key_hash', LicenseKey::hash($data['licenseKey'])) - ->lockForUpdate() - ->first(); - - if (! $license) { - throw ValidationException::withMessages(['licenseKey' => 'The licence key is not valid.']); - } - - if (! $license->is_usable) { - return response()->json(['message' => 'This licence is expired or has been revoked.'], 403); - } - - $installationID = strtolower($data['installationID']); - $activation = LicenseActivation::where('license_id', $license->id) - ->where('installation_id', $installationID) - ->first(); - - if ($activation && $activation->deactivated_at === null) { - $activation->update([ - 'last_seen_at' => now(), - 'platform' => $data['platform'], - 'app_version' => $data['appVersion'], - ]); - } else { - $activeCount = LicenseActivation::where('license_id', $license->id) - ->whereNull('deactivated_at') - ->count(); - - if ($activeCount >= $license->activation_limit) { - return response()->json(['message' => 'This licence has reached its activation limit. Deactivate another Mac first.'], 409); - } - - if ($activation) { - $activation->update([ - 'deactivated_at' => null, - 'activated_at' => now(), - 'last_seen_at' => now(), - 'platform' => $data['platform'], - 'app_version' => $data['appVersion'], - ]); - } else { - LicenseActivation::create([ - 'license_id' => $license->id, - 'installation_id' => $installationID, - 'platform' => $data['platform'], - 'app_version' => $data['appVersion'], - 'activated_at' => now(), - 'last_seen_at' => now(), - ]); - } - } - - return response()->json([ - 'licenseToken' => $signer->sign($license, $data['installationID']), - ]); - }); - } - - public function validateInstallation( - Request $request, - LicenseTokenVerifier $verifier, - LicenseSigner $signer, - ): JsonResponse { - $data = $request->validate([ - 'licenseToken' => ['required', 'string', 'max:8192'], - 'installationID' => ['required', 'uuid'], - 'appVersion' => ['required', 'string', 'max:32'], - 'platform' => ['required', 'string', 'max:32'], - ]); - - try { - $payload = $verifier->verify($data['licenseToken']); - } catch (\Throwable) { - return response()->json(['message' => 'The licence token is invalid.'], 400); - } - - if (strcasecmp((string) ($payload['installationID'] ?? ''), $data['installationID']) !== 0) { - return response()->json(['message' => 'The installation does not match this licence token.'], 400); - } - - return DB::transaction(function () use ($data, $payload, $signer) { - $license = License::whereKey((string) ($payload['licenseID'] ?? '')) - ->lockForUpdate() - ->first(); - - if (! $license || ! $license->is_usable) { - return response()->json(['message' => 'This licence is expired or has been revoked.'], 403); - } - - $activation = LicenseActivation::where('license_id', $license->id) - ->where('installation_id', strtolower($data['installationID'])) - ->first(); - - if (! $activation || $activation->deactivated_at !== null) { - return response()->json(['message' => 'This Mac has been deactivated from the licence portal.'], 403); - } - - $activation->update([ - 'last_seen_at' => now(), - 'platform' => $data['platform'], - 'app_version' => $data['appVersion'], - ]); - - return response()->json([ - 'active' => true, - // Refreshing the signed token also applies subscription edition/expiry changes - // without requiring the user to reactivate manually. - 'licenseToken' => $signer->sign($license, $data['installationID']), - ]); - }); - } - - public function deactivate(Request $request, LicenseTokenVerifier $verifier): JsonResponse - { - $data = $request->validate([ - 'licenseToken' => ['required', 'string', 'max:8192'], - 'installationID' => ['required', 'uuid'], - ]); - - try { - $payload = $verifier->verify($data['licenseToken']); - } catch (\Throwable) { - return response()->json(['message' => 'The licence token is invalid.'], 400); - } - - if (strcasecmp((string) ($payload['installationID'] ?? ''), $data['installationID']) !== 0) { - return response()->json(['message' => 'The installation does not match this licence token.'], 400); - } - - LicenseActivation::where('license_id', (string) ($payload['licenseID'] ?? '')) - ->where('installation_id', strtolower($data['installationID'])) - ->whereNull('deactivated_at') - ->update(['deactivated_at' => now(), 'last_seen_at' => now()]); - - return response()->json((object) []); - } -} diff --git a/Licensing/Http/Controllers/Api/PaymentWebhookController.php b/Licensing/Http/Controllers/Api/PaymentWebhookController.php deleted file mode 100755 index 61d9512..0000000 --- a/Licensing/Http/Controllers/Api/PaymentWebhookController.php +++ /dev/null @@ -1,191 +0,0 @@ -getContent(); - - $verifier = match ($provider) { - 'paddle' => app(PaddleWebhookVerifier::class), - 'lemon-squeezy' => app(LemonSqueezyWebhookVerifier::class), - default => null, - }; - - if (! $verifier) { - return response()->json([ - 'message' => 'Unknown webhook provider.', - ], 404); - } - - try { - $verifier->verify($request, $raw); - } catch (Throwable $e) { - report($e); - - return response()->json([ - 'message' => 'Invalid webhook signature.', - ], 400); - } - - try { - $payload = json_decode( - $raw, - true, - flags: JSON_THROW_ON_ERROR - ); - } catch (Throwable) { - return response()->json([ - 'message' => 'Invalid JSON.', - ], 400); - } - - $externalID = (string) ( - $provider === 'paddle' - ? ($payload['event_id'] ?? '') - : ($payload['meta']['event_name'] ?? '') - .':' - .($payload['data']['id'] ?? '') - ); - - $eventType = (string) ( - $provider === 'paddle' - ? ($payload['event_type'] ?? 'unknown') - : ($payload['meta']['event_name'] ?? 'unknown') - ); - - if ($externalID === '') { - return response()->json([ - 'message' => 'Webhook event has no stable identifier.', - ], 400); - } - - $payloadHash = hash('sha256', $raw); - - try { - /* - * Paddle can deliver several related events concurrently. - * - * Laravel retries this complete transaction when MySQL reports - * a deadlock. Five attempts gives short-lived lock contention - * enough opportunity to resolve without acknowledging an event - * that was not actually processed. - */ - DB::transaction( - function () use ( - $provider, - $externalID, - $eventType, - $payload, - $payloadHash, - $provisioner - ): void { - /* - * Insert the event if this is its first delivery. - * - * Once present, lock the row so duplicate deliveries of - * the same event cannot provision concurrently. - */ - LicenseWebhookEvent::firstOrCreate( - [ - 'provider' => $provider, - 'external_event_id' => $externalID, - ], - [ - 'event_type' => $eventType, - 'payload_sha256' => $payloadHash, - 'payload' => $payload, - ] - ); - - $event = LicenseWebhookEvent::query() - ->where('provider', $provider) - ->where('external_event_id', $externalID) - ->lockForUpdate() - ->firstOrFail(); - - if ($event->processed_at) { - return; - } - - if (! hash_equals( - (string) $event->payload_sha256, - $payloadHash - )) { - throw new RuntimeException( - 'Webhook ID was reused with a different payload.' - ); - } - - try { - $provisioner->provision( - $provider, - $payload - ); - - $event->processed_at = now(); - $event->processing_error = null; - $event->save(); - } catch (Throwable $e) { - /* - * This assignment is useful when the surrounding - * transaction eventually succeeds. If processing - * throws, the transaction itself is rolled back and - * Paddle will retry the event. - */ - $event->processing_error = mb_substr( - $e->getMessage(), - 0, - 4000 - ); - - $event->save(); - - throw $e; - } - }, - 5 - ); - } catch (Throwable $e) { - report($e); - return response()->json([ - 'message' => 'Webhook could not be processed.', - ], 500); - } - - /* - * Delivery deliberately happens outside the webhook transaction. - * - * A temporary SMTP failure must not undo a successfully processed - * payment event. The outbox retains anything that still needs - * delivery. - */ - try { - $delivery->deliverPending(10); - } catch (Throwable $e) { - report($e); - } - - return response()->json([ - 'ok' => true, - ]); - } -} diff --git a/Licensing/Http/Controllers/LicensingController.php b/Licensing/Http/Controllers/LicensingController.php deleted file mode 100644 index 8ece907..0000000 --- a/Licensing/Http/Controllers/LicensingController.php +++ /dev/null @@ -1,79 +0,0 @@ -whereRaw('lower(customer_email) = ?', [strtolower($request->user()->email)]); - } - - public function index(Request $request) - { - $licenses = $this->owned($request)->latest()->get(); - $hasProfessional = $licenses->contains(fn (License $license) => $license->is_usable && $license->edition === 'Professional'); - - return view('portal.index', compact('licenses', 'hasProfessional')); - } - - public function show(Request $request, License $license) - { - $this->authorise($request, $license); - $license->load(['product', 'activations']); - - return view('portal.show', compact('license')); - } - - public function upgrade(Request $request, License $license): RedirectResponse - { - $this->authorise($request, $license); - abort_unless($license->payment_provider === 'paddle' && $license->external_subscription_id, 422); - abort_unless($license->edition === 'Personal' && $license->is_usable, 422); - - $targetPriceID = $this->professionalPriceFor($license); - $targetProduct = LicenseProduct::query() - ->where('payment_provider', 'paddle') - ->where('external_price_id', $targetPriceID) - ->where('active', true) - ->firstOrFail(); - - try { - $subscription = $this->paddle->updateSubscriptionPrice($license->external_subscription_id, $targetPriceID); - } catch (RuntimeException $exception) { - return back()->withErrors(['licence' => $exception->getMessage()]); - } - - // Apply the entitlement immediately. Paddle webhooks remain authoritative and - // will reconcile subscription status/current-period details afterwards. - $license->update([ - 'license_product_id' => $targetProduct->id, - 'edition' => $targetProduct->edition, - 'activation_limit' => $targetProduct->activation_limit, - 'max_major_version' => $targetProduct->max_major_version, - 'external_price_id' => $targetPriceID, - 'subscription_status' => (string) ($subscription['status'] ?? $license->subscription_status), - 'revoked_at' => null, - 'revoke_reason' => null, - ]); - - return redirect()->route('portal.show', $license)->with('status', 'Upgraded to Professional. Your existing Paddle subscription was changed; no second subscription was created.'); - } - - public function cancel(Request $request, License $license): RedirectResponse - { - $this->authorise($request, $license); - abort_unless($license->payment_provider === 'paddle' && $license->external_subscription_id, 422); - abort_unless(! in_array($license->subscription_status, ['canceled'], true), 422); - - try { - $this->paddle->cancelSubscriptionImmediately($license->external_subscription_id); - } catch (RuntimeException $exception) { - return back()->withErrors(['licence' => $exception->getMessage()]); - } - - $license->update([ - 'subscription_status' => 'canceled', - 'expires_at' => now(), - 'revoked_at' => now(), - 'revoke_reason' => 'Subscription canceled from licence portal', - 'cancel_scheduled_at' => null, - ]); - - return redirect()->route('portal.index')->with('status', 'Subscription canceled in Paddle and the licence is no longer active.'); - } - - public function deactivate(Request $request, License $license, int $activation) - { - $this->authorise($request, $license); - $item = $license->activations()->whereKey($activation)->firstOrFail(); - if (! $item->deactivated_at) { - $item->update(['deactivated_at' => now()]); - } - - return back()->with('status', 'Mac deactivated. SonarDesk will detect this through the licensing API.'); - } - - public function resend(Request $request, License $license) - { - $this->authorise($request, $license); - $out = LicenseDeliveryOutbox::where('license_id', $license->id)->first(); - if (! $out) { - return back()->withErrors(['licence' => 'This licence was issued manually and its full key is not recoverable. Contact support.']); - } - - $out->update([ - 'recipient_email' => strtolower($request->user()->email), - 'delivered_at' => null, - ]); - $this->delivery->deliverPending(1); - - return back()->with('status', 'Licence key sent to your verified email address.'); - } - - private function authorise(Request $request, License $license): void - { - abort_unless(strtolower((string) $license->customer_email) === strtolower($request->user()->email), 404); - } - - private function professionalPriceFor(License $license): string - { - $paddle = config('paddle'); - $current = (string) $license->external_price_id; - - if ($current === (string) ($paddle['personal_annual_price_id'] ?? '')) { - return (string) ($paddle['professional_annual_price_id'] ?? ''); - } - - return (string) ($paddle['professional_monthly_price_id'] ?? ''); - } -} diff --git a/Licensing/Http/Middleware/.gitkeep b/Licensing/Http/Middleware/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Http/Requests/.gitkeep b/Licensing/Http/Requests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Models/.gitkeep b/Licensing/Models/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Models/License.php b/Licensing/Models/License.php deleted file mode 100755 index 86e5c6a..0000000 --- a/Licensing/Models/License.php +++ /dev/null @@ -1,36 +0,0 @@ - 'datetime', 'revoked_at' => 'datetime', 'activation_limit' => 'integer', 'max_major_version' => 'integer', 'current_period_ends_at' => 'datetime', 'cancel_scheduled_at' => 'datetime']; - - public function product(): BelongsTo - { - return $this->belongsTo(LicenseProduct::class, 'license_product_id'); - } - - public function activations(): HasMany - { - return $this->hasMany(LicenseActivation::class); - } - - public function getIsUsableAttribute(): bool - { - return $this->revoked_at === null && ($this->expires_at === null || $this->expires_at->isFuture()); - } -} diff --git a/Licensing/Models/LicenseActivation.php b/Licensing/Models/LicenseActivation.php deleted file mode 100755 index fb454a0..0000000 --- a/Licensing/Models/LicenseActivation.php +++ /dev/null @@ -1,14 +0,0 @@ - 'datetime', 'last_seen_at' => 'datetime', 'deactivated_at' => 'datetime']; -} diff --git a/Licensing/Models/LicenseDeliveryOutbox.php b/Licensing/Models/LicenseDeliveryOutbox.php deleted file mode 100755 index 89d1d5f..0000000 --- a/Licensing/Models/LicenseDeliveryOutbox.php +++ /dev/null @@ -1,20 +0,0 @@ - 'datetime']; - - public function license(): BelongsTo - { - return $this->belongsTo(License::class); - } -} diff --git a/Licensing/Models/LicenseProduct.php b/Licensing/Models/LicenseProduct.php deleted file mode 100755 index eb8a4cc..0000000 --- a/Licensing/Models/LicenseProduct.php +++ /dev/null @@ -1,12 +0,0 @@ - 'boolean', 'activation_limit' => 'integer', 'max_major_version' => 'integer']; -} diff --git a/Licensing/Models/LicenseWebhookEvent.php b/Licensing/Models/LicenseWebhookEvent.php deleted file mode 100755 index a8cc130..0000000 --- a/Licensing/Models/LicenseWebhookEvent.php +++ /dev/null @@ -1,12 +0,0 @@ - 'array', 'processed_at' => 'datetime']; -} diff --git a/Licensing/Providers/.gitkeep b/Licensing/Providers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Providers/IntegratedLicensingServiceProvider.php b/Licensing/Providers/IntegratedLicensingServiceProvider.php deleted file mode 100644 index d55e06b..0000000 --- a/Licensing/Providers/IntegratedLicensingServiceProvider.php +++ /dev/null @@ -1,35 +0,0 @@ - [ - Limit::perMinute(12)->by($request->ip()), - Limit::perMinute(6)->by(hash('sha256', (string) $request->input('installationID'))), - ]); - - $this->loadMigrationsFrom(__DIR__.'/../Database/Migrations'); - $this->loadViewsFrom(__DIR__.'/../Resources/views', 'licensing'); - $this->mergeConfigFrom(__DIR__.'/../Config/config.php', 'licensing'); - - // Explicit role-based authorization; deny access by default. - Gate::define('manage-sonardesk-licensing', static fn (User $user): bool => - $user->hasAnyRole(['Super Admin', 'super-admin', 'Administrator']) - ); - - Route::prefix('api')->middleware('api')->group(__DIR__.'/../Routes/api.php'); - Route::group([], __DIR__.'/../Routes/admin.php'); - } -} diff --git a/Licensing/Providers/LicensingServiceProvider.php b/Licensing/Providers/LicensingServiceProvider.php deleted file mode 100644 index c514a59..0000000 --- a/Licensing/Providers/LicensingServiceProvider.php +++ /dev/null @@ -1,171 +0,0 @@ - Limit::perMinute(5) - ->by(strtolower((string) $request->input('email')).'|'.$request->ip())); - - RateLimiter::for('registration', fn (Request $request) => - Limit::perHour(5)->by($request->ip()) - ); - - RateLimiter::for('license-activation', function (Request $request): array { - return [ - Limit::perMinute(12)->by($request->ip()), - Limit::perMinute(6)->by(hash('sha256', (string) $request->input('installationID'))), - ]; - }); - $this->registerTranslations(); - $this->registerConfig(); - $this->registerViews(); - $this->registerHelpers(); - $this->registerCommands(); - $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); - } - - /** - * Register the service provider. - * - * @return void - */ - public function register(): void - { - $this->app->register(RouteServiceProvider::class); - } - - /** - * Register the helpers. - * - * @return void - */ - protected function registerHelpers(): void - { - foreach (glob(__DIR__.'/../Helpers/*.php') as $filename) { - require_once $filename; - } - } - - /** - * Dynamically Register Commands. - * - * @return void - */ - public function registerCommands(): void - { - if (!$this->app->runningInConsole()) { - return; - } - $commandPath = __DIR__ . '/../Console'; - $commands = collect(File::allFiles($commandPath)) - ->map(function ($file) { - $class = 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension(); - return $class; - }) - ->filter(function ($class) { - return class_exists($class); - }) - ->values() - ->all(); - $this->commands($commands); - } - - /** - * Register config. - * - * @return void - */ - protected function registerConfig(): void - { - $this->publishes([ - module_path(ModuleInfo::name(), 'Config/config.php') => config_path( ModuleInfo::nameLower(). '.php'), - ], 'config'); - $this->mergeConfigFrom( - module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower() - ); - } - - /** - * Register views. - * - * @return void - */ - public function registerViews(): void - { - $viewPath = resource_path('views/modules/' . ModuleInfo::nameLower()); - $sourcePath = module_path(ModuleInfo::name(), 'Resources/views'); - $this->publishes([ - $sourcePath => $viewPath - ], ['views', ModuleInfo::nameLower() . '-module-views']); - $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower()); - } - - /** - * Register translations. - * - * @return void - */ - public function registerTranslations(): void - { - $langPath = resource_path('lang/modules/' . ModuleInfo::nameLower()); - - if (is_dir($langPath)) { - $this->loadTranslationsFrom($langPath, ModuleInfo::nameLower()); - } else { - $this->loadTranslationsFrom(module_path(ModuleInfo::name(), 'Resources/lang'), ModuleInfo::nameLower()); - } - } - - /** - * Regsiter View Paths - * @return array - */ - private function getPublishableViewPaths(): array - { - $paths = []; - foreach (Config::get('view.paths') as $path) { - if (is_dir($path . '/modules/' . ModuleInfo::nameLower())) { - $paths[] = $path . '/modules/' . ModuleInfo::nameLower(); - } - } - return $paths; - } -} diff --git a/Licensing/Providers/RouteServiceProvider.php b/Licensing/Providers/RouteServiceProvider.php deleted file mode 100644 index c691ba8..0000000 --- a/Licensing/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,69 +0,0 @@ -mapApiRoutes(); - - $this->mapWebRoutes(); - } - - /** - * Define the "web" routes for the application. - * - * These routes all receive session state, CSRF protection, etc. - * - * @return void - */ - protected function mapWebRoutes() - { - Route::middleware('web') - ->namespace($this->moduleNamespace) - ->group(module_path('Licensing', '/Routes/web.php')); - } - - /** - * Define the "api" routes for the application. - * - * These routes are typically stateless. - * - * @return void - */ - protected function mapApiRoutes() - { - Route::prefix('api') - ->middleware('api') - ->namespace($this->moduleNamespace) - ->group(module_path('Licensing', '/Routes/api.php')); - } -} diff --git a/Licensing/Resources/assets/.gitkeep b/Licensing/Resources/assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Resources/assets/js/app.js b/Licensing/Resources/assets/js/app.js deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Resources/assets/sass/app.scss b/Licensing/Resources/assets/sass/app.scss deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Resources/lang/.gitkeep b/Licensing/Resources/lang/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Resources/views/.gitkeep b/Licensing/Resources/views/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Resources/views/admin/index.blade.php b/Licensing/Resources/views/admin/index.blade.php deleted file mode 100644 index 8c72c09..0000000 --- a/Licensing/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,3 +0,0 @@ -@extends('licensing::admin.layout') -@section('title', 'Licensing Overview') -@section('content')

SonarDesk licensing

Total licences
{{ number_format($licenseCount) }}
Usable licences
{{ number_format($activeCount) }}
Products
{{ number_format($productCount) }}

Recent licences

@forelse($recentLicenses as $license)@empty@endforelse
CustomerProductEditionStatusCreated
{{ $license->customer_email ?: 'Unknown customer' }}{{ $license->product?->name ?? '—' }}{{ $license->edition }}{{ $license->is_usable ? 'Usable' : 'Inactive' }}{{ $license->created_at?->format('d M Y') }}
No licences found.

View all licences →

@endsection diff --git a/Licensing/Resources/views/admin/layout.blade.php b/Licensing/Resources/views/admin/layout.blade.php deleted file mode 100644 index 4bd80e4..0000000 --- a/Licensing/Resources/views/admin/layout.blade.php +++ /dev/null @@ -1,3 +0,0 @@ -@yield('title', 'SonarDesk Licensing') · ABSDev
ABSDev / SonarDesk
@yield('content')
diff --git a/Licensing/Resources/views/admin/licenses.blade.php b/Licensing/Resources/views/admin/licenses.blade.php deleted file mode 100644 index 9f3f47d..0000000 --- a/Licensing/Resources/views/admin/licenses.blade.php +++ /dev/null @@ -1,3 +0,0 @@ -@extends('licensing::admin.layout') -@section('title', 'Licences') -@section('content')

Licences

@forelse($licenses as $license)@empty@endforelse
CustomerProductEditionProviderStatusCreated
{{ $license->customer_email ?: 'Unknown customer' }}{{ $license->product?->name ?? '—' }}{{ $license->edition }}{{ $license->payment_provider ?? '—' }}{{ $license->is_usable ? 'Usable' : 'Inactive' }}{{ $license->created_at?->format('d M Y') }}
No licences found.
{{ $licenses->links() }}
@endsection diff --git a/Licensing/Resources/views/admin/show.blade.php b/Licensing/Resources/views/admin/show.blade.php deleted file mode 100644 index 42f12b3..0000000 --- a/Licensing/Resources/views/admin/show.blade.php +++ /dev/null @@ -1,3 +0,0 @@ -@extends('licensing::admin.layout') -@section('title', 'Licence details') -@section('content')

Licence details

Customer and licence

Customer
{{ $license->customer_name ?? '—' }}
Email
{{ $license->customer_email ?? '—' }}
Product
{{ $license->product?->name ?? '—' }}
Edition
{{ $license->edition }}
Key (last four only)
•••• {{ $license->license_key_last4 }}
Status
{{ $license->is_usable ? 'Usable' : 'Inactive' }}

Subscription

Provider
{{ $license->payment_provider ?? '—' }}
Subscription status
{{ $license->subscription_status ?? '—' }}
Period ends
{{ $license->current_period_ends_at?->format('d M Y') ?? '—' }}
Expires
{{ $license->expires_at?->format('d M Y') ?? '—' }}
Activation limit
{{ $license->activation_limit }}

Device activations

@forelse($license->activations as $activation)@empty@endforelse
PlatformVersionActivatedLast seenStatus
{{ $activation->platform ?? '—' }}{{ $activation->app_version ?? '—' }}{{ $activation->activated_at?->format('d M Y H:i') ?? '—' }}{{ $activation->last_seen_at?->format('d M Y H:i') ?? '—' }}{{ $activation->deactivated_at ? 'Deactivated' : 'Active' }}
No device activations.
@endsection diff --git a/Licensing/Resources/views/index.blade.php b/Licensing/Resources/views/index.blade.php deleted file mode 100644 index 4277b06..0000000 --- a/Licensing/Resources/views/index.blade.php +++ /dev/null @@ -1,9 +0,0 @@ -@extends('licensing::layouts.master') - -@section('content') -

Hello World

- -

- This view is loaded from module: {!! config('licensing.name') !!} -

-@endsection diff --git a/Licensing/Resources/views/layouts/master.blade.php b/Licensing/Resources/views/layouts/master.blade.php deleted file mode 100644 index 1e51f1c..0000000 --- a/Licensing/Resources/views/layouts/master.blade.php +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - Module Licensing - - {{-- Laravel Mix - CSS File --}} - {{-- --}} - - - - @yield('content') - - {{-- Laravel Mix - JS File --}} - {{-- --}} - - diff --git a/Licensing/Routes/.gitkeep b/Licensing/Routes/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Routes/admin.php b/Licensing/Routes/admin.php deleted file mode 100644 index 592524a..0000000 --- a/Licensing/Routes/admin.php +++ /dev/null @@ -1,14 +0,0 @@ -prefix('administration/sonardesk') - ->name('administration.sonardesk.') - ->group(function (): void { - Route::get('/', [LicensingDashboardController::class, 'index'])->name('index'); - Route::get('/licenses', [LicensingDashboardController::class, 'licenses'])->name('licenses'); - Route::get('/licenses/{license}', [LicensingDashboardController::class, 'show'])->name('licenses.show'); - }); diff --git a/Licensing/Routes/api.php b/Licensing/Routes/api.php deleted file mode 100644 index a923714..0000000 --- a/Licensing/Routes/api.php +++ /dev/null @@ -1,12 +0,0 @@ -group(function (): void { - Route::post('/activate', [LicenseActivationController::class, 'activate'])->middleware('throttle:license-activation'); - Route::post('/validate', [LicenseActivationController::class, 'validateInstallation'])->middleware('throttle:license-activation'); - Route::post('/deactivate', [LicenseActivationController::class, 'deactivate'])->middleware('throttle:license-activation'); - Route::post('/webhooks/{provider}', PaymentWebhookController::class)->whereIn('provider', ['paddle', 'lemon-squeezy']); -}); diff --git a/Licensing/Routes/web.php b/Licensing/Routes/web.php deleted file mode 100644 index cfe4d88..0000000 --- a/Licensing/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('portal')->name('portal.')->group(function (): void { - Route::get('/', [PortalController::class, 'index'])->name('index'); - Route::get('/licences/{license}', [PortalController::class, 'show'])->name('show'); - Route::post('/licences/{license}/upgrade', [PortalController::class, 'upgrade'])->middleware('throttle:5,1')->name('upgrade'); - Route::delete('/licences/{license}/subscription', [PortalController::class, 'cancel'])->middleware('throttle:5,1')->name('cancel'); - Route::post('/licences/{license}/resend', [PortalController::class, 'resend'])->middleware('throttle:3,10')->name('resend'); - Route::delete('/licences/{license}/activations/{activation}', [PortalController::class, 'deactivate'])->name('deactivate'); -}); diff --git a/Licensing/Services/LicenseDeliveryService.php b/Licensing/Services/LicenseDeliveryService.php deleted file mode 100755 index 053600d..0000000 --- a/Licensing/Services/LicenseDeliveryService.php +++ /dev/null @@ -1,54 +0,0 @@ -with('license') - ->whereNull('delivered_at') - ->whereNotNull('recipient_email') - ->where('recipient_email', '!=', '') - ->oldest() - ->limit(max(1, min($limit, 100))) - ->get(); - - foreach ($pending as $outbox) { - $license = $outbox->license; - if (! $license) { - continue; - } - - $plain = Crypt::decryptString($outbox->encrypted_license_key); - $recipient = strtolower(trim((string) $outbox->recipient_email)); - if ($recipient === '') { - continue; - } - - $data = [ - 'customerName' => $license->customer_name, - 'edition' => $license->edition, - 'licenseKey' => $plain, - 'expiresAt' => $license->expires_at, - 'portalUrl' => config('sonardesk.portal_url') ?: url('/portal'), - 'downloadUrl' => route('release'), - ]; - - Mail::send(['html' => 'emails.license-key', 'text' => 'emails.license-key-text'], $data, function ($message) use ($recipient): void { - $message->to($recipient)->subject('Your SonarDesk licence key'); - }); - - $outbox->update(['delivered_at' => now()]); - $delivered++; - } - - return $delivered; - } -} diff --git a/Licensing/Services/LicenseIssuer.php b/Licensing/Services/LicenseIssuer.php deleted file mode 100755 index 7171dd8..0000000 --- a/Licensing/Services/LicenseIssuer.php +++ /dev/null @@ -1,46 +0,0 @@ -where('external_order_id', $orderID)->first(); - if ($existing) { - return [$existing, null]; - } - } - $plain = LicenseKey::generate(); - $license = License::create([ - 'license_product_id' => $product->id, - 'license_key_hash' => LicenseKey::hash($plain), - 'license_key_last4' => substr(LicenseKey::normalize($plain), -4), - 'customer_name' => $name, - 'customer_email' => $email ? strtolower(trim($email)) : null, - 'edition' => $product->edition, - 'activation_limit' => $product->activation_limit, - 'max_major_version' => $product->max_major_version, - 'payment_provider' => $provider, - 'external_order_id' => $orderID, - 'external_customer_id' => $customerID, - ]); - if ($queueDelivery) { - LicenseDeliveryOutbox::updateOrCreate( - ['license_id' => $license->id], - ['encrypted_license_key' => Crypt::encryptString($plain), 'recipient_email' => $license->customer_email, 'delivered_at' => null] - ); - } - - return [$license, $plain]; - }); - } -} diff --git a/Licensing/Services/LicenseKey.php b/Licensing/Services/LicenseKey.php deleted file mode 100755 index 4e2501f..0000000 --- a/Licensing/Services/LicenseKey.php +++ /dev/null @@ -1,27 +0,0 @@ -privateSeed(); - $pair = sodium_crypto_sign_seed_keypair($seed); - $secret = sodium_crypto_sign_secretkey($pair); - - $payload = [ - 'customerEmail' => $license->customer_email, - 'customerName' => $license->customer_name, - 'edition' => $license->edition, - 'expiresAt' => $license->expires_at?->utc()->format('Y-m-d\\TH:i:s\\Z'), - 'installationID' => strtolower($installationID), - 'issuedAt' => ($issuedAt ?? now())->utc()->format('Y-m-d\\TH:i:s\\Z'), - 'licenseID' => strtolower((string) $license->id), - 'maxMajorVersion' => $license->max_major_version, - ]; - ksort($payload, SORT_STRING); - $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); - $sig = sodium_crypto_sign_detached($json, $secret); - - return 'RS3.'.self::b64url($json).'.'.self::b64url($sig); - } - - public function publicKeyBase64(): string - { - $pair = sodium_crypto_sign_seed_keypair($this->privateSeed()); - - return base64_encode(sodium_crypto_sign_publickey($pair)); - } - - private function privateSeed(): string - { - $path = config('sonardesk.private_key_path'); - if (! $path || ! is_file($path)) { - throw new RuntimeException('SONARDESK_LICENSE_PRIVATE_KEY_PATH is not configured or does not exist.'); - } - $decoded = base64_decode(trim((string) file_get_contents($path)), true); - if ($decoded === false || strlen($decoded) !== SODIUM_CRYPTO_SIGN_SEEDBYTES) { - throw new RuntimeException('The SonarDesk private key must be a Base64-encoded 32-byte Ed25519 seed.'); - } - - return $decoded; - } - - private static function b64url(string $value): string - { - return rtrim(strtr(base64_encode($value), '+/', '-_'), '='); - } -} diff --git a/Licensing/Services/LicenseTokenVerifier.php b/Licensing/Services/LicenseTokenVerifier.php deleted file mode 100755 index 052f633..0000000 --- a/Licensing/Services/LicenseTokenVerifier.php +++ /dev/null @@ -1,58 +0,0 @@ -decode($parts[1]); - $signature = $this->decode($parts[2]); - // Derive the verification key from the same private seed used by LicenseSigner. - // Keeping a separate SONARDESK_LICENSE_PUBLIC_KEY allowed the two keys to drift: - // activation would sign a perfectly valid token with the new private key, then - // /validate would reject that same token using a stale public key. - $public = $this->publicKeyFromPrivateSeed(); - if (! sodium_crypto_sign_verify_detached($signature, $payload, $public)) { - throw new RuntimeException('Licence token signature is invalid.'); - } - $decoded = json_decode($payload, true, flags: JSON_THROW_ON_ERROR); - - return is_array($decoded) ? $decoded : throw new RuntimeException('Licence token payload is invalid.'); - } - - private function publicKeyFromPrivateSeed(): string - { - $path = config('sonardesk.private_key_path'); - if (! $path || ! is_file($path)) { - throw new RuntimeException('SONARDESK_LICENSE_PRIVATE_KEY_PATH is not configured or does not exist.'); - } - - $seed = base64_decode(trim((string) file_get_contents($path)), true); - if ($seed === false || strlen($seed) !== SODIUM_CRYPTO_SIGN_SEEDBYTES) { - throw new RuntimeException('The SonarDesk private key must be a Base64-encoded 32-byte Ed25519 seed.'); - } - - $pair = sodium_crypto_sign_seed_keypair($seed); - - return sodium_crypto_sign_publickey($pair); - } - - private function decode(string $value): string - { - $value = strtr($value, '-_', '+/'); - $value .= str_repeat('=', (4 - strlen($value) % 4) % 4); - $out = base64_decode($value, true); - if ($out === false) { - throw new RuntimeException('Invalid Base64URL value.'); - } - - return $out; - } -} diff --git a/Licensing/Support/.gitkeep b/Licensing/Support/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Licensing/Support/ModuleInfo.php b/Licensing/Support/ModuleInfo.php deleted file mode 100644 index 28b9983..0000000 --- a/Licensing/Support/ModuleInfo.php +++ /dev/null @@ -1,26 +0,0 @@ -morphMany(LocalizedContent::class, 'translatable'); - } - - public function translated(string $field, ?string $locale = null, mixed $fallback = null): mixed - { - $locale ??= app()->getLocale(); - $translation = $this->translations->first(fn (LocalizedContent $item): bool => $item->field === $field && $item->locale?->code === $locale); - if ($translation && filled($translation->value)) { - return $translation->value; - } - - return $fallback ?? $this->getAttribute($field); - } - - public function setTranslation(string $field, string $localeCode, ?string $value, ?int $userId = null): LocalizedContent - { - $locale = Locale::query()->where('code', $localeCode)->firstOrFail(); - - return $this->translations()->updateOrCreate( - ['locale_id' => $locale->id, 'field' => $field], - ['value' => $value, 'updated_by' => $userId] - ); - } -} diff --git a/Localization/Config/config.php b/Localization/Config/config.php deleted file mode 100644 index da2392d..0000000 --- a/Localization/Config/config.php +++ /dev/null @@ -1,16 +0,0 @@ - env('APP_LOCALE', 'en'), - 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), - 'session_key' => 'platform_locale', - 'supported_subjects' => [ - 'pages' => ['model' => Page::class, 'label' => 'Pages', 'fields' => ['title', 'summary', 'content', 'meta_title', 'meta_description']], - 'blog' => ['model' => Blog::class, 'label' => 'Blog', 'fields' => ['title', 'excerpt', 'content', 'meta_title', 'meta_description']], - 'news' => ['model' => News::class, 'label' => 'News', 'fields' => ['title', 'excerpt', 'content', 'meta_title', 'meta_description']], - ], -]; diff --git a/Localization/Database/Migrations/2026_07_24_06000_create_localization_tables.php b/Localization/Database/Migrations/2026_07_24_06000_create_localization_tables.php deleted file mode 100644 index 24f8aba..0000000 --- a/Localization/Database/Migrations/2026_07_24_06000_create_localization_tables.php +++ /dev/null @@ -1,46 +0,0 @@ -id(); - $table->string('code', 20)->unique(); - $table->string('name', 100); - $table->string('native_name', 100); - $table->string('regional_code', 20)->nullable(); - $table->string('direction', 3)->default('ltr'); - $table->string('date_format', 40)->default('d/m/Y'); - $table->string('time_format', 40)->default('H:i'); - $table->string('timezone', 80)->default('UTC'); - $table->string('currency_code', 3)->nullable(); - $table->string('currency_symbol', 10)->nullable(); - $table->unsignedInteger('position')->default(0); - $table->boolean('is_default')->default(false); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - Schema::create('localized_contents', function (Blueprint $table): void { - $table->id(); - $table->morphs('translatable'); - $table->foreignId('locale_id')->constrained()->cascadeOnDelete(); - $table->string('field', 100); - $table->longText('value')->nullable(); - $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamps(); - $table->unique(['translatable_type', 'translatable_id', 'locale_id', 'field'], 'localized_content_unique'); - $table->index(['locale_id', 'field']); - }); - } - - public function down(): void - { - Schema::dropIfExists('localized_contents'); - Schema::dropIfExists('locales'); - } -}; diff --git a/Localization/Database/Seeders/LocaleSeeder.php b/Localization/Database/Seeders/LocaleSeeder.php deleted file mode 100644 index 1bf793d..0000000 --- a/Localization/Database/Seeders/LocaleSeeder.php +++ /dev/null @@ -1,19 +0,0 @@ -updateOrCreate(['code' => 'en'], [ - 'name' => 'English', 'native_name' => 'English', 'regional_code' => 'en-GB', - 'direction' => 'ltr', 'date_format' => 'd/m/Y', 'time_format' => 'H:i', - 'timezone' => config('app.timezone', 'UTC'), 'currency_code' => 'GBP', - 'currency_symbol' => '£', 'position' => 0, 'is_default' => true, 'is_active' => true, - ]); - } -} diff --git a/Localization/Http/Controllers/LocaleController.php b/Localization/Http/Controllers/LocaleController.php deleted file mode 100644 index 8e3a54c..0000000 --- a/Localization/Http/Controllers/LocaleController.php +++ /dev/null @@ -1,78 +0,0 @@ -user()->can('localization.manage'), 403); - $subjects = collect(config('localization.supported_subjects'))->map(function (array $config): array { - $model = $config['model']; - - return $config + ['records' => $model::query()->latest()->limit(8)->get()]; - }); - - return view('localization::admin.index', ['locales' => Locale::query()->orderBy('position')->get(), 'subjects' => $subjects]); - } - - public function switch(string $locale, LocaleManager $manager): RedirectResponse - { - $manager->activate($locale); - - return back(); - } - - public function store(Request $request): RedirectResponse - { - abort_unless($request->user()->can('localization.manage'), 403); - $data = $request->validate($this->rules()); - if (($data['is_default'] ?? false) === true) { - Locale::query()->update(['is_default' => false]); - } - Locale::query()->create($data); - - return back()->with('success', 'Locale created.'); - } - - public function update(Request $request, Locale $locale): RedirectResponse - { - abort_unless($request->user()->can('localization.manage'), 403); - $data = $request->validate($this->rules($locale)); - if (($data['is_default'] ?? false) === true) { - Locale::query()->whereKeyNot($locale->id)->update(['is_default' => false]); - } - $locale->update($data); - - return back()->with('success', 'Locale updated.'); - } - - public function destroy(Request $request, Locale $locale): RedirectResponse - { - abort_unless($request->user()->can('localization.manage'), 403); - abort_if($locale->is_default, 422, 'The default locale cannot be deleted.'); - $locale->delete(); - - return back()->with('success', 'Locale deleted.'); - } - - private function rules(?Locale $locale = null): array - { - return [ - 'code' => ['required', 'string', 'max:20', 'unique:locales,code,'.($locale?->id ?? 'NULL')], - 'name' => ['required', 'string', 'max:100'], 'native_name' => ['required', 'string', 'max:100'], - 'regional_code' => ['nullable', 'string', 'max:20'], 'direction' => ['required', 'in:ltr,rtl'], - 'date_format' => ['required', 'string', 'max:40'], 'time_format' => ['required', 'string', 'max:40'], - 'timezone' => ['required', 'timezone'], 'currency_code' => ['nullable', 'string', 'size:3'], - 'currency_symbol' => ['nullable', 'string', 'max:10'], 'position' => ['nullable', 'integer', 'min:0'], - 'is_default' => ['sometimes', 'boolean'], 'is_active' => ['sometimes', 'boolean'], - ]; - } -} diff --git a/Localization/Http/Controllers/TranslationController.php b/Localization/Http/Controllers/TranslationController.php deleted file mode 100644 index 69fd8b5..0000000 --- a/Localization/Http/Controllers/TranslationController.php +++ /dev/null @@ -1,44 +0,0 @@ -user()->can('localization.translate'), 403); - [$record, $config] = $this->resolve($subject, $id); - $record->load('translations.locale'); - - return view('localization::admin.translate', ['record' => $record, 'subject' => $subject, 'config' => $config, 'locales' => Locale::query()->active()->get()]); - } - - public function update(Request $request, string $subject, int $id): RedirectResponse - { - abort_unless($request->user()->can('localization.translate'), 403); - [$record, $config] = $this->resolve($subject, $id); - $data = $request->validate(['translations' => ['array'], 'translations.*' => ['array']]); - foreach ($data['translations'] ?? [] as $localeCode => $fields) { - foreach ($config['fields'] as $field) { - $record->setTranslation($field, $localeCode, $fields[$field] ?? null, $request->user()->id); - } - } - - return back()->with('success', 'Translations saved.'); - } - - private function resolve(string $subject, int $id): array - { - $config = config("localization.supported_subjects.$subject"); - abort_unless(is_array($config), 404); - $record = $config['model']::query()->findOrFail($id); - - return [$record, $config]; - } -} diff --git a/Localization/Http/Middleware/SetLocale.php b/Localization/Http/Middleware/SetLocale.php deleted file mode 100644 index 1d93a23..0000000 --- a/Localization/Http/Middleware/SetLocale.php +++ /dev/null @@ -1,53 +0,0 @@ -route('locale'); - - if (! $code && $request->hasSession()) { - $code = $request->session()->get($sessionKey); - } - - if (! Schema::hasTable('locales')) { - app()->setLocale(is_string($code) && $code !== '' ? $code : $fallback); - - return $next($request); - } - - if (! $code) { - $code = Locale::query() - ->where('is_default', true) - ->value('code') ?: $fallback; - } - - $isActive = Locale::query() - ->where('code', $code) - ->where('is_active', true) - ->exists(); - - if ($isActive) { - app()->setLocale((string) $code); - - if ($request->hasSession()) { - $request->session()->put($sessionKey, $code); - } - } else { - app()->setLocale($fallback); - } - - return $next($request); - } -} diff --git a/Localization/Models/Locale.php b/Localization/Models/Locale.php deleted file mode 100644 index 97fa973..0000000 --- a/Localization/Models/Locale.php +++ /dev/null @@ -1,27 +0,0 @@ - 'boolean', 'is_active' => 'boolean']; - } - - public function translations(): HasMany - { - return $this->hasMany(LocalizedContent::class); - } - - public function scopeActive(Builder $query): Builder - { - return $query->where('is_active', true)->orderBy('position')->orderBy('name'); - } -} diff --git a/Localization/Models/LocalizedContent.php b/Localization/Models/LocalizedContent.php deleted file mode 100644 index 2a10675..0000000 --- a/Localization/Models/LocalizedContent.php +++ /dev/null @@ -1,28 +0,0 @@ -morphTo(); - } - - public function locale(): BelongsTo - { - return $this->belongsTo(Locale::class); - } - - public function editor(): BelongsTo - { - return $this->belongsTo(User::class, 'updated_by'); - } -} diff --git a/Localization/Providers/LocalizationServiceProvider.php b/Localization/Providers/LocalizationServiceProvider.php deleted file mode 100644 index 6383fa8..0000000 --- a/Localization/Providers/LocalizationServiceProvider.php +++ /dev/null @@ -1,22 +0,0 @@ -mergeConfigFrom(module_path('Localization', 'Config/config.php'), 'localization'); - $this->app->singleton(LocaleManager::class); - $this->app->register(RouteServiceProvider::class); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Localization', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Localization', 'Resources/views'), 'localization'); - } -} diff --git a/Localization/Providers/RouteServiceProvider.php b/Localization/Providers/RouteServiceProvider.php deleted file mode 100644 index 39dae19..0000000 --- a/Localization/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Localization', 'Routes/web.php')); - } -} diff --git a/Localization/Resources/views/admin/index.blade.php b/Localization/Resources/views/admin/index.blade.php deleted file mode 100644 index 4dca7c8..0000000 --- a/Localization/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -
LocalisationLanguages, regional formats and translated content.
Locales@foreach($locales as $locale)
@csrf @method('PUT')
Save
@endforeach
Add locale
@csrfAdd locale
Translate content
@foreach($subjects as $key=>$subject)
{{ $subject['label'] }}
@foreach($subject['records'] as $record){{ $record->title ?? ('#'.$record->id) }}@endforeach
@endforeach
diff --git a/Localization/Resources/views/admin/translate.blade.php b/Localization/Resources/views/admin/translate.blade.php deleted file mode 100644 index c71edbc..0000000 --- a/Localization/Resources/views/admin/translate.blade.php +++ /dev/null @@ -1 +0,0 @@ -
Translate: {{ $record->title ?? ('#'.$record->id) }}{{ $config['label'] }} · source language values are shown for reference.
@csrf @method('PUT')@foreach($locales as $locale)
{{ $locale->native_name }}{{ $locale->code }}
@foreach($config['fields'] as $field)@php($existing=$record->translations->first(fn($item)=>$item->locale_id===$locale->id&&$item->field===$field)?->value)
{{ str($field)->headline() }}
Source: {{ str(strip_tags((string)$record->getAttribute($field)))->limit(180) }}
@if(in_array($field,['content','summary','excerpt','meta_description'])){{ $existing }}@else@endif
@endforeach
@endforeach
Save translations
diff --git a/Localization/Routes/web.php b/Localization/Routes/web.php deleted file mode 100644 index 119020e..0000000 --- a/Localization/Routes/web.php +++ /dev/null @@ -1,15 +0,0 @@ -name('localization.switch'); -Route::middleware(['auth', 'verified'])->prefix('administration/localization')->name('administration.localization.')->group(function (): void { - Route::get('/', [LocaleController::class, 'index'])->name('index'); - Route::post('/locales', [LocaleController::class, 'store'])->name('locales.store'); - Route::put('/locales/{locale}', [LocaleController::class, 'update'])->name('locales.update'); - Route::delete('/locales/{locale}', [LocaleController::class, 'destroy'])->name('locales.destroy'); - Route::get('/translate/{subject}/{id}', [TranslationController::class, 'edit'])->name('translations.edit'); - Route::put('/translate/{subject}/{id}', [TranslationController::class, 'update'])->name('translations.update'); -}); diff --git a/Localization/Services/LocaleManager.php b/Localization/Services/LocaleManager.php deleted file mode 100644 index 0454a58..0000000 --- a/Localization/Services/LocaleManager.php +++ /dev/null @@ -1,28 +0,0 @@ -active()->get(); - } - - public function default(): ?Locale - { - return Locale::query()->where('is_default', true)->first() ?? Locale::query()->active()->first(); - } - - public function activate(string $code): Locale - { - $locale = Locale::query()->active()->where('code', $code)->firstOrFail(); - app()->setLocale($locale->code); - session()->put(config('localization.session_key'), $locale->code); - - return $locale; - } -} diff --git a/Localization/Tests/Feature/ModuleBootTest.php b/Localization/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index c4aea49..0000000 --- a/Localization/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,14 +0,0 @@ -assertTrue(app()->providerIsLoaded(LocalizationServiceProvider::class)); - } -} diff --git a/Localization/module.json b/Localization/module.json deleted file mode 100644 index a5e28f7..0000000 --- a/Localization/module.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "Localization", - "alias": "localization", - "description": "Locales, translated content, regional settings and language switching.", - "keywords": ["localization", "translation", "locale", "language"], - "priority": 36, - "providers": ["Modules\\Localization\\Providers\\LocalizationServiceProvider"], - "files": [] -} diff --git a/Locations/Config/config.php b/Locations/Config/config.php deleted file mode 100644 index ba035af..0000000 --- a/Locations/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Locations']; diff --git a/Locations/Database/Migrations/2026_07_24_04400_create_locations_table.php b/Locations/Database/Migrations/2026_07_24_04400_create_locations_table.php deleted file mode 100644 index 8ef54fb..0000000 --- a/Locations/Database/Migrations/2026_07_24_04400_create_locations_table.php +++ /dev/null @@ -1,34 +0,0 @@ -id(); - $t->string('name'); - $t->string('slug')->unique(); - $t->string('type')->default('office'); - $t->text('address')->nullable(); - $t->string('postcode')->nullable(); - $t->string('country')->nullable(); - $t->string('phone')->nullable(); - $t->string('email')->nullable(); - $t->decimal('latitude', 10, 7)->nullable(); - $t->decimal('longitude', 10, 7)->nullable(); - $t->json('opening_hours')->nullable(); - $t->boolean('is_active')->default(true); - $t->unsignedInteger('sort_order')->default(0); - $t->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('locations'); - } -}; diff --git a/Locations/Http/Controllers/LocationController.php b/Locations/Http/Controllers/LocationController.php deleted file mode 100644 index a1b2606..0000000 --- a/Locations/Http/Controllers/LocationController.php +++ /dev/null @@ -1,31 +0,0 @@ - Location::orderBy('sort_order')->orderBy('name')->paginate(25)]); - } - - public function store(Request $r): RedirectResponse - { - Location::create($r->validate(['name' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255', 'unique:locations,slug'], 'type' => ['required', 'in:office,store,warehouse,venue,service'], 'address' => ['nullable', 'string'], 'postcode' => ['nullable', 'string', 'max:30'], 'country' => ['nullable', 'string', 'max:120'], 'phone' => ['nullable', 'string', 'max:80'], 'email' => ['nullable', 'email', 'max:255'], 'latitude' => ['nullable', 'numeric', 'between:-90,90'], 'longitude' => ['nullable', 'numeric', 'between:-180,180'], 'sort_order' => ['nullable', 'integer', 'min:0']]) + ['is_active' => $r->boolean('is_active', true)]); - - return back()->with('status', 'Location created.'); - } - - public function destroy(Location $location): RedirectResponse - { - $location->delete(); - - return back()->with('status', 'Location deleted.'); - } -} diff --git a/Locations/Models/Location.php b/Locations/Models/Location.php deleted file mode 100644 index 913fd40..0000000 --- a/Locations/Models/Location.php +++ /dev/null @@ -1,15 +0,0 @@ - 'decimal:7', 'longitude' => 'decimal:7', 'opening_hours' => 'array', 'is_active' => 'boolean']; - } -} diff --git a/Locations/Providers/LocationsServiceProvider.php b/Locations/Providers/LocationsServiceProvider.php deleted file mode 100644 index de2419b..0000000 --- a/Locations/Providers/LocationsServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Locations', 'Config/config.php'), 'locations'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Locations', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Locations', 'Resources/views'), 'locations'); - } -} diff --git a/Locations/Providers/RouteServiceProvider.php b/Locations/Providers/RouteServiceProvider.php deleted file mode 100644 index cb2ee3e..0000000 --- a/Locations/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Locations', 'Routes/web.php')); - } -} diff --git a/Locations/Resources/views/admin/index.blade.php b/Locations/Resources/views/admin/index.blade.php deleted file mode 100644 index 3b1f2c1..0000000 --- a/Locations/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Locations

Manage offices, stores, venues and service locations.

@if(session('status'))
{{ session('status') }}
@endif
@csrf

Add location

Create location
@forelse($locations as $location)@empty@endforelse
LocationTypeContactStatus
{{ $location->name }}
{{ $location->address }}
{{ ucfirst($location->type) }}{{ $location->phone ?: $location->email ?: '—' }}{{ $location->is_active ? 'Active' : 'Inactive' }}
@csrf @method('DELETE')Delete
No locations yet.
{{ $locations->links() }}
diff --git a/Locations/Routes/web.php b/Locations/Routes/web.php deleted file mode 100644 index 2e5d437..0000000 --- a/Locations/Routes/web.php +++ /dev/null @@ -1,10 +0,0 @@ -prefix('administration/locations')->name('administration.locations.')->group(function (): void { - Route::get('/', [LocationController::class, 'index'])->name('index'); - Route::post('/', [LocationController::class, 'store'])->name('store'); - Route::delete('/{location}', [LocationController::class, 'destroy'])->name('destroy'); -}); diff --git a/Locations/Tests/Feature/ModuleBootTest.php b/Locations/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index c8dec12..0000000 --- a/Locations/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('Locations', config('locations.name')); - } -} diff --git a/Locations/module.json b/Locations/module.json deleted file mode 100644 index 86865e1..0000000 --- a/Locations/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Locations", - "alias": "locations", - "version": "1.0.0", - "description": "Office and service location management.", - "keywords": ["business", "locations"], - "priority": 44, - "providers": ["Modules\\Locations\\Providers\\LocationsServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Marketplace/Config/config.php b/Marketplace/Config/config.php deleted file mode 100644 index 2274c18..0000000 --- a/Marketplace/Config/config.php +++ /dev/null @@ -1,14 +0,0 @@ - (bool) env('MARKETPLACE_ENABLED', true), - 'catalogue_url' => env('MARKETPLACE_CATALOGUE_URL'), - 'allow_remote_install' => (bool) env('MARKETPLACE_ALLOW_REMOTE_INSTALL', false), - 'require_checksum' => (bool) env('MARKETPLACE_REQUIRE_CHECKSUM', true), - 'require_signature' => (bool) env('MARKETPLACE_REQUIRE_SIGNATURE', false), - 'trusted_publishers' => array_values(array_filter(array_map('trim', explode(',', (string) env('MARKETPLACE_TRUSTED_PUBLISHERS', ''))))), - 'storage_disk' => env('MARKETPLACE_STORAGE_DISK', 'local'), - 'package_path' => env('MARKETPLACE_PACKAGE_PATH', 'marketplace/packages'), - 'snapshot_path' => env('MARKETPLACE_SNAPSHOT_PATH', 'marketplace/snapshots'), - 'supported_types' => ['module', 'theme', 'extension'], -]; diff --git a/Marketplace/Console/Commands/MarketplaceAuditCommand.php b/Marketplace/Console/Commands/MarketplaceAuditCommand.php deleted file mode 100644 index b8a36b0..0000000 --- a/Marketplace/Console/Commands/MarketplaceAuditCommand.php +++ /dev/null @@ -1,24 +0,0 @@ -with('package')->get(); - foreach ($packages as $package) { - $this->line(sprintf('%s %s [%s]%s', $package->package_name, $package->version, $package->status, $package->hasUpdate() ? ' update available' : '')); - } - $this->info($packages->count().' installed package(s) audited.'); - - return self::SUCCESS; - } -} diff --git a/Marketplace/Console/Commands/MarketplaceSyncCommand.php b/Marketplace/Console/Commands/MarketplaceSyncCommand.php deleted file mode 100644 index 060aa58..0000000 --- a/Marketplace/Console/Commands/MarketplaceSyncCommand.php +++ /dev/null @@ -1,24 +0,0 @@ -warn('MARKETPLACE_CATALOGUE_URL is not configured.'); - - return self::SUCCESS; - } - $this->info('Catalogue synchronisation is configured. Remote installs remain disabled unless explicitly enabled.'); - - return self::SUCCESS; - } -} diff --git a/Marketplace/Database/Migrations/2026_07_24_170000_create_marketplace_tables.php b/Marketplace/Database/Migrations/2026_07_24_170000_create_marketplace_tables.php deleted file mode 100644 index eb2a3b9..0000000 --- a/Marketplace/Database/Migrations/2026_07_24_170000_create_marketplace_tables.php +++ /dev/null @@ -1,96 +0,0 @@ -id(); - $table->string('name'); - $table->string('slug')->unique(); - $table->string('email')->nullable(); - $table->string('website')->nullable(); - $table->text('public_key')->nullable(); - $table->boolean('is_verified')->default(false)->index(); - $table->json('metadata')->nullable(); - $table->timestamps(); - }); - Schema::create('marketplace_packages', function (Blueprint $table) { - $table->id(); - $table->foreignId('publisher_id')->nullable()->constrained('marketplace_publishers')->nullOnDelete(); - $table->string('type', 30)->index(); - $table->string('name'); - $table->string('slug')->unique(); - $table->string('package_name')->unique(); - $table->text('description')->nullable(); - $table->string('latest_version')->nullable(); - $table->string('homepage')->nullable(); - $table->string('repository_url')->nullable(); - $table->string('download_url')->nullable(); - $table->string('checksum', 128)->nullable(); - $table->text('signature')->nullable(); - $table->json('manifest')->nullable(); - $table->boolean('is_featured')->default(false)->index(); - $table->boolean('is_active')->default(true)->index(); - $table->timestamp('catalogue_updated_at')->nullable(); - $table->timestamps(); - }); - Schema::create('marketplace_installed_packages', function (Blueprint $table) { - $table->id(); - $table->foreignId('marketplace_package_id')->nullable()->constrained('marketplace_packages')->nullOnDelete(); - $table->string('package_name')->unique(); - $table->string('type', 30)->index(); - $table->string('version'); - $table->string('status', 30)->default('installed')->index(); - $table->json('manifest')->nullable(); - $table->string('checksum', 128)->nullable(); - $table->string('install_path')->nullable(); - $table->boolean('is_enabled')->default(true)->index(); - $table->foreignId('installed_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamp('installed_at')->nullable(); - $table->timestamp('updated_at_marketplace')->nullable(); - $table->timestamps(); - }); - Schema::create('marketplace_installations', function (Blueprint $table) { - $table->id(); - $table->foreignId('installed_package_id')->nullable()->constrained('marketplace_installed_packages')->nullOnDelete(); - $table->string('package_name')->index(); - $table->string('operation', 30)->index(); - $table->string('from_version')->nullable(); - $table->string('to_version')->nullable(); - $table->string('status', 30)->default('pending')->index(); - $table->string('snapshot_path')->nullable(); - $table->text('message')->nullable(); - $table->json('context')->nullable(); - $table->foreignId('performed_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamp('started_at')->nullable(); - $table->timestamp('completed_at')->nullable(); - $table->timestamps(); - }); - Schema::create('marketplace_licences', function (Blueprint $table) { - $table->id(); - $table->foreignId('marketplace_package_id')->nullable()->constrained('marketplace_packages')->nullOnDelete(); - $table->string('licence_key')->nullable(); - $table->string('status', 30)->default('inactive')->index(); - $table->string('plan')->nullable(); - $table->string('licensed_to')->nullable(); - $table->timestamp('activated_at')->nullable(); - $table->timestamp('expires_at')->nullable(); - $table->json('metadata')->nullable(); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('marketplace_licences'); - Schema::dropIfExists('marketplace_installations'); - Schema::dropIfExists('marketplace_installed_packages'); - Schema::dropIfExists('marketplace_packages'); - Schema::dropIfExists('marketplace_publishers'); - } -}; diff --git a/Marketplace/Http/Controllers/MarketplaceApiController.php b/Marketplace/Http/Controllers/MarketplaceApiController.php deleted file mode 100644 index 958d8f3..0000000 --- a/Marketplace/Http/Controllers/MarketplaceApiController.php +++ /dev/null @@ -1,40 +0,0 @@ -json(MarketplacePackage::query()->with('publisher')->where('is_active', true)->when($request->filled('type'), fn ($q) => $q->where('type', $request->string('type')))->paginate(min(100, max(1, $request->integer('per_page', 25))))); - } - - public function installed(): JsonResponse - { - return response()->json(InstalledPackage::query()->with('package')->paginate(50)); - } - - public function install(MarketplacePackage $package, MarketplaceManager $manager, Request $request): JsonResponse - { - return response()->json($manager->install($package, $request->user()?->id)->load('package'), 201); - } - - public function toggle(InstalledPackage $package, MarketplaceManager $manager, Request $request): JsonResponse - { - return response()->json($manager->setEnabled($package, ! $package->is_enabled, $request->user()?->id)); - } - - public function remove(InstalledPackage $package, MarketplaceManager $manager, Request $request): JsonResponse - { - $manager->remove($package, $request->user()?->id); - - return response()->json(['status' => 'removed']); - } -} diff --git a/Marketplace/Http/Controllers/MarketplaceController.php b/Marketplace/Http/Controllers/MarketplaceController.php deleted file mode 100644 index 45744a0..0000000 --- a/Marketplace/Http/Controllers/MarketplaceController.php +++ /dev/null @@ -1,64 +0,0 @@ -with('publisher')->where('is_active', true)->when($request->filled('type'), fn ($q) => $q->where('type', $request->string('type')))->when($request->filled('search'), fn ($q) => $q->where(fn ($q) => $q->where('name', 'like', '%'.$request->string('search').'%')->orWhere('description', 'like', '%'.$request->string('search').'%')))->latest('is_featured')->paginate(20)->withQueryString(); - - return view('marketplace::index', compact('packages')); - } - - public function installed(): View - { - $packages = InstalledPackage::query()->with('package')->latest()->paginate(25); - - return view('marketplace::installed', compact('packages')); - } - - public function install(MarketplacePackage $package, MarketplaceManager $manager, Request $request): RedirectResponse - { - $manager->install($package, $request->user()?->id); - - return back()->with('status', 'Package installed successfully.'); - } - - public function update(MarketplacePackage $package, MarketplaceManager $manager, Request $request): RedirectResponse - { - $manager->install($package, $request->user()?->id); - - return back()->with('status', 'Package updated successfully.'); - } - - public function toggle(InstalledPackage $package, MarketplaceManager $manager, Request $request): RedirectResponse - { - $manager->setEnabled($package, ! $package->is_enabled, $request->user()?->id); - - return back()->with('status', 'Package state updated.'); - } - - public function remove(InstalledPackage $package, MarketplaceManager $manager, Request $request): RedirectResponse - { - $manager->remove($package, $request->user()?->id); - - return back()->with('status', 'Package removed.'); - } - - public function rollback(InstalledPackage $package, Installation $installation, MarketplaceManager $manager, Request $request): RedirectResponse - { - $manager->rollback($package, $installation, $request->user()?->id); - - return back()->with('status','Package rolled back.'); - } -} diff --git a/Marketplace/Models/Installation.php b/Marketplace/Models/Installation.php deleted file mode 100644 index c152b8c..0000000 --- a/Marketplace/Models/Installation.php +++ /dev/null @@ -1,14 +0,0 @@ - 'array', 'started_at' => 'datetime', 'completed_at' => 'datetime']; -} diff --git a/Marketplace/Models/InstalledPackage.php b/Marketplace/Models/InstalledPackage.php deleted file mode 100644 index bb7eda5..0000000 --- a/Marketplace/Models/InstalledPackage.php +++ /dev/null @@ -1,31 +0,0 @@ - 'array', 'is_enabled' => 'boolean', 'installed_at' => 'datetime', 'updated_at_marketplace' => 'datetime']; - - public function package(): BelongsTo - { - return $this->belongsTo(MarketplacePackage::class, 'marketplace_package_id'); - } - - public function installations(): HasMany - { - return $this->hasMany(Installation::class, 'installed_package_id'); - } - - public function hasUpdate(): bool - { - return $this->package?->latest_version && version_compare($this->package->latest_version, $this->version, '>'); - } -} diff --git a/Marketplace/Models/MarketplaceLicence.php b/Marketplace/Models/MarketplaceLicence.php deleted file mode 100644 index 22038e8..0000000 --- a/Marketplace/Models/MarketplaceLicence.php +++ /dev/null @@ -1,16 +0,0 @@ - 'datetime', 'expires_at' => 'datetime', 'metadata' => 'array']; -} diff --git a/Marketplace/Models/MarketplacePackage.php b/Marketplace/Models/MarketplacePackage.php deleted file mode 100644 index ad4a0c2..0000000 --- a/Marketplace/Models/MarketplacePackage.php +++ /dev/null @@ -1,26 +0,0 @@ - 'array', 'is_featured' => 'boolean', 'is_active' => 'boolean', 'catalogue_updated_at' => 'datetime']; - - public function publisher(): BelongsTo - { - return $this->belongsTo(Publisher::class); - } - - public function licences(): HasMany - { - return $this->hasMany(MarketplaceLicence::class, 'marketplace_package_id'); - } -} diff --git a/Marketplace/Models/Publisher.php b/Marketplace/Models/Publisher.php deleted file mode 100644 index 16558ef..0000000 --- a/Marketplace/Models/Publisher.php +++ /dev/null @@ -1,20 +0,0 @@ - 'boolean', 'metadata' => 'array']; - - public function packages(): HasMany - { - return $this->hasMany(MarketplacePackage::class, 'publisher_id'); - } -} diff --git a/Marketplace/Providers/MarketplaceServiceProvider.php b/Marketplace/Providers/MarketplaceServiceProvider.php deleted file mode 100644 index 0edd17f..0000000 --- a/Marketplace/Providers/MarketplaceServiceProvider.php +++ /dev/null @@ -1,36 +0,0 @@ -mergeConfigFrom(module_path('Marketplace', 'Config/config.php'), 'marketplace'); - $this->app->singleton(PackageManifest::class); - $this->app->singleton(CompatibilityChecker::class); - $this->app->singleton(DependencyResolver::class); - $this->app->singleton(PackageVerifier::class); - $this->app->singleton(MarketplaceManager::class); - $this->app->register(RouteServiceProvider::class); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Marketplace', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Marketplace', 'Resources/views'), 'marketplace'); - $this->loadTranslationsFrom(module_path('Marketplace', 'Resources/lang'), 'marketplace'); - if ($this->app->runningInConsole()) { - $this->commands([MarketplaceSyncCommand::class, MarketplaceAuditCommand::class]); - } - } -} diff --git a/Marketplace/Providers/RouteServiceProvider.php b/Marketplace/Providers/RouteServiceProvider.php deleted file mode 100644 index 6bf0725..0000000 --- a/Marketplace/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Marketplace', 'Routes/web.php')); - Route::middleware('api')->prefix('api/v1')->group(module_path('Marketplace', 'Routes/api.php')); - } -} diff --git a/Marketplace/Resources/views/index.blade.php b/Marketplace/Resources/views/index.blade.php deleted file mode 100644 index 6399b57..0000000 --- a/Marketplace/Resources/views/index.blade.php +++ /dev/null @@ -1,97 +0,0 @@ - -
-
-
- Marketplace - - Discover verified modules, themes and extensions. - -
- - - Installed packages - -
- -
- - - - - - @foreach (config('marketplace.supported_types', []) as $type) - - @endforeach - - - Filter - - -
- @forelse ($packages as $package) -
-
-
-
-

{{ $package->name }}

- - @if ($package->is_featured) - Featured - @endif -
- -

- {{ $package->package_name }} - · {{ \Illuminate\Support\Str::headline($package->type) }} - · {{ $package->latest_version ?: 'No release' }} -

-
- - @can('marketplace.install') -
- @csrf - - - Install - -
- @endcan -
- -

{{ $package->description }}

- - @if ($package->publisher) -

- Published by {{ $package->publisher->name }} - - @if ($package->publisher->is_verified) - · Verified - @endif -

- @endif -
- @empty -
- No marketplace packages found. -
- @endforelse -
- - {{ $packages->links() }} -
-
diff --git a/Marketplace/Resources/views/installed.blade.php b/Marketplace/Resources/views/installed.blade.php deleted file mode 100644 index 7609352..0000000 --- a/Marketplace/Resources/views/installed.blade.php +++ /dev/null @@ -1,104 +0,0 @@ - -
-
-
- Installed Packages - - Manage package state, updates and removals. - -
- - - Browse marketplace - -
- -
- - - - - - - - - - - - - @forelse ($packages as $package) - - - - - - - - - - @empty - - - - @endforelse - -
PackageVersionStatusUpdateActions
-
{{ $package->package_name }}
-
- {{ \Illuminate\Support\Str::headline($package->type) }} -
-
{{ $package->version }}{{ ucfirst($package->status) }} - {{ $package->hasUpdate() ? $package->package->latest_version : 'Current' }} - -
- @can('marketplace.update') -
- @csrf - - - {{ $package->is_enabled ? 'Disable' : 'Enable' }} - -
- - @if ($package->hasUpdate()) -
- @csrf - - - Update - -
- @endif - @endcan - - @can('marketplace.remove') -
- @csrf - @method('DELETE') - - - Remove - -
- @endcan -
-
- No packages installed through the marketplace. -
-
- - {{ $packages->links() }} -
-
diff --git a/Marketplace/Routes/api.php b/Marketplace/Routes/api.php deleted file mode 100644 index 5c04c0e..0000000 --- a/Marketplace/Routes/api.php +++ /dev/null @@ -1,12 +0,0 @@ -prefix('marketplace')->name('api.marketplace.')->group(function () { - Route::get('/packages', [MarketplaceApiController::class, 'packages'])->middleware('permission:marketplace.view')->name('packages'); - Route::get('/installed', [MarketplaceApiController::class, 'installed'])->middleware('permission:marketplace.view')->name('installed'); - Route::post('/packages/{package}/install', [MarketplaceApiController::class, 'install'])->middleware('permission:marketplace.install')->name('install'); - Route::post('/installed/{package}/toggle', [MarketplaceApiController::class, 'toggle'])->middleware('permission:marketplace.update')->name('toggle'); - Route::delete('/installed/{package}', [MarketplaceApiController::class, 'remove'])->middleware('permission:marketplace.remove')->name('remove'); -}); diff --git a/Marketplace/Routes/web.php b/Marketplace/Routes/web.php deleted file mode 100644 index 2ac0b30..0000000 --- a/Marketplace/Routes/web.php +++ /dev/null @@ -1,14 +0,0 @@ -prefix('administration/marketplace')->name('administration.marketplace.')->group(function () { - Route::get('/', [MarketplaceController::class, 'index'])->middleware('permission:marketplace.view')->name('index'); - Route::get('/installed', [MarketplaceController::class, 'installed'])->middleware('permission:marketplace.view')->name('installed'); - Route::post('/packages/{package}/install', [MarketplaceController::class, 'install'])->middleware('permission:marketplace.install')->name('install'); - Route::post('/packages/{package}/update', [MarketplaceController::class, 'update'])->middleware('permission:marketplace.update')->name('update'); - Route::post('/installed/{package}/toggle', [MarketplaceController::class, 'toggle'])->middleware('permission:marketplace.update')->name('toggle'); - Route::delete('/installed/{package}', [MarketplaceController::class, 'remove'])->middleware('permission:marketplace.remove')->name('remove'); - Route::post('/installed/{package}/rollback/{installation}', [MarketplaceController::class, 'rollback'])->middleware('permission:marketplace.update')->name('rollback'); -}); diff --git a/Marketplace/Services/CompatibilityChecker.php b/Marketplace/Services/CompatibilityChecker.php deleted file mode 100644 index 4ef2e12..0000000 --- a/Marketplace/Services/CompatibilityChecker.php +++ /dev/null @@ -1,30 +0,0 @@ -constraints->matches(PHP_VERSION, $php)) { - $errors[] = "Requires PHP $php; running ".PHP_VERSION.'.'; - } - if (! $this->constraints->matches(App::version(), $laravel)) { - $errors[] = "Requires Laravel $laravel; running ".App::version().'.'; - } - if (! $this->constraints->matches($platformVersion, $platform)) { - $errors[] = "Requires Content Manager $platform; running $platformVersion."; - } - - return ['compatible' => $errors === [], 'errors' => $errors]; - } -} diff --git a/Marketplace/Services/DependencyResolver.php b/Marketplace/Services/DependencyResolver.php deleted file mode 100644 index 02c711a..0000000 --- a/Marketplace/Services/DependencyResolver.php +++ /dev/null @@ -1,29 +0,0 @@ - $constraint) { - $installed = InstalledPackage::query()->where('package_name', $package)->where('status', 'installed')->first(); - if (! $installed) { - $missing[$package] = $constraint; - - continue; - } - if (! $this->constraints->matches($installed->version, (string) $constraint)) { - $incompatible[$package] = ['required' => $constraint, 'installed' => $installed->version]; - } - } - - return ['satisfied' => $missing === [] && $incompatible === [], 'missing' => $missing, 'incompatible' => $incompatible]; - } -} diff --git a/Marketplace/Services/MarketplaceManager.php b/Marketplace/Services/MarketplaceManager.php deleted file mode 100644 index a457800..0000000 --- a/Marketplace/Services/MarketplaceManager.php +++ /dev/null @@ -1,72 +0,0 @@ -manifests->parse($package->manifest ?: ['name' => $package->package_name, 'type' => $package->type, 'version' => $package->latest_version]); - $compatibility = $this->compatibility->check($manifest); - if (! $compatibility['compatible']) { - throw new RuntimeException(implode(' ', $compatibility['errors'])); - } - $dependencies = $this->dependencies->resolve($manifest); - if (! $dependencies['satisfied']) { - throw new RuntimeException('Package dependencies are not satisfied.'); - } - - return DB::transaction(function () use ($package, $manifest, $userId) { - $installed = InstalledPackage::query()->firstOrNew(['package_name' => $package->package_name]); - $from = $installed->exists ? $installed->version : null; - $installed->fill(['marketplace_package_id' => $package->id, 'type' => $package->type, 'version' => $manifest['version'], 'status' => 'installed', 'manifest' => $manifest, 'checksum' => $package->checksum, 'is_enabled' => true, 'installed_by' => $userId, 'installed_at' => $installed->installed_at ?: now(), 'updated_at_marketplace' => now()])->save(); - $this->history($installed, $from ? 'update' : 'install', $from, $manifest['version'], $userId, 'completed'); - - return $installed->refresh(); - }); - } - - public function setEnabled(InstalledPackage $package, bool $enabled, ?int $userId = null): InstalledPackage - { - $package->update(['is_enabled' => $enabled, 'status' => $enabled ? 'installed' : 'disabled']); - $this->history($package, $enabled ? 'enable' : 'disable', $package->version, $package->version, $userId, 'completed'); - - return $package->refresh(); - } - - public function remove(InstalledPackage $package, ?int $userId = null): void - { - DB::transaction(function () use ($package, $userId) { - $this->history($package, 'remove', $package->version, null, $userId, 'completed'); - $package->update(['status' => 'removed', 'is_enabled' => false]); - }); - } - - public function rollback(InstalledPackage $package, Installation $installation, ?int $userId = null): InstalledPackage - { - if ($installation->package_name !== $package->package_name || blank($installation->from_version)) { - throw new RuntimeException('This installation cannot be rolled back.'); - } - $current = $package->version; - $package->update(['version' => $installation->from_version, 'status' => 'installed', 'is_enabled' => true]); - $this->history($package, 'rollback', $current, $installation->from_version, $userId, 'completed'); - - return $package->refresh(); - } - - private function history(InstalledPackage $package, string $operation, ?string $from, ?string $to, ?int $userId, string $status): Installation - { - return Installation::query()->create(['installed_package_id' => $package->id, 'package_name' => $package->package_name, 'operation' => $operation, 'from_version' => $from, 'to_version' => $to, 'status' => $status, 'performed_by' => $userId, 'started_at' => now(), 'completed_at' => now()]); - } -} diff --git a/Marketplace/Services/PackageManifest.php b/Marketplace/Services/PackageManifest.php deleted file mode 100644 index 2fecb73..0000000 --- a/Marketplace/Services/PackageManifest.php +++ /dev/null @@ -1,38 +0,0 @@ - 'The package manifest is not valid JSON.']); - } - $manifest = $decoded; - } - foreach (['name', 'type', 'version'] as $field) { - if (! filled(Arr::get($manifest, $field))) { - throw ValidationException::withMessages(['manifest' => "Manifest field [$field] is required."]); - } - } - if (! in_array($manifest['type'], config('marketplace.supported_types', []), true)) { - throw ValidationException::withMessages(['manifest' => 'Unsupported marketplace package type.']); - } - $manifest['requires'] = $manifest['requires'] ?? []; - $manifest['content_manager'] = $manifest['content_manager'] ?? '*'; - $manifest['laravel'] = $manifest['laravel'] ?? '*'; - - return $manifest; - } - - public function fromFile(string $path): array - { - return $this->parse((string) file_get_contents($path)); - } -} diff --git a/Marketplace/Services/PackageVerifier.php b/Marketplace/Services/PackageVerifier.php deleted file mode 100644 index 1f909f7..0000000 --- a/Marketplace/Services/PackageVerifier.php +++ /dev/null @@ -1,28 +0,0 @@ - true, 'checksum' => $actual, 'signature_present' => filled($manifest['signature'] ?? null)]; - } -} diff --git a/Marketplace/Services/VersionConstraint.php b/Marketplace/Services/VersionConstraint.php deleted file mode 100644 index 14e206c..0000000 --- a/Marketplace/Services/VersionConstraint.php +++ /dev/null @@ -1,47 +0,0 @@ -=') && version_compare($version, ($major + 1).'.0.0', '<'); - - continue; - } - if (str_starts_with($part, '~')) { - $base = substr($part, 1); - $bits = array_pad(explode('.', $base), 3, '0'); - $upper = $bits[0].'.'.((int) $bits[1] + 1).'.0'; - $ok = $ok && version_compare($version, $base, '>=') && version_compare($version, $upper, '<'); - - continue; - } - if (preg_match('/^(>=|<=|>|<|=)?\s*([0-9][0-9A-Za-z.\-+]*)$/', $part, $m)) { - $ok = $ok && version_compare($version, $m[2], $m[1] ?: '='); - } else { - $ok = $ok && $version === $part; - } - } - if ($ok) { - return true; - } - } - - return false; - } -} diff --git a/Marketplace/Tests/Feature/MarketplaceModuleTest.php b/Marketplace/Tests/Feature/MarketplaceModuleTest.php deleted file mode 100644 index 663809b..0000000 --- a/Marketplace/Tests/Feature/MarketplaceModuleTest.php +++ /dev/null @@ -1,23 +0,0 @@ -parse(['name' => 'vendor/example', 'type' => 'module', 'version' => '1.0.0']); - $this->assertSame('module', $manifest['type']); - $this->assertSame([], $manifest['requires']); - } - - public function test_current_platform_is_compatible_with_wildcard_manifest(): void - { - $result = app(CompatibilityChecker::class)->check(['php' => '*', 'laravel' => '*', 'content_manager' => '*']); - $this->assertTrue($result['compatible']); - } -} diff --git a/Marketplace/module.json b/Marketplace/module.json deleted file mode 100644 index 936c435..0000000 --- a/Marketplace/module.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"Marketplace","alias":"marketplace","version":"1.0.0","description":"Secure package catalogue, installation, updates, licences, publishers and rollback management.","keywords":["marketplace","modules","themes","extensions","packages"],"priority":170,"providers":["Modules\\Marketplace\\Providers\\MarketplaceServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Audit","Developer","Security"]} diff --git a/Media/Config/config.php b/Media/Config/config.php deleted file mode 100644 index ba65b69..0000000 --- a/Media/Config/config.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Media', - 'enabled' => true, -]; diff --git a/Media/Database/Migrations/2026_07_23_000300_create_media_table.php b/Media/Database/Migrations/2026_07_23_000300_create_media_table.php deleted file mode 100644 index 581799e..0000000 --- a/Media/Database/Migrations/2026_07_23_000300_create_media_table.php +++ /dev/null @@ -1,37 +0,0 @@ -id(); - $table->uuid('uuid')->unique(); - $table->nullableMorphs('attachable'); - $table->string('collection')->default('default')->index(); - $table->string('disk')->default('public'); - $table->string('path'); - $table->string('original_name'); - $table->string('mime_type')->nullable(); - $table->unsignedBigInteger('size')->default(0); - $table->string('title')->nullable(); - $table->string('alt_text')->nullable(); - $table->text('caption')->nullable(); - $table->json('metadata')->nullable(); - $table->unsignedInteger('sort_order')->default(0); - $table->foreignId('uploaded_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamps(); - $table->softDeletes(); - $table->index(['attachable_type', 'attachable_id', 'collection', 'sort_order'], 'media_attachable_collection_index'); - }); - } - - public function down(): void - { - Schema::dropIfExists('media'); - } -}; diff --git a/Media/Http/Controllers/MediaController.php b/Media/Http/Controllers/MediaController.php deleted file mode 100644 index ee5c15e..0000000 --- a/Media/Http/Controllers/MediaController.php +++ /dev/null @@ -1,34 +0,0 @@ - Media::query()->latest()->paginate(30)]); - } - - public function store(Request $request) - { - $request->validate(['file' => ['required', 'file', 'max:20480'], 'title' => ['nullable', 'string', 'max:255'], 'alt_text' => ['nullable', 'string', 'max:255']]); - $file = $request->file('file'); - $path = $file->store('media', 'public'); - Media::create(['disk' => 'public', 'path' => $path, 'original_name' => $file->getClientOriginalName(), 'mime_type' => $file->getMimeType(), 'size' => $file->getSize(), 'title' => $request->string('title')->toString() ?: pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME), 'alt_text' => $request->string('alt_text')->toString() ?: null, 'uploaded_by' => $request->user()->id]); - - return back()->with('status', 'Media uploaded.'); - } - - public function destroy(Media $medium) - { - Storage::disk($medium->disk)->delete($medium->path); - $medium->delete(); - - return back()->with('status', 'Media deleted.'); - } -} diff --git a/Media/Models/Media.php b/Media/Models/Media.php deleted file mode 100644 index 56af7ee..0000000 --- a/Media/Models/Media.php +++ /dev/null @@ -1,26 +0,0 @@ - $m->uuid ??= (string) Str::uuid()); - } - - protected function casts(): array - { - return ['metadata' => 'array', 'size' => 'integer', 'sort_order' => 'integer']; - } -} diff --git a/Media/Providers/MediaServiceProvider.php b/Media/Providers/MediaServiceProvider.php deleted file mode 100644 index eee1f9a..0000000 --- a/Media/Providers/MediaServiceProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); - $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower()); - } -} diff --git a/Media/Providers/RouteServiceProvider.php b/Media/Providers/RouteServiceProvider.php deleted file mode 100644 index ec522c8..0000000 --- a/Media/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,17 +0,0 @@ -routes(function (): void { - Route::middleware('web')->group(module_path('Media', 'Routes/web.php')); - }); - } -} diff --git a/Media/Resources/views/index.blade.php b/Media/Resources/views/index.blade.php deleted file mode 100644 index e574feb..0000000 --- a/Media/Resources/views/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Media library

Upload and manage reusable files.

@if(session('status'))
{{ session('status') }}
@endif
@csrf
Upload
@forelse($media as $item)
@if(str_starts_with((string)$item->mime_type,'image/')){{ $item->alt_text }}@else
@endif
{{ $item->title ?: $item->original_name }}
{{ Number::fileSize($item->size) }}
@csrf @method('DELETE')Delete
@empty
No media uploaded.
@endforelse
{{ $media->links() }}
diff --git a/Media/Routes/web.php b/Media/Routes/web.php deleted file mode 100644 index 172cab2..0000000 --- a/Media/Routes/web.php +++ /dev/null @@ -1,10 +0,0 @@ -prefix('administration/media')->name('administration.media.')->group(function (): void { - Route::get('/', [MediaController::class, 'index'])->name('index'); - Route::post('/', [MediaController::class, 'store'])->name('store'); - Route::delete('/{medium}', [MediaController::class, 'destroy'])->name('destroy'); -}); diff --git a/Media/Support/ModuleInfo.php b/Media/Support/ModuleInfo.php deleted file mode 100644 index 91bfbd1..0000000 --- a/Media/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBe('Media'); -}); diff --git a/Media/composer.json b/Media/composer.json deleted file mode 100644 index 3d22a93..0000000 --- a/Media/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "absdev/media-module", - "description": "Media foundation module for the ABSDEV Laravel application platform.", - "type": "laravel-module", - "autoload": { - "psr-4": { - "Modules\\Media\\": "" - } - } -} diff --git a/Media/module.json b/Media/module.json deleted file mode 100644 index 23a97df..0000000 --- a/Media/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Media", - "alias": "media", - "version": "1.0.0", - "description": "Media foundation module for the ABSDEV Laravel application platform.", - "keywords": [ - "laravel", - "modules", - "media" - ], - "priority": 10, - "providers": [ - "Modules\\Media\\Providers\\MediaServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core" - ] -} diff --git a/Menus/Config/config.php b/Menus/Config/config.php deleted file mode 100644 index 634ea78..0000000 --- a/Menus/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Menus']; diff --git a/Menus/Database/Migrations/2026_07_24_000200_create_menus_table.php b/Menus/Database/Migrations/2026_07_24_000200_create_menus_table.php deleted file mode 100644 index 6ab61c5..0000000 --- a/Menus/Database/Migrations/2026_07_24_000200_create_menus_table.php +++ /dev/null @@ -1,26 +0,0 @@ -id(); - $table->string('name'); - $table->string('location'); - $table->json('items')->nullable(); - $table->boolean('enabled')->default(true); - $table->timestamps(); - $table->softDeletes(); - }); - } - - public function down(): void - { - Schema::dropIfExists('menus'); - } -}; diff --git a/Menus/Http/Controllers/MenuController.php b/Menus/Http/Controllers/MenuController.php deleted file mode 100644 index 3154304..0000000 --- a/Menus/Http/Controllers/MenuController.php +++ /dev/null @@ -1,90 +0,0 @@ - Menu::query()->orderBy('location')->paginate(30)]); - } - - public function create(): View - { - return view('menus::admin.form', ['locations' => $this->locations()]); - } - - public function store(Request $request): RedirectResponse - { - Menu::create($this->validated($request)); - - return redirect()->route('administration.menus.index')->with('status', 'Menu created. Every header or footer using this location has been updated.'); - } - - public function edit(Menu $menu): View - { - return view('menus::admin.form', ['record' => $menu, 'locations' => $this->locations()]); - } - - public function update(Request $request, Menu $menu): RedirectResponse - { - $menu->update($this->validated($request)); - - return redirect()->route('administration.menus.index')->with('status', 'Menu updated everywhere it is used.'); - } - - public function destroy(Menu $menu): RedirectResponse - { - $menu->delete(); - - return back()->with('status', 'Menu deleted. Components using its location will use their safe fallback.'); - } - - /** @return array */ - private function validated(Request $request): array - { - $data = $request->validate([ - 'name' => ['required', 'string', 'max:255'], - 'location' => ['required', 'string', 'max:100', 'regex:/^[a-z0-9_-]+$/'], - 'items_json' => ['nullable', 'string'], - 'enabled' => ['nullable', 'boolean'], - ]); - - try { - $items = json_decode($data['items_json'] ?: '[]', true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException) { - throw ValidationException::withMessages(['items_json' => 'The menu items could not be saved. Please reload the editor and try again.']); - } - - if (! is_array($items)) { - throw ValidationException::withMessages(['items_json' => 'Menu items must be a list.']); - } - - return [ - 'name' => $data['name'], - 'location' => $data['location'], - 'items' => array_values($items), - 'enabled' => $request->boolean('enabled'), - ]; - } - - /** @return array */ - private function locations(): array - { - return [ - 'primary' => 'Primary navigation', - 'footer' => 'Footer — Explore', - 'footer_account' => 'Footer — Account', - 'footer_legal' => 'Footer — Legal', - 'mobile' => 'Mobile navigation override', - ]; - } -} diff --git a/Menus/Models/Menu.php b/Menus/Models/Menu.php deleted file mode 100644 index 0f957c6..0000000 --- a/Menus/Models/Menu.php +++ /dev/null @@ -1,24 +0,0 @@ - 'array', - 'enabled' => 'boolean', - ]; - } -} diff --git a/Menus/Providers/MenusServiceProvider.php b/Menus/Providers/MenusServiceProvider.php deleted file mode 100644 index 9572a1a..0000000 --- a/Menus/Providers/MenusServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Menus', 'Config/config.php'), 'menus'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Menus', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Menus', 'Resources/views'), 'menus'); - } -} diff --git a/Menus/Providers/RouteServiceProvider.php b/Menus/Providers/RouteServiceProvider.php deleted file mode 100644 index 2e19213..0000000 --- a/Menus/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -routes(fn () => Route::middleware('web')->group(module_path('Menus', 'Routes/web.php'))); - } -} diff --git a/Menus/Resources/views/admin/form.blade.php b/Menus/Resources/views/admin/form.blade.php deleted file mode 100644 index 73a9d39..0000000 --- a/Menus/Resources/views/admin/form.blade.php +++ /dev/null @@ -1,57 +0,0 @@ - - @php($initialItems = old('items_json', json_encode($record->items ?? [], JSON_UNESCAPED_SLASHES))) -
-
-

Reusable navigation

-

{{ isset($record) ? 'Edit' : 'Create' }} menu

-

Edit links once. Every header and footer assigned to this menu location updates automatically.

-
- -
- @csrf - @isset($record) @method('PUT') @endisset - -
- - - -
- -
-

Menu items

Relative paths such as /blog, anchors and full external URLs are supported.

- - @error('items_json')

{{ $message }}

@enderror - -
- -
This menu has no links. Add the first link above.
-
-
- -
CancelSave menu
-
-
- - @push('scripts') - - @endpush -
diff --git a/Menus/Resources/views/admin/index.blade.php b/Menus/Resources/views/admin/index.blade.php deleted file mode 100644 index db7b2a4..0000000 --- a/Menus/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,14 +0,0 @@ - -
-

Single source of navigation

Menus

Reusable menus update every header and footer that references their location.

- @if(session('status'))
{{ session('status') }}
@endif -
- @forelse($records as $record) -
{{ $record->location }}

{{ $record->name }}

{{ count($record->items ?? []) }} links · {{ $record->enabled ? 'Enabled' : 'Disabled' }}

Edit menu
@csrf @method('DELETE')
- @empty -

No menus yet

Run the Saasland installer or create your first reusable menu.

- @endforelse -
- {{ $records->links() }} -
-
diff --git a/Menus/Routes/web.php b/Menus/Routes/web.php deleted file mode 100644 index f76c43f..0000000 --- a/Menus/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/menus')->name('administration.menus.')->group(function (): void { - Route::get('/', [MenuController::class, 'index'])->name('index'); - Route::get('/create', [MenuController::class, 'create'])->name('create'); - Route::post('/', [MenuController::class, 'store'])->name('store'); - Route::get('/{menu}/edit', [MenuController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{menu}', [MenuController::class, 'update'])->name('update'); - Route::delete('/{menu}', [MenuController::class, 'destroy'])->name('destroy'); -}); diff --git a/Menus/Support/ModuleInfo.php b/Menus/Support/ModuleInfo.php deleted file mode 100644 index e96b94b..0000000 --- a/Menus/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -create(['name' => 'Primary', 'location' => 'primary', 'enabled' => true, 'items' => [['enabled' => true, 'label' => 'Home', 'url' => '/']]]); - expect(app(\Modules\Stylist\Support\FrontendNavigation::class)->primary())->toHaveCount(1)->and(app(\Modules\Stylist\Support\FrontendNavigation::class)->primary()[0]['label'])->toBe('Home'); -}); diff --git a/Menus/Tests/Feature/ModuleBootTest.php b/Menus/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 516d963..0000000 --- a/Menus/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,7 +0,0 @@ -toBeTrue(); -}); diff --git a/Menus/module.json b/Menus/module.json deleted file mode 100644 index 6474504..0000000 --- a/Menus/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Menus", - "alias": "menus", - "version": "1.0.0", - "description": "Menus CMS module.", - "keywords": [ - "cms", - "menus" - ], - "priority": 22, - "providers": [ - "Modules\\Menus\\Providers\\MenusServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core", - "Administration" - ] -} diff --git a/News/Config/config.php b/News/Config/config.php deleted file mode 100644 index 3909f94..0000000 --- a/News/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'News']; diff --git a/News/Database/Migrations/2026_07_24_03042_create_news_articles_table.php b/News/Database/Migrations/2026_07_24_03042_create_news_articles_table.php deleted file mode 100644 index d939ffc..0000000 --- a/News/Database/Migrations/2026_07_24_03042_create_news_articles_table.php +++ /dev/null @@ -1,40 +0,0 @@ -id(); - $table->foreignId('author_id')->nullable()->constrained('authors')->nullOnDelete(); - $table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete(); - $table->string('title'); - $table->string('slug')->unique(); - $table->text('excerpt')->nullable(); - $table->longText('content')->nullable(); - $table->string('featured_image')->nullable(); - $table->string('status')->default('draft')->index(); - $table->boolean('is_featured')->default(false); - $table->timestamp('published_at')->nullable()->index(); - $table->timestamp('expires_at')->nullable(); - $table->string('meta_title')->nullable(); - $table->text('meta_description')->nullable(); - $table->timestamps(); - }); - Schema::create('news_article_tag', function (Blueprint $table): void { - $table->foreignId('news_article_id')->constrained('news_articles')->cascadeOnDelete(); - $table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete(); - $table->primary(['news_article_id', 'tag_id']); - }); - } - - public function down(): void - { - Schema::dropIfExists('news_article_tag'); - Schema::dropIfExists('news_articles'); - } -}; diff --git a/News/Http/Controllers/NewsController.php b/News/Http/Controllers/NewsController.php deleted file mode 100644 index cc69cc2..0000000 --- a/News/Http/Controllers/NewsController.php +++ /dev/null @@ -1,66 +0,0 @@ - News::with(['author', 'category'])->latest()->paginate(25)]); - } - - public function create(): View - { - return view('news::admin.form', $this->lookups()); - } - - public function store(Request $r): RedirectResponse - { - $record = News::create($this->data($r)); - $record->tags()->sync($r->input('tag_ids', [])); - - return redirect()->route('administration.news.index')->with('status', 'News created.'); - } - - public function edit(News $news_article): View - { - return view('news::admin.form', array_merge($this->lookups(), ['record' => $news_article])); - } - - public function update(Request $r, News $news_article): RedirectResponse - { - $news_article->update($this->data($r)); - $news_article->tags()->sync($r->input('tag_ids', [])); - - return redirect()->route('administration.news.index')->with('status', 'News updated.'); - } - - public function destroy(News $news_article): RedirectResponse - { - $news_article->delete(); - - return back()->with('status', 'News deleted.'); - } - - private function lookups(): array - { - return ['authors' => Author::where('is_active', true)->orderBy('name')->get(), 'categories' => Category::where('is_active', true)->orderBy('name')->get(), 'tags' => Tag::orderBy('name')->get()]; - } - - private function data(Request $r): array - { - $d = $r->validate(['author_id' => ['nullable', 'exists:authors,id'], 'category_id' => ['nullable', 'exists:categories,id'], 'title' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255'], 'excerpt' => ['nullable', 'string'], 'content' => ['nullable', 'string'], 'featured_image' => ['nullable', 'string', 'max:255'], 'status' => ['required', 'in:draft,review,published,archived'], 'is_featured' => ['sometimes', 'boolean'], 'published_at' => ['nullable', 'date'], 'expires_at' => ['nullable', 'date', 'after:published_at'], 'meta_title' => ['nullable', 'string', 'max:255'], 'meta_description' => ['nullable', 'string']]); - $d['is_featured'] = $r->boolean('is_featured'); - - return $d; - } -} diff --git a/News/Http/Controllers/PublicNewsController.php b/News/Http/Controllers/PublicNewsController.php deleted file mode 100644 index 5783072..0000000 --- a/News/Http/Controllers/PublicNewsController.php +++ /dev/null @@ -1,20 +0,0 @@ - News::published()->with(['author', 'category'])->latest('published_at')->paginate(12)]); - } - - public function show(string $slug): View - { - return view('news::public.show', ['record' => News::published()->with(['author', 'category', 'tags'])->where('slug', $slug)->firstOrFail()]); - } -} diff --git a/News/Models/News.php b/News/Models/News.php deleted file mode 100644 index 586c952..0000000 --- a/News/Models/News.php +++ /dev/null @@ -1,48 +0,0 @@ - 'boolean', 'published_at' => 'datetime', 'expires_at' => 'datetime']; - } - - public function author(): BelongsTo - { - return $this->belongsTo(Author::class); - } - - public function category(): BelongsTo - { - return $this->belongsTo(Category::class); - } - - public function tags(): BelongsToMany - { - return $this->belongsToMany(Tag::class, 'news_article_tag'); - } - - public function scopePublished(Builder $query): Builder - { - return $query->where('status', 'published')->where(fn (Builder $q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()))->where(fn (Builder $q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())); - } -} diff --git a/News/Providers/NewsServiceProvider.php b/News/Providers/NewsServiceProvider.php deleted file mode 100644 index 959998d..0000000 --- a/News/Providers/NewsServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('News', 'Config/config.php'), 'news'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('News', 'Database/Migrations')); - $this->loadViewsFrom(module_path('News', 'Resources/views'), 'news'); - } -} diff --git a/News/Providers/RouteServiceProvider.php b/News/Providers/RouteServiceProvider.php deleted file mode 100644 index cfa0850..0000000 --- a/News/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('News', 'Routes/web.php')); - } -} diff --git a/News/Resources/views/admin/form.blade.php b/News/Resources/views/admin/form.blade.php deleted file mode 100644 index 2e122f0..0000000 --- a/News/Resources/views/admin/form.blade.php +++ /dev/null @@ -1 +0,0 @@ -
@csrf @isset($record) @method('PUT') @endisset
@foreach($authors as $author)@endforeach@foreach($categories as $category)@endforeach@foreach(['draft','review','published','archived'] as $status)@endforeach
{{ old('excerpt',$record->excerpt ?? '') }}{{ old('content',$record->content ?? '') }}
@foreach($tags as $tag)@endforeach
{{ old('meta_description',$record->meta_description ?? '') }}Save
diff --git a/News/Resources/views/admin/index.blade.php b/News/Resources/views/admin/index.blade.php deleted file mode 100644 index f8860b7..0000000 --- a/News/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

News

Manage publishing news.

Create
@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $record)@empty@endforelse
Name / titleStatusActions
{{ $record->title ?? $record->name }}{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}Edit
@csrf @method('DELETE')Delete
No records yet.
{{ $records->links() }}
diff --git a/News/Resources/views/public/index.blade.php b/News/Resources/views/public/index.blade.php deleted file mode 100644 index 5413c83..0000000 --- a/News/Resources/views/public/index.blade.php +++ /dev/null @@ -1,31 +0,0 @@ -@extends('stylist-theme-saasland::layouts.app') - -@php - $title = 'News'; - $description = 'The latest announcements, releases and company updates.'; - $eyebrow = 'Updates'; - $breadcrumbs = ['News' => null]; -@endphp - -@section('content') -
-
-
- @forelse($records as $record) -
-
-
- -

{{ $record->title }}

- @if($record->excerpt)

{{ $record->excerpt }}

@endif - Read update → -
-
- @empty -
No news has been published yet.
- @endforelse -
-
{{ $records->links() }}
-
-
-@endsection diff --git a/News/Resources/views/public/show.blade.php b/News/Resources/views/public/show.blade.php deleted file mode 100644 index ddb3795..0000000 --- a/News/Resources/views/public/show.blade.php +++ /dev/null @@ -1,23 +0,0 @@ -@extends('stylist-theme-saasland::layouts.app') - -@php - $title = $record->title; - $description = $record->excerpt; - $eyebrow = 'News'; - $breadcrumbs = ['News' => route('news.public.index'), $record->title => null]; -@endphp - -@section('content') -
-
-
- - @if($record->excerpt)

{{ $record->excerpt }}

@endif -
{!! nl2br(e($record->content)) !!}
-
-
-
-@endsection diff --git a/News/Routes/web.php b/News/Routes/web.php deleted file mode 100644 index 672eba1..0000000 --- a/News/Routes/web.php +++ /dev/null @@ -1,16 +0,0 @@ -prefix('administration/news')->name('administration.news.')->group(function (): void { - Route::get('/', [NewsController::class, 'index'])->name('index'); - Route::get('/create', [NewsController::class, 'create'])->name('create'); - Route::post('/', [NewsController::class, 'store'])->name('store'); - Route::get('/{news_article}/edit', [NewsController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{news_article}', [NewsController::class, 'update'])->name('update'); - Route::delete('/{news_article}', [NewsController::class, 'destroy'])->name('destroy'); -}); -Route::get('/news', [PublicNewsController::class, 'index'])->name('news.public.index'); -Route::get('/news/{slug}', [PublicNewsController::class, 'show'])->name('news.public.show'); diff --git a/News/Tests/Feature/ModuleBootTest.php b/News/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index dbe1c8a..0000000 --- a/News/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('News', config('news.name')); - } -} diff --git a/News/module.json b/News/module.json deleted file mode 100644 index dcb3490..0000000 --- a/News/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "News", - "alias": "news", - "version": "1.0.0", - "description": "News publishing module.", - "keywords": ["publishing", "news"], - "priority": 30, - "providers": ["Modules\\News\\Providers\\NewsServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Newsletter/Config/config.php b/Newsletter/Config/config.php deleted file mode 100644 index 293dea2..0000000 --- a/Newsletter/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Newsletter']; diff --git a/Newsletter/Database/Migrations/2026_07_24_04100_create_newsletter_tables.php b/Newsletter/Database/Migrations/2026_07_24_04100_create_newsletter_tables.php deleted file mode 100644 index 613856a..0000000 --- a/Newsletter/Database/Migrations/2026_07_24_04100_create_newsletter_tables.php +++ /dev/null @@ -1,57 +0,0 @@ -id(); - $t->string('name'); - $t->text('description')->nullable(); - $t->boolean('is_active')->default(true); - $t->timestamps(); - }); - Schema::create('newsletter_subscribers', function (Blueprint $t): void { - $t->id(); - $t->string('email')->unique(); - $t->string('name')->nullable(); - $t->string('status')->default('pending')->index(); - $t->string('confirmation_token')->nullable()->unique(); - $t->timestamp('confirmed_at')->nullable(); - $t->timestamp('unsubscribed_at')->nullable(); - $t->json('metadata')->nullable(); - $t->timestamps(); - }); - Schema::create('newsletter_list_subscriber', function (Blueprint $t): void { - $t->foreignId('list_id')->constrained('newsletter_lists')->cascadeOnDelete(); - $t->foreignId('subscriber_id')->constrained('newsletter_subscribers')->cascadeOnDelete(); - $t->primary(['list_id', 'subscriber_id']); - }); - Schema::create('newsletter_campaigns', function (Blueprint $t): void { - $t->id(); - $t->foreignId('list_id')->nullable()->constrained('newsletter_lists')->nullOnDelete(); - $t->string('name'); - $t->string('subject'); - $t->longText('content')->nullable(); - $t->string('status')->default('draft')->index(); - $t->timestamp('scheduled_at')->nullable(); - $t->timestamp('sent_at')->nullable(); - $t->unsignedInteger('sent_count')->default(0); - $t->unsignedInteger('open_count')->default(0); - $t->unsignedInteger('click_count')->default(0); - $t->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('newsletter_campaigns'); - Schema::dropIfExists('newsletter_list_subscriber'); - Schema::dropIfExists('newsletter_subscribers'); - Schema::dropIfExists('newsletter_lists'); - } -}; diff --git a/Newsletter/Http/Controllers/NewsletterController.php b/Newsletter/Http/Controllers/NewsletterController.php deleted file mode 100644 index 8e37e2c..0000000 --- a/Newsletter/Http/Controllers/NewsletterController.php +++ /dev/null @@ -1,52 +0,0 @@ - Subscriber::latest()->paginate(20), 'campaigns' => Campaign::latest()->limit(10)->get(), 'lists' => MailingList::orderBy('name')->get()]); - } - - public function storeSubscriber(Request $r): RedirectResponse - { - $d = $r->validate(['email' => ['required', 'email', 'unique:newsletter_subscribers,email'], 'name' => ['nullable', 'string', 'max:255']]); - Subscriber::create($d + ['status' => 'confirmed', 'confirmation_token' => Str::random(48), 'confirmed_at' => now()]); - - return back()->with('status', 'Subscriber added.'); - } - - public function storeCampaign(Request $r): RedirectResponse - { - Campaign::create($r->validate(['list_id' => ['nullable', 'exists:newsletter_lists,id'], 'name' => ['required', 'string', 'max:255'], 'subject' => ['required', 'string', 'max:255'], 'content' => ['nullable', 'string'], 'status' => ['required', 'in:draft,scheduled,sent'], 'scheduled_at' => ['nullable', 'date']])); - - return back()->with('status', 'Campaign created.'); - } - - public function publicSubscribe(Request $r): RedirectResponse - { - $data = $r->validate(['email' => ['required', 'email', 'max:255'], 'name' => ['nullable', 'string', 'max:255']]); - Subscriber::query()->updateOrCreate( - ['email' => strtolower($data['email'])], - ['name' => $data['name'] ?? null, 'status' => 'confirmed', 'confirmation_token' => Str::random(48), 'confirmed_at' => now()] - ); - - return back()->with('status', 'Thank you for subscribing.'); - } - - public function destroySubscriber(Subscriber $subscriber): RedirectResponse - { - $subscriber->delete(); - - return back()->with('status', 'Subscriber deleted.'); - } -} diff --git a/Newsletter/Models/Campaign.php b/Newsletter/Models/Campaign.php deleted file mode 100644 index 924a6a8..0000000 --- a/Newsletter/Models/Campaign.php +++ /dev/null @@ -1,17 +0,0 @@ - 'datetime', 'sent_at' => 'datetime']; - } -} diff --git a/Newsletter/Models/MailingList.php b/Newsletter/Models/MailingList.php deleted file mode 100644 index 786b233..0000000 --- a/Newsletter/Models/MailingList.php +++ /dev/null @@ -1,17 +0,0 @@ - 'boolean']; - } -} diff --git a/Newsletter/Models/Subscriber.php b/Newsletter/Models/Subscriber.php deleted file mode 100644 index 9d0bd30..0000000 --- a/Newsletter/Models/Subscriber.php +++ /dev/null @@ -1,17 +0,0 @@ - 'datetime', 'unsubscribed_at' => 'datetime', 'metadata' => 'array']; - } -} diff --git a/Newsletter/Providers/NewsletterServiceProvider.php b/Newsletter/Providers/NewsletterServiceProvider.php deleted file mode 100644 index d10726f..0000000 --- a/Newsletter/Providers/NewsletterServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Newsletter', 'Config/config.php'), 'newsletter'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Newsletter', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Newsletter', 'Resources/views'), 'newsletter'); - } -} diff --git a/Newsletter/Providers/RouteServiceProvider.php b/Newsletter/Providers/RouteServiceProvider.php deleted file mode 100644 index 47472fc..0000000 --- a/Newsletter/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Newsletter', 'Routes/web.php')); - } -} diff --git a/Newsletter/Resources/views/admin/index.blade.php b/Newsletter/Resources/views/admin/index.blade.php deleted file mode 100644 index 93c0489..0000000 --- a/Newsletter/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Newsletter

Manage subscribers and email campaigns.

@if(session('status'))
{{ session('status') }}
@endif
@csrf

Add subscriber

Add subscriber
@csrf

Create campaign

Create campaign
@forelse($subscribers as $subscriber)@empty@endforelse
SubscriberStatusConfirmed
{{ $subscriber->email }}
{{ $subscriber->name }}
{{ ucfirst($subscriber->status) }}{{ optional($subscriber->confirmed_at)->format('j M Y') ?: '—' }}
@csrf @method('DELETE')Delete
No subscribers yet.
{{ $subscribers->links() }}
diff --git a/Newsletter/Routes/web.php b/Newsletter/Routes/web.php deleted file mode 100644 index 045e8f9..0000000 --- a/Newsletter/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/newsletter')->name('administration.newsletter.')->group(function (): void { - Route::get('/', [NewsletterController::class, 'index'])->name('index'); - Route::post('/subscribers', [NewsletterController::class, 'storeSubscriber'])->name('subscribers.store'); - Route::delete('/subscribers/{subscriber}', [NewsletterController::class, 'destroySubscriber'])->name('subscribers.destroy'); - Route::post('/campaigns', [NewsletterController::class, 'storeCampaign'])->name('campaigns.store'); -}); - -Route::post('/newsletter/subscribe', [NewsletterController::class, 'publicSubscribe'])->middleware('throttle:10,1')->name('newsletter.public.subscribe'); diff --git a/Newsletter/Tests/Feature/ModuleBootTest.php b/Newsletter/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index d76ba2b..0000000 --- a/Newsletter/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('Newsletter', config('newsletter.name')); - } -} diff --git a/Newsletter/module.json b/Newsletter/module.json deleted file mode 100644 index 620f465..0000000 --- a/Newsletter/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Newsletter", - "alias": "newsletter", - "version": "1.0.0", - "description": "Subscriber lists and email campaign management.", - "keywords": ["business", "newsletter"], - "priority": 41, - "providers": ["Modules\\Newsletter\\Providers\\NewsletterServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Pages/Config/config.php b/Pages/Config/config.php deleted file mode 100644 index 68aabbf..0000000 --- a/Pages/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Pages']; diff --git a/Pages/Database/Migrations/2026_07_24_000100_create_pages_table.php b/Pages/Database/Migrations/2026_07_24_000100_create_pages_table.php deleted file mode 100644 index d4cb696..0000000 --- a/Pages/Database/Migrations/2026_07_24_000100_create_pages_table.php +++ /dev/null @@ -1,29 +0,0 @@ -id(); - $table->string('title'); - $table->string('slug'); - $table->text('summary')->nullable(); - $table->longText('content')->nullable(); - $table->string('status')->default('draft'); - $table->timestamp('published_at')->nullable(); - $table->boolean('is_home')->default(false); - $table->timestamps(); - $table->softDeletes(); - }); - } - - public function down(): void - { - Schema::dropIfExists('pages'); - } -}; diff --git a/Pages/Database/Migrations/2026_07_24_030700_enhance_pages_for_publishing.php b/Pages/Database/Migrations/2026_07_24_030700_enhance_pages_for_publishing.php deleted file mode 100644 index ac8810e..0000000 --- a/Pages/Database/Migrations/2026_07_24_030700_enhance_pages_for_publishing.php +++ /dev/null @@ -1,37 +0,0 @@ -string('template')->default('default'); - $table->timestamp('expires_at')->nullable(); - $table->string('preview_token', 64)->nullable()->unique(); - $table->foreignId('locked_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamp('locked_at')->nullable(); - }); - Schema::create('page_revisions', function (Blueprint $table): void { - $table->id(); - $table->foreignId('page_id')->constrained('pages')->cascadeOnDelete(); - $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); - $table->string('title'); - $table->longText('content')->nullable(); - $table->json('snapshot'); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('page_revisions'); - Schema::table('pages', function (Blueprint $table): void { - $table->dropConstrainedForeignId('locked_by'); - $table->dropColumn(['template', 'expires_at', 'preview_token', 'locked_at']); - }); - } -}; diff --git a/Pages/Http/Controllers/PageController.php b/Pages/Http/Controllers/PageController.php deleted file mode 100644 index f9f2e02..0000000 --- a/Pages/Http/Controllers/PageController.php +++ /dev/null @@ -1,151 +0,0 @@ -withCount('contentPlacements') - ->when($request->string('search')->isNotEmpty(), function ($query) use ($request): void { - $search = $request->string('search')->toString(); - $query->where(fn ($page) => $page - ->where('title', 'like', "%{$search}%") - ->orWhere('slug', 'like', "%{$search}%") - ->orWhere('summary', 'like', "%{$search}%")); - }) - ->when($request->string('status')->isNotEmpty(), fn ($query) => $query->where('status', $request->string('status')->toString())) - ->orderByDesc('is_home') - ->orderBy('title') - ->paginate(24) - ->withQueryString(); - - return view('pages::admin.index', compact('records')); - } - - public function create(): View - { - return view('pages::admin.form', ['record' => new Page(['status' => 'draft', 'template' => 'default'])]); - } - - public function store(Request $request): RedirectResponse - { - $data = $this->validated($request); - $data['slug'] = $this->uniqueSlug($data['slug'] ?: $data['title']); - $data['is_home'] = $request->boolean('is_home'); - $data['published_at'] = $data['status'] === 'published' ? ($data['published_at'] ?? now()) : null; - - $page = Page::query()->create($data); - $this->enforceSingleHomepage($page); - - return redirect()->route('administration.pages.edit', $page)->with('status', 'Page created. Add content sections when you are ready.'); - } - - public function edit(Page $page): View - { - $page->loadCount('contentPlacements'); - - return view('pages::admin.form', ['record' => $page]); - } - - public function update(Request $request, Page $page): RedirectResponse - { - $oldSlug = $page->slug; - $data = $this->validated($request, $page); - $data['slug'] = $this->uniqueSlug($data['slug'] ?: $data['title'], $page); - $data['is_home'] = $request->boolean('is_home'); - $data['published_at'] = $data['status'] === 'published' ? ($data['published_at'] ?? $page->published_at ?? now()) : null; - - $page->update($data); - $this->enforceSingleHomepage($page); - - if ($oldSlug !== $page->slug) { - PageContent::query()->where('page_id', $page->id)->orWhere('page_key', $oldSlug)->update(['page_key' => $page->slug, 'page_id' => $page->id]); - } - - return back()->with('status', 'Page saved.'); - } - - public function duplicate(Request $request, Page $page): RedirectResponse - { - $copy = $page->replicate(['slug', 'status', 'published_at', 'preview_token', 'locked_by', 'locked_at', 'is_home']); - $copy->title = $page->title.' copy'; - $copy->slug = $this->uniqueSlug($copy->title); - $copy->status = 'draft'; - $copy->published_at = null; - $copy->is_home = false; - $copy->save(); - - foreach ($page->contentPlacements()->orderBy('region')->orderBy('sort_order')->get() as $placement) { - $copy->contentPlacements()->create([ - 'page_key' => $copy->slug, - 'content_document_id' => $placement->content_document_id, - 'region' => $placement->region, - 'sort_order' => $placement->sort_order, - 'is_enabled' => $placement->is_enabled, - 'visibility_rules' => $placement->visibility_rules, - 'presentation' => $placement->presentation, - 'starts_at' => $placement->starts_at, - 'ends_at' => $placement->ends_at, - ]); - } - - return redirect()->route('administration.pages.edit', $copy)->with('status', 'Draft copy created with the same content structure.'); - } - - public function destroy(Page $page): RedirectResponse - { - if ($page->is_home) { - return back()->withErrors(['page' => 'Choose another homepage before deleting the current homepage.']); - } - - $page->delete(); - - return redirect()->route('administration.pages.index')->with('status', 'Page moved to the bin.'); - } - - private function validated(Request $request, ?Page $page = null): array - { - return $request->validate([ - 'title' => ['required', 'string', 'max:255'], - 'slug' => ['nullable', 'string', 'max:255', Rule::unique('pages', 'slug')->ignore($page?->id)->whereNull('deleted_at')], - 'summary' => ['nullable', 'string', 'max:1000'], - 'content' => ['nullable', 'string'], - 'status' => ['required', Rule::in(['draft', 'published'])], - 'template' => ['required', Rule::in(['default', 'landing', 'full-width'])], - 'published_at' => ['nullable', 'date'], - 'expires_at' => ['nullable', 'date', 'after:published_at'], - 'is_home' => ['sometimes', 'boolean'], - ]); - } - - private function uniqueSlug(string $value, ?Page $except = null): string - { - $base = Str::slug($value) ?: 'page'; - $slug = $base; - $suffix = 2; - - while (Page::withTrashed()->where('slug', $slug)->when($except, fn ($query) => $query->whereKeyNot($except->getKey()))->exists()) { - $slug = $base.'-'.$suffix++; - } - - return $slug; - } - - private function enforceSingleHomepage(Page $page): void - { - if ($page->is_home) { - Page::query()->whereKeyNot($page->id)->where('is_home', true)->update(['is_home' => false]); - } - } -} diff --git a/Pages/Http/Controllers/PublicPageController.php b/Pages/Http/Controllers/PublicPageController.php deleted file mode 100644 index c9e531f..0000000 --- a/Pages/Http/Controllers/PublicPageController.php +++ /dev/null @@ -1,17 +0,0 @@ -where('slug', $slug)->where('status', 'published')->where(fn ($q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()))->firstOrFail(); - - return view('pages::public.show', compact('page')); - } -} diff --git a/Pages/Models/Page.php b/Pages/Models/Page.php deleted file mode 100644 index 22e2d3f..0000000 --- a/Pages/Models/Page.php +++ /dev/null @@ -1,35 +0,0 @@ - 'datetime', - 'expires_at' => 'datetime', - 'is_home' => 'boolean', - ]; - } - - public function contentPlacements(): HasMany - { - return $this->hasMany(PageContent::class)->orderBy('region')->orderBy('sort_order'); - } -} diff --git a/Pages/Providers/PagesServiceProvider.php b/Pages/Providers/PagesServiceProvider.php deleted file mode 100644 index 0a1d101..0000000 --- a/Pages/Providers/PagesServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Pages', 'Config/config.php'), 'pages'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Pages', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Pages', 'Resources/views'), 'pages'); - } -} diff --git a/Pages/Providers/RouteServiceProvider.php b/Pages/Providers/RouteServiceProvider.php deleted file mode 100644 index 7984a9e..0000000 --- a/Pages/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -routes(fn () => Route::middleware('web')->group(module_path('Pages', 'Routes/web.php'))); - } -} diff --git a/Pages/Resources/views/admin/form.blade.php b/Pages/Resources/views/admin/form.blade.php deleted file mode 100644 index 3a513d2..0000000 --- a/Pages/Resources/views/admin/form.blade.php +++ /dev/null @@ -1,87 +0,0 @@ - -
-
-
- ← Pages -
-

{{ $record->exists ? $record->title : 'New page' }}

- @if($record->exists && $record->is_home)Homepage@endif -
-

Page details, search metadata, publishing and content structure.

-
- @if($record->exists) -
- @if($record->status === 'published')Open live page@endif - Edit content -
@csrfDuplicate
-
- @endif -
- - @if(session('status'))
{{ session('status') }}
@endif - @if($errors->any())
    @foreach($errors->all() as $error)
  • {{ $error }}
  • @endforeach
@endif - -
- -
- -
- @csrf @if($record->exists) @method('PUT') @endif -
-
- -

Page identity

Use a clear title editors and visitors can recognise.

- - {{ old('summary', $record->summary) }} -
- - -

Content structure

Build the page with ordered, reusable sections in the visual composer.

@if($record->exists){{ $record->content_placements_count ?? 0 }} sections@endif
- @if($record->exists) -

Page composer

Add hero areas, text, feeds, calls to action and shared site sections, then drag them into order.

Open content composer
- @else -
Save the page first, then add and arrange its content sections.
- @endif -
Legacy body content

Kept for compatibility with older templates. New pages should use the content composer.

-
-
- -
- -

URL and presentation

Keep URLs short, readable and stable.

-
/
- -
-

Search preview

{{ url('/') }}/
{{ old('title', $record->title ?: 'Page title') }}

{{ Str::limit(old('summary', $record->summary ?: 'Add a concise summary to improve how this page appears in search and content listings.'), 160) }}

-
- -
-

Publishing schedule

Publish immediately or define a controlled availability window.

-
-
- -
- -
- - -
diff --git a/Pages/Resources/views/admin/index.blade.php b/Pages/Resources/views/admin/index.blade.php deleted file mode 100644 index 1858e3a..0000000 --- a/Pages/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,75 +0,0 @@ - -
-
-
-

Website

-

Pages

-

Create, organise, compose and publish every page from one place.

-
-
- Content library - New page -
-
- - @if(session('status')) -
{{ session('status') }}
- @endif - @if($errors->any()) -
{{ $errors->first() }}
- @endif - -
- - - - - - -
FilterReset
- - -
- @forelse($records as $record) - @php - $isLive = $record->status === 'published' && (!$record->published_at || $record->published_at->isPast()) && (!$record->expires_at || $record->expires_at->isFuture()); - @endphp -
-
-
-
-

{{ $record->title }}

- @if($record->is_home)Homepage@endif - {{ $isLive ? 'Live' : ucfirst($record->status) }} -
-

/{{ $record->slug }}

-
-
-
{{ $record->content_placements_count }}
-
sections
-
-
- -

{{ Str::limit($record->summary ?: 'No page summary has been added yet.', 150) }}

- -
-
Updated {{ $record->updated_at?->diffForHumans() }} · {{ ucfirst($record->template ?? 'default') }}
-
- @if($record->status === 'published')View@endif - Content - Edit page -
-
-
- @empty -
-

No pages found

-

Create your first page or clear the current filters.

- Create page -
- @endforelse -
- - {{ $records->links() }} -
-
diff --git a/Pages/Resources/views/public/show.blade.php b/Pages/Resources/views/public/show.blade.php deleted file mode 100644 index f066034..0000000 --- a/Pages/Resources/views/public/show.blade.php +++ /dev/null @@ -1,22 +0,0 @@ -@extends('stylist-theme-saasland::layouts.app') - -@php - $title = $page->title; - $description = $page->summary; - $eyebrow = 'Page'; - $breadcrumbs = [$page->title => null]; -@endphp - -@section('content') -
-
-
- @if ($page->summary) -

{{ $page->summary }}

- @endif - -
{!! $page->content !!}
-
-
-
-@endsection diff --git a/Pages/Routes/web.php b/Pages/Routes/web.php deleted file mode 100644 index dadc7f4..0000000 --- a/Pages/Routes/web.php +++ /dev/null @@ -1,17 +0,0 @@ -prefix('administration/pages')->name('administration.pages.')->group(function (): void { - Route::get('/', [PageController::class, 'index'])->name('index'); - Route::get('/create', [PageController::class, 'create'])->name('create'); - Route::post('/', [PageController::class, 'store'])->name('store'); - Route::get('/{page}/edit', [PageController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{page}', [PageController::class, 'update'])->name('update'); - Route::post('/{page}/duplicate', [PageController::class, 'duplicate'])->name('duplicate'); - Route::delete('/{page}', [PageController::class, 'destroy'])->name('destroy'); -}); - -Route::get('/pages/{slug}', [PublicPageController::class, 'show'])->name('pages.public.show'); diff --git a/Pages/Tests/Feature/ModuleBootTest.php b/Pages/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 2a22cc4..0000000 --- a/Pages/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,7 +0,0 @@ -toBeTrue(); -}); diff --git a/Pages/module.json b/Pages/module.json deleted file mode 100644 index d891304..0000000 --- a/Pages/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Pages", - "alias": "pages", - "version": "1.0.0", - "description": "Pages CMS module.", - "keywords": [ - "cms", - "pages" - ], - "priority": 21, - "providers": [ - "Modules\\Pages\\Providers\\PagesServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core", - "Administration" - ] -} diff --git a/Products/Config/config.php b/Products/Config/config.php deleted file mode 100644 index 3bfe783..0000000 --- a/Products/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Products']; diff --git a/Products/Database/Migrations/2026_07_24_105054_create_products_table.php b/Products/Database/Migrations/2026_07_24_105054_create_products_table.php deleted file mode 100644 index a009d1a..0000000 --- a/Products/Database/Migrations/2026_07_24_105054_create_products_table.php +++ /dev/null @@ -1,23 +0,0 @@ -id(); - $table->string('name'); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('products'); - } -}; diff --git a/Products/Database/Migrations/2026_07_24_150000_add_pricing_fields_to_products_table.php b/Products/Database/Migrations/2026_07_24_150000_add_pricing_fields_to_products_table.php deleted file mode 100644 index f6b9a80..0000000 --- a/Products/Database/Migrations/2026_07_24_150000_add_pricing_fields_to_products_table.php +++ /dev/null @@ -1,34 +0,0 @@ -string('product_type')->default('product')->after('name'); - $table->text('description')->nullable()->after('product_type'); - $table->decimal('price', 12, 2)->nullable()->after('description'); - $table->string('currency', 3)->default('GBP')->after('price'); - $table->string('billing_period')->nullable()->after('currency'); - $table->json('features')->nullable()->after('billing_period'); - $table->string('cta_label')->nullable()->after('features'); - $table->string('cta_url')->nullable()->after('cta_label'); - $table->boolean('is_featured')->default(false)->after('cta_url'); - $table->unsignedInteger('sort_order')->default(0)->after('is_featured'); - }); - } - - public function down(): void - { - Schema::table('products', function (Blueprint $table): void { - $table->dropColumn([ - 'product_type', 'description', 'price', 'currency', 'billing_period', - 'features', 'cta_label', 'cta_url', 'is_featured', 'sort_order', - ]); - }); - } -}; diff --git a/Products/Http/Controllers/ProductController.php b/Products/Http/Controllers/ProductController.php deleted file mode 100644 index c3ccf6f..0000000 --- a/Products/Http/Controllers/ProductController.php +++ /dev/null @@ -1,23 +0,0 @@ - Product::query()->latest()->paginate()]); - } - - public function store(StoreProductRequest $request): RedirectResponse - { - Product::query()->create($request->validated()); - - return back()->with('status', 'Product created.'); - } -} diff --git a/Products/Http/Requests/StoreProductRequest.php b/Products/Http/Requests/StoreProductRequest.php deleted file mode 100644 index ae953bc..0000000 --- a/Products/Http/Requests/StoreProductRequest.php +++ /dev/null @@ -1,45 +0,0 @@ -user() !== null; - } - - protected function prepareForValidation(): void - { - $this->merge([ - 'is_active' => $this->boolean('is_active'), - 'is_featured' => $this->boolean('is_featured'), - 'features' => collect(preg_split('/\r\n|\r|\n/', (string) $this->input('features'))) - ->map(fn (string $feature) => trim($feature)) - ->filter() - ->values() - ->all(), - ]); - } - - public function rules(): array - { - return [ - 'name' => ['required', 'string', 'max:255'], - 'product_type' => ['required', 'in:product,plan'], - 'description' => ['nullable', 'string'], - 'price' => ['nullable', 'numeric', 'min:0'], - 'currency' => ['required', 'string', 'size:3'], - 'billing_period' => ['nullable', 'string', 'max:40'], - 'features' => ['array'], - 'features.*' => ['string', 'max:255'], - 'cta_label' => ['nullable', 'string', 'max:80'], - 'cta_url' => ['nullable', 'string', 'max:2048'], - 'is_featured' => ['boolean'], - 'sort_order' => ['integer', 'min:0'], - 'is_active' => ['boolean'], - ]; - } -} diff --git a/Products/Models/Product.php b/Products/Models/Product.php deleted file mode 100644 index 30dbf31..0000000 --- a/Products/Models/Product.php +++ /dev/null @@ -1,21 +0,0 @@ - 'decimal:2', - 'features' => 'array', - 'is_active' => 'boolean', - 'is_featured' => 'boolean', - 'sort_order' => 'integer', - ]; - } -} diff --git a/Products/Providers/ProductsServiceProvider.php b/Products/Providers/ProductsServiceProvider.php deleted file mode 100644 index 98d5e46..0000000 --- a/Products/Providers/ProductsServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Products', 'Config/config.php'), 'products'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Products', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Products', 'Resources/views'), 'products'); - } -} diff --git a/Products/Providers/RouteServiceProvider.php b/Products/Providers/RouteServiceProvider.php deleted file mode 100644 index 337a787..0000000 --- a/Products/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,14 +0,0 @@ -group(module_path('Products', 'Routes/web.php')); - } -} diff --git a/Products/Resources/views/admin/products/form.blade.php b/Products/Resources/views/admin/products/form.blade.php deleted file mode 100644 index a98563c..0000000 --- a/Products/Resources/views/admin/products/form.blade.php +++ /dev/null @@ -1,25 +0,0 @@ -
- @csrf -
- - - - - - - - - -
- - -
- - -
-
- - -
- {{ __('Save Product') }} - diff --git a/Products/Resources/views/admin/products/index.blade.php b/Products/Resources/views/admin/products/index.blade.php deleted file mode 100644 index 757785c..0000000 --- a/Products/Resources/views/admin/products/index.blade.php +++ /dev/null @@ -1,26 +0,0 @@ - -
-
-

Products & Pricing

-

Manage catalogue products and public-facing pricing plans from one place.

-
-
- @include('products::admin.products.form') -
-
- @forelse ($records as $record) -
-
-

{{ $record->name }}

{{ ucfirst($record->product_type ?? 'product') }}

- @if($record->is_featured)Featured@endif -
- @if($record->price !== null)

{{ $record->currency }} {{ $record->price }} /{{ $record->billing_period }}

@else

Custom

@endif -

{{ $record->description }}

-
- @empty -

No products or plans have been created.

- @endforelse -
- {{ $records->links() }} -
-
diff --git a/Products/Routes/web.php b/Products/Routes/web.php deleted file mode 100644 index eecbc02..0000000 --- a/Products/Routes/web.php +++ /dev/null @@ -1,7 +0,0 @@ -prefix('administration/products')->name('administration.products.')->group(function (): void { - // Register module routes here. -}); diff --git a/Products/Tests/Feature/ProductCrudTest.php b/Products/Tests/Feature/ProductCrudTest.php deleted file mode 100644 index f596dc9..0000000 --- a/Products/Tests/Feature/ProductCrudTest.php +++ /dev/null @@ -1,7 +0,0 @@ -create(['name' => 'Example']))->toBeInstanceOf(Product::class); -}); diff --git a/Products/Tests/Feature/ProductsModuleBootTest.php b/Products/Tests/Feature/ProductsModuleBootTest.php deleted file mode 100644 index f89049e..0000000 --- a/Products/Tests/Feature/ProductsModuleBootTest.php +++ /dev/null @@ -1,7 +0,0 @@ -providerIsLoaded(ProductsServiceProvider::class))->toBeTrue(); -}); diff --git a/Products/module.json b/Products/module.json deleted file mode 100644 index 5473559..0000000 --- a/Products/module.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "Products", - "alias": "products", - "version": "1.0.0", - "description": "Products module.", - "keywords": [ - "products" - ], - "priority": 100, - "providers": [ - "Modules\\Products\\Providers\\ProductsServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core", - "Administration" - ] -} diff --git a/Redirects/Config/config.php b/Redirects/Config/config.php deleted file mode 100644 index e5170bd..0000000 --- a/Redirects/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Redirects']; diff --git a/Redirects/Database/Migrations/2026_07_24_000300_create_redirects_table.php b/Redirects/Database/Migrations/2026_07_24_000300_create_redirects_table.php deleted file mode 100644 index 5635594..0000000 --- a/Redirects/Database/Migrations/2026_07_24_000300_create_redirects_table.php +++ /dev/null @@ -1,26 +0,0 @@ -id(); - $table->string('source'); - $table->string('destination'); - $table->unsignedInteger('status_code')->default(301); - $table->boolean('enabled')->default(true); - $table->timestamps(); - $table->softDeletes(); - }); - } - - public function down(): void - { - Schema::dropIfExists('redirects'); - } -}; diff --git a/Redirects/Http/Controllers/RedirectController.php b/Redirects/Http/Controllers/RedirectController.php deleted file mode 100644 index efb0a93..0000000 --- a/Redirects/Http/Controllers/RedirectController.php +++ /dev/null @@ -1,58 +0,0 @@ - Redirect::query()->latest()->paginate(20)]); - } - - public function create(): View - { - return view('redirects::admin.form'); - } - - public function store(Request $request): RedirectResponse - { - $data = $request->validate(['source' => ['required', 'string', 'max:255'], - 'destination' => ['required', 'string', 'max:255'], - 'status_code' => ['nullable', 'integer', 'min:0'], - 'enabled' => ['sometimes', 'boolean']]); - $data['enabled'] = $request->boolean('enabled'); - Redirect::create($data); - - return redirect()->route('administration.redirects.index')->with('status', 'Redirect created.'); - } - - public function edit(Redirect $redirect): View - { - return view('redirects::admin.form', ['record' => $redirect]); - } - - public function update(Request $request, Redirect $redirect): RedirectResponse - { - $data = $request->validate(['source' => ['required', 'string', 'max:255'], - 'destination' => ['required', 'string', 'max:255'], - 'status_code' => ['nullable', 'integer', 'min:0'], - 'enabled' => ['sometimes', 'boolean']]); - $data['enabled'] = $request->boolean('enabled'); - $redirect->update($data); - - return redirect()->route('administration.redirects.index')->with('status', 'Redirect updated.'); - } - - public function destroy(Redirect $redirect): RedirectResponse - { - $redirect->delete(); - - return back()->with('status', 'Redirect deleted.'); - } -} diff --git a/Redirects/Models/Redirect.php b/Redirects/Models/Redirect.php deleted file mode 100644 index ba4db4e..0000000 --- a/Redirects/Models/Redirect.php +++ /dev/null @@ -1,24 +0,0 @@ - 'integer', - 'enabled' => 'boolean', - ]; - } -} diff --git a/Redirects/Providers/RedirectsServiceProvider.php b/Redirects/Providers/RedirectsServiceProvider.php deleted file mode 100644 index d32dbda..0000000 --- a/Redirects/Providers/RedirectsServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Redirects', 'Config/config.php'), 'redirects'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Redirects', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Redirects', 'Resources/views'), 'redirects'); - } -} diff --git a/Redirects/Providers/RouteServiceProvider.php b/Redirects/Providers/RouteServiceProvider.php deleted file mode 100644 index ba5a142..0000000 --- a/Redirects/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -routes(fn () => Route::middleware('web')->group(module_path('Redirects', 'Routes/web.php'))); - } -} diff --git a/Redirects/Resources/views/admin/form.blade.php b/Redirects/Resources/views/admin/form.blade.php deleted file mode 100644 index 18dccf9..0000000 --- a/Redirects/Resources/views/admin/form.blade.php +++ /dev/null @@ -1,8 +0,0 @@ - -

{{ isset($record) ? 'Edit' : 'Create' }} Redirect

@csrf @isset($record) @method('PUT') @endisset -
@error('source')

{{ $message }}

@enderror
-
@error('destination')

{{ $message }}

@enderror
-
@error('status_code')

{{ $message }}

@enderror
- -
Cancel
-
diff --git a/Redirects/Resources/views/admin/index.blade.php b/Redirects/Resources/views/admin/index.blade.php deleted file mode 100644 index cf7590b..0000000 --- a/Redirects/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,5 +0,0 @@ - -

Redirects

Manage redirects content.

Add new
-@if(session('status'))
{{ session('status') }}
@endif -
@forelse($records as $record)@empty@endforelse
SourceDestinationStatus CodeActions
{{ is_array($record->source) ? json_encode($record->source) : Str::limit((string) $record->source, 70) }}{{ is_array($record->destination) ? json_encode($record->destination) : Str::limit((string) $record->destination, 70) }}{{ is_array($record->status_code) ? json_encode($record->status_code) : Str::limit((string) $record->status_code, 70) }}Edit
@csrf @method('DELETE')
No records yet.
{{ $records->links() }}
-
diff --git a/Redirects/Routes/web.php b/Redirects/Routes/web.php deleted file mode 100644 index 9faf228..0000000 --- a/Redirects/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/redirects')->name('administration.redirects.')->group(function (): void { - Route::get('/', [RedirectController::class, 'index'])->name('index'); - Route::get('/create', [RedirectController::class, 'create'])->name('create'); - Route::post('/', [RedirectController::class, 'store'])->name('store'); - Route::get('/{redirect}/edit', [RedirectController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{redirect}', [RedirectController::class, 'update'])->name('update'); - Route::delete('/{redirect}', [RedirectController::class, 'destroy'])->name('destroy'); -}); diff --git a/Redirects/Support/ModuleInfo.php b/Redirects/Support/ModuleInfo.php deleted file mode 100644 index a226dac..0000000 --- a/Redirects/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBeTrue(); -}); diff --git a/Redirects/module.json b/Redirects/module.json deleted file mode 100644 index cebfd49..0000000 --- a/Redirects/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Redirects", - "alias": "redirects", - "version": "1.0.0", - "description": "Redirects CMS module.", - "keywords": [ - "cms", - "redirects" - ], - "priority": 23, - "providers": [ - "Modules\\Redirects\\Providers\\RedirectsServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core", - "Administration" - ] -} diff --git a/Roles/Config/config.php b/Roles/Config/config.php deleted file mode 100644 index 60fa263..0000000 --- a/Roles/Config/config.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Roles', - 'enabled' => true, -]; diff --git a/Roles/Database/Migrations/2026_07_23_000050_create_permission_tables.php b/Roles/Database/Migrations/2026_07_23_000050_create_permission_tables.php deleted file mode 100644 index d74934a..0000000 --- a/Roles/Database/Migrations/2026_07_23_000050_create_permission_tables.php +++ /dev/null @@ -1,119 +0,0 @@ -id(); - $table->string('name'); - $table->string('guard_name'); - $table->timestamps(); - - $table->unique(['name', 'guard_name']); - }); - - Schema::create($tableNames['roles'], function (Blueprint $table): void { - $table->id(); - $table->string('name'); - $table->string('guard_name'); - $table->timestamps(); - - $table->unique(['name', 'guard_name']); - }); - - Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ( - $tableNames, - $pivotPermission, - $modelMorphKey - ): void { - $table->unsignedBigInteger($pivotPermission); - $table->string('model_type'); - $table->unsignedBigInteger($modelMorphKey); - - $table->index([$modelMorphKey, 'model_type'], 'model_has_permissions_model_id_model_type_index'); - $table->foreign($pivotPermission) - ->references('id') - ->on($tableNames['permissions']) - ->cascadeOnDelete(); - - $table->primary( - [$pivotPermission, $modelMorphKey, 'model_type'], - 'model_has_permissions_permission_model_type_primary' - ); - }); - - Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ( - $tableNames, - $pivotRole, - $modelMorphKey - ): void { - $table->unsignedBigInteger($pivotRole); - $table->string('model_type'); - $table->unsignedBigInteger($modelMorphKey); - - $table->index([$modelMorphKey, 'model_type'], 'model_has_roles_model_id_model_type_index'); - $table->foreign($pivotRole) - ->references('id') - ->on($tableNames['roles']) - ->cascadeOnDelete(); - - $table->primary( - [$pivotRole, $modelMorphKey, 'model_type'], - 'model_has_roles_role_model_type_primary' - ); - }); - - Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ( - $tableNames, - $pivotRole, - $pivotPermission - ): void { - $table->unsignedBigInteger($pivotPermission); - $table->unsignedBigInteger($pivotRole); - - $table->foreign($pivotPermission) - ->references('id') - ->on($tableNames['permissions']) - ->cascadeOnDelete(); - $table->foreign($pivotRole) - ->references('id') - ->on($tableNames['roles']) - ->cascadeOnDelete(); - - $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); - }); - - app('cache')->store(config('permission.cache.store') === 'default' - ? null - : config('permission.cache.store')) - ->forget(config('permission.cache.key')); - } - - public function down(): void - { - $tableNames = config('permission.table_names'); - - Schema::dropIfExists($tableNames['role_has_permissions']); - Schema::dropIfExists($tableNames['model_has_roles']); - Schema::dropIfExists($tableNames['model_has_permissions']); - Schema::dropIfExists($tableNames['roles']); - Schema::dropIfExists($tableNames['permissions']); - } -}; diff --git a/Roles/Database/Seeders/RolePermissionSeeder.php b/Roles/Database/Seeders/RolePermissionSeeder.php deleted file mode 100644 index 9322669..0000000 --- a/Roles/Database/Seeders/RolePermissionSeeder.php +++ /dev/null @@ -1,96 +0,0 @@ -forgetCachedPermissions(); - - $permissions = [ - 'administration.access', - 'users.view', - 'users.create', - 'users.update', - 'users.delete', - 'roles.manage', - 'settings.manage', - 'audit.view', - 'media.view', - 'media.upload', - 'media.update', - 'media.delete', - 'seo.manage', - 'search.manage', - 'themes.manage', - 'pages.manage', - 'menus.manage', - 'redirects.manage', - 'forms.manage', - 'documents.manage', - 'banners.manage', - 'faq.manage', - 'team.manage', - 'testimonials.manage', - 'blog.manage', - 'news.manage', - 'categories.manage', - 'tags.manage', - 'authors.manage', - 'comments.moderate', - 'crm.manage', - 'newsletter.manage', - 'events.manage', - 'careers.manage', - 'locations.manage', - 'workflow.view', - 'workflow.manage', - 'workflow.review', - 'localization.manage', - 'localization.translate', - 'sites.view', - 'sites.manage', - 'sites.assign', - 'developer.access', - 'api.view', - 'api.manage', - 'enterprise.view', - 'enterprise.manage', - 'commerce.view', - 'commerce.manage', - 'commerce.orders.manage', - 'commerce.products.manage', - 'security.view', - 'security.manage', - 'security.audit', - 'automation.view', - 'automation.manage', - 'automation.execute', - 'ai.view', - 'ai.manage', - 'ai.execute', - 'analytics.view', - 'analytics.manage', - 'analytics.export', - 'builder.view', 'builder.manage', 'builder.publish', - 'marketplace.view', 'marketplace.install', 'marketplace.update', 'marketplace.remove', - 'cloud-devops.view', 'cloud-devops.manage', 'cloud-devops.deploy', 'cloud-devops.rollback', - ]; - - foreach ($permissions as $permission) { - Permission::findOrCreate($permission, 'web'); - } - - $administrator = Role::findOrCreate('admin', 'web'); - $administrator->syncPermissions($permissions); - - $registrar->forgetCachedPermissions(); - } -} diff --git a/Roles/Http/Controllers/RoleController.php b/Roles/Http/Controllers/RoleController.php deleted file mode 100644 index d5b0e16..0000000 --- a/Roles/Http/Controllers/RoleController.php +++ /dev/null @@ -1,58 +0,0 @@ - Role::withCount(['users', 'permissions'])->orderBy('name')->get()]); - } - - public function create() - { - return view('roles::form', ['role' => new Role(['guard_name' => 'web']), 'permissions' => Permission::orderBy('name')->get()]); - } - - public function store(Request $request) - { - $data = $this->validated($request); - $role = Role::create(['name' => $data['name'], 'guard_name' => 'web']); - $role->syncPermissions($data['permissions'] ?? []); - - return redirect()->route('administration.roles.index')->with('status', 'Role created.'); - } - - public function edit(Role $role) - { - return view('roles::form', ['role' => $role->load('permissions'), 'permissions' => Permission::orderBy('name')->get()]); - } - - public function update(Request $request, Role $role) - { - $data = $this->validated($request, $role); - $role->update(['name' => $data['name']]); - $role->syncPermissions($data['permissions'] ?? []); - - return redirect()->route('administration.roles.index')->with('status', 'Role updated.'); - } - - public function destroy(Role $role) - { - abort_if($role->name === 'admin', 422, 'The administrator role cannot be deleted.'); - $role->delete(); - - return back()->with('status', 'Role deleted.'); - } - - private function validated(Request $request, ?Role $role = null): array - { - return $request->validate(['name' => ['required', 'string', 'max:100', Rule::unique('roles', 'name')->ignore($role)], 'permissions' => ['array'], 'permissions.*' => ['string', 'exists:permissions,name']]); - } -} diff --git a/Roles/Providers/RolesServiceProvider.php b/Roles/Providers/RolesServiceProvider.php deleted file mode 100644 index 120c1c0..0000000 --- a/Roles/Providers/RolesServiceProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); - $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower()); - } -} diff --git a/Roles/Providers/RouteServiceProvider.php b/Roles/Providers/RouteServiceProvider.php deleted file mode 100644 index a8104fd..0000000 --- a/Roles/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,17 +0,0 @@ -routes(function (): void { - Route::middleware('web')->group(module_path('Roles', 'Routes/web.php')); - }); - } -} diff --git a/Roles/Resources/views/form.blade.php b/Roles/Resources/views/form.blade.php deleted file mode 100644 index 202fec9..0000000 --- a/Roles/Resources/views/form.blade.php +++ /dev/null @@ -1 +0,0 @@ -

{{ $role->exists ? 'Edit role' : 'Create role' }}

@csrf @if($role->exists)@method('PUT')@endif
Permissions
@foreach($permissions as $permission)@endforeach
CancelSave
diff --git a/Roles/Resources/views/index.blade.php b/Roles/Resources/views/index.blade.php deleted file mode 100644 index 6ccb021..0000000 --- a/Roles/Resources/views/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Roles & permissions

Control administrative access.

Create role
@if(session('status'))
{{ session('status') }}
@endif
@foreach($roles as $role)
{{ $role->name }}
{{ $role->users_count }} users · {{ $role->permissions_count }} permissions
Edit@if($role->name!=='admin')
@csrf @method('DELETE')Delete
@endif
@endforeach
diff --git a/Roles/Routes/web.php b/Roles/Routes/web.php deleted file mode 100644 index f611599..0000000 --- a/Roles/Routes/web.php +++ /dev/null @@ -1,8 +0,0 @@ -prefix('administration/roles')->name('administration.roles.')->group(function (): void { - Route::resource('', RoleController::class)->parameters(['' => 'role'])->except(['show']); -}); diff --git a/Roles/Support/ModuleInfo.php b/Roles/Support/ModuleInfo.php deleted file mode 100644 index 339da0a..0000000 --- a/Roles/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBe('Roles'); -}); diff --git a/Roles/composer.json b/Roles/composer.json deleted file mode 100644 index 0bdaa9f..0000000 --- a/Roles/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "absdev/roles-module", - "description": "Roles foundation module for the ABSDEV Laravel application platform.", - "type": "laravel-module", - "autoload": { - "psr-4": { - "Modules\\Roles\\": "" - } - } -} diff --git a/Roles/module.json b/Roles/module.json deleted file mode 100644 index b3f52b7..0000000 --- a/Roles/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Roles", - "alias": "roles", - "version": "1.0.0", - "description": "Roles foundation module for the ABSDEV Laravel application platform.", - "keywords": [ - "laravel", - "modules", - "roles" - ], - "priority": 10, - "providers": [ - "Modules\\Roles\\Providers\\RolesServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core" - ] -} diff --git a/Search/Config/config.php b/Search/Config/config.php deleted file mode 100644 index 9b3b517..0000000 --- a/Search/Config/config.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Search', - 'enabled' => true, -]; diff --git a/Search/Database/Migrations/2026_07_23_000500_create_search_index_table.php b/Search/Database/Migrations/2026_07_23_000500_create_search_index_table.php deleted file mode 100644 index ca700a7..0000000 --- a/Search/Database/Migrations/2026_07_23_000500_create_search_index_table.php +++ /dev/null @@ -1,30 +0,0 @@ -id(); - $table->morphs('searchable'); - $table->string('title')->index(); - $table->text('summary')->nullable(); - $table->longText('content')->nullable(); - $table->string('url')->nullable(); - $table->json('metadata')->nullable(); - $table->boolean('is_public')->default(true)->index(); - $table->timestamp('published_at')->nullable()->index(); - $table->timestamps(); - $table->unique(['searchable_type', 'searchable_id']); - }); - } - - public function down(): void - { - Schema::dropIfExists('search_index'); - } -}; diff --git a/Search/Http/Controllers/SearchController.php b/Search/Http/Controllers/SearchController.php deleted file mode 100644 index 9c591c6..0000000 --- a/Search/Http/Controllers/SearchController.php +++ /dev/null @@ -1,18 +0,0 @@ -query('q')); - $results = DB::table('search_index')->when($q !== '', fn ($query) => $query->where(fn ($w) => $w->where('title', 'like', "%{$q}%")->orWhere('summary', 'like', "%{$q}%")->orWhere('content', 'like', "%{$q}%")))->latest('updated_at')->paginate(25)->withQueryString(); - - return view('search::index', compact('results', 'q')); - } -} diff --git a/Search/Providers/RouteServiceProvider.php b/Search/Providers/RouteServiceProvider.php deleted file mode 100644 index d94acd2..0000000 --- a/Search/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,17 +0,0 @@ -routes(function (): void { - Route::middleware('web')->group(module_path('Search', 'Routes/web.php')); - }); - } -} diff --git a/Search/Providers/SearchServiceProvider.php b/Search/Providers/SearchServiceProvider.php deleted file mode 100644 index 12ad9c9..0000000 --- a/Search/Providers/SearchServiceProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); - $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower()); - } -} diff --git a/Search/Resources/views/index.blade.php b/Search/Resources/views/index.blade.php deleted file mode 100644 index 07ea084..0000000 --- a/Search/Resources/views/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Search index

Inspect content currently available to platform search.

Search
@forelse($results as $result)
{{ $result->title }}
{{ str($result->summary ?: $result->content)->limit(180) }}
{{ $result->searchable_type }} #{{ $result->searchable_id }}
@empty
No indexed content found.
@endforelse
{{ $results->links() }}
diff --git a/Search/Routes/web.php b/Search/Routes/web.php deleted file mode 100644 index ad6c1f7..0000000 --- a/Search/Routes/web.php +++ /dev/null @@ -1,6 +0,0 @@ -get('administration/search', [SearchController::class, 'index'])->name('administration.search.index'); diff --git a/Search/Support/ModuleInfo.php b/Search/Support/ModuleInfo.php deleted file mode 100644 index 98f93c7..0000000 --- a/Search/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBe('Search'); -}); diff --git a/Search/composer.json b/Search/composer.json deleted file mode 100644 index 62e3b1a..0000000 --- a/Search/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "absdev/search-module", - "description": "Search foundation module for the ABSDEV Laravel application platform.", - "type": "laravel-module", - "autoload": { - "psr-4": { - "Modules\\Search\\": "" - } - } -} diff --git a/Search/module.json b/Search/module.json deleted file mode 100644 index 6eda4a2..0000000 --- a/Search/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Search", - "alias": "search", - "version": "1.0.0", - "description": "Search foundation module for the ABSDEV Laravel application platform.", - "keywords": [ - "laravel", - "modules", - "search" - ], - "priority": 10, - "providers": [ - "Modules\\Search\\Providers\\SearchServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core" - ] -} diff --git a/Security/Config/config.php b/Security/Config/config.php deleted file mode 100644 index b4e3156..0000000 --- a/Security/Config/config.php +++ /dev/null @@ -1,9 +0,0 @@ - (bool) env('SECURITY_RECORD_AUTH_EVENTS', true), - 'trusted_device_days' => (int) env('SECURITY_TRUSTED_DEVICE_DAYS', 30), - 'session_idle_minutes' => (int) env('SECURITY_SESSION_IDLE_MINUTES', 120), - 'max_failed_logins' => (int) env('SECURITY_MAX_FAILED_LOGINS', 10), - 'lockout_minutes' => (int) env('SECURITY_LOCKOUT_MINUTES', 15), -]; diff --git a/Security/Database/Migrations/2026_07_24_120000_create_security_tables.php b/Security/Database/Migrations/2026_07_24_120000_create_security_tables.php deleted file mode 100644 index a393e34..0000000 --- a/Security/Database/Migrations/2026_07_24_120000_create_security_tables.php +++ /dev/null @@ -1,76 +0,0 @@ -id(); - $table->string('name')->default('Default security policy'); - $table->unsignedSmallInteger('minimum_password_length')->default(12); - $table->boolean('require_mixed_case')->default(true); - $table->boolean('require_numbers')->default(true); - $table->boolean('require_symbols')->default(true); - $table->boolean('require_mfa')->default(false); - $table->unsignedInteger('password_expiry_days')->nullable(); - $table->unsignedSmallInteger('password_history')->default(5); - $table->unsignedSmallInteger('max_failed_logins')->default(10); - $table->unsignedSmallInteger('lockout_minutes')->default(15); - $table->unsignedSmallInteger('session_idle_minutes')->default(120); - $table->boolean('single_session')->default(false); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - Schema::create('security_login_events', function (Blueprint $table) { - $table->id(); - $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); - $table->string('email')->nullable()->index(); - $table->string('event', 30)->index(); - $table->string('ip_address', 45)->nullable()->index(); - $table->text('user_agent')->nullable(); - $table->string('session_id')->nullable()->index(); - $table->string('reason')->nullable(); - $table->json('metadata')->nullable(); - $table->timestamp('occurred_at')->useCurrent(); - $table->timestamps(); - }); - Schema::create('security_devices', function (Blueprint $table) { - $table->id(); - $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - $table->string('name'); - $table->string('fingerprint', 64)->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->timestamp('trusted_until')->nullable(); - $table->timestamp('last_seen_at')->nullable(); - $table->timestamp('revoked_at')->nullable(); - $table->timestamps(); - $table->unique(['user_id', 'fingerprint']); - }); - Schema::create('security_ip_rules', function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->string('type', 10)->default('deny'); - $table->string('address', 64); - $table->text('description')->nullable(); - $table->boolean('applies_to_admin')->default(true); - $table->boolean('applies_to_login')->default(true); - $table->boolean('is_active')->default(true); - $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); - $table->timestamps(); - $table->index(['type', 'is_active']); - }); - } - - public function down(): void - { - Schema::dropIfExists('security_ip_rules'); - Schema::dropIfExists('security_devices'); - Schema::dropIfExists('security_login_events'); - Schema::dropIfExists('security_policies'); - } -}; diff --git a/Security/Http/Controllers/SecurityController.php b/Security/Http/Controllers/SecurityController.php deleted file mode 100644 index 77e2633..0000000 --- a/Security/Http/Controllers/SecurityController.php +++ /dev/null @@ -1,52 +0,0 @@ - SecurityPolicy::first(), 'events' => LoginEvent::latest('occurred_at')->paginate(25), 'devices' => TrustedDevice::whereNull('revoked_at')->latest('last_seen_at')->limit(20)->get(), 'rules' => IpRule::latest()->get(), 'failed24h' => LoginEvent::where('event', 'failed')->where('occurred_at', '>=', now()->subDay())->count()]); - } - - public function savePolicy(Request $request): RedirectResponse - { - $data = $request->validate(['minimum_password_length' => ['required', 'integer', 'min:8', 'max:128'], 'password_expiry_days' => ['nullable', 'integer', 'min:1'], 'password_history' => ['required', 'integer', 'min:0', 'max:24'], 'max_failed_logins' => ['required', 'integer', 'min:1', 'max:100'], 'lockout_minutes' => ['required', 'integer', 'min:1', 'max:1440'], 'session_idle_minutes' => ['required', 'integer', 'min:5', 'max:10080']]); - foreach (['require_mixed_case', 'require_numbers', 'require_symbols', 'require_mfa', 'single_session'] as $key) { - $data[$key] = $request->boolean($key); - } - SecurityPolicy::updateOrCreate(['id' => SecurityPolicy::value('id')], $data + ['name' => 'Default security policy', 'is_active' => true]); - - return back()->with('status', 'Security policy updated.'); - } - - public function storeRule(Request $request): RedirectResponse - { - IpRule::create($request->validate(['name' => ['required', 'string', 'max:255'], 'type' => ['required', 'in:allow,deny'], 'address' => ['required', 'string', 'max:64'], 'description' => ['nullable', 'string']]) + ['applies_to_admin' => $request->boolean('applies_to_admin'), 'applies_to_login' => $request->boolean('applies_to_login'), 'is_active' => true, 'created_by' => $request->user()->id]); - - return back()->with('status', 'IP rule created.'); - } - - public function destroyRule(IpRule $rule): RedirectResponse - { - $rule->delete(); - - return back()->with('status', 'IP rule deleted.'); - } - - public function revokeDevice(TrustedDevice $device): RedirectResponse - { - $device->update(['revoked_at' => now()]); - - return back()->with('status', 'Device trust revoked.'); - } -} diff --git a/Security/Http/Middleware/EnforceIpRestrictions.php b/Security/Http/Middleware/EnforceIpRestrictions.php deleted file mode 100644 index e009e1e..0000000 --- a/Security/Http/Middleware/EnforceIpRestrictions.php +++ /dev/null @@ -1,20 +0,0 @@ -rules->isBlocked((string) $request->ip(), $scope), 403, 'Access is not permitted from this IP address.'); - - return $next($request); - } -} diff --git a/Security/Listeners/RecordFailedLogin.php b/Security/Listeners/RecordFailedLogin.php deleted file mode 100644 index 6bcd118..0000000 --- a/Security/Listeners/RecordFailedLogin.php +++ /dev/null @@ -1,19 +0,0 @@ -user : null; - LoginEvent::create(['user_id' => $user?->getAuthIdentifier(), 'email' => $user?->email ?? (property_exists($event, 'credentials') ? ($event->credentials['email'] ?? null) : null), 'event' => 'failed', 'ip_address' => $request?->ip(), 'user_agent' => $request?->userAgent(), 'session_id' => $request?->hasSession() ? $request->session()->getId() : null, 'occurred_at' => now()]); - } -} diff --git a/Security/Listeners/RecordLogin.php b/Security/Listeners/RecordLogin.php deleted file mode 100644 index 83face4..0000000 --- a/Security/Listeners/RecordLogin.php +++ /dev/null @@ -1,19 +0,0 @@ -user : null; - LoginEvent::create(['user_id' => $user?->getAuthIdentifier(), 'email' => $user?->email ?? (property_exists($event, 'credentials') ? ($event->credentials['email'] ?? null) : null), 'event' => 'login', 'ip_address' => $request?->ip(), 'user_agent' => $request?->userAgent(), 'session_id' => $request?->hasSession() ? $request->session()->getId() : null, 'occurred_at' => now()]); - } -} diff --git a/Security/Listeners/RecordLogout.php b/Security/Listeners/RecordLogout.php deleted file mode 100644 index f29feed..0000000 --- a/Security/Listeners/RecordLogout.php +++ /dev/null @@ -1,19 +0,0 @@ -user : null; - LoginEvent::create(['user_id' => $user?->getAuthIdentifier(), 'email' => $user?->email ?? (property_exists($event, 'credentials') ? ($event->credentials['email'] ?? null) : null), 'event' => 'logout', 'ip_address' => $request?->ip(), 'user_agent' => $request?->userAgent(), 'session_id' => $request?->hasSession() ? $request->session()->getId() : null, 'occurred_at' => now()]); - } -} diff --git a/Security/Models/IpRule.php b/Security/Models/IpRule.php deleted file mode 100644 index 2996dfe..0000000 --- a/Security/Models/IpRule.php +++ /dev/null @@ -1,14 +0,0 @@ - 'boolean', 'applies_to_login' => 'boolean', 'is_active' => 'boolean']; -} diff --git a/Security/Models/LoginEvent.php b/Security/Models/LoginEvent.php deleted file mode 100644 index 1fce0ab..0000000 --- a/Security/Models/LoginEvent.php +++ /dev/null @@ -1,14 +0,0 @@ - 'array', 'occurred_at' => 'datetime']; -} diff --git a/Security/Models/SecurityPolicy.php b/Security/Models/SecurityPolicy.php deleted file mode 100644 index e790c60..0000000 --- a/Security/Models/SecurityPolicy.php +++ /dev/null @@ -1,12 +0,0 @@ - 'boolean', 'require_numbers' => 'boolean', 'require_symbols' => 'boolean', 'require_mfa' => 'boolean', 'single_session' => 'boolean', 'is_active' => 'boolean']; -} diff --git a/Security/Models/TrustedDevice.php b/Security/Models/TrustedDevice.php deleted file mode 100644 index 6777067..0000000 --- a/Security/Models/TrustedDevice.php +++ /dev/null @@ -1,14 +0,0 @@ - 'datetime', 'last_seen_at' => 'datetime', 'revoked_at' => 'datetime']; -} diff --git a/Security/Providers/RouteServiceProvider.php b/Security/Providers/RouteServiceProvider.php deleted file mode 100644 index ed3f4c3..0000000 --- a/Security/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,14 +0,0 @@ -group(module_path('Security', 'Routes/web.php')); - } -} diff --git a/Security/Providers/SecurityServiceProvider.php b/Security/Providers/SecurityServiceProvider.php deleted file mode 100644 index cf9e892..0000000 --- a/Security/Providers/SecurityServiceProvider.php +++ /dev/null @@ -1,33 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Security', 'Config/config.php'), 'security'); - } - - public function boot(Router $router): void - { - $this->loadMigrationsFrom(module_path('Security', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Security', 'Resources/views'), 'security'); - $router->aliasMiddleware('security.ip', EnforceIpRestrictions::class); - Event::listen(Login::class, RecordLogin::class); - Event::listen(Failed::class, RecordFailedLogin::class); - Event::listen(Logout::class, RecordLogout::class); - } -} diff --git a/Security/Resources/views/admin/index.blade.php b/Security/Resources/views/admin/index.blade.php deleted file mode 100644 index b07de13..0000000 --- a/Security/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,5 +0,0 @@ -

Security Centre

Identity policy, authentication history, trusted devices and network access controls.

@if(session('status'))
{{ session('status') }}
@endif -
Failed logins (24h)
{{ $failed24h }}
Active trusted devices
{{ $devices->count() }}
IP rules
{{ $rules->count() }}
MFA required
{{ $policy?->require_mfa ? 'Yes' : 'No' }}
-
@csrf @method('PUT')

Security policy

Save policy
-
@csrf

Add IP rule

Create rule
@foreach($rules as $rule)
{{ $rule->name }}{{ $rule->type }}
{{ $rule->address }}
@csrf @method('DELETE')Delete
@endforeach
-
Authentication history
@forelse($events as $event)@empty@endforelse
WhenEventUserIP addressClient
{{ $event->occurred_at?->format('d M Y H:i:s') }}{{ $event->event }}{{ $event->email ?: 'Unknown' }}{{ $event->ip_address ?: '—' }}{{ $event->user_agent ?: '—' }}
No authentication events recorded.
{{ $events->links() }}
diff --git a/Security/Routes/web.php b/Security/Routes/web.php deleted file mode 100644 index b92281e..0000000 --- a/Security/Routes/web.php +++ /dev/null @@ -1,12 +0,0 @@ -name('administration.security.')->middleware(['auth', 'permission:security.view', 'security.ip:admin'])->group(function () { - Route::get('/', [SecurityController::class, 'index'])->name('index'); - Route::put('/policy', [SecurityController::class, 'savePolicy'])->middleware('permission:security.manage')->name('policy.update'); - Route::post('/ip-rules', [SecurityController::class, 'storeRule'])->middleware('permission:security.manage')->name('rules.store'); - Route::delete('/ip-rules/{rule}', [SecurityController::class, 'destroyRule'])->middleware('permission:security.manage')->name('rules.destroy'); - Route::delete('/devices/{device}', [SecurityController::class, 'revokeDevice'])->middleware('permission:security.manage')->name('devices.revoke'); -}); diff --git a/Security/Services/IpRuleService.php b/Security/Services/IpRuleService.php deleted file mode 100644 index 490f000..0000000 --- a/Security/Services/IpRuleService.php +++ /dev/null @@ -1,39 +0,0 @@ -where('is_active', true)->where($column, true)->get(); - $allow = $rules->where('type', 'allow'); - if ($allow->isNotEmpty() && ! $allow->contains(fn (IpRule $rule) => $this->matches($ip, $rule->address))) { - return true; - } - - return $rules->where('type', 'deny')->contains(fn (IpRule $rule) => $this->matches($ip, $rule->address)); - } - - private function matches(string $ip, string $rule): bool - { - if ($ip === $rule) { - return true; - } - if (! str_contains($rule, '/')) { - return false; - } - [$subnet,$bits] = explode('/', $rule, 2); - $bits = (int) $bits; - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - $mask = -1 << (32 - $bits); - - return (ip2long($ip) & $mask) === (ip2long($subnet) & $mask); - } - - return false; - } -} diff --git a/Security/Tests/Feature/ModuleBootTest.php b/Security/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 7f2e420..0000000 --- a/Security/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,18 +0,0 @@ -assertTrue(app('router')->has('administration.security.index')); - } - - public function test_security_middleware_alias_is_registered(): void - { - $this->assertArrayHasKey('security.ip', app('router')->getMiddleware()); - } -} diff --git a/Security/module.json b/Security/module.json deleted file mode 100644 index 515918b..0000000 --- a/Security/module.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"Security","alias":"security","version":"1.0.0","description":"Identity policies, login history, trusted devices, IP restrictions and session security.","keywords":["identity","security","sessions","devices","authentication"],"priority":120,"providers":["Modules\\Security\\Providers\\SecurityServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Audit"]} diff --git a/Seo/Config/config.php b/Seo/Config/config.php deleted file mode 100644 index b2892d1..0000000 --- a/Seo/Config/config.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Seo', - 'enabled' => true, -]; diff --git a/Seo/Database/Migrations/2026_07_23_000400_create_seo_metadata_table.php b/Seo/Database/Migrations/2026_07_23_000400_create_seo_metadata_table.php deleted file mode 100644 index 1867c1e..0000000 --- a/Seo/Database/Migrations/2026_07_23_000400_create_seo_metadata_table.php +++ /dev/null @@ -1,36 +0,0 @@ -id(); - $table->morphs('seoable'); - $table->string('title')->nullable(); - $table->text('description')->nullable(); - $table->string('canonical_url')->nullable(); - $table->string('robots')->default('index,follow'); - $table->string('og_title')->nullable(); - $table->text('og_description')->nullable(); - $table->string('og_image')->nullable(); - $table->string('twitter_card')->default('summary_large_image'); - $table->string('schema_type')->nullable(); - $table->json('schema_data')->nullable(); - $table->boolean('include_in_sitemap')->default(true); - $table->decimal('sitemap_priority', 2, 1)->default(0.5); - $table->string('sitemap_change_frequency')->default('monthly'); - $table->timestamps(); - $table->unique(['seoable_type', 'seoable_id']); - }); - } - - public function down(): void - { - Schema::dropIfExists('seo_metadata'); - } -}; diff --git a/Seo/Http/Controllers/SeoController.php b/Seo/Http/Controllers/SeoController.php deleted file mode 100644 index 99349c0..0000000 --- a/Seo/Http/Controllers/SeoController.php +++ /dev/null @@ -1,27 +0,0 @@ -pluck('value', 'key'); - - return view('seo::index', compact('values')); - } - - public function update(Request $request) - { - $data = $request->validate(['site_title' => ['nullable', 'string', 'max:255'], 'meta_description' => ['nullable', 'string', 'max:500'], 'robots' => ['required', 'string', 'max:100'], 'canonical_base' => ['nullable', 'url', 'max:255']]); - foreach ($data as $key => $value) { - Setting::updateOrCreate(['group' => 'seo', 'key' => $key], ['value' => $value, 'type' => 'string', 'is_public' => true]); - } - -return back()->with('status', 'SEO defaults saved.'); - } -} diff --git a/Seo/Models/SeoMetadata.php b/Seo/Models/SeoMetadata.php deleted file mode 100644 index fa17538..0000000 --- a/Seo/Models/SeoMetadata.php +++ /dev/null @@ -1,17 +0,0 @@ - 'array', 'include_in_sitemap' => 'boolean', 'sitemap_priority' => 'decimal:1']; - } -} diff --git a/Seo/Providers/RouteServiceProvider.php b/Seo/Providers/RouteServiceProvider.php deleted file mode 100644 index d2a362d..0000000 --- a/Seo/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,17 +0,0 @@ -routes(function (): void { - Route::middleware('web')->group(module_path('Seo', 'Routes/web.php')); - }); - } -} diff --git a/Seo/Providers/SeoServiceProvider.php b/Seo/Providers/SeoServiceProvider.php deleted file mode 100644 index 1a134fc..0000000 --- a/Seo/Providers/SeoServiceProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); - $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower()); - } -} diff --git a/Seo/Resources/views/index.blade.php b/Seo/Resources/views/index.blade.php deleted file mode 100644 index 9939a86..0000000 --- a/Seo/Resources/views/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

SEO defaults

Site-wide metadata used when content does not provide its own values.

@if(session('status'))
{{ session('status') }}
@endif
@csrf @method('PUT'){{ old('meta_description',$values['meta_description']??'') }}
Save SEO defaults
diff --git a/Seo/Routes/web.php b/Seo/Routes/web.php deleted file mode 100644 index fa3652f..0000000 --- a/Seo/Routes/web.php +++ /dev/null @@ -1,9 +0,0 @@ -prefix('administration/seo')->name('administration.seo.')->group(function (): void { - Route::get('/', [SeoController::class, 'index'])->name('index'); - Route::put('/', [SeoController::class, 'update'])->name('update'); -}); diff --git a/Seo/Support/ModuleInfo.php b/Seo/Support/ModuleInfo.php deleted file mode 100644 index ac5145e..0000000 --- a/Seo/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBe('Seo'); -}); diff --git a/Seo/composer.json b/Seo/composer.json deleted file mode 100644 index a7eae0a..0000000 --- a/Seo/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "absdev/seo-module", - "description": "Seo foundation module for the ABSDEV Laravel application platform.", - "type": "laravel-module", - "autoload": { - "psr-4": { - "Modules\\Seo\\": "" - } - } -} diff --git a/Seo/module.json b/Seo/module.json deleted file mode 100644 index 4d886f8..0000000 --- a/Seo/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Seo", - "alias": "seo", - "version": "1.0.0", - "description": "Seo foundation module for the ABSDEV Laravel application platform.", - "keywords": [ - "laravel", - "modules", - "seo" - ], - "priority": 10, - "providers": [ - "Modules\\Seo\\Providers\\SeoServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core" - ] -} diff --git a/Settings/Config/config.php b/Settings/Config/config.php deleted file mode 100644 index c9342ba..0000000 --- a/Settings/Config/config.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Settings', - 'enabled' => true, -]; diff --git a/Settings/Database/Migrations/2026_07_23_000100_create_settings_table.php b/Settings/Database/Migrations/2026_07_23_000100_create_settings_table.php deleted file mode 100644 index 62b7134..0000000 --- a/Settings/Database/Migrations/2026_07_23_000100_create_settings_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->string('group')->default('general')->index(); - $table->string('key'); - $table->longText('value')->nullable(); - $table->string('type')->default('string'); - $table->boolean('is_public')->default(false); - $table->timestamps(); - $table->unique(['group', 'key']); - }); - } - - public function down(): void - { - Schema::dropIfExists('settings'); - } -}; diff --git a/Settings/Http/Controllers/SettingController.php b/Settings/Http/Controllers/SettingController.php deleted file mode 100644 index ceb3f78..0000000 --- a/Settings/Http/Controllers/SettingController.php +++ /dev/null @@ -1,109 +0,0 @@ - $settings->site(), - 'sections' => $this->sections($themes), - ]); - } - - public function update(Request $request, SettingsRepository $settings, Loader $themes): RedirectResponse - { - $rules = []; - foreach ($this->sections($themes) as $fields) { - foreach ($fields as $key => $definition) { - $rules["settings.$key"] = $definition['rules']; - } - } - - $validated = $request->validate($rules); - foreach ($validated['settings'] ?? [] as $key => $value) { - $settings->put($key, $value, 'site', 'string', true); - } - - return back()->with('status', 'Site settings saved. The active frontend and backend themes have been updated.'); - } - - /** @return array>> */ - private function sections(Loader $themes): array - { - $text = ['type' => 'text', 'rules' => ['nullable', 'string', 'max:255']]; - $url = ['type' => 'text', 'rules' => ['nullable', 'string', 'max:2048'], 'placeholder' => '/path or https://example.com']; - $textarea = ['type' => 'textarea', 'rules' => ['nullable', 'string', 'max:5000']]; - - $backendThemes = $themes->all() - ->filter(fn ($theme): bool => $theme->type() === 'backend') - ->mapWithKeys(fn ($theme): array => [$theme->key() => $theme->name()]) - ->all(); - - $frontendThemes = $themes->all() - ->filter(fn ($theme): bool => $theme->type() === 'frontend') - ->mapWithKeys(fn ($theme): array => [$theme->key() => $theme->name()]) - ->all(); - - return [ - 'Themes' => [ - 'backend_theme' => [ - 'type' => 'select', - 'rules' => ['required', 'string', Rule::in(array_keys($backendThemes))], - 'label' => 'Backend theme', - 'help' => 'Used for the authenticated administration area.', - 'options' => $backendThemes, - ], - 'frontend_theme' => [ - 'type' => 'select', - 'rules' => ['required', 'string', Rule::in(array_keys($frontendThemes))], - 'label' => 'Frontend theme', - 'help' => 'Used for the public website.', - 'options' => $frontendThemes, - ], - ], - 'General' => [ - 'site_name' => $text + ['label' => 'Site name', 'help' => 'Used in the header, footer, browser titles and metadata.'], - 'tagline' => $text + ['label' => 'Tagline'], - 'site_description' => $textarea + ['label' => 'Site description'], - 'company_name' => $text + ['label' => 'Legal company name'], - 'site_url' => $url + ['label' => 'Canonical site URL'], - 'copyright' => $text + ['label' => 'Copyright text', 'help' => 'Leave blank to generate it automatically.'], - ], - 'Branding' => [ - 'logo_url' => $url + ['label' => 'Primary logo'], - 'logo_dark_url' => $url + ['label' => 'Dark-mode logo'], - 'favicon_url' => $url + ['label' => 'Favicon'], - ], - 'Contact' => [ - 'contact_email' => ['type' => 'email', 'rules' => ['nullable', 'email', 'max:255'], 'label' => 'Public email'], - 'contact_phone' => $text + ['label' => 'Public telephone'], - 'address' => $textarea + ['label' => 'Address'], - ], - 'Social' => [ - 'facebook_url' => $url + ['label' => 'Facebook'], - 'x_url' => $url + ['label' => 'X / Twitter'], - 'linkedin_url' => $url + ['label' => 'LinkedIn'], - 'instagram_url' => $url + ['label' => 'Instagram'], - ], - 'Account navigation' => [ - 'dashboard_label' => $text + ['label' => 'Dashboard label'], - 'login_label' => $text + ['label' => 'Login label'], - 'registration_label' => $text + ['label' => 'Registration label'], - ], - 'SEO defaults' => [ - 'default_meta_title' => $text + ['label' => 'Default meta title', 'help' => 'Leave blank to use the site name.'], - 'default_meta_description' => $textarea + ['label' => 'Default meta description', 'help' => 'Leave blank to use the site description.'], - ], - ]; - } -} diff --git a/Settings/Models/Setting.php b/Settings/Models/Setting.php deleted file mode 100644 index 79c0ed6..0000000 --- a/Settings/Models/Setting.php +++ /dev/null @@ -1,15 +0,0 @@ - 'boolean']; - } -} diff --git a/Settings/Providers/RouteServiceProvider.php b/Settings/Providers/RouteServiceProvider.php deleted file mode 100644 index a5a3776..0000000 --- a/Settings/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,17 +0,0 @@ -routes(function (): void { - Route::middleware('web')->group(module_path('Settings', 'Routes/web.php')); - }); - } -} diff --git a/Settings/Providers/SettingsServiceProvider.php b/Settings/Providers/SettingsServiceProvider.php deleted file mode 100644 index 6a5ea24..0000000 --- a/Settings/Providers/SettingsServiceProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); - $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower()); - } -} diff --git a/Settings/Resources/views/index.blade.php b/Settings/Resources/views/index.blade.php deleted file mode 100644 index fcf669e..0000000 --- a/Settings/Resources/views/index.blade.php +++ /dev/null @@ -1,55 +0,0 @@ - -
-
-
-

Single source of truth

-

Site settings

-

- Global identity, branding, contact details and SEO are entered once here and reused by every theme, page, header and footer. -

-
- Manage menus -
- - @if (session('status')) -
{{ session('status') }}
- @endif - -
- @csrf - @method('PUT') - - @foreach ($sections as $section => $fields) -
-
-

{{ $section }}

-
-
- @foreach ($fields as $key => $field) - - @endforeach -
-
- @endforeach - -
- Save site settings -
-
-
-
diff --git a/Settings/Routes/web.php b/Settings/Routes/web.php deleted file mode 100644 index 3f28a23..0000000 --- a/Settings/Routes/web.php +++ /dev/null @@ -1,9 +0,0 @@ -prefix('administration/settings')->name('administration.settings.')->group(function (): void { - Route::get('/', [SettingController::class, 'index'])->name('index'); - Route::put('/', [SettingController::class, 'update'])->name('update'); -}); diff --git a/Settings/Services/SettingsRepository.php b/Settings/Services/SettingsRepository.php deleted file mode 100644 index 4e80ad7..0000000 --- a/Settings/Services/SettingsRepository.php +++ /dev/null @@ -1,137 +0,0 @@ -tableExists()) { - return $default; - } - - /** @var array{value: ?string, type: string}|null $setting */ - $setting = Cache::rememberForever( - "settings.value.v2.$group.$key", - fn (): ?array => Setting::query() - ->where([ - 'group' => $group, - 'key' => $key, - ]) - ->first(['value', 'type']) - ?->only(['value', 'type']) - ); - - return $setting !== null - ? $this->decode($setting['value'], $setting['type']) - : $default; - } - - /** @return array */ - public function group(string $group): array - { - if (! $this->tableExists()) { - return []; - } - - return Cache::rememberForever("settings.group.$group", function () use ($group): array { - return Setting::query() - ->where('group', $group) - ->get() - ->mapWithKeys(fn (Setting $setting): array => [ - $setting->key => $this->decode($setting->value, $setting->type), - ]) - ->all(); - }); - } - - /** @return array */ - public function site(): array - { - $defaults = [ - 'site_name' => config('app.name', 'Content Manager'), - 'tagline' => '', - 'site_description' => 'Modern software for modern companies.', - 'company_name' => config('app.name', 'Content Manager'), - 'site_url' => url('/'), - 'logo_url' => '', - 'logo_dark_url' => '', - 'favicon_url' => '/favicon.ico', - 'contact_email' => '', - 'contact_phone' => '', - 'address' => '', - 'facebook_url' => '', - 'x_url' => '', - 'linkedin_url' => '', - 'instagram_url' => '', - 'copyright' => '', - 'dashboard_label' => 'Dashboard', - 'login_label' => 'Log in', - 'registration_label' => 'Get started', - 'default_meta_title' => '', - 'default_meta_description' => '', - 'backend_theme' => config('stylist.default_backend_theme', 'admin'), - 'frontend_theme' => config('stylist.default_frontend_theme', 'saasland'), - ]; - - return array_replace($defaults, $this->group('site')); - } - - public function put(string $key, mixed $value, string $group = 'general', string $type = 'string', bool $public = false): Setting - { - $setting = Setting::query()->updateOrCreate( - compact('group', 'key'), - [ - 'value' => $this->encode($value, $type), - 'type' => $type, - 'is_public' => $public, - ] - ); - - // Remove both the legacy model cache and the scalar-only cache. - Cache::forget("settings.$group.$key"); - Cache::forget("settings.value.v2.$group.$key"); - Cache::forget("settings.group.$group"); - - return $setting; - } - - private function tableExists(): bool - { - try { - return Schema::hasTable((new Setting())->getTable()); - } catch (\Throwable) { - return false; - } - } - - private function encode(mixed $value, string $type): ?string - { - if ($value === null) { - return null; - } - - return match ($type) { - 'boolean' => filter_var($value, FILTER_VALIDATE_BOOL) ? '1' : '0', - 'integer' => (string) (int) $value, - 'float' => (string) (float) $value, - 'json', 'array' => json_encode($value, JSON_THROW_ON_ERROR), - default => (string) $value, - }; - } - - private function decode(?string $value, string $type): mixed - { - return match ($type) { - 'boolean' => filter_var($value, FILTER_VALIDATE_BOOL), - 'integer' => (int) $value, - 'float' => (float) $value, - 'json', 'array' => filled($value) ? json_decode($value, true, 512, JSON_THROW_ON_ERROR) : [], - default => $value, - }; - } -} diff --git a/Settings/Support/ModuleInfo.php b/Settings/Support/ModuleInfo.php deleted file mode 100644 index 6c5d3ee..0000000 --- a/Settings/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBe('Settings'); -}); diff --git a/Settings/Tests/Feature/SiteSettingsSingleSourceTest.php b/Settings/Tests/Feature/SiteSettingsSingleSourceTest.php deleted file mode 100644 index 41b4f99..0000000 --- a/Settings/Tests/Feature/SiteSettingsSingleSourceTest.php +++ /dev/null @@ -1,9 +0,0 @@ -put('site_name', 'Unified CMS', 'site', 'string', true); - expect($repository->site()['site_name'])->toBe('Unified CMS'); -}); diff --git a/Settings/Tests/Feature/ThemeSelectionSettingsTest.php b/Settings/Tests/Feature/ThemeSelectionSettingsTest.php deleted file mode 100644 index cae583a..0000000 --- a/Settings/Tests/Feature/ThemeSelectionSettingsTest.php +++ /dev/null @@ -1,27 +0,0 @@ -put('frontend_theme', 'swiss', 'site', 'string', true); - $settings->put('backend_theme', 'admin', 'site', 'string', true); - - expect($settings->site()) - ->frontend_theme->toBe('swiss') - ->backend_theme->toBe('admin'); - - expect(app(Stylist::class)->frontend()->key())->toBe('swiss') - ->and(app(Stylist::class)->backend()->key())->toBe('admin'); -}); - -it('falls back to configured themes when stored theme keys are unavailable', function (): void { - $settings = app(SettingsRepository::class); - $settings->put('frontend_theme', 'missing-theme', 'site', 'string', true); - $settings->put('backend_theme', 'missing-theme', 'site', 'string', true); - - expect(app(Stylist::class)->frontend()->key())->toBe(config('stylist.default_frontend_theme')) - ->and(app(Stylist::class)->backend()->key())->toBe(config('stylist.default_backend_theme')); -}); diff --git a/Settings/composer.json b/Settings/composer.json deleted file mode 100644 index 2701db4..0000000 --- a/Settings/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "absdev/settings-module", - "description": "Settings foundation module for the ABSDEV Laravel application platform.", - "type": "laravel-module", - "autoload": { - "psr-4": { - "Modules\\Settings\\": "" - } - } -} diff --git a/Settings/module.json b/Settings/module.json deleted file mode 100644 index a65856b..0000000 --- a/Settings/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Settings", - "alias": "settings", - "version": "1.0.0", - "description": "Settings foundation module for the ABSDEV Laravel application platform.", - "keywords": [ - "laravel", - "modules", - "settings" - ], - "priority": 10, - "providers": [ - "Modules\\Settings\\Providers\\SettingsServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core" - ] -} diff --git a/Sitemap/Config/config.php b/Sitemap/Config/config.php deleted file mode 100644 index 02edfd4..0000000 --- a/Sitemap/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Sitemap']; diff --git a/Sitemap/Http/Controllers/SitemapController.php b/Sitemap/Http/Controllers/SitemapController.php deleted file mode 100644 index 3486a49..0000000 --- a/Sitemap/Http/Controllers/SitemapController.php +++ /dev/null @@ -1,23 +0,0 @@ -push(['loc' => url('/'), 'updated_at' => now()]); - Page::query()->where('status', 'published')->get()->each(fn ($r) => $urls->push(['loc' => route('pages.public.show', $r->slug), 'updated_at' => $r->updated_at])); - Blog::published()->get()->each(fn ($r) => $urls->push(['loc' => route('blog.public.show', $r->slug), 'updated_at' => $r->updated_at])); - News::published()->get()->each(fn ($r) => $urls->push(['loc' => route('news.public.show', $r->slug), 'updated_at' => $r->updated_at])); - - return response()->view('sitemap::index', compact('urls'))->header('Content-Type', 'application/xml; charset=UTF-8'); - } -} diff --git a/Sitemap/Providers/RouteServiceProvider.php b/Sitemap/Providers/RouteServiceProvider.php deleted file mode 100644 index d3894cc..0000000 --- a/Sitemap/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Sitemap', 'Routes/web.php')); - } -} diff --git a/Sitemap/Providers/SitemapServiceProvider.php b/Sitemap/Providers/SitemapServiceProvider.php deleted file mode 100644 index 4549e61..0000000 --- a/Sitemap/Providers/SitemapServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Sitemap', 'Config/config.php'), 'sitemap'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Sitemap', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Sitemap', 'Resources/views'), 'sitemap'); - } -} diff --git a/Sitemap/Resources/views/index.blade.php b/Sitemap/Resources/views/index.blade.php deleted file mode 100644 index 0464fe2..0000000 --- a/Sitemap/Resources/views/index.blade.php +++ /dev/null @@ -1,7 +0,0 @@ -{!! '' !!} -@foreach($urls as $url) - - {{ $url['loc'] }} - {{ $url['updated_at']->toAtomString() }} - - @endforeach diff --git a/Sitemap/Routes/web.php b/Sitemap/Routes/web.php deleted file mode 100644 index e6ebc68..0000000 --- a/Sitemap/Routes/web.php +++ /dev/null @@ -1,6 +0,0 @@ -name('sitemap.index'); diff --git a/Sitemap/Tests/Feature/ModuleBootTest.php b/Sitemap/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 4b1361f..0000000 --- a/Sitemap/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('Sitemap', config('sitemap.name')); - } -} diff --git a/Sitemap/module.json b/Sitemap/module.json deleted file mode 100644 index ecf9761..0000000 --- a/Sitemap/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Sitemap", - "alias": "sitemap", - "version": "1.0.0", - "description": "Sitemap publishing module.", - "keywords": ["publishing", "sitemap"], - "priority": 30, - "providers": ["Modules\\Sitemap\\Providers\\SitemapServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Sites/Concerns/BelongsToSites.php b/Sites/Concerns/BelongsToSites.php deleted file mode 100644 index 7634d6e..0000000 --- a/Sites/Concerns/BelongsToSites.php +++ /dev/null @@ -1,32 +0,0 @@ -morphToMany(Site::class, 'content', 'site_content', 'content_id', 'site_id')->withPivot('is_primary')->withTimestamps(); - } - - public function scopeForCurrentSite(Builder $query): Builder - { - $site = app(SiteManager::class)->current(); - - return $site ? $query->whereHas('sites', fn (Builder $sites) => $sites->whereKey($site->id)) : $query; - } - - public function assignToSite(Site|int $site, bool $primary = false): void - { - $id = $site instanceof Site ? $site->getKey() : $site; - if ($primary) { - $this->sites()->updateExistingPivot($this->sites()->pluck('sites.id'), ['is_primary' => false]); - } - $this->sites()->syncWithoutDetaching([$id => ['is_primary' => $primary]]); - } -} diff --git a/Sites/Config/config.php b/Sites/Config/config.php deleted file mode 100644 index 4941723..0000000 --- a/Sites/Config/config.php +++ /dev/null @@ -1,16 +0,0 @@ - true, - 'session_key' => 'platform_site_id', - 'central_domains' => array_values(array_filter(explode(',', (string) env('CENTRAL_DOMAINS', '')))), - 'content_types' => [ - 'pages' => Page::class, - 'blog' => Blog::class, - 'news' => News::class, - ], -]; diff --git a/Sites/Contracts/SiteAwareModule.php b/Sites/Contracts/SiteAwareModule.php deleted file mode 100644 index 61f13f5..0000000 --- a/Sites/Contracts/SiteAwareModule.php +++ /dev/null @@ -1,10 +0,0 @@ -id(); - $table->string('name'); - $table->string('key')->unique(); - $table->string('status')->default('active'); - $table->string('theme')->nullable(); - $table->string('default_locale', 20)->default('en'); - $table->string('timezone')->default('UTC'); - $table->boolean('is_default')->default(false); - $table->timestamps(); - }); - Schema::create('site_domains', function (Blueprint $table): void { - $table->id(); - $table->foreignId('site_id')->constrained()->cascadeOnDelete(); - $table->string('domain')->unique(); - $table->boolean('is_primary')->default(false); - $table->boolean('redirect_to_primary')->default(false); - $table->boolean('force_https')->default(true); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - Schema::create('site_settings', function (Blueprint $table): void { - $table->id(); - $table->foreignId('site_id')->constrained()->cascadeOnDelete(); - $table->string('group')->default('general'); - $table->string('key'); - $table->longText('value')->nullable(); - $table->string('type')->default('string'); - $table->timestamps(); - $table->unique(['site_id', 'key']); - }); - Schema::create('site_content', function (Blueprint $table): void { - $table->id(); - $table->foreignId('site_id')->constrained()->cascadeOnDelete(); - $table->string('content_type'); - $table->unsignedBigInteger('content_id'); - $table->boolean('is_primary')->default(false); - $table->timestamps(); - $table->unique(['site_id', 'content_type', 'content_id']); - $table->index(['content_type', 'content_id']); - }); - Schema::create('site_user', function (Blueprint $table): void { - $table->foreignId('site_id')->constrained()->cascadeOnDelete(); - $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - $table->string('role')->default('editor'); - $table->timestamps(); - $table->primary(['site_id', 'user_id']); - }); - } - - public function down(): void - { - Schema::dropIfExists('site_user'); - Schema::dropIfExists('site_content'); - Schema::dropIfExists('site_settings'); - Schema::dropIfExists('site_domains'); - Schema::dropIfExists('sites'); - } -}; diff --git a/Sites/Database/Seeders/SiteSeeder.php b/Sites/Database/Seeders/SiteSeeder.php deleted file mode 100644 index 905f213..0000000 --- a/Sites/Database/Seeders/SiteSeeder.php +++ /dev/null @@ -1,16 +0,0 @@ -updateOrCreate(['key' => 'default'], ['name' => 'Default Site', 'status' => 'active', 'default_locale' => config('app.locale', 'en'), 'timezone' => config('app.timezone', 'UTC'), 'is_default' => true]); - $site->domains()->updateOrCreate(['domain' => $host], ['is_primary' => true, 'redirect_to_primary' => false, 'force_https' => str_starts_with((string) config('app.url'), 'https://'), 'is_active' => true]); - } -} diff --git a/Sites/Http/Controllers/DomainController.php b/Sites/Http/Controllers/DomainController.php deleted file mode 100644 index 90e2dca..0000000 --- a/Sites/Http/Controllers/DomainController.php +++ /dev/null @@ -1,33 +0,0 @@ -validate(['domain' => ['required', 'string', 'max:255', 'regex:/^[a-z0-9.-]+$/i', Rule::unique('site_domains', 'domain')], 'is_primary' => ['sometimes', 'boolean'], 'redirect_to_primary' => ['sometimes', 'boolean'], 'force_https' => ['sometimes', 'boolean'], 'is_active' => ['sometimes', 'boolean']]); - if ($request->boolean('is_primary')) { - $site->domains()->update(['is_primary' => false]); - } - $site->domains()->create($data + ['is_primary' => $request->boolean('is_primary'), 'redirect_to_primary' => $request->boolean('redirect_to_primary'), 'force_https' => $request->boolean('force_https'), 'is_active' => $request->boolean('is_active', true)]); - - return back()->with('success', 'Domain added.'); - } - - public function destroy(Site $site, SiteDomain $domain): RedirectResponse - { - abort_unless($domain->site_id === $site->id, 404); - abort_if($domain->is_primary && $site->domains()->count() === 1, 422, 'A site must retain a domain.'); - $domain->delete(); - - return back()->with('success', 'Domain removed.'); - } -} diff --git a/Sites/Http/Controllers/SiteController.php b/Sites/Http/Controllers/SiteController.php deleted file mode 100644 index 3e719f9..0000000 --- a/Sites/Http/Controllers/SiteController.php +++ /dev/null @@ -1,68 +0,0 @@ - Site::query()->with('domains')->orderByDesc('is_default')->orderBy('name')->paginate(20)]); - } - - public function create(): View - { - return view('sites::admin.sites.form', ['site' => new Site]); - } - - public function store(Request $request): RedirectResponse - { - $site = Site::query()->create($this->validated($request)); - - return redirect()->route('administration.sites.edit', $site)->with('success', 'Site created.'); - } - - public function edit(Site $site): View - { - return view('sites::admin.sites.form', ['site' => $site->load(['domains', 'settings'])]); - } - - public function update(Request $request, Site $site): RedirectResponse - { - DB::transaction(function () use ($request, $site): void { - $data = $this->validated($request, $site); - if ($data['is_default']) { - Site::query()->whereKeyNot($site->id)->update(['is_default' => false]); - } $site->update($data); - }); - - return back()->with('success', 'Site updated.'); - } - - public function destroy(Site $site): RedirectResponse - { - abort_if($site->is_default, 422, 'The default site cannot be deleted.'); - $site->delete(); - - return redirect()->route('administration.sites.index')->with('success', 'Site deleted.'); - } - - public function switch(Request $request, Site $site): RedirectResponse - { - $request->session()->put(config('sites.session_key'), $site->id); - - return back()->with('success', "Site context changed to {$site->name}."); - } - - private function validated(Request $request, ?Site $site = null): array - { - return $request->validate(['name' => ['required', 'string', 'max:150'], 'key' => ['required', 'alpha_dash', 'max:100', Rule::unique('sites', 'key')->ignore($site)], 'status' => ['required', Rule::in(['active', 'inactive', 'maintenance'])], 'theme' => ['nullable', 'string', 'max:100'], 'default_locale' => ['required', 'string', 'max:20'], 'timezone' => ['required', 'timezone'], 'is_default' => ['sometimes', 'boolean']]) + ['is_default' => $request->boolean('is_default')]; - } -} diff --git a/Sites/Http/Middleware/ResolveSite.php b/Sites/Http/Middleware/ResolveSite.php deleted file mode 100644 index fc3207d..0000000 --- a/Sites/Http/Middleware/ResolveSite.php +++ /dev/null @@ -1,24 +0,0 @@ -sites->resolve($request); - if ($site) { - config(['app.url' => $request->getSchemeAndHttpHost(), 'app.timezone' => $site->timezone ?: config('app.timezone')]); - date_default_timezone_set((string) config('app.timezone')); - } - - return $next($request); - } -} diff --git a/Sites/Models/Site.php b/Sites/Models/Site.php deleted file mode 100644 index a9c2689..0000000 --- a/Sites/Models/Site.php +++ /dev/null @@ -1,44 +0,0 @@ - 'boolean']; - } - - public function domains(): HasMany - { - return $this->hasMany(SiteDomain::class); - } - - public function settings(): HasMany - { - return $this->hasMany(SiteSetting::class); - } - - public function assignments(): HasMany - { - return $this->hasMany(SiteContent::class); - } - - public function scopeActive(Builder $query): Builder - { - return $query->where('status', 'active'); - } - - public function setting(string $key, mixed $default = null): mixed - { - $setting = $this->relationLoaded('settings') ? $this->settings->firstWhere('key', $key) : $this->settings()->where('key', $key)->first(); - - return $setting?->typedValue() ?? $default; - } -} diff --git a/Sites/Models/SiteContent.php b/Sites/Models/SiteContent.php deleted file mode 100644 index c359fbb..0000000 --- a/Sites/Models/SiteContent.php +++ /dev/null @@ -1,29 +0,0 @@ - 'boolean']; - } - - public function site(): BelongsTo - { - return $this->belongsTo(Site::class); - } - - public function content(): MorphTo - { - return $this->morphTo(__FUNCTION__, 'content_type', 'content_id'); - } -} diff --git a/Sites/Models/SiteDomain.php b/Sites/Models/SiteDomain.php deleted file mode 100644 index b92e2cf..0000000 --- a/Sites/Models/SiteDomain.php +++ /dev/null @@ -1,21 +0,0 @@ - 'boolean', 'redirect_to_primary' => 'boolean', 'force_https' => 'boolean', 'is_active' => 'boolean']; - } - - public function site(): BelongsTo - { - return $this->belongsTo(Site::class); - } -} diff --git a/Sites/Models/SiteSetting.php b/Sites/Models/SiteSetting.php deleted file mode 100644 index 249314a..0000000 --- a/Sites/Models/SiteSetting.php +++ /dev/null @@ -1,26 +0,0 @@ -belongsTo(Site::class); - } - - public function typedValue(): mixed - { - return match ($this->type) { - 'boolean' => filter_var($this->value, FILTER_VALIDATE_BOOLEAN), - 'integer' => (int) $this->value, - 'json' => json_decode((string) $this->value, true), - default => $this->value, - }; - } -} diff --git a/Sites/Providers/RouteServiceProvider.php b/Sites/Providers/RouteServiceProvider.php deleted file mode 100644 index f4820fa..0000000 --- a/Sites/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Sites', 'Routes/web.php')); - } -} diff --git a/Sites/Providers/SitesServiceProvider.php b/Sites/Providers/SitesServiceProvider.php deleted file mode 100644 index fbfd18f..0000000 --- a/Sites/Providers/SitesServiceProvider.php +++ /dev/null @@ -1,22 +0,0 @@ -mergeConfigFrom(module_path('Sites', 'Config/config.php'), 'sites'); - $this->app->singleton(SiteManager::class); - $this->app->register(RouteServiceProvider::class); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Sites', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Sites', 'Resources/views'), 'sites'); - } -} diff --git a/Sites/Resources/views/admin/sites/form.blade.php b/Sites/Resources/views/admin/sites/form.blade.php deleted file mode 100644 index 987ee09..0000000 --- a/Sites/Resources/views/admin/sites/form.blade.php +++ /dev/null @@ -1 +0,0 @@ -
{{ $site->exists ? 'Edit site' : 'New site' }}Configure the site identity, theme and regional defaults.
@csrf @if($site->exists) @method('PUT') @endif
Default site
Save siteCancel
@if($site->exists)
Domains
@foreach($site->domains as $domain)
{{ $domain->domain }} @if($domain->is_primary)Primary@endif
@csrf @method('DELETE')Remove
@endforeach
@csrf
PrimaryForce HTTPSActive
Add domain
@endif
diff --git a/Sites/Resources/views/admin/sites/index.blade.php b/Sites/Resources/views/admin/sites/index.blade.php deleted file mode 100644 index dc7414e..0000000 --- a/Sites/Resources/views/admin/sites/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -
SitesManage websites, domains, themes and regional defaults.
New site
@foreach($sites as $site)@endforeach
SiteDomainsThemeStatus
{{ $site->name }} @if($site->is_default)Default@endif{{ $site->domains->pluck('domain')->join(', ') ?: 'No domains' }}{{ $site->theme ?: 'Inherited' }}{{ ucfirst($site->status) }}Edit
{{ $sites->links() }}
diff --git a/Sites/Routes/web.php b/Sites/Routes/web.php deleted file mode 100644 index 78ebd55..0000000 --- a/Sites/Routes/web.php +++ /dev/null @@ -1,12 +0,0 @@ -prefix('administration')->name('administration.')->group(function (): void { - Route::post('sites/{site}/switch', [SiteController::class, 'switch'])->name('sites.switch'); - Route::resource('sites', SiteController::class)->except('show'); - Route::post('sites/{site}/domains', [DomainController::class, 'store'])->name('sites.domains.store'); - Route::delete('sites/{site}/domains/{domain}', [DomainController::class, 'destroy'])->name('sites.domains.destroy'); -}); diff --git a/Sites/Services/SiteManager.php b/Sites/Services/SiteManager.php deleted file mode 100644 index c9e1306..0000000 --- a/Sites/Services/SiteManager.php +++ /dev/null @@ -1,76 +0,0 @@ -current; - } - - public function setCurrent(?Site $site): void - { - $this->current = $site; - - if ($site) { - app()->instance(Site::class, $site); - app()->instance('currentSite', $site); - } - } - - public function all(): Collection - { - if (! $this->tablesExist()) { - return collect(); - } - - return Site::query() - ->active() - ->orderByDesc('is_default') - ->orderBy('name') - ->get(); - } - - public function resolve(Request $request): ?Site - { - if (! $this->tablesExist()) { - $this->setCurrent(null); - - return null; - } - - $host = strtolower(preg_replace('/:\d+$/', '', $request->getHost())); - - $site = Site::query() - ->with(['domains', 'settings']) - ->whereHas('domains', fn ($query) => $query - ->where('domain', $host) - ->where('is_active', true)) - ->first(); - - if (! $site && config('sites.fallback_to_default')) { - $site = Site::query() - ->with(['domains', 'settings']) - ->active() - ->where('is_default', true) - ->first(); - } - - $this->setCurrent($site); - - return $site; - } - - private function tablesExist(): bool - { - return Schema::hasTable('sites') && Schema::hasTable('site_domains'); - } -} diff --git a/Sites/Tests/Feature/SitesModuleBootTest.php b/Sites/Tests/Feature/SitesModuleBootTest.php deleted file mode 100644 index 94916ed..0000000 --- a/Sites/Tests/Feature/SitesModuleBootTest.php +++ /dev/null @@ -1,14 +0,0 @@ -assertTrue(app('router')->has('administration.sites.index')); - $this->assertTrue(app('router')->has('administration.sites.domains.store')); - } -} diff --git a/Sites/module.json b/Sites/module.json deleted file mode 100644 index a8886dd..0000000 --- a/Sites/module.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "Sites", - "alias": "sites", - "description": "Multi-site domains, context, settings, content assignments and administration.", - "keywords": ["multisite", "sites", "domains", "tenancy"], - "priority": 35, - "providers": ["Modules\\Sites\\Providers\\SitesServiceProvider"], - "files": [] -} diff --git a/Stylist/Config/config.php b/Stylist/Config/config.php deleted file mode 100644 index 71b15d3..0000000 --- a/Stylist/Config/config.php +++ /dev/null @@ -1,29 +0,0 @@ - env('STYLIST_DEFAULT_THEME', 'admin'), - 'default_backend_theme' => env('STYLIST_BACKEND_THEME', 'admin'), - 'default_frontend_theme' => env('STYLIST_FRONTEND_THEME', 'saasland'), - - /* - | Themes are application resources, never module resources. - | Every theme is self-contained and owns its views and assets. - */ - 'themes_path' => env('STYLIST_THEMES_PATH', resource_path('themes')), - - 'tenant_theme_column' => 'theme_key', - 'tenant_brand_columns' => [ - 'brand_primary' => 'brand-primary', - 'brand_secondary' => 'brand-secondary', - 'brand_accent' => 'brand-accent', - ], - - 'fallback_variables' => [ - 'brand-primary' => '#2563eb', - 'brand-secondary' => '#0f172a', - 'brand-accent' => '#22c55e', - 'surface' => '#ffffff', - 'surface-muted' => '#f8fafc', - 'text-primary' => '#0f172a', - ], -]; diff --git a/Stylist/Console/CreateThemeCommand.php b/Stylist/Console/CreateThemeCommand.php deleted file mode 100644 index c7e1dff..0000000 --- a/Stylist/Console/CreateThemeCommand.php +++ /dev/null @@ -1,64 +0,0 @@ -argument('key'))); - if (! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $key)) { - $this->error('Theme keys may contain lowercase letters, numbers, dashes, and underscores only.'); - - return self::FAILURE; - } - - $path = rtrim((string) config('stylist.themes_path'), '/\\').DIRECTORY_SEPARATOR.$key; - if ($this->files->exists($path)) { - $this->error("Theme [{$key}] already exists."); - - return self::FAILURE; - } - - $parent = trim((string) $this->option('parent')); - $name = trim((string) ($this->option('name') ?: str($key)->headline())); - $this->files->ensureDirectoryExists($path.'/views'); - - $manifest = [ - 'manifest_version' => 1, - 'name' => $name, - 'slug' => $key, - 'version' => '1.0.0', - 'description' => "Child theme for {$name}.", - 'extends' => $parent !== '' ? $parent : null, - 'entry_view' => 'home', - 'views' => true, - 'variables' => [], - 'settings' => [], - ]; - - if ($manifest['extends'] === null) { - unset($manifest['extends']); - } - - $this->files->put($path.'/theme.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL); - $this->files->put($path.'/views/.gitkeep', ''); - - $this->info("Theme [{$key}] created at {$path}."); - $this->line('Shared views will resolve through its parent; only add genuine overrides.'); - - return self::SUCCESS; - } -} diff --git a/Stylist/Console/FindDuplicateViewsCommand.php b/Stylist/Console/FindDuplicateViewsCommand.php deleted file mode 100644 index 2304ca1..0000000 --- a/Stylist/Console/FindDuplicateViewsCommand.php +++ /dev/null @@ -1,40 +0,0 @@ -inspector->duplicateViews(); - if ($duplicates === []) { - $this->info('No duplicate theme views found.'); - - return self::SUCCESS; - } - - $rows = []; - foreach ($duplicates as $view => $owners) { - foreach ($owners as $owner) { - $rows[] = [$view, $owner['theme'], $owner['path']]; - } - } - - $this->table(['View', 'Theme', 'Path'], $rows); - $this->error(count($duplicates).' duplicate view task(s) found.'); - - return self::FAILURE; - } -} diff --git a/Stylist/Console/InspectThemesCommand.php b/Stylist/Console/InspectThemesCommand.php deleted file mode 100644 index 0764a1f..0000000 --- a/Stylist/Console/InspectThemesCommand.php +++ /dev/null @@ -1,28 +0,0 @@ -inspector->themes(); - $this->table(['Key', 'Name', 'Parent', 'Entry view', 'Views', 'Path'], array_map(fn (array $theme): array => [ - $theme['key'], $theme['name'], $theme['parent'] ?? '—', $theme['entry_view'], $theme['views'], $theme['path'], - ], $themes)); - - return self::SUCCESS; - } -} diff --git a/Stylist/Console/PublishAssetsCommand.php b/Stylist/Console/PublishAssetsCommand.php deleted file mode 100644 index ab661fa..0000000 --- a/Stylist/Console/PublishAssetsCommand.php +++ /dev/null @@ -1,34 +0,0 @@ -option('force'); - - $this->call('vendor:publish', [ - '--tag' => 'stylist-config', - '--force' => $force, - ]); - - $themesPath = (string) config('stylist.themes_path', resource_path('themes')); - - if (! is_dir($themesPath) && ! mkdir($themesPath, 0775, true) && ! is_dir($themesPath)) { - $this->components->error("Unable to create theme directory: {$themesPath}"); - - return self::FAILURE; - } - - $this->components->info("Stylist config published. Themes remain in: {$themesPath}"); - - return self::SUCCESS; - } -} diff --git a/Stylist/Console/ThemeDoctorCommand.php b/Stylist/Console/ThemeDoctorCommand.php deleted file mode 100644 index d59cb3d..0000000 --- a/Stylist/Console/ThemeDoctorCommand.php +++ /dev/null @@ -1,38 +0,0 @@ -inspector->issues(); - if ($issues === []) { - $this->info('Stylist is healthy. No theme issues found.'); - - return self::SUCCESS; - } - - $this->table(['Severity', 'Theme', 'Message'], array_map(fn (array $issue): array => [ - strtoupper($issue['severity']), $issue['theme'], $issue['message'], - ], $issues)); - - $errors = count(array_filter($issues, fn (array $issue): bool => $issue['severity'] === 'error')); - $warnings = count($issues) - $errors; - $this->line("{$errors} error(s), {$warnings} warning(s)."); - - return $errors > 0 ? self::FAILURE : self::SUCCESS; - } -} diff --git a/Stylist/Console/ValidateThemeCommand.php b/Stylist/Console/ValidateThemeCommand.php deleted file mode 100644 index 5e21f93..0000000 --- a/Stylist/Console/ValidateThemeCommand.php +++ /dev/null @@ -1,108 +0,0 @@ -resolveThemePath((string) $this->argument('theme')); - $theme = $this->loader->fromPath($path); - $issues = $this->validator->validate($theme); - - $this->info("Theme: {$theme->name()} ({$theme->key()})"); - $this->line('Manifest schema: '.$theme->manifestVersion()); - - if ($issues === []) { - $this->info('Theme validation passed with no issues.'); - - return self::SUCCESS; - } - - $this->table( - ['Severity', 'File', 'Message'], - array_map(fn (array $issue) => [ - strtoupper($issue['severity']), - $issue['file'], - $issue['message'], - ], $issues), - ); - - $errors = count(array_filter($issues, fn (array $issue) => $issue['severity'] === 'error')); - $warnings = count($issues) - $errors; - $this->line("{$errors} error(s), {$warnings} warning(s)."); - - return $errors > 0 ? self::FAILURE : self::SUCCESS; - } catch (\Throwable $exception) { - $this->error($exception->getMessage()); - - return self::FAILURE; - } finally { - if ($temporary !== null) { - $this->files->deleteDirectory($temporary); - } - } - } - - /** @return array{0:string,1:?string} */ - private function resolveThemePath(string $input): array - { - if ($this->files->isDirectory($input)) { - return [realpath($input) ?: $input, null]; - } - - if ($this->files->isFile($input) && strtolower(pathinfo($input, PATHINFO_EXTENSION)) === 'zip') { - $temporary = storage_path('app/stylist/validate-'.bin2hex(random_bytes(8))); - $this->files->ensureDirectoryExists($temporary); - - $zip = new ZipArchive; - if ($zip->open($input) !== true) { - throw new RuntimeException('The theme ZIP could not be opened.'); - } - - for ($index = 0; $index < $zip->numFiles; $index++) { - $name = (string) $zip->getNameIndex($index); - if (str_contains($name, '..') || str_starts_with($name, '/') || str_contains($name, "\0")) { - $zip->close(); - throw new RuntimeException('The ZIP contains an unsafe path.'); - } - } - - $zip->extractTo($temporary); - $zip->close(); - - $manifest = collect($this->files->allFiles($temporary)) - ->first(fn ($file) => $file->getFilename() === 'theme.json'); - - if (! $manifest) { - throw new RuntimeException('The package does not contain theme.json.'); - } - - return [$manifest->getPath(), $temporary]; - } - - return [$this->loader->find($input)->path(), null]; - } -} diff --git a/Stylist/Database/Migrations/2026_07_08_000000_add_stylist_fields_to_companies_table.php b/Stylist/Database/Migrations/2026_07_08_000000_add_stylist_fields_to_companies_table.php deleted file mode 100644 index 2396070..0000000 --- a/Stylist/Database/Migrations/2026_07_08_000000_add_stylist_fields_to_companies_table.php +++ /dev/null @@ -1,56 +0,0 @@ -string('theme_key')->default('default')->after('timezone'); - } - - if (! Schema::hasColumn('companies', 'brand_primary')) { - $table->string('brand_primary')->nullable()->after('theme_key'); - } - - if (! Schema::hasColumn('companies', 'brand_secondary')) { - $table->string('brand_secondary')->nullable()->after('brand_primary'); - } - - if (! Schema::hasColumn('companies', 'brand_accent')) { - $table->string('brand_accent')->nullable()->after('brand_secondary'); - } - - if (! Schema::hasColumn('companies', 'logo_path')) { - $table->string('logo_path')->nullable()->after('brand_accent'); - } - - if (! Schema::hasColumn('companies', 'theme_settings')) { - $table->json('theme_settings')->nullable()->after('logo_path'); - } - }); - } - - public function down(): void - { - if (! Schema::hasTable('companies')) { - return; - } - - Schema::table('companies', function (Blueprint $table): void { - foreach (['theme_settings', 'logo_path', 'brand_accent', 'brand_secondary', 'brand_primary', 'theme_key'] as $column) { - if (Schema::hasColumn('companies', $column)) { - $table->dropColumn($column); - } - } - }); - } -}; diff --git a/Stylist/Database/Migrations/2026_07_21_000001_create_stylist_theme_settings_table.php b/Stylist/Database/Migrations/2026_07_21_000001_create_stylist_theme_settings_table.php deleted file mode 100644 index 57b6955..0000000 --- a/Stylist/Database/Migrations/2026_07_21_000001_create_stylist_theme_settings_table.php +++ /dev/null @@ -1,24 +0,0 @@ -id(); - $table->string('theme_key')->unique(); - $table->string('variant')->default('light'); - $table->json('settings')->nullable(); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('stylist_theme_settings'); - } -}; diff --git a/Stylist/Diagnostics/ThemeInspector.php b/Stylist/Diagnostics/ThemeInspector.php deleted file mode 100644 index 371d543..0000000 --- a/Stylist/Diagnostics/ThemeInspector.php +++ /dev/null @@ -1,147 +0,0 @@ -> */ - public function themes(): array - { - return $this->loader->all()->map(function (Theme $theme): array { - return [ - 'key' => $theme->key(), - 'name' => $theme->name(), - 'parent' => $theme->parent(), - 'path' => $theme->path(), - 'views' => count($this->viewFiles($theme)), - 'entry_view' => $theme->entryView(), - ]; - })->values()->all(); - } - - /** @return array> */ - public function duplicateViews(): array - { - $ownership = []; - - foreach ($this->loader->all() as $theme) { - foreach ($this->viewFiles($theme) as $relative => $absolute) { - $ownership[$relative][] = [ - 'theme' => $theme->key(), - 'path' => $absolute, - ]; - } - } - - return array_filter($ownership, fn (array $owners): bool => count($owners) > 1); - } - - /** @return array */ - public function issues(): array - { - $issues = []; - $themes = $this->loader->all(); - - foreach ($themes as $theme) { - $parent = $theme->parent(); - - if ($parent !== null && ! $themes->has($parent)) { - $issues[] = [ - 'severity' => 'error', - 'theme' => $theme->key(), - 'message' => "Parent theme [{$parent}] is not installed.", - ]; - } - - if ($this->hasInheritanceCycle($theme, $themes->all())) { - $issues[] = [ - 'severity' => 'error', - 'theme' => $theme->key(), - 'message' => 'Theme inheritance contains a cycle.', - ]; - } - - $entry = $theme->path().'/views/'.str_replace('.', '/', $theme->entryView()).'.blade.php'; - if (! $this->files->exists($entry) && $parent === null) { - $issues[] = [ - 'severity' => 'warning', - 'theme' => $theme->key(), - 'message' => "Entry view [{$theme->entryView()}] does not exist in this theme.", - ]; - } - } - - foreach ($this->duplicateViews() as $view => $owners) { - $issues[] = [ - 'severity' => 'error', - 'theme' => implode(', ', array_column($owners, 'theme')), - 'message' => "View [{$view}] has multiple owners.", - ]; - } - - $default = (string) config('stylist.default_theme'); - if ($default !== '' && ! $themes->has($default)) { - $issues[] = [ - 'severity' => 'warning', - 'theme' => $default, - 'message' => 'Configured default theme is not installed.', - ]; - } - - return $issues; - } - - /** @return array */ - private function viewFiles(Theme $theme): array - { - $root = $theme->path().'/views'; - if (! $this->files->isDirectory($root)) { - return []; - } - - $views = []; - foreach ($this->files->allFiles($root) as $file) { - if (! str_ends_with($file->getFilename(), '.blade.php')) { - continue; - } - - $absolute = str_replace('\\', '/', $file->getPathname()); - $relative = ltrim(str_replace(str_replace('\\', '/', $root), '', $absolute), '/'); - $views[$relative] = $absolute; - } - - ksort($views); - - return $views; - } - - /** @param array $themes */ - private function hasInheritanceCycle(Theme $theme, array $themes): bool - { - $seen = []; - $key = $theme->key(); - - while (isset($themes[$key])) { - if (isset($seen[$key])) { - return true; - } - - $seen[$key] = true; - $key = $themes[$key]->parent() ?? ''; - if ($key === '') { - return false; - } - } - - return false; - } -} diff --git a/Stylist/Facades/StylistFacade.php b/Stylist/Facades/StylistFacade.php deleted file mode 100644 index 77d173e..0000000 --- a/Stylist/Facades/StylistFacade.php +++ /dev/null @@ -1,13 +0,0 @@ -make($view, $data, $mergeData); - } -} - -if (! function_exists('stylist_theme_view')) { - function stylist_theme_view(string $theme, string $view, array $data = [], array $mergeData = []): View - { - return app(ThemeViewResolver::class)->makeForTheme($theme, $view, $data, $mergeData); - } -} - -if (! function_exists('stylist_frontend_view')) { - function stylist_frontend_view(string $view, array $data = [], array $mergeData = []): View - { - return app(ThemeViewResolver::class)->makeForTheme(stylist()->frontend()->key(), $view, $data, $mergeData); - } -} - -if (! function_exists('stylist_backend_view')) { - function stylist_backend_view(string $view, array $data = [], array $mergeData = []): View - { - return app(ThemeViewResolver::class)->makeForTheme(stylist()->backend()->key(), $view, $data, $mergeData); - } -} - -if (! function_exists('theme_asset')) { - function theme_asset(string $path): string - { - return app(UrlGenerator::class)->asset($path); - } -} - -if (! function_exists('theme_switcher')) { - function theme_switcher(): HtmlString - { - return stylist_html()->switcher(); - } -} diff --git a/Stylist/Html/ThemeHtmlBuilder.php b/Stylist/Html/ThemeHtmlBuilder.php deleted file mode 100644 index b7b3c16..0000000 --- a/Stylist/Html/ThemeHtmlBuilder.php +++ /dev/null @@ -1,99 +0,0 @@ -stylist->current()->stylesheet(); - if (! $this->stylist->current()->has($path)) { - return new HtmlString(''); - } - - return new HtmlString(''); - } - - public function script(?string $path = null): HtmlString - { - $path ??= $this->stylist->current()->script(); - if (! $this->stylist->current()->has($path)) { - return new HtmlString(''); - } - - return new HtmlString(''); - } - - public function variables(): HtmlString - { - $theme = $this->stylist->current(); - $default = $this->declarations($this->stylist->variables()); - $rules = [':root{'.$default.'}']; - - foreach ($theme->appearances() as $name => $definition) { - $vars = (array) data_get($definition, 'variables', []); - if ($vars) { - $rules[] = ':root[data-theme="'.$this->selector($name).'"]{'.$this->declarations($vars).'}'; - } - } - foreach ($theme->accents() as $name => $definition) { - $vars = (array) data_get($definition, 'variables', []); - if ($vars) { - $rules[] = ':root[data-accent="'.$this->selector($name).'"]{'.$this->declarations($vars).'}'; - } - } - - if ($theme->appearances() === [] && $theme->accents() === []) { - return new HtmlString(''); - } - - $appearance = json_encode($this->stylist->appearance(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); - $accent = json_encode($this->stylist->accent(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); - $themeKey = json_encode($theme->key(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); - $hasAppearances = $theme->appearances() !== [] ? 'true' : 'false'; - $hasAccents = $theme->accents() !== [] ? 'true' : 'false'; - - $script = <<(function(){var d=document.documentElement,k={$themeKey},ha={$hasAppearances},hc={$hasAccents},a=ha?(localStorage.getItem('stylist:'+k+':appearance')||{$appearance}):'',c=hc?(localStorage.getItem('stylist:'+k+':accent')||{$accent}):'';function apply(){if(ha){var dark=a==='dark'||((a==='auto'||a==='system')&&matchMedia('(prefers-color-scheme: dark)').matches);d.dataset.theme=dark?'dark':'light';d.dataset.appearance=a;d.classList.toggle('dark',dark)}if(hc){d.dataset.accent=c}}apply();window.StylistTheme={setAppearance:function(v){if(!ha)return;a=v;localStorage.setItem('stylist:'+k+':appearance',v);apply()},setAccent:function(v){if(!hc)return;c=v;localStorage.setItem('stylist:'+k+':accent',v);apply()},appearance:function(){return a},accent:function(){return c}};if(ha){matchMedia('(prefers-color-scheme: dark)').addEventListener?.('change',function(){if(a==='auto'||a==='system')apply()})}document.dispatchEvent(new CustomEvent('stylist:ready',{detail:window.StylistTheme}))})(); -JS; - - return new HtmlString(''.$script); - } - - public function switcher(): HtmlString - { - $theme = $this->stylist->current(); - $appearances = array_keys($theme->appearances()); - $accents = array_keys($theme->accents()); - - if ($appearances === [] && $accents === []) { - return new HtmlString(''); - } - - $appearanceOptions = collect($appearances)->map(fn ($name) => '')->implode(''); - $accentOptions = collect($accents)->map(fn ($name) => '')->implode(''); - $appearanceSelect = $appearances ? '' : ''; - $accentSelect = $accents ? '' : ''; - - return new HtmlString('
'.$appearanceSelect.$accentSelect.'
'); - } - - private function declarations(array $variables): string - { - return collect($variables) - ->filter(fn ($value, $key) => is_string($key) && (is_string($value) || is_numeric($value))) - ->map(fn ($value, string $key) => '--'.ltrim($key, '-').': '.e((string) $value).';') - ->implode(''); - } - - private function selector(string $value): string - { - return preg_replace('/[^a-z0-9_-]/i', '', $value) ?: 'default'; - } -} diff --git a/Stylist/Html/ThemeSeoBuilder.php b/Stylist/Html/ThemeSeoBuilder.php deleted file mode 100644 index 28d5840..0000000 --- a/Stylist/Html/ThemeSeoBuilder.php +++ /dev/null @@ -1,44 +0,0 @@ -stylist->current(); - $seo = array_replace($theme->seo(), $overrides); - $settings = $this->stylist->settings(); - $title = (string) ($seo['title'] ?? $settings['site_title'] ?? $theme->name()); - $description = (string) ($seo['description'] ?? $settings['site_description'] ?? $theme->description()); - $image = $seo['image'] ?? $theme->preview(); - if ($image && $theme->has($image)) { - $image = $this->urls->asset($image); - } - $tags = [''.e($title).'']; - if ($description !== '') { - $tags[] = ''; - } - foreach (['og:title' => $title, 'og:description' => $description, 'twitter:card' => 'summary_large_image', 'twitter:title' => $title, 'twitter:description' => $description] as $property => $content) { - if ($content === '') { - continue; - }$attribute = str_starts_with($property, 'og:') ? 'property' : 'name'; - $tags[] = ''; - } - if ($image) { - $tags[] = ''; - $tags[] = ''; - } - if (isset($seo['json_ld']) && is_array($seo['json_ld'])) { - $tags[] = ''; - } - - return new HtmlString(implode("\n", $tags)); - } -} diff --git a/Stylist/Http/Controllers/StylistController.php b/Stylist/Http/Controllers/StylistController.php deleted file mode 100644 index 219359e..0000000 --- a/Stylist/Http/Controllers/StylistController.php +++ /dev/null @@ -1,23 +0,0 @@ - $stylist->current(), 'themes' => $stylist->all(), 'variables' => $stylist->variables(), - 'settingsService' => $settings, 'accessibility' => $accessibility, 'urls' => $urls, - 'activeFrontendTheme' => $siteSettings->get('frontend_theme', config('stylist.default_frontend_theme', 'saasland'), 'site'), - 'activeBackendTheme' => $siteSettings->get('backend_theme', config('stylist.default_backend_theme', 'admin'), 'site'), - ]); - } -} diff --git a/Stylist/Http/Controllers/ThemeAssetController.php b/Stylist/Http/Controllers/ThemeAssetController.php deleted file mode 100644 index e7695d1..0000000 --- a/Stylist/Http/Controllers/ThemeAssetController.php +++ /dev/null @@ -1,64 +0,0 @@ -find($theme); - $themeRoot = realpath($loadedTheme->path()); - abort_unless($themeRoot !== false, 404); - - $asset = realpath($themeRoot.DIRECTORY_SEPARATOR.$normalised); - - abort_unless( - $asset !== false - && str_starts_with($asset, $themeRoot.DIRECTORY_SEPARATOR) - && is_file($asset), - 404 - ); - - $mime = match ($extension) { - 'css' => 'text/css; charset=UTF-8', - 'js', 'mjs' => 'application/javascript; charset=UTF-8', - 'svg' => 'image/svg+xml', - 'json', 'map', 'webmanifest' => 'application/json', - 'woff' => 'font/woff', - 'woff2' => 'font/woff2', - 'ttf' => 'font/ttf', - 'otf' => 'font/otf', - default => mime_content_type($asset) ?: 'application/octet-stream', - }; - - return response()->file($asset, [ - 'Content-Type' => $mime, - 'Cache-Control' => app()->isProduction() - ? 'public, max-age=31536000, immutable' - : 'no-cache, private', - 'X-Content-Type-Options' => 'nosniff', - 'Content-Security-Policy' => "default-src 'none'; style-src 'unsafe-inline'", - ]); - } -} diff --git a/Stylist/Http/Controllers/ThemeManagementController.php b/Stylist/Http/Controllers/ThemeManagementController.php deleted file mode 100644 index 3c4f12f..0000000 --- a/Stylist/Http/Controllers/ThemeManagementController.php +++ /dev/null @@ -1,87 +0,0 @@ -find($theme); - abort_unless(in_array($item->type(), ['frontend', 'backend'], true), 422, 'Only frontend and backend themes can be activated.'); - - $settings->put($item->type().'_theme', $item->key(), 'site', 'string', true); - - return back()->with('status', $item->name().' is now the active '.$item->type().' theme.'); - } - - public function preview(string $theme, Loader $loader, ThemeViewResolver $views) - { - $item = $loader->find($theme); - - return $views->makeForTheme($item->key(), $item->entryView(), [ - 'themePreview' => true, - 'previewTheme' => $item, - ]); - } - - public function settings(Request $request, string $theme, Loader $loader, ThemeSettings $settings) - { - $item = $loader->find($theme); - $settings->save( - $item, - (array) $request->input('settings', []), - (string) $request->input('variant', 'light'), - $request->string('appearance')->toString() ?: null, - $request->string('accent')->toString() ?: null, - ); - - return back()->with('status', 'Theme settings saved.'); - } - - public function install(Request $request, ThemePackageManager $packages) - { - $request->validate(['theme' => ['required', 'file', 'mimes:zip', 'max:51200']]); - $theme = $packages->install($request->file('theme')); - - return back()->with('status', $theme->name().' installed.'); - } - - public function uninstall(string $theme, Loader $loader, ThemePackageManager $packages) - { - $item = $loader->find($theme); - $packages->uninstall($item); - - return back()->with('status', $item->name().' uninstalled.'); - } - - public function export(string $theme, Loader $loader, ThemePackageManager $packages) - { - $item = $loader->find($theme); - $path = $packages->export($item); - - return response()->download($path)->deleteFileAfterSend(); - } - - public function optimize(string $theme, Loader $loader, ThemeOptimizer $optimizer) - { - $item = $loader->find($theme); - $result = $optimizer->optimize($item); - - return back()->with('status', "Optimised {$result['css']} CSS, {$result['js']} JS and {$result['images']} images."); - } - - public function validateTheme(string $theme, Loader $loader, ThemeAccessibilityValidator $validator) - { - return response()->json(['issues' => $validator->validate($loader->find($theme))]); - } -} diff --git a/Stylist/Livewire/ThemePreview.php b/Stylist/Livewire/ThemePreview.php deleted file mode 100644 index 5e8560d..0000000 --- a/Stylist/Livewire/ThemePreview.php +++ /dev/null @@ -1,21 +0,0 @@ - 12, - 'warehouses' => 34, - 'users' => 156, - ]; - - public function render() - { - return stylist_view('livewire.theme-preview', [ - 'stats' => $this->stats, - ]); - } -} diff --git a/Stylist/Models/ThemeSetting.php b/Stylist/Models/ThemeSetting.php deleted file mode 100644 index fb01e6f..0000000 --- a/Stylist/Models/ThemeSetting.php +++ /dev/null @@ -1,14 +0,0 @@ - 'array']; -} diff --git a/Stylist/Providers/RouteServiceProvider.php b/Stylist/Providers/RouteServiceProvider.php deleted file mode 100644 index 68adb78..0000000 --- a/Stylist/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,22 +0,0 @@ -group(module_path('Stylist', 'Routes/assets.php')); - - Route::middleware('web') - ->prefix('stylist') - ->name('stylist.') - ->group(module_path('Stylist', 'Routes/web.php')); - } -} diff --git a/Stylist/Providers/StylistServiceProvider.php b/Stylist/Providers/StylistServiceProvider.php deleted file mode 100644 index 31ee054..0000000 --- a/Stylist/Providers/StylistServiceProvider.php +++ /dev/null @@ -1,215 +0,0 @@ -app->register(RouteServiceProvider::class); - - $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()); - - $this->app->singleton('stylist', fn ($app) => $app->make(Stylist::class)); - $this->app->singleton('stylist.html', fn ($app) => $app->make(ThemeHtmlBuilder::class)); - $this->app->singleton('stylist.seo', fn ($app) => $app->make(ThemeSeoBuilder::class)); - } - - public function boot(): void - { - $this->registerViews(); - $this->registerThemeViews(); - $this->registerTranslations(); - $this->registerPublishing(); - $this->registerHelpers(); - $this->registerBlade(); - - if ($this->app->runningInConsole()) { - $this->commands([ - PublishAssetsCommand::class, - ValidateThemeCommand::class, - InspectThemesCommand::class, - FindDuplicateViewsCommand::class, - ThemeDoctorCommand::class, - CreateThemeCommand::class, - ]); - } - - $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); - - View::composer('*', function ($view): void { - if (! $this->app->bound(Stylist::class)) { - return; - } - - $view->with('activeTheme', $this->app->make(Stylist::class)->current()); - }); - - - View::composer('stylist-theme-saasland::*', function ($view): void { - $theme = $this->app->make(Loader::class)->find('saasland'); - $settings = $this->app->make(ThemeSettings::class)->get($theme); - $siteSettings = $this->app->make(SettingsRepository::class)->site(); - $navigation = $this->app->make(FrontendNavigation::class); - $homepage = $this->app->make(FrontendContent::class)->homepage($settings); - - $view->with([ - 'siteSettings' => $siteSettings, - 'siteTitle' => $siteSettings['site_name'], - 'siteDescription' => $siteSettings['site_description'], - 'logoUrl' => $siteSettings['logo_url'] ?: null, - 'facebookUrl' => $siteSettings['facebook_url'] ?: null, - 'xUrl' => $siteSettings['x_url'] ?: null, - 'linkedinUrl' => $siteSettings['linkedin_url'] ?: null, - 'primaryNavigation' => $navigation->primary(), - 'footerNavigation' => $navigation->footer(), - 'themeSettings' => $settings, - ...$homepage, - ]); - }); - } - - private function registerViews(): void - { - $sourcePath = module_path(ModuleInfo::name(), 'Resources/views'); - $viewPath = resource_path('views/modules/'.ModuleInfo::nameLower()); - - $this->publishes([$sourcePath => $viewPath], ['views', 'stylist-module-views']); - $this->loadViewsFrom([$viewPath, $sourcePath], ModuleInfo::nameLower()); - } - - private function registerThemeViews(): void - { - foreach ($this->themeRoots() as $root) { - if (! is_dir($root)) { - logger()->warning('Stylist theme root does not exist', [ - 'root' => $root, - ]); - - continue; - } - - $themeDirectories = glob($root.'/*', GLOB_ONLYDIR) ?: []; - - foreach ($themeDirectories as $themeDirectory) { - $themeViewsPath = $themeDirectory.'/views'; - - if (! is_dir($themeViewsPath)) { - continue; - } - - $themeKey = strtolower(basename($themeDirectory)); - - // Themes always live outside the module, normally in resources/themes. - // Register the canonical namespace and the legacy namespace as an alias - // to the same application-owned directory. The alias keeps existing - // routes and Blade templates working without reintroducing module themes. - $namespaces = [ - "stylist-theme-{$themeKey}", - "stylist-module-theme-{$themeKey}", - ]; - - $componentsPath = $themeViewsPath.'/components'; - - // The common theme is the application-wide owner of layouts, - // authentication, settings and shared components. Livewire 4 - // resolves its default full-page layout through layouts::app, - // so point that compatibility namespace at the common theme. - if ($themeKey === 'common') { - $layoutsPath = $componentsPath.'/layouts'; - - if (is_dir($layoutsPath)) { - View::addNamespace('layouts', $layoutsPath); - Blade::anonymousComponentPath($layoutsPath, 'layouts'); - } - - View::addNamespace('common', $themeViewsPath); - - if (is_dir($componentsPath)) { - Blade::anonymousComponentPath($componentsPath, 'common'); - } - } - - foreach ($namespaces as $namespace) { - View::addNamespace($namespace, $themeViewsPath); - - if (is_dir($componentsPath)) { - Blade::anonymousComponentPath($componentsPath, $namespace); - } - } - } - } - } - - private function normalisePath(string $path): string - { - return rtrim( - str_replace('\\', '/', realpath($path) ?: $path), - '/' - ); - } - - /** @return array */ - private function themeRoots(): array - { - return array_values(array_unique(array_filter([ - (string) config('stylist.themes_path'), - ]))); - } - - private function registerTranslations(): void - { - $langPath = resource_path('lang/modules/'.ModuleInfo::nameLower()); - $sourcePath = module_path(ModuleInfo::name(), 'Resources/lang'); - - $this->loadTranslationsFrom(is_dir($langPath) ? $langPath : $sourcePath, ModuleInfo::nameLower()); - } - - private function registerPublishing(): void - { - $this->publishes([ - module_path(ModuleInfo::name(), 'Config/config.php') => config_path('stylist.php'), - ], ['config', 'stylist-config']); - - } - - private function registerHelpers(): void - { - foreach (glob(module_path(ModuleInfo::name(), 'Helpers/*.php')) ?: [] as $filename) { - require_once $filename; - } - } - - private function registerBlade(): void - { - Blade::directive('themeVariables', fn () => 'variables(); ?>'); - Blade::directive('themeStylesheet', fn ($expression) => 'stylesheet('.($expression ?: 'null').'); ?>'); - Blade::directive('themeScript', fn ($expression) => 'script('.($expression ?: 'null').'); ?>'); - Blade::directive('themeSwitcher', fn () => 'switcher(); ?>'); - Blade::directive('themeSeo', fn ($expression) => 'render('.($expression ?: '[]').'); ?>'); - } -} diff --git a/Stylist/README.md b/Stylist/README.md deleted file mode 100644 index 810bf53..0000000 --- a/Stylist/README.md +++ /dev/null @@ -1,183 +0,0 @@ -# Stylist Module - -Tenant-aware styling module for Laravel 13 / nwidart Laravel Modules. - -## Theme layout - -Each theme is self-contained: - -```text -resources/themes/{theme}/ -├── theme.json -├── css/theme.css -└── views/ - ├── layouts/app.blade.php - ├── components/page-hero.blade.php - └── livewire/theme-preview.blade.php -``` - -The active tenant/company resolves to a theme key such as `default`, `mpba`, or `abbott`. - -## Rendering themed views - -Use the helper from controllers, Blade components, and Livewire components: - -```php -return stylist_view('layouts.app'); -``` - -It checks views in this order: - -```text -stylist-theme-{active-theme}::{view} -stylist-theme-{default-theme}::{view} -stylist::{view} -``` - -## Livewire - -Keep the PHP component class shared and put the template in each theme: - -```text -Livewire/ThemePreview.php -resources/themes/default/views/livewire/theme-preview.blade.php -resources/themes/mpba/views/livewire/theme-preview.blade.php -``` - -The component renders with: - -```php -return stylist_view('livewire.theme-preview', ['stats' => $this->stats]); -``` - -## Tenant assignment - -Add or use these fields on `companies`: - -```text -theme_key -brand_primary -brand_secondary -brand_accent -logo_path -favicon_path -theme_settings -``` - -Then assign: - -```php -$company->update(['theme_key' => 'mpba']); -``` - -## Publishing - -```bash -php artisan vendor:publish --tag=stylist-config -# or -php artisan stylist:publish-assets - -Themes are not published from or stored inside the module. Install them directly under `resources/themes`. -``` - -## Blade helpers - -```blade - - @themeVariables - @themeStylesheet - -``` - -## Self-contained application themes - -Application themes live in `resources/themes/{theme}` and own their runtime assets: - -```text -resources/themes/example/ -├── theme.json -├── css/theme.css -├── js/app.js -└── views/home.blade.php -``` - -Use `@themeStylesheet('css/theme.css')`, `@themeScript('js/app.js')`, or `theme_asset('images/example.svg')`. Assets are served from the selected theme directory by the `stylist.theme.asset` route; they do not need to be copied into `public/themes` or added to the global Vite inputs. - - -## Appearance and colour variants - -Themes may declare `appearances` (`light`, `dark`, `auto`) and named `accents` in `theme.json`. Include `@themeVariables` in the document `` before the theme stylesheet to prevent a flash of the wrong appearance. Add `@themeSwitcher` anywhere in a Blade layout for a ready-made runtime selector. User choices are stored in localStorage and are applied without rebuilding assets or reloading the page. - -## Theme manifest compatibility - -Stylist accepts versioned manifests while remaining compatible with older themes. New manifests should declare: - -```json -{ - "manifest_version": 1, - "key": "design-gallery", - "name": "Design Gallery" -} -``` - -`manifest_version` defaults to `1` when omitted. Unknown manifest fields are preserved and ignored by Stylist unless the current version explicitly uses them, allowing Theme Builder to add future metadata without preventing theme discovery. - -Appearance and accent features are optional. Stylist derives the internal capability flags only when the corresponding definitions exist: - -```json -{ - "appearances": ["light", "dark", "system"], - "accents": ["blue", "neutral"], - "default_appearance": "system", - "default_accent": "neutral" -} -``` - -Legacy themes with none of these fields continue to load unchanged and do not receive appearance or accent controls. - -Additional boolean capabilities may be declared explicitly: - -```json -{ - "capabilities": { - "marketplace": true - } -} -``` - -## Validate a theme - -Validate an installed theme key, unpacked theme directory, or ZIP package: - -```bash -php artisan stylist:validate design-gallery -php artisan stylist:validate resources/themes/design-gallery -php artisan stylist:validate ~/Downloads/design-gallery.zip -``` - -The command checks the manifest, required entry view, stylesheet, script, declared assets, preview and accessibility issues. It exits with a failure status when errors are found. - -## Theme inheritance and diagnostics - -Shared application views belong to the `common` theme. Child themes declare inheritance without copying those views: - -```json -{ - "name": "Dashboard", - "slug": "admin", - "extends": "common" -} -``` - -Resolution order is the active theme, each declared parent, the configured fallback, the common theme, module defaults, then Laravel's normal views. - -Use these maintenance commands: - -```bash -php artisan stylist:inspect -php artisan stylist:duplicates -php artisan stylist:doctor -php artisan stylist:create-theme customer --parent=common --name="Customer Portal" -``` - -`stylist:duplicates` fails when the same relative Blade task path exists in more than one theme. Add a child view only when it is a deliberate replacement; this application enforces one authoritative view per task. diff --git a/Stylist/Resources/assets/css/app.css b/Stylist/Resources/assets/css/app.css deleted file mode 100644 index 32b4fbc..0000000 --- a/Stylist/Resources/assets/css/app.css +++ /dev/null @@ -1,5 +0,0 @@ -[data-theme] { - --brand-primary: var(--brand-primary, #2563eb); - --brand-secondary: var(--brand-secondary, #0f172a); - --brand-accent: var(--brand-accent, #22c55e); -} diff --git a/Stylist/Resources/assets/js/app.js b/Stylist/Resources/assets/js/app.js deleted file mode 100644 index f65cc9b..0000000 --- a/Stylist/Resources/assets/js/app.js +++ /dev/null @@ -1 +0,0 @@ -import '../css/app.css'; diff --git a/Stylist/Resources/lang/en/messages.php b/Stylist/Resources/lang/en/messages.php deleted file mode 100644 index 56494b1..0000000 --- a/Stylist/Resources/lang/en/messages.php +++ /dev/null @@ -1,5 +0,0 @@ - 'Stylist', -]; diff --git a/Stylist/Resources/views/index.blade.php b/Stylist/Resources/views/index.blade.php deleted file mode 100644 index 19ffd23..0000000 --- a/Stylist/Resources/views/index.blade.php +++ /dev/null @@ -1,78 +0,0 @@ - - - - @if (session('status')) -
{{ session('status') }}
- @endif - @if ($errors->any()) -
{{ $errors->first() }}
- @endif - -
- @csrf - - -
- -
- @foreach ($themes as $item) - @php($saved = $settingsService->get($item)) - @php($variant = $settingsService->variant($item)) - @php($issues = $accessibility->validate($item)) - @php($appearance = $settingsService->appearance($item)) - @php($accent = $settingsService->accent($item)) -
-
- @if ($item->preview() && $item->has($item->preview())) - {{ $item->name() }} theme preview - @else -
Add preview.webp to this theme
- @endif -
-
-

{{ $item->name() }}

v{{ $item->version() }}@if($item->author()) · {{ $item->author() }}@endif

@if(($item->type() === 'frontend' && $item->key() === $activeFrontendTheme) || ($item->type() === 'backend' && $item->key() === $activeBackendTheme))Active {{ ucfirst($item->type()) }}@endif
- @if($item->description())

{{ $item->description() }}

@endif - @if($item->parent())

Child of {{ $item->parent() }}

@endif -
- @if(in_array($item->type(), ['frontend', 'backend'], true)) -
@csrf
- Preview - @endif - Export ZIP -
@csrf
- @if(str_starts_with(realpath($item->path()) ?: '', realpath(config('stylist.themes_path')) ?: '/__never__')) -
@csrf @method('DELETE')
- @endif -
-
- Theme settings -
@csrf @method('PUT') - @if($item->variants()) - - @endif - @if($item->supports('dark_mode') || $item->appearances()) - - @endif - @if($item->accents()) -
Accent colour
@foreach($item->accents() as $name => $definition)@endforeach
- @endif - @foreach($item->settingsSchema() as $key => $definition) - @php($definition = is_array($definition) ? $definition : ['default' => $definition]) - - @endforeach - -
-
-
Accessibility: {{ count($issues) ? count($issues).' issue(s)' : 'passed static checks' }}
-
-
- @endforeach -
-
diff --git a/Stylist/Routes/assets.php b/Stylist/Routes/assets.php deleted file mode 100644 index 9662db6..0000000 --- a/Stylist/Routes/assets.php +++ /dev/null @@ -1,16 +0,0 @@ -where('theme', '[A-Za-z0-9_-]+') - ->where('path', '.*') - ->name('stylist.theme.asset'); diff --git a/Stylist/Routes/web.php b/Stylist/Routes/web.php deleted file mode 100644 index c39ec85..0000000 --- a/Stylist/Routes/web.php +++ /dev/null @@ -1,17 +0,0 @@ -group(function (): void { - Route::get('/', StylistController::class)->name('index'); - Route::post('/install', [ThemeManagementController::class, 'install'])->name('install'); - Route::put('/{theme}/settings', [ThemeManagementController::class, 'settings'])->name('settings'); - Route::post('/{theme}/activate', [ThemeManagementController::class, 'activate'])->name('activate'); - Route::get('/{theme}/preview', [ThemeManagementController::class, 'preview'])->name('preview'); - Route::delete('/{theme}', [ThemeManagementController::class, 'uninstall'])->name('uninstall'); - Route::get('/{theme}/export', [ThemeManagementController::class, 'export'])->name('export'); - Route::post('/{theme}/optimize', [ThemeManagementController::class, 'optimize'])->name('optimize'); - Route::get('/{theme}/accessibility', [ThemeManagementController::class, 'validateTheme'])->name('accessibility'); -}); diff --git a/Stylist/Services/FrontendContent.php b/Stylist/Services/FrontendContent.php deleted file mode 100644 index e57aac5..0000000 --- a/Stylist/Services/FrontendContent.php +++ /dev/null @@ -1,102 +0,0 @@ - */ - public function homepage(array $settings): array - { - // Always provide source collections. Visibility is owned by page placements - // in the composable content system; the legacy theme still applies its - // existing show_* settings while no composition has been installed. - return [ - 'pricingPlans' => $this->pricingPlans(), - 'testimonials' => $this->testimonials(), - 'faqs' => $this->faqs(), - 'teamMembers' => $this->team(), - 'latestBlogPosts' => $this->blogPosts(), - 'latestNewsArticles' => $this->newsArticles(), - ]; - } - - private function pricingPlans(): Collection - { - return $this->query('products', fn () => Product::query() - ->where('is_active', true) - ->where('product_type', 'plan') - ->orderBy('sort_order') - ->orderBy('id') - ->limit(6) - ->get()); - } - - private function testimonials(): Collection - { - return $this->query('testimonials', fn () => Testimonial::query() - ->where('enabled', true) - ->latest('id') - ->limit(6) - ->get()); - } - - private function faqs(): Collection - { - return $this->query('faqs', fn () => Faq::query() - ->where('enabled', true) - ->orderBy('sort_order') - ->limit(12) - ->get()); - } - - private function team(): Collection - { - return $this->query('team_members', fn () => TeamMember::query() - ->where('enabled', true) - ->orderBy('sort_order') - ->limit(8) - ->get()); - } - - private function blogPosts(): Collection - { - return $this->query('blog_posts', fn () => Blog::query() - ->published() - ->latest('published_at') - ->limit(3) - ->get()); - } - - private function newsArticles(): Collection - { - return $this->query('news_articles', fn () => News::query() - ->published() - ->latest('published_at') - ->limit(3) - ->get()); - } - - private function query(string $table, callable $callback): Collection - { - try { - return Schema::hasTable($table) ? $callback() : collect(); - } catch (Throwable) { - return collect(); - } - } - - private function enabled(array $settings, string $key, bool $default): bool - { - return filter_var($settings[$key] ?? $default, FILTER_VALIDATE_BOOL); - } -} diff --git a/Stylist/Services/TenantThemeResolver.php b/Stylist/Services/TenantThemeResolver.php deleted file mode 100644 index 58d4920..0000000 --- a/Stylist/Services/TenantThemeResolver.php +++ /dev/null @@ -1,65 +0,0 @@ -context(); - $column = config('stylist.tenant_theme_column', 'theme_key'); - $user = Auth::user(); - $company = $user?->company ?? null; - - if ($company && filled($company->{$column} ?? null)) { - return (string) $company->{$column}; - } - - if (function_exists('tenant')) { - $tenant = tenant(); - if ($tenant && filled($tenant->{$column} ?? null)) { - return (string) $tenant->{$column}; - } - } - - $settingKey = $context === 'frontend' ? 'frontend_theme' : 'backend_theme'; - $default = $context === 'frontend' - ? config('stylist.default_frontend_theme', 'saasland') - : config('stylist.default_backend_theme', config('stylist.default_theme', 'admin')); - - return (string) $this->settings->get($settingKey, $default, 'site'); - } - - public function context(): string - { - if (app()->runningInConsole()) { - return 'backend'; - } - - $request = request(); - - return $request->routeIs('home', 'blog.public.*', 'news.public.*', 'pages.public.*') - || ! $request->user() - ? 'frontend' - : 'backend'; - } - - public function variables(): array - { - $variables = config('stylist.fallback_variables', []); - $company = Auth::user()?->company ?? null; - - foreach (config('stylist.tenant_brand_columns', []) as $column => $variable) { - if ($company && filled($company->{$column} ?? null)) { - $variables[$variable] = (string) $company->{$column}; - } - } - - return $variables; - } -} diff --git a/Stylist/Services/ThemeAccessibilityValidator.php b/Stylist/Services/ThemeAccessibilityValidator.php deleted file mode 100644 index 70b68bb..0000000 --- a/Stylist/Services/ThemeAccessibilityValidator.php +++ /dev/null @@ -1,32 +0,0 @@ - */ - public function validate(Theme $theme): array - { - $issues = []; - foreach ($this->files->allFiles($theme->path().'/views') as $file) { - $content = $file->getContents(); - $relative = $file->getRelativePathname(); - if (preg_match_all('/]*\balt=)[^>]*>/i', $content, $matches)) { - $issues[] = ['severity' => 'error', 'message' => count($matches[0]).' image(s) missing alt text.', 'file' => $relative]; - } - if (preg_match('/]*\blang=)/i', $content)) { - $issues[] = ['severity' => 'warning', 'message' => 'HTML element is missing a lang attribute.', 'file' => $relative]; - } - if (preg_match_all('/]*>\s*(?:<[^>]+>\s*)*<\/button>/is', $content, $matches)) { - $issues[] = ['severity' => 'warning', 'message' => count($matches[0]).' button(s) may lack an accessible name.', 'file' => $relative]; - } - } - - return $issues; - } -} diff --git a/Stylist/Services/ThemeOptimizer.php b/Stylist/Services/ThemeOptimizer.php deleted file mode 100644 index def6927..0000000 --- a/Stylist/Services/ThemeOptimizer.php +++ /dev/null @@ -1,57 +0,0 @@ - 0, 'js' => 0, 'images' => 0]; - foreach (['css' => 'css', 'js' => 'js'] as $type => $folder) { - $directory = $theme->path().'/'.$folder; - if (! $this->files->isDirectory($directory)) { - continue; - } - foreach ($this->files->files($directory) as $file) { - if ($file->getExtension() !== $type || str_ends_with($file->getFilename(), '.min.'.$type)) { - continue; - } - $content = $file->getContents(); - $content = preg_replace('!/\*.*?\*/!s', '', $content) ?? $content; - $content = preg_replace('/\s+/', ' ', $content) ?? $content; - $content = preg_replace('/\s*([{}:;,>])\s*/', '$1', $content) ?? $content; - $target = $file->getPath().'/'.$file->getBasename('.'.$type).'.min.'.$type; - $this->files->put($target, trim($content)); - $result[$type]++; - } - } - if (function_exists('imagewebp')) { - foreach (['images', 'icons'] as $folder) { - $directory = $theme->path().'/'.$folder; - if (! $this->files->isDirectory($directory)) { - continue; - } - foreach ($this->files->files($directory) as $file) { - $ext = strtolower($file->getExtension()); - if (! in_array($ext, ['jpg', 'jpeg', 'png'], true)) { - continue; - } - $image = $ext === 'png' ? @imagecreatefrompng($file->getPathname()) : @imagecreatefromjpeg($file->getPathname()); - if ($image) { - imagepalettetotruecolor($image); - imagewebp($image, $file->getPath().'/'.$file->getBasename('.'.$ext).'.webp', 82); - imagedestroy($image); - $result['images']++; - } - } - } - } - - return $result; - } -} diff --git a/Stylist/Services/ThemePackageManager.php b/Stylist/Services/ThemePackageManager.php deleted file mode 100644 index 56d1a4a..0000000 --- a/Stylist/Services/ThemePackageManager.php +++ /dev/null @@ -1,98 +0,0 @@ -getClientOriginalExtension()) !== 'zip') { - throw new RuntimeException('Only ZIP theme packages may be installed.'); - } - $temporary = storage_path('app/stylist/install-'.bin2hex(random_bytes(8))); - $this->files->ensureDirectoryExists($temporary); - $zip = new ZipArchive; - if ($zip->open($upload->getRealPath()) !== true) { - throw new RuntimeException('The uploaded ZIP could not be opened.'); - } - for ($i = 0; $i < $zip->numFiles; $i++) { - $name = $zip->getNameIndex($i); - if (str_contains($name, '..') || str_starts_with($name, '/') || str_contains($name, "\0")) { - $zip->close(); - throw new RuntimeException('The ZIP contains an unsafe path.'); - } - } - $zip->extractTo($temporary); - $zip->close(); - $manifest = collect($this->files->allFiles($temporary))->first(fn ($file) => $file->getFilename() === 'theme.json'); - if (! $manifest) { - $this->files->deleteDirectory($temporary); - throw new RuntimeException('The package does not contain theme.json.'); - } - $source = $manifest->getPath(); - $theme = $this->loader->fromPath($source); - if ($theme->parent() && ! $this->loader->all()->has($theme->parent())) { - throw new RuntimeException('Parent theme '.$theme->parent().' is not installed.'); - } - $issues = $this->validator->validate($theme); - if (collect($issues)->contains('severity', 'error')) { - throw new RuntimeException('Accessibility validation failed: '.collect($issues)->where('severity', 'error')->pluck('message')->implode(' ')); - } - $target = rtrim((string) config('stylist.themes_path'), '/').'/'.$theme->key(); - if ($this->files->isDirectory($target)) { - $this->files->deleteDirectory($target); - } - $this->files->copyDirectory($source, $target); - $this->files->deleteDirectory($temporary); - $this->loader->flush(); - - return $this->loader->find($theme->key()); - } - - public function uninstall(Theme $theme): void - { - $active = $theme->type() === 'frontend' - ? $this->settings->get('frontend_theme', config('stylist.default_frontend_theme'), 'site') - : $this->settings->get('backend_theme', config('stylist.default_backend_theme'), 'site'); - if ($active === $theme->key()) { - throw new RuntimeException('The active theme cannot be uninstalled. Activate another theme first.'); - } - $projectRoot = realpath((string) config('stylist.themes_path')); - $themePath = realpath($theme->path()); - if (! $projectRoot || ! $themePath || ! str_starts_with($themePath, $projectRoot.DIRECTORY_SEPARATOR)) { - throw new RuntimeException('Bundled themes cannot be uninstalled.'); - } - if (collect($this->loader->all())->contains(fn (Theme $candidate) => $candidate->parent() === $theme->key())) { - throw new RuntimeException('This theme has installed child themes.'); - } - $this->files->deleteDirectory($themePath); - $this->loader->flush(); - } - - public function export(Theme $theme): string - { - $directory = storage_path('app/stylist/exports'); - $this->files->ensureDirectoryExists($directory); - $path = $directory.'/'.$theme->key().'-'.$theme->version().'.zip'; - $zip = new ZipArchive; - if ($zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { - throw new RuntimeException('Unable to create theme archive.'); - } - foreach ($this->files->allFiles($theme->path()) as $file) { - $zip->addFile($file->getPathname(), $theme->key().'/'.$file->getRelativePathname()); - } - $zip->close(); - - return $path; - } -} diff --git a/Stylist/Services/ThemeSettings.php b/Stylist/Services/ThemeSettings.php deleted file mode 100644 index 0a1f253..0000000 --- a/Stylist/Services/ThemeSettings.php +++ /dev/null @@ -1,97 +0,0 @@ -stored($theme); - $defaults = collect($theme->settingsSchema()) - ->mapWithKeys(fn ($definition, $key) => [$key => is_array($definition) ? ($definition['default'] ?? null) : $definition]) - ->all(); - - return array_replace($defaults, array_diff_key((array) ($stored?->settings ?? []), array_flip([self::APPEARANCE_KEY, self::ACCENT_KEY]))); - } - - public function variant(Theme $theme): string - { - $variant = (string) ($this->stored($theme)?->variant ?? array_key_first($theme->variants()) ?? 'light'); - - return array_key_exists($variant, $theme->variants()) ? $variant : (array_key_first($theme->variants()) ?? 'light'); - } - - public function appearance(Theme $theme): string - { - $stored = (array) ($this->stored($theme)?->settings ?? []); - $allowed = array_keys($theme->appearances()); - if ($allowed === []) { - return ''; - } - $appearance = (string) ($stored[self::APPEARANCE_KEY] ?? $theme->defaultAppearance()); - - return in_array($appearance, $allowed, true) ? $appearance : (string) array_key_first($theme->appearances()); - } - - public function accent(Theme $theme): string - { - $stored = (array) ($this->stored($theme)?->settings ?? []); - $allowed = array_keys($theme->accents()); - if ($allowed === []) { - return ''; - } - $accent = (string) ($stored[self::ACCENT_KEY] ?? $theme->defaultAccent()); - - return in_array($accent, $allowed, true) ? $accent : (string) array_key_first($theme->accents()); - } - - public function save(Theme $theme, array $settings, string $variant, ?string $appearance = null, ?string $accent = null): ThemeSetting - { - $settings = array_intersect_key($settings, array_flip(array_keys($theme->settingsSchema()))); - $existing = (array) ($this->stored($theme)?->settings ?? []); - - $allowedAppearances = array_keys($theme->appearances()); - if ($allowedAppearances !== []) { - $appearance ??= (string) ($existing[self::APPEARANCE_KEY] ?? $theme->defaultAppearance()); - if (! in_array($appearance, $allowedAppearances, true)) { - $appearance = (string) array_key_first($theme->appearances()); - } - $settings[self::APPEARANCE_KEY] = $appearance; - } - - $allowedAccents = array_keys($theme->accents()); - if ($allowedAccents !== []) { - $accent ??= (string) ($existing[self::ACCENT_KEY] ?? $theme->defaultAccent()); - if (! in_array($accent, $allowedAccents, true)) { - $accent = (string) array_key_first($theme->accents()); - } - $settings[self::ACCENT_KEY] = $accent; - } - - if ($theme->variants() && ! array_key_exists($variant, $theme->variants())) { - $variant = array_key_first($theme->variants()); - } - - return ThemeSetting::query()->updateOrCreate( - ['theme_key' => $theme->key()], - ['settings' => $settings, 'variant' => $variant] - ); - } - - private function stored(Theme $theme): ?ThemeSetting - { - try { - return ThemeSetting::query()->where('theme_key', $theme->key())->first(); - } catch (QueryException) { - return null; - } - } -} diff --git a/Stylist/Services/ThemeValidator.php b/Stylist/Services/ThemeValidator.php deleted file mode 100644 index 472397c..0000000 --- a/Stylist/Services/ThemeValidator.php +++ /dev/null @@ -1,74 +0,0 @@ - */ - public function validatePath(string $path): array - { - $theme = $this->loader->fromPath($path); - - return $this->validate($theme); - } - - /** @return array */ - public function validate(Theme $theme): array - { - $issues = []; - $entryView = 'views/'.str_replace('.', '/', $theme->entryView()).'.blade.php'; - - if (! $theme->has($entryView)) { - $issues[] = [ - 'severity' => 'error', - 'message' => "Entry view [{$theme->entryView()}] was not found.", - 'file' => $entryView, - ]; - } - - foreach (['stylesheet' => $theme->stylesheet(), 'script' => $theme->script()] as $label => $relativePath) { - if ($relativePath !== '' && ! $theme->has($relativePath)) { - $issues[] = [ - 'severity' => 'error', - 'message' => ucfirst($label)." file [{$relativePath}] was not found.", - 'file' => $relativePath, - ]; - } - } - - $preview = $theme->preview(); - if ($preview !== null && ! $theme->has($preview)) { - $issues[] = [ - 'severity' => 'warning', - 'message' => "Preview file [{$preview}] was not found.", - 'file' => $preview, - ]; - } - - foreach ((array) ($theme->meta()['assets'] ?? []) as $asset) { - if (! is_string($asset) || $asset === '') { - continue; - } - - if (! $theme->has($asset)) { - $issues[] = [ - 'severity' => 'warning', - 'message' => "Declared asset [{$asset}] was not found.", - 'file' => $asset, - ]; - } - } - - return array_merge($issues, $this->accessibility->validate($theme)); - } -} diff --git a/Stylist/Support/FrontendNavigation.php b/Stylist/Support/FrontendNavigation.php deleted file mode 100644 index 711f089..0000000 --- a/Stylist/Support/FrontendNavigation.php +++ /dev/null @@ -1,76 +0,0 @@ -> */ - public function primary(): array - { - return $this->location('primary', $this->fallbackPrimary()); - } - - /** @return array> */ - public function footer(): array - { - return $this->location('footer', $this->fallbackPrimary()); - } - - /** @param array> $fallback - * @return array> - */ - public function location(string $location, array $fallback = []): array - { - try { - if (! Schema::hasTable('menus')) { - return $fallback; - } - - $menu = Menu::query()->where('enabled', true)->where('location', $location)->first(); - if (! $menu || ! is_array($menu->items)) { - return $fallback; - } - - $items = $this->normaliseItems($menu->items); - - return $items === [] ? $fallback : $items; - } catch (Throwable) { - return $fallback; - } - } - - /** @param array $items - * @return array> - */ - private function normaliseItems(array $items): array - { - return collect($items) - ->filter(fn (mixed $item): bool => is_array($item) && filter_var($item['enabled'] ?? true, FILTER_VALIDATE_BOOL)) - ->map(function (array $item): ?array { - $label = trim((string) ($item['label'] ?? '')); - $url = trim((string) ($item['url'] ?? '')); - if ($label === '' || $url === '') return null; - - return [ - 'label' => $label, - 'url' => $url, - 'target' => ($item['target'] ?? '') === '_blank' ? '_blank' : null, - 'children' => $this->normaliseItems((array) ($item['children'] ?? [])), - ]; - })->filter()->values()->all(); - } - - /** @return array> */ - private function fallbackPrimary(): array - { - $items = [['label' => 'Home', 'url' => url('/'), 'target' => null, 'children' => []]]; - if (Route::has('blog.public.index')) $items[] = ['label' => 'Blog', 'url' => route('blog.public.index'), 'target' => null, 'children' => []]; - if (Route::has('news.public.index')) $items[] = ['label' => 'News', 'url' => route('news.public.index'), 'target' => null, 'children' => []]; - return $items; - } -} diff --git a/Stylist/Support/ModuleInfo.php b/Stylist/Support/ModuleInfo.php deleted file mode 100644 index 6c2f429..0000000 --- a/Stylist/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -assertTrue(view()->exists($view), "Expected common view [{$view}] to exist."); - } - } - - public function test_authentication_and_settings_views_are_not_duplicated(): void - { - $this->assertDirectoryDoesNotExist(resource_path('views')); - $this->assertDirectoryDoesNotExist(resource_path('themes/admin/views/livewire')); - $this->assertDirectoryDoesNotExist(resource_path('themes/welcome/views/livewire')); - - $commonViews = resource_path('themes/common/views'); - - foreach ([ - 'livewire/auth/login.blade.php', - 'livewire/auth/register.blade.php', - 'livewire/auth/forgot-password.blade.php', - 'livewire/auth/reset-password.blade.php', - 'livewire/settings/profile.blade.php', - 'livewire/settings/appearance.blade.php', - 'livewire/settings/security.blade.php', - ] as $relativePath) { - $matches = collect(File::allFiles(resource_path('themes'))) - ->filter(fn ($file) => str_ends_with( - str_replace('\\', '/', $file->getPathname()), - '/views/'.$relativePath - )); - - $this->assertCount(1, $matches, "Expected one authoritative view for [{$relativePath}]."); - $this->assertStringStartsWith( - str_replace('\\', '/', $commonViews), - str_replace('\\', '/', $matches->first()->getPathname()) - ); - } - } -} diff --git a/Stylist/Tests/Feature/LegacyThemeNamespaceAliasTest.php b/Stylist/Tests/Feature/LegacyThemeNamespaceAliasTest.php deleted file mode 100644 index 2d0fd45..0000000 --- a/Stylist/Tests/Feature/LegacyThemeNamespaceAliasTest.php +++ /dev/null @@ -1,14 +0,0 @@ -assertTrue(view()->exists('stylist-module-theme-admin::dashboard')); - $this->assertTrue(view()->exists('stylist-theme-admin::dashboard')); - } -} diff --git a/Stylist/Tests/Feature/StylistModuleTest.php b/Stylist/Tests/Feature/StylistModuleTest.php deleted file mode 100644 index 386ac03..0000000 --- a/Stylist/Tests/Feature/StylistModuleTest.php +++ /dev/null @@ -1,21 +0,0 @@ -assertInstanceOf(Stylist::class, app(Stylist::class)); - } - - public function test_default_theme_resolves(): void - { - config(['stylist.default_theme' => 'admin']); - - $this->assertSame('admin', app(Stylist::class)->current()->key()); - } -} diff --git a/Stylist/Tests/Feature/ThemeAssetControllerTest.php b/Stylist/Tests/Feature/ThemeAssetControllerTest.php deleted file mode 100644 index 876ee02..0000000 --- a/Stylist/Tests/Feature/ThemeAssetControllerTest.php +++ /dev/null @@ -1,41 +0,0 @@ - resource_path('themes')]); - app(Loader::class)->flush(); - - $this->get(route('stylist.theme.asset', [ - 'theme' => 'saasland', - 'path' => 'css/theme.css', - ]))->assertOk()->assertHeader('X-Content-Type-Options', 'nosniff'); - } - - public function test_asset_url_uses_public_themes_path(): void - { - $url = route('stylist.theme.asset', [ - 'theme' => 'saasland', - 'path' => 'images/example.jpg', - ], false); - - $this->assertSame('/themes/saasland/images/example.jpg', $url); - } - - public function test_non_asset_theme_files_are_not_served(): void - { - config(['stylist.themes_path' => resource_path('themes')]); - app(Loader::class)->flush(); - - $this->get(route('stylist.theme.asset', [ - 'theme' => 'saasland', - 'path' => 'views/home.blade.php', - ]))->assertNotFound(); - } -} diff --git a/Stylist/Tests/Feature/ThemeHtmlBuilderTest.php b/Stylist/Tests/Feature/ThemeHtmlBuilderTest.php deleted file mode 100644 index 65b03aa..0000000 --- a/Stylist/Tests/Feature/ThemeHtmlBuilderTest.php +++ /dev/null @@ -1,18 +0,0 @@ - '#111827']); - - $html = app(ThemeHtmlBuilder::class)->variables()->toHtml(); - - $this->assertStringContainsString('--brand-primary: #111827;', $html); - } -} diff --git a/Stylist/Tests/Feature/ThemeInheritanceTest.php b/Stylist/Tests/Feature/ThemeInheritanceTest.php deleted file mode 100644 index 495f581..0000000 --- a/Stylist/Tests/Feature/ThemeInheritanceTest.php +++ /dev/null @@ -1,95 +0,0 @@ -themesPath = storage_path('framework/testing/stylist-inheritance-'.uniqid()); - mkdir($this->themesPath, 0777, true); - config([ - 'stylist.themes_path' => $this->themesPath, - 'stylist.default_theme' => 'common', - ]); - app(Loader::class)->flush(); - } - - protected function tearDown(): void - { - app(Filesystem::class)->deleteDirectory($this->themesPath); - parent::tearDown(); - } - - public function test_child_theme_resolves_views_through_parent_without_copying_them(): void - { - $this->theme('common', null, ['components/layouts/app.blade.php' => '
{{ $slot }}
']); - $this->theme('admin', 'common', ['dashboard.blade.php' => '

Dashboard

']); - app(Loader::class)->flush(); - - $candidates = app(ThemeViewResolver::class)->candidatesForTheme('admin', 'components.layouts.app'); - - $this->assertSame('stylist-theme-admin::components.layouts.app', $candidates[0]); - $this->assertContains('stylist-theme-common::components.layouts.app', $candidates); - $this->assertLessThan( - array_search('stylist-theme-common::components.layouts.app', $candidates, true), - array_search('stylist-theme-admin::components.layouts.app', $candidates, true), - ); - } - - public function test_parent_alias_is_normalised_to_extends(): void - { - $path = $this->themesPath.'/child'; - mkdir($path, 0777, true); - file_put_contents($path.'/theme.json', json_encode([ - 'name' => 'Child', - 'parent' => 'common', - ])); - - $theme = app(Loader::class)->fromPath($path); - - $this->assertSame('common', $theme->parent()); - } - - public function test_duplicate_view_ownership_is_reported(): void - { - $this->theme('common', null, ['livewire/auth/login.blade.php' => 'common']); - $this->theme('admin', 'common', ['livewire/auth/login.blade.php' => 'admin']); - app(Loader::class)->flush(); - - $duplicates = app(ThemeInspector::class)->duplicateViews(); - - $this->assertArrayHasKey('livewire/auth/login.blade.php', $duplicates); - $this->assertEqualsCanonicalizing(['common', 'admin'], array_column($duplicates['livewire/auth/login.blade.php'], 'theme')); - } - - /** @param array $views */ - private function theme(string $key, ?string $parent, array $views): void - { - $path = $this->themesPath.'/'.$key; - mkdir($path.'/views', 0777, true); - file_put_contents($path.'/theme.json', json_encode(array_filter([ - 'name' => ucfirst($key), - 'slug' => $key, - 'extends' => $parent, - ], fn ($value) => $value !== null))); - - foreach ($views as $relative => $contents) { - $file = $path.'/views/'.$relative; - if (! is_dir(dirname($file))) { - mkdir(dirname($file), 0777, true); - } - file_put_contents($file, $contents); - } - } -} diff --git a/Stylist/Tests/Feature/ThemeManifestTest.php b/Stylist/Tests/Feature/ThemeManifestTest.php deleted file mode 100644 index 11d681e..0000000 --- a/Stylist/Tests/Feature/ThemeManifestTest.php +++ /dev/null @@ -1,157 +0,0 @@ -themesPath = storage_path('framework/testing/stylist-themes-'.uniqid()); - mkdir($this->themesPath, 0777, true); - config(['stylist.themes_path' => $this->themesPath]); - app(Loader::class)->flush(); - } - - protected function tearDown(): void - { - app(Filesystem::class)->deleteDirectory($this->themesPath); - parent::tearDown(); - } - - public function test_manifest_is_normalised(): void - { - $path = $this->themesPath.'/sample'; - mkdir($path.'/css', 0777, true); - file_put_contents($path.'/theme.json', json_encode(['name' => 'Sample'])); - file_put_contents($path.'/css/theme.css', 'body{}'); - - $theme = app(Loader::class)->fromPath($path); - - $this->assertSame('sample', $theme->key()); - $this->assertSame('css/theme.css', $theme->stylesheet()); - $this->assertSame('js/app.js', $theme->script()); - $this->assertSame('home', $theme->entryView()); - } - - public function test_unsafe_manifest_path_is_rejected(): void - { - $path = $this->themesPath.'/unsafe'; - mkdir($path, 0777, true); - file_put_contents($path.'/theme.json', json_encode([ - 'name' => 'Unsafe', - 'stylesheet' => '../secret.css', - ])); - - $this->expectException(InvalidThemeManifestException::class); - app(Loader::class)->fromPath($path); - } - - public function test_phase_one_manifest_optional_capabilities_are_normalised(): void - { - $path = $this->themesPath.'/design-gallery'; - mkdir($path.'/css', 0777, true); - mkdir($path.'/js', 0777, true); - file_put_contents($path.'/theme.json', json_encode([ - 'key' => 'design-gallery', - 'name' => 'Design Gallery', - 'stylesheet' => 'css/app.css', - 'script' => 'js/app.js', - 'appearances' => ['light', 'dark', 'system'], - 'accents' => ['blue', 'emerald', 'neutral'], - 'default_appearance' => 'system', - 'default_accent' => 'neutral', - // Phase 1 incorrectly duplicated accents here; Stylist must not - // reject the whole theme because of that compact legacy list. - 'variants' => ['blue', 'emerald', 'neutral'], - ])); - file_put_contents($path.'/css/app.css', 'body{}'); - file_put_contents($path.'/js/app.js', ''); - - $theme = app(Loader::class)->fromPath($path); - - $this->assertSame(['light', 'dark', 'system'], array_keys($theme->appearances())); - $this->assertSame(['blue', 'emerald', 'neutral'], array_keys($theme->accents())); - $this->assertSame([], $theme->variants()); - $this->assertSame('system', $theme->defaultAppearance()); - $this->assertSame('neutral', $theme->defaultAccent()); - } - - public function test_legacy_manifest_does_not_gain_appearance_or_accent_capabilities(): void - { - $path = $this->themesPath.'/legacy'; - mkdir($path.'/css', 0777, true); - file_put_contents($path.'/theme.json', json_encode(['name' => 'Legacy'])); - file_put_contents($path.'/css/theme.css', 'body{}'); - - $theme = app(Loader::class)->fromPath($path); - - $this->assertSame([], $theme->appearances()); - $this->assertSame([], $theme->accents()); - $this->assertSame('', $theme->defaultAppearance()); - $this->assertSame('', $theme->defaultAccent()); - } - - public function test_defaults_must_reference_declared_optional_capabilities(): void - { - $path = $this->themesPath.'/invalid-default'; - mkdir($path, 0777, true); - file_put_contents($path.'/theme.json', json_encode([ - 'name' => 'Invalid Default', - 'appearances' => ['light', 'dark'], - 'default_appearance' => 'system', - ])); - - $this->expectException(InvalidThemeManifestException::class); - app(Loader::class)->fromPath($path); - } - - public function test_manifest_version_and_capabilities_are_normalised(): void - { - $path = $this->themesPath.'/capable'; - mkdir($path.'/css', 0777, true); - mkdir($path.'/js', 0777, true); - mkdir($path.'/views', 0777, true); - file_put_contents($path.'/theme.json', json_encode([ - 'manifest_version' => 1, - 'name' => 'Capable', - 'appearances' => ['light', 'dark'], - 'accents' => ['blue'], - 'capabilities' => ['marketplace' => true], - ])); - file_put_contents($path.'/css/theme.css', 'body{}'); - file_put_contents($path.'/js/app.js', ''); - file_put_contents($path.'/views/home.blade.php', '
Capable
'); - - $theme = app(Loader::class)->fromPath($path); - - $this->assertSame(1, $theme->manifestVersion()); - $this->assertTrue($theme->hasCapability('appearances')); - $this->assertTrue($theme->hasCapability('accents')); - $this->assertTrue($theme->hasCapability('marketplace')); - $this->assertFalse($theme->hasCapability('preview')); - } - - public function test_unknown_manifest_fields_are_preserved_without_preventing_loading(): void - { - $path = $this->themesPath.'/future'; - mkdir($path.'/css', 0777, true); - file_put_contents($path.'/theme.json', json_encode([ - 'name' => 'Future', - 'future_builder_metadata' => ['anything' => true], - ])); - file_put_contents($path.'/css/theme.css', 'body{}'); - - $theme = app(Loader::class)->fromPath($path); - - $this->assertSame(['anything' => true], $theme->meta()['future_builder_metadata']); - } -} diff --git a/Stylist/Tests/Feature/ThemePackageRoundTripTest.php b/Stylist/Tests/Feature/ThemePackageRoundTripTest.php deleted file mode 100644 index 633b3dc..0000000 --- a/Stylist/Tests/Feature/ThemePackageRoundTripTest.php +++ /dev/null @@ -1,79 +0,0 @@ -themesPath = storage_path('framework/testing/stylist-installed-'.uniqid()); - $this->workspace = storage_path('framework/testing/stylist-package-'.uniqid()); - mkdir($this->themesPath, 0777, true); - mkdir($this->workspace, 0777, true); - config(['stylist.themes_path' => $this->themesPath]); - app(Loader::class)->flush(); - } - - protected function tearDown(): void - { - app(Filesystem::class)->deleteDirectory($this->themesPath); - app(Filesystem::class)->deleteDirectory($this->workspace); - app(Loader::class)->flush(); - parent::tearDown(); - } - - public function test_generated_theme_zip_installs_discovers_and_renders(): void - { - $source = $this->workspace.'/design-gallery'; - mkdir($source.'/css', 0777, true); - mkdir($source.'/js', 0777, true); - mkdir($source.'/views', 0777, true); - - file_put_contents($source.'/theme.json', json_encode([ - 'manifest_version' => 1, - 'key' => 'design-gallery', - 'name' => 'Design Gallery', - 'stylesheet' => 'css/app.css', - 'script' => 'js/app.js', - 'entry_view' => 'home', - 'appearances' => ['light', 'dark', 'system'], - 'accents' => ['blue', 'neutral'], - 'default_appearance' => 'system', - 'default_accent' => 'neutral', - ])); - file_put_contents($source.'/css/app.css', 'body{}'); - file_put_contents($source.'/js/app.js', ''); - file_put_contents($source.'/views/home.blade.php', '
Design Gallery works
'); - - $zipPath = $this->workspace.'/design-gallery.zip'; - $zip = new ZipArchive; - $this->assertTrue($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE)); - foreach (app(Filesystem::class)->allFiles($source) as $file) { - $zip->addFile($file->getPathname(), 'design-gallery/'.$file->getRelativePathname()); - } - $zip->close(); - - $upload = new UploadedFile($zipPath, 'design-gallery.zip', 'application/zip', null, true); - $theme = app(ThemePackageManager::class)->install($upload); - - $this->assertSame('design-gallery', $theme->key()); - $this->assertTrue(app(Loader::class)->all()->has('design-gallery')); - $this->assertStringContainsString( - 'Design Gallery works', - stylist_theme_view('design-gallery', 'home')->render(), - ); - } -} diff --git a/Stylist/Tests/Feature/ThemeViewResolverTest.php b/Stylist/Tests/Feature/ThemeViewResolverTest.php deleted file mode 100644 index 0ae83e8..0000000 --- a/Stylist/Tests/Feature/ThemeViewResolverTest.php +++ /dev/null @@ -1,22 +0,0 @@ - 'admin', - 'stylist.themes_path' => resource_path('themes'), - ]); - - $candidates = app(ThemeViewResolver::class)->candidates('components.page-hero'); - - $this->assertContains('stylist-theme-admin::components.page-hero', $candidates); - $this->assertContains('stylist::components.page-hero', $candidates); - } -} diff --git a/Stylist/Tests/Feature/WelcomeThemeViewTest.php b/Stylist/Tests/Feature/WelcomeThemeViewTest.php deleted file mode 100644 index ec7624b..0000000 --- a/Stylist/Tests/Feature/WelcomeThemeViewTest.php +++ /dev/null @@ -1,21 +0,0 @@ -assertTrue(view()->exists('stylist-theme-saasland::home')); - } - - public function test_home_route_renders_the_saasland_theme(): void - { - $this->get('/') - ->assertOk() - ->assertViewIs('stylist-theme-saasland::home') - ->assertSee('Transform Your Business With Our SaaS Solution'); - } -} diff --git a/Stylist/Theme/Exceptions/InvalidThemeManifestException.php b/Stylist/Theme/Exceptions/InvalidThemeManifestException.php deleted file mode 100644 index a548f9f..0000000 --- a/Stylist/Theme/Exceptions/InvalidThemeManifestException.php +++ /dev/null @@ -1,18 +0,0 @@ - $errors */ - public static function forPath(string $path, array $errors): self - { - return new self(sprintf( - 'Invalid theme manifest at [%s]: %s', - rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'theme.json', - implode(' ', $errors), - )); - } -} diff --git a/Stylist/Theme/Exceptions/ThemeJsonNotFoundException.php b/Stylist/Theme/Exceptions/ThemeJsonNotFoundException.php deleted file mode 100644 index 746d52e..0000000 --- a/Stylist/Theme/Exceptions/ThemeJsonNotFoundException.php +++ /dev/null @@ -1,13 +0,0 @@ -files->exists($manifest)) { - throw ThemeJsonNotFoundException::forPath($path); - } - - return json_decode($this->files->get($manifest), true, flags: JSON_THROW_ON_ERROR); - } -} diff --git a/Stylist/Theme/Loader.php b/Stylist/Theme/Loader.php deleted file mode 100644 index e93708d..0000000 --- a/Stylist/Theme/Loader.php +++ /dev/null @@ -1,83 +0,0 @@ -themes !== null) { - return $this->themes; - } - - $themes = collect(); - - foreach ($this->roots() as $root) { - if (! $this->files->isDirectory($root)) { - continue; - } - - foreach ($this->files->directories($root) as $themePath) { - try { - $theme = $this->fromPath($themePath); - $themes->put($theme->key(), $theme); - } catch (\Throwable $exception) { - report($exception); - } - } - } - - return $this->themes = $themes->sortKeys(); - } - - public function find(string $key): Theme - { - $theme = $this->all()->get($key); - - if (! $theme) { - throw ThemeNotFoundException::forKey($key); - } - - return $theme; - } - - public function fromPath(string $path): Theme - { - $data = $this->validator->validate($this->json->read($path), $path); - - return new Theme( - key: $data['key'], - name: $data['name'], - path: rtrim($path, DIRECTORY_SEPARATOR), - stylesheet: $data['stylesheet'], - script: $data['script'], - variables: $data['variables'], - meta: $data, - ); - } - - public function flush(): void - { - $this->themes = null; - } - - /** @return array */ - public function roots(): array - { - return array_values(array_unique(array_filter([ - (string) config('stylist.themes_path'), - ]))); - } -} diff --git a/Stylist/Theme/ManifestValidator.php b/Stylist/Theme/ManifestValidator.php deleted file mode 100644 index cc75998..0000000 --- a/Stylist/Theme/ManifestValidator.php +++ /dev/null @@ -1,179 +0,0 @@ - */ - public function validate(array $manifest, string $path): array - { - $errors = []; - $manifestVersion = $manifest['manifest_version'] ?? $manifest['schema_version'] ?? 1; - if ((is_string($manifestVersion) && ctype_digit($manifestVersion))) { - $manifestVersion = (int) $manifestVersion; - } - if (! is_int($manifestVersion) || $manifestVersion < 1) { - $errors[] = 'The manifest_version must be a positive integer.'; - } - - $key = $manifest['key'] ?? $manifest['slug'] ?? basename($path); - - if (! is_string($key) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $key)) { - $errors[] = 'The key/slug must contain only lowercase letters, numbers, dashes, and underscores.'; - } - if (isset($manifest['name']) && (! is_string($manifest['name']) || trim($manifest['name']) === '')) { - $errors[] = 'The name must be a non-empty string.'; - } - if (isset($manifest['version']) && (! is_string($manifest['version']) || ! preg_match('/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/', $manifest['version']))) { - $errors[] = 'The version must use semantic versioning, for example 1.2.0.'; - } - if (isset($manifest['type']) && ! in_array($manifest['type'], ['frontend', 'backend', 'common'], true)) { - $errors[] = 'The type must be frontend, backend, or common.'; - } - $parent = $manifest['extends'] ?? $manifest['parent'] ?? null; - if ($parent !== null && (! is_string($parent) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $parent))) { - $errors[] = 'The extends/parent value must be a valid theme key.'; - } - - foreach (['stylesheet', 'script', 'preview'] as $field) { - if (isset($manifest[$field]) && ! $this->isSafeRelativePath($manifest[$field])) { - $errors[] = "The {$field} value must be a safe relative path."; - } - } - - foreach (['assets', 'variables', 'supports', 'capabilities', 'variants', 'appearances', 'accents', 'settings', 'seo'] as $field) { - if (isset($manifest[$field]) && ! is_array($manifest[$field])) { - $errors[] = "The {$field} value must be an object or array."; - } - } - - $variants = $this->normaliseDefinitions($manifest['variants'] ?? [], 'variants', $errors, false); - $appearances = $this->normaliseDefinitions($manifest['appearances'] ?? [], 'appearances', $errors); - $accents = $this->normaliseDefinitions($manifest['accents'] ?? [], 'accents', $errors); - $capabilities = $this->normaliseCapabilities($manifest['capabilities'] ?? [], $errors); - $capabilities['appearances'] = $appearances !== []; - $capabilities['accents'] = $accents !== []; - $capabilities['preview'] = isset($manifest['preview']) || is_file($path.'/preview.webp'); - - $defaultAppearance = $manifest['default_appearance'] ?? null; - if ($defaultAppearance !== null) { - if (! is_string($defaultAppearance) || $defaultAppearance === '') { - $errors[] = 'The default_appearance value must be a non-empty string.'; - } elseif ($appearances === [] || ! array_key_exists($defaultAppearance, $appearances)) { - $errors[] = 'The default_appearance value must be declared in appearances.'; - } - } - - $defaultAccent = $manifest['default_accent'] ?? null; - if ($defaultAccent !== null) { - if (! is_string($defaultAccent) || $defaultAccent === '') { - $errors[] = 'The default_accent value must be a non-empty string.'; - } elseif ($accents === [] || ! array_key_exists($defaultAccent, $accents)) { - $errors[] = 'The default_accent value must be declared in accents.'; - } - } - - if ($errors !== []) { - throw InvalidThemeManifestException::forPath($path, $errors); - } - - $manifest['manifest_version'] = $manifestVersion; - unset($manifest['schema_version']); - $manifest['key'] = $key; - $manifest['slug'] = $key; - $manifest['name'] = $manifest['name'] ?? str($key)->headline()->toString(); - $manifest['extends'] = $parent; - $manifest['version'] = (string) ($manifest['version'] ?? '1.0.0'); - $manifest['entry_view'] = (string) ($manifest['entry_view'] ?? 'home'); - $manifest['stylesheet'] = $manifest['stylesheet'] ?? $manifest['css'] ?? 'css/theme.css'; - $manifest['script'] = $manifest['script'] ?? 'js/app.js'; - $manifest['assets'] = is_array($manifest['assets'] ?? null) ? $manifest['assets'] : []; - $manifest['variables'] = is_array($manifest['variables'] ?? null) ? $manifest['variables'] : []; - $manifest['supports'] = is_array($manifest['supports'] ?? null) ? $manifest['supports'] : []; - $manifest['capabilities'] = $capabilities; - $manifest['variants'] = $variants; - $manifest['appearances'] = $appearances; - $manifest['accents'] = $accents; - $manifest['settings'] = is_array($manifest['settings'] ?? null) ? $manifest['settings'] : []; - $manifest['seo'] = is_array($manifest['seo'] ?? null) ? $manifest['seo'] : []; - $manifest['preview'] = $manifest['preview'] ?? (is_file($path.'/preview.webp') ? 'preview.webp' : null); - - return $manifest; - } - - /** - * Compact lists such as ["light", "dark"] are accepted and converted to - * keyed empty definitions. Object definitions remain unchanged. - * - * @param array $value - * @param array $errors - * @return array> - */ - private function normaliseDefinitions(array $value, string $field, array &$errors, bool $allowCompact = true): array - { - if ($value === []) { - return []; - } - - if (array_is_list($value)) { - if (! $allowCompact && $field === 'variants') { - // Older Theme Builder Phase 1 builds incorrectly wrote accent names - // to variants. Ignore that list while preserving object-style variants. - return []; - } - - $normalised = []; - foreach ($value as $name) { - if (! is_string($name) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $name)) { - $errors[] = "Every {$field} entry must be a valid lowercase key."; - - continue; - } - $normalised[$name] = []; - } - - return $normalised; - } - - $normalised = []; - foreach ($value as $name => $definition) { - if (! is_string($name) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $name) || ! is_array($definition)) { - $errors[] = "Every {$field} definition must use a valid lowercase key and an object value."; - - continue; - } - $normalised[$name] = $definition; - } - - return $normalised; - } - - /** - * @param array $value - * @param array $errors - * @return array - */ - private function normaliseCapabilities(array $value, array &$errors): array - { - $normalised = []; - - foreach ($value as $name => $enabled) { - if (! is_string($name) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $name) || ! is_bool($enabled)) { - $errors[] = 'Every capability must use a valid lowercase key and a boolean value.'; - - continue; - } - - $normalised[$name] = $enabled; - } - - return $normalised; - } - - private function isSafeRelativePath(mixed $path): bool - { - return is_string($path) && $path !== '' && ! str_starts_with($path, '/') && ! str_contains($path, '..') && ! str_contains($path, "\0"); - } -} diff --git a/Stylist/Theme/Stylist.php b/Stylist/Theme/Stylist.php deleted file mode 100644 index 82793ef..0000000 --- a/Stylist/Theme/Stylist.php +++ /dev/null @@ -1,103 +0,0 @@ -resolver->resolve($context); - try { - return $this->loader->find($requested); - } catch (ThemeNotFoundException) { - $fallback = $context === 'frontend' - ? (string) config('stylist.default_frontend_theme', 'saasland') - : (string) config('stylist.default_backend_theme', config('stylist.default_theme', 'admin')); - if ($requested !== $fallback) { - return $this->loader->find($fallback); - } - throw ThemeNotFoundException::forKey($requested); - } - } - - public function frontend(): Theme - { - return $this->current('frontend'); - } - - public function backend(): Theme - { - return $this->current('backend'); - } - - public function variables(): array - { - $theme = $this->current(); - $variant = $this->settings->variant($theme); - $variantVars = (array) data_get($theme->variants(), $variant.'.variables', []); - $appearanceVars = (array) data_get($theme->appearances(), $this->appearance().'.variables', []); - $accentVars = (array) data_get($theme->accents(), $this->accent().'.variables', []); - $settingVars = collect($this->settings->get($theme)) - ->filter(fn ($value, $key) => is_string($key) && str_starts_with($key, '--')) - ->mapWithKeys(fn ($value, $key) => [ltrim($key, '-') => $value]) - ->all(); - - return array_replace( - (array) config('stylist.fallback_variables', []), - $this->inheritedVariables($theme), - $variantVars, - $appearanceVars, - $accentVars, - $settingVars, - $this->resolver->variables(), - ); - } - - public function settings(): array - { - return $this->settings->get($this->current()); - } - - public function variant(): string - { - return $this->settings->variant($this->current()); - } - - public function appearance(): string - { - return $this->settings->appearance($this->current()); - } - - public function accent(): string - { - return $this->settings->accent($this->current()); - } - - public function all() - { - return $this->loader->all(); - } - - private function inheritedVariables(Theme $theme, array $seen = []): array - { - if (in_array($theme->key(), $seen, true)) { - return []; - } - $seen[] = $theme->key(); - $parent = []; - if ($theme->parent()) { - try { - $parent = $this->inheritedVariables($this->loader->find($theme->parent()), $seen); - } catch (ThemeNotFoundException) { - } - } - - return array_replace($parent, $theme->variables()); - } -} diff --git a/Stylist/Theme/Theme.php b/Stylist/Theme/Theme.php deleted file mode 100644 index 2010173..0000000 --- a/Stylist/Theme/Theme.php +++ /dev/null @@ -1,161 +0,0 @@ -key; - } - - public function name(): string - { - return $this->name; - } - - public function path(): string - { - return $this->path; - } - - public function stylesheet(): string - { - return $this->stylesheet; - } - - public function script(): string - { - return $this->script; - } - - public function variables(): array - { - return $this->variables; - } - - public function meta(): array - { - return $this->meta; - } - - public function manifestVersion(): int - { - return (int) ($this->meta['manifest_version'] ?? 1); - } - - public function version(): string - { - return (string) ($this->meta['version'] ?? '1.0.0'); - } - - public function description(): string - { - return (string) ($this->meta['description'] ?? ''); - } - - public function type(): string - { - return (string) ($this->meta['type'] ?? 'frontend'); - } - - public function author(): string - { - return (string) ($this->meta['author'] ?? ''); - } - - public function parent(): ?string - { - $parent = $this->meta['extends'] ?? null; - - return is_string($parent) && $parent !== '' ? $parent : null; - } - - public function preview(): ?string - { - $preview = $this->meta['preview'] ?? null; - - return is_string($preview) && $preview !== '' ? $preview : null; - } - - public function variants(): array - { - return (array) ($this->meta['variants'] ?? []); - } - - public function appearances(): array - { - return (array) ($this->meta['appearances'] ?? []); - } - - public function accents(): array - { - return (array) ($this->meta['accents'] ?? []); - } - - public function defaultAppearance(): string - { - return (string) ($this->meta['default_appearance'] ?? array_key_first($this->appearances()) ?? ''); - } - - public function defaultAccent(): string - { - return (string) ($this->meta['default_accent'] ?? array_key_first($this->accents()) ?? ''); - } - - public function settingsSchema(): array - { - return (array) ($this->meta['settings'] ?? []); - } - - public function seo(): array - { - return (array) ($this->meta['seo'] ?? []); - } - - public function entryView(): string - { - return (string) ($this->meta['entry_view'] ?? 'home'); - } - - public function capabilities(): array - { - return (array) ($this->meta['capabilities'] ?? []); - } - - public function hasCapability(string $feature): bool - { - return (bool) ($this->capabilities()[$feature] ?? false); - } - - public function supports(string $feature): bool - { - return (bool) data_get($this->meta, 'supports.'.$feature, $this->hasCapability($feature)); - } - - public function has(string $relativePath): bool - { - return is_file($this->path.DIRECTORY_SEPARATOR.ltrim($relativePath, '/')); - } - - public function absolutePath(string $relativePath): string - { - return $this->path.DIRECTORY_SEPARATOR.ltrim($relativePath, '/'); - } - - /** @deprecated Theme assets are served through UrlGenerator. */ - public function assetsPath(): string - { - return 'stylist/theme-assets/'.$this->key; - } -} diff --git a/Stylist/Theme/UrlGenerator.php b/Stylist/Theme/UrlGenerator.php deleted file mode 100644 index 77d08f8..0000000 --- a/Stylist/Theme/UrlGenerator.php +++ /dev/null @@ -1,20 +0,0 @@ -forTheme($this->stylist->current(), $path); - } - - public function forTheme(Theme|string $theme, string $path): string - { - $key = $theme instanceof Theme ? $theme->key() : $theme; - - return route('stylist.theme.asset', ['theme' => $key, 'path' => ltrim($path, '/')]); - } -} diff --git a/Stylist/View/ThemeViewResolver.php b/Stylist/View/ThemeViewResolver.php deleted file mode 100644 index 91d263d..0000000 --- a/Stylist/View/ThemeViewResolver.php +++ /dev/null @@ -1,130 +0,0 @@ -> */ - private array $candidateCache = []; - - public function __construct( - private readonly Stylist $stylist, - private readonly ViewFactory $views, - private readonly Loader $loader, - ) {} - - public function make(string $view, array $data = [], array $mergeData = []): View - { - return $this->views->first( - $this->candidates($view), - array_replace([ - 'themeSettings' => $this->stylist->settings(), - 'themeVariant' => $this->stylist->variant(), - ], $data), - $mergeData, - ); - } - - public function makeForTheme(string $theme, string $view, array $data = [], array $mergeData = []): View - { - return $this->views->first($this->candidatesForTheme($theme, $view), $data, $mergeData); - } - - /** @return array */ - public function candidates(string $view): array - { - return $this->candidatesForTheme($this->stylist->current()->key(), $view); - } - - /** @return array */ - public function candidatesForTheme(string $theme, string $view): array - { - $view = ltrim($view, '.'); - $cacheKey = $theme.'|'.$view; - - if (isset($this->candidateCache[$cacheKey])) { - return $this->candidateCache[$cacheKey]; - } - - $keys = $this->inheritanceChain($theme); - $fallback = (string) config('stylist.default_theme', 'common'); - - if ($fallback !== '' && ! in_array($fallback, $keys, true)) { - try { - $keys = array_merge($keys, $this->inheritanceChain($fallback)); - } catch (ThemeNotFoundException) { - // A missing configured fallback is diagnosed by stylist:doctor. - } - } - - if (! in_array('common', $keys, true)) { - try { - $keys = array_merge($keys, $this->inheritanceChain('common')); - } catch (ThemeNotFoundException) { - // The module fallback below remains available. - } - } - - $candidates = []; - foreach (array_values(array_unique($keys)) as $key) { - $this->registerNamespace($key); - $candidates[] = "stylist-theme-{$key}::{$view}"; - $candidates[] = "stylist-module-theme-{$key}::{$view}"; - } - - $candidates[] = "stylist::{$view}"; - $candidates[] = $view; - - return $this->candidateCache[$cacheKey] = array_values(array_unique($candidates)); - } - - public function flush(): void - { - $this->candidateCache = []; - } - - /** @return array */ - private function inheritanceChain(string $theme): array - { - $keys = []; - $seen = []; - $key = $theme; - - while ($key !== '') { - if (isset($seen[$key])) { - throw new RuntimeException('Theme inheritance cycle detected at ['.$key.'].'); - } - - $seen[$key] = true; - $loaded = $this->loader->find($key); - $keys[] = $loaded->key(); - $key = $loaded->parent() ?? ''; - } - - return $keys; - } - - private function registerNamespace(string $theme): void - { - try { - $loaded = $this->loader->find($theme); - } catch (ThemeNotFoundException) { - return; - } - - $viewsPath = $loaded->path().DIRECTORY_SEPARATOR.'views'; - if (! is_dir($viewsPath)) { - return; - } - - $this->views->addNamespace("stylist-theme-{$theme}", $viewsPath); - $this->views->addNamespace("stylist-module-theme-{$theme}", $viewsPath); - } -} diff --git a/Stylist/changelog.yaml b/Stylist/changelog.yaml deleted file mode 100644 index 7a8e4e0..0000000 --- a/Stylist/changelog.yaml +++ /dev/null @@ -1,24 +0,0 @@ -latest: 1.1.0 -releases: - - version: 1.1.0 - date: 2026-07-08 - changes: - - Added self-contained theme folders with theme.json, CSS, Blade views, and Livewire views. - - Added ThemeViewResolver and stylist_view() fallback helper. - - Added default and MPBA example themes. - - Added theme publishing support. - - Updated Vite inputs to include theme CSS files. - - version: 1.0.0 - date: 2026-07-08 - changes: - - Initial Laravel 13 Stylist module. - - - version: 1.1.0 - date: 2026-07-23 - changes: - - Added optional manifest_version/schema_version normalisation. - - Added capability normalisation for appearances, accents, previews and future features. - - Added stylist:validate for installed themes, directories and ZIP packages. - - Added complete package validation during installation. - - Added dynamic view namespace registration after installing a theme. - - Added manifest compatibility and install-render regression coverage. diff --git a/Stylist/composer.json b/Stylist/composer.json deleted file mode 100644 index d537e3a..0000000 --- a/Stylist/composer.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "mpba/stylist-module", - "description": "Tenant-aware theme and asset switching module for Laravel 13.", - "type": "laravel-module", - "require": { - "php": "^8.3", - "illuminate/support": "^13.0", - "illuminate/view": "^13.0", - "illuminate/filesystem": "^13.0", - "illuminate/console": "^13.0" - }, - "autoload": { - "psr-4": { - "Modules\\Stylist\\": "" - } - }, - "autoload-dev": { - "psr-4": { - "Modules\\Stylist\\Tests\\": "Tests/" - } - } -} diff --git a/Stylist/module.json b/Stylist/module.json deleted file mode 100644 index 214c2d2..0000000 --- a/Stylist/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Stylist", - "alias": "stylist", - "version": "1.1.0", - "description": "Tenant-aware theme and asset switching module for Laravel 13.", - "keywords": [ - "laravel", - "modules", - "themes", - "tenancy", - "styling" - ], - "priority": 0, - "providers": [ - "Modules\\Stylist\\Providers\\StylistServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [] -} diff --git a/Stylist/package.json b/Stylist/package.json deleted file mode 100644 index e0b33af..0000000 --- a/Stylist/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scripts": { - "dev": "vite", - "build": "vite build", - - "stylist:dev": "vite --config Modules/Stylist/vite.config.js", - "stylist:build": "vite build --config Modules/Stylist/vite.config.js", - - "matches:dev": "vite --config Modules/Matches/vite.config.js", - "matches:build": "vite build --config Modules/Matches/vite.config.js", - - "modules:build": "npm run stylist:build && npm run matches:build", - "build:all": "npm run build && npm run modules:build" - } -} diff --git a/Stylist/vite.config.js b/Stylist/vite.config.js deleted file mode 100644 index de55470..0000000 --- a/Stylist/vite.config.js +++ /dev/null @@ -1,25 +0,0 @@ -import { defineConfig } from 'vite'; -import laravel from 'laravel-vite-plugin'; -import tailwindcss from '@tailwindcss/vite'; - -export default defineConfig({ - plugins: [ - laravel({ - input: [ - 'Modules/Stylist/Resources/assets/css/app.css', - 'Modules/Stylist/Resources/assets/js/app.js', - ], - - refresh: [ - 'Modules/Stylist/Resources/views/**', - 'resources/themes/**/views/**', - 'Modules/Stylist/App/**', - 'Modules/Stylist/routes/**', - ], - - buildDirectory: 'build/stylist', - }), - - tailwindcss(), - ], -}); diff --git a/Tags/Config/config.php b/Tags/Config/config.php deleted file mode 100644 index 4f63319..0000000 --- a/Tags/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Tags']; diff --git a/Tags/Database/Migrations/2026_07_24_030200_create_tags_table.php b/Tags/Database/Migrations/2026_07_24_030200_create_tags_table.php deleted file mode 100644 index 8a969e7..0000000 --- a/Tags/Database/Migrations/2026_07_24_030200_create_tags_table.php +++ /dev/null @@ -1,25 +0,0 @@ -id(); - $table->string('name'); - $table->string('slug')->unique(); - $table->text('description')->nullable(); - $table->unsignedInteger('usage_count')->default(0); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('tags'); - } -}; diff --git a/Tags/Http/Controllers/TagController.php b/Tags/Http/Controllers/TagController.php deleted file mode 100644 index 08e0a08..0000000 --- a/Tags/Http/Controllers/TagController.php +++ /dev/null @@ -1,48 +0,0 @@ - Tag::orderByDesc('usage_count')->paginate(30)]); - } - - public function create(): View - { - return view('tags::admin.form'); - } - - public function store(Request $request): RedirectResponse - { - Tag::create($request->validate(['name' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255', 'unique:tags,slug'], 'description' => ['nullable', 'string']])); - - return redirect()->route('administration.tags.index')->with('status', 'Tag created.'); - } - - public function edit(Tag $tag): View - { - return view('tags::admin.form', ['record' => $tag]); - } - - public function update(Request $request, Tag $tag): RedirectResponse - { - $tag->update($request->validate(['name' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255', 'unique:tags,slug,'.$tag->id], 'description' => ['nullable', 'string']])); - - return redirect()->route('administration.tags.index')->with('status', 'Tag updated.'); - } - - public function destroy(Tag $tag): RedirectResponse - { - $tag->delete(); - - return back()->with('status', 'Tag deleted.'); - } -} diff --git a/Tags/Models/Tag.php b/Tags/Models/Tag.php deleted file mode 100644 index da7b39c..0000000 --- a/Tags/Models/Tag.php +++ /dev/null @@ -1,15 +0,0 @@ - 'integer']; - } -} diff --git a/Tags/Providers/RouteServiceProvider.php b/Tags/Providers/RouteServiceProvider.php deleted file mode 100644 index 3605636..0000000 --- a/Tags/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Tags', 'Routes/web.php')); - } -} diff --git a/Tags/Providers/TagsServiceProvider.php b/Tags/Providers/TagsServiceProvider.php deleted file mode 100644 index 51407ff..0000000 --- a/Tags/Providers/TagsServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Tags', 'Config/config.php'), 'tags'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Tags', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Tags', 'Resources/views'), 'tags'); - } -} diff --git a/Tags/Resources/views/admin/form.blade.php b/Tags/Resources/views/admin/form.blade.php deleted file mode 100644 index 9dfe384..0000000 --- a/Tags/Resources/views/admin/form.blade.php +++ /dev/null @@ -1 +0,0 @@ -
@csrf @isset($record) @method('PUT') @endisset {{ old('description',$record->description ?? '') }}Save
diff --git a/Tags/Resources/views/admin/index.blade.php b/Tags/Resources/views/admin/index.blade.php deleted file mode 100644 index 537b5d9..0000000 --- a/Tags/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -

Tags

Manage publishing tags.

Create
@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $record)@empty@endforelse
Name / titleStatusActions
{{ $record->title ?? $record->name }}{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}Edit
@csrf @method('DELETE')Delete
No records yet.
{{ $records->links() }}
diff --git a/Tags/Routes/web.php b/Tags/Routes/web.php deleted file mode 100644 index 2b425f6..0000000 --- a/Tags/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/tags')->name('administration.tags.')->group(function (): void { - Route::get('/', [TagController::class, 'index'])->name('index'); - Route::get('/create', [TagController::class, 'create'])->name('create'); - Route::post('/', [TagController::class, 'store'])->name('store'); - Route::get('/{tag}/edit', [TagController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{tag}', [TagController::class, 'update'])->name('update'); - Route::delete('/{tag}', [TagController::class, 'destroy'])->name('destroy'); -}); diff --git a/Tags/Tests/Feature/ModuleBootTest.php b/Tags/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index 1bc2dd9..0000000 --- a/Tags/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame('Tags', config('tags.name')); - } -} diff --git a/Tags/module.json b/Tags/module.json deleted file mode 100644 index 53001bb..0000000 --- a/Tags/module.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Tags", - "alias": "tags", - "version": "1.0.0", - "description": "Tags publishing module.", - "keywords": ["publishing", "tags"], - "priority": 30, - "providers": ["Modules\\Tags\\Providers\\TagsServiceProvider"], - "aliases": {}, - "files": [], - "requires": ["Core", "Administration"] -} diff --git a/Team/Config/config.php b/Team/Config/config.php deleted file mode 100644 index 3342295..0000000 --- a/Team/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Team']; diff --git a/Team/Database/Migrations/2026_07_24_000800_create_team_members_table.php b/Team/Database/Migrations/2026_07_24_000800_create_team_members_table.php deleted file mode 100644 index 67bfdcc..0000000 --- a/Team/Database/Migrations/2026_07_24_000800_create_team_members_table.php +++ /dev/null @@ -1,28 +0,0 @@ -id(); - $table->string('name'); - $table->string('job_title')->nullable(); - $table->text('biography')->nullable(); - $table->string('email')->nullable(); - $table->unsignedInteger('sort_order')->default(0); - $table->boolean('enabled')->default(true); - $table->timestamps(); - $table->softDeletes(); - }); - } - - public function down(): void - { - Schema::dropIfExists('team_members'); - } -}; diff --git a/Team/Http/Controllers/TeamMemberController.php b/Team/Http/Controllers/TeamMemberController.php deleted file mode 100644 index 270e4ad..0000000 --- a/Team/Http/Controllers/TeamMemberController.php +++ /dev/null @@ -1,62 +0,0 @@ - TeamMember::query()->latest()->paginate(20)]); - } - - public function create(): View - { - return view('team::admin.form'); - } - - public function store(Request $request): RedirectResponse - { - $data = $request->validate(['name' => ['required', 'string', 'max:255'], - 'job_title' => ['nullable', 'string'], - 'biography' => ['nullable', 'string'], - 'email' => ['nullable', 'string'], - 'sort_order' => ['nullable', 'integer', 'min:0'], - 'enabled' => ['sometimes', 'boolean']]); - $data['enabled'] = $request->boolean('enabled'); - TeamMember::create($data); - - return redirect()->route('administration.team.index')->with('status', 'TeamMember created.'); - } - - public function edit(TeamMember $teamMember): View - { - return view('team::admin.form', ['record' => $teamMember]); - } - - public function update(Request $request, TeamMember $teamMember): RedirectResponse - { - $data = $request->validate(['name' => ['required', 'string', 'max:255'], - 'job_title' => ['nullable', 'string'], - 'biography' => ['nullable', 'string'], - 'email' => ['nullable', 'string'], - 'sort_order' => ['nullable', 'integer', 'min:0'], - 'enabled' => ['sometimes', 'boolean']]); - $data['enabled'] = $request->boolean('enabled'); - $teamMember->update($data); - - return redirect()->route('administration.team.index')->with('status', 'TeamMember updated.'); - } - - public function destroy(TeamMember $teamMember): RedirectResponse - { - $teamMember->delete(); - - return back()->with('status', 'TeamMember deleted.'); - } -} diff --git a/Team/Models/TeamMember.php b/Team/Models/TeamMember.php deleted file mode 100644 index 1ada6eb..0000000 --- a/Team/Models/TeamMember.php +++ /dev/null @@ -1,24 +0,0 @@ - 'integer', - 'enabled' => 'boolean', - ]; - } -} diff --git a/Team/Providers/RouteServiceProvider.php b/Team/Providers/RouteServiceProvider.php deleted file mode 100644 index a50f441..0000000 --- a/Team/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -routes(fn () => Route::middleware('web')->group(module_path('Team', 'Routes/web.php'))); - } -} diff --git a/Team/Providers/TeamServiceProvider.php b/Team/Providers/TeamServiceProvider.php deleted file mode 100644 index 6cdcac1..0000000 --- a/Team/Providers/TeamServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Team', 'Config/config.php'), 'team'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Team', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Team', 'Resources/views'), 'team'); - } -} diff --git a/Team/Resources/views/admin/form.blade.php b/Team/Resources/views/admin/form.blade.php deleted file mode 100644 index 621f780..0000000 --- a/Team/Resources/views/admin/form.blade.php +++ /dev/null @@ -1,10 +0,0 @@ - -

{{ isset($record) ? 'Edit' : 'Create' }} Team member

@csrf @isset($record) @method('PUT') @endisset -
@error('name')

{{ $message }}

@enderror
-
@error('job_title')

{{ $message }}

@enderror
-
@error('biography')

{{ $message }}

@enderror
-
@error('email')

{{ $message }}

@enderror
-
@error('sort_order')

{{ $message }}

@enderror
- -
Cancel
-
diff --git a/Team/Resources/views/admin/index.blade.php b/Team/Resources/views/admin/index.blade.php deleted file mode 100644 index 6345c12..0000000 --- a/Team/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,5 +0,0 @@ - -

Team

Manage team content.

Add new
-@if(session('status'))
{{ session('status') }}
@endif -
@forelse($records as $record)@empty@endforelse
NameJob TitleBiographyActions
{{ is_array($record->name) ? json_encode($record->name) : Str::limit((string) $record->name, 70) }}{{ is_array($record->job_title) ? json_encode($record->job_title) : Str::limit((string) $record->job_title, 70) }}{{ is_array($record->biography) ? json_encode($record->biography) : Str::limit((string) $record->biography, 70) }}Edit
@csrf @method('DELETE')
No records yet.
{{ $records->links() }}
-
diff --git a/Team/Routes/web.php b/Team/Routes/web.php deleted file mode 100644 index 0c6678d..0000000 --- a/Team/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/team')->name('administration.team.')->group(function (): void { - Route::get('/', [TeamMemberController::class, 'index'])->name('index'); - Route::get('/create', [TeamMemberController::class, 'create'])->name('create'); - Route::post('/', [TeamMemberController::class, 'store'])->name('store'); - Route::get('/{teamMember}/edit', [TeamMemberController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{teamMember}', [TeamMemberController::class, 'update'])->name('update'); - Route::delete('/{teamMember}', [TeamMemberController::class, 'destroy'])->name('destroy'); -}); diff --git a/Team/Support/ModuleInfo.php b/Team/Support/ModuleInfo.php deleted file mode 100644 index 4fcd387..0000000 --- a/Team/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBeTrue(); -}); diff --git a/Team/module.json b/Team/module.json deleted file mode 100644 index 7cbacd8..0000000 --- a/Team/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Team", - "alias": "team", - "version": "1.0.0", - "description": "Team CMS module.", - "keywords": [ - "cms", - "team" - ], - "priority": 28, - "providers": [ - "Modules\\Team\\Providers\\TeamServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core", - "Administration" - ] -} diff --git a/Testimonials/Config/config.php b/Testimonials/Config/config.php deleted file mode 100644 index ab940e8..0000000 --- a/Testimonials/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Testimonials']; diff --git a/Testimonials/Database/Migrations/2026_07_24_000900_create_testimonials_table.php b/Testimonials/Database/Migrations/2026_07_24_000900_create_testimonials_table.php deleted file mode 100644 index bebe283..0000000 --- a/Testimonials/Database/Migrations/2026_07_24_000900_create_testimonials_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->string('name'); - $table->string('company')->nullable(); - - $table->unsignedInteger('rating')->default(5); - $table->boolean('enabled')->default(true); - $table->timestamps(); - $table->softDeletes(); - }); - } - - public function down(): void - { - Schema::dropIfExists('testimonials'); - } -}; diff --git a/Testimonials/Database/Migrations/2026_07_27_070000_add_content_to_testimonials_table.php b/Testimonials/Database/Migrations/2026_07_27_070000_add_content_to_testimonials_table.php deleted file mode 100644 index e81f8a9..0000000 --- a/Testimonials/Database/Migrations/2026_07_27_070000_add_content_to_testimonials_table.php +++ /dev/null @@ -1,26 +0,0 @@ -text('content')->nullable()->after('company'); - }); - } - } - - public function down(): void - { - if (Schema::hasTable('testimonials') && Schema::hasColumn('testimonials', 'content')) { - Schema::table('testimonials', function (Blueprint $table): void { - $table->dropColumn('content'); - }); - } - } -}; diff --git a/Testimonials/Http/Controllers/TestimonialController.php b/Testimonials/Http/Controllers/TestimonialController.php deleted file mode 100644 index 908e974..0000000 --- a/Testimonials/Http/Controllers/TestimonialController.php +++ /dev/null @@ -1,60 +0,0 @@ - Testimonial::query()->latest()->paginate(20)]); - } - - public function create(): View - { - return view('testimonials::admin.form'); - } - - public function store(Request $request): RedirectResponse - { - $data = $request->validate(['name' => ['required', 'string', 'max:255'], - 'company' => ['nullable', 'string'], - 'content' => ['required', 'string'], - 'rating' => ['nullable', 'integer', 'min:0'], - 'enabled' => ['sometimes', 'boolean']]); - $data['enabled'] = $request->boolean('enabled'); - Testimonial::create($data); - - return redirect()->route('administration.testimonials.index')->with('status', 'Testimonial created.'); - } - - public function edit(Testimonial $testimonial): View - { - return view('testimonials::admin.form', ['record' => $testimonial]); - } - - public function update(Request $request, Testimonial $testimonial): RedirectResponse - { - $data = $request->validate(['name' => ['required', 'string', 'max:255'], - 'company' => ['nullable', 'string'], - 'content' => ['required', 'string'], - 'rating' => ['nullable', 'integer', 'min:0'], - 'enabled' => ['sometimes', 'boolean']]); - $data['enabled'] = $request->boolean('enabled'); - $testimonial->update($data); - - return redirect()->route('administration.testimonials.index')->with('status', 'Testimonial updated.'); - } - - public function destroy(Testimonial $testimonial): RedirectResponse - { - $testimonial->delete(); - - return back()->with('status', 'Testimonial deleted.'); - } -} diff --git a/Testimonials/Models/Testimonial.php b/Testimonials/Models/Testimonial.php deleted file mode 100644 index 590160f..0000000 --- a/Testimonials/Models/Testimonial.php +++ /dev/null @@ -1,24 +0,0 @@ - 'integer', - 'enabled' => 'boolean', - ]; - } -} diff --git a/Testimonials/Providers/RouteServiceProvider.php b/Testimonials/Providers/RouteServiceProvider.php deleted file mode 100644 index 98aac75..0000000 --- a/Testimonials/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -routes(fn () => Route::middleware('web')->group(module_path('Testimonials', 'Routes/web.php'))); - } -} diff --git a/Testimonials/Providers/TestimonialsServiceProvider.php b/Testimonials/Providers/TestimonialsServiceProvider.php deleted file mode 100644 index fde762a..0000000 --- a/Testimonials/Providers/TestimonialsServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path('Testimonials', 'Config/config.php'), 'testimonials'); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Testimonials', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Testimonials', 'Resources/views'), 'testimonials'); - } -} diff --git a/Testimonials/Resources/views/admin/form.blade.php b/Testimonials/Resources/views/admin/form.blade.php deleted file mode 100644 index c6f4e86..0000000 --- a/Testimonials/Resources/views/admin/form.blade.php +++ /dev/null @@ -1,9 +0,0 @@ - -

{{ isset($record) ? 'Edit' : 'Create' }} Testimonial

@csrf @isset($record) @method('PUT') @endisset -
@error('name')

{{ $message }}

@enderror
-
@error('company')

{{ $message }}

@enderror
-
@error('content')

{{ $message }}

@enderror
-
@error('rating')

{{ $message }}

@enderror
- -
Cancel
-
diff --git a/Testimonials/Resources/views/admin/index.blade.php b/Testimonials/Resources/views/admin/index.blade.php deleted file mode 100644 index ab55427..0000000 --- a/Testimonials/Resources/views/admin/index.blade.php +++ /dev/null @@ -1,5 +0,0 @@ - -

Testimonials

Manage testimonials content.

Add new
-@if(session('status'))
{{ session('status') }}
@endif -
@forelse($records as $record)@empty@endforelse
NameCompanyContentActions
{{ is_array($record->name) ? json_encode($record->name) : Str::limit((string) $record->name, 70) }}{{ is_array($record->company) ? json_encode($record->company) : Str::limit((string) $record->company, 70) }}{{ is_array($record->content) ? json_encode($record->content) : Str::limit((string) $record->content, 70) }}Edit
@csrf @method('DELETE')
No records yet.
{{ $records->links() }}
-
diff --git a/Testimonials/Routes/web.php b/Testimonials/Routes/web.php deleted file mode 100644 index 3527aff..0000000 --- a/Testimonials/Routes/web.php +++ /dev/null @@ -1,13 +0,0 @@ -prefix('administration/testimonials')->name('administration.testimonials.')->group(function (): void { - Route::get('/', [TestimonialController::class, 'index'])->name('index'); - Route::get('/create', [TestimonialController::class, 'create'])->name('create'); - Route::post('/', [TestimonialController::class, 'store'])->name('store'); - Route::get('/{testimonial}/edit', [TestimonialController::class, 'edit'])->name('edit'); - Route::match(['put', 'patch'], '/{testimonial}', [TestimonialController::class, 'update'])->name('update'); - Route::delete('/{testimonial}', [TestimonialController::class, 'destroy'])->name('destroy'); -}); diff --git a/Testimonials/Support/ModuleInfo.php b/Testimonials/Support/ModuleInfo.php deleted file mode 100644 index afc0468..0000000 --- a/Testimonials/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBeTrue(); -}); diff --git a/Testimonials/module.json b/Testimonials/module.json deleted file mode 100644 index 9ee5cfa..0000000 --- a/Testimonials/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Testimonials", - "alias": "testimonials", - "version": "1.0.0", - "description": "Testimonials CMS module.", - "keywords": [ - "cms", - "testimonials" - ], - "priority": 29, - "providers": [ - "Modules\\Testimonials\\Providers\\TestimonialsServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core", - "Administration" - ] -} diff --git a/Users/Config/config.php b/Users/Config/config.php deleted file mode 100644 index d0471c5..0000000 --- a/Users/Config/config.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Users', - 'enabled' => true, -]; diff --git a/Users/Http/Controllers/UserController.php b/Users/Http/Controllers/UserController.php deleted file mode 100644 index 79191a5..0000000 --- a/Users/Http/Controllers/UserController.php +++ /dev/null @@ -1,56 +0,0 @@ - User::with('roles')->latest()->paginate(20)]); - } - - public function create() - { - return view('stylist-theme-admin::administration.users.create', ['roles' => Role::orderBy('name')->pluck('name')]); - } - - public function store(Request $request) - { - $data = $request->validate(['name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', 'unique:users,email'], 'password' => ['required', 'string', 'min:8', 'confirmed'], 'roles' => ['array'], 'roles.*' => ['string', 'exists:roles,name']]); - $user = User::create($data); - $user->syncRoles($data['roles'] ?? []); - - return redirect()->route('administration.users.index')->with('status', 'User created.'); - } - - public function edit(User $user) - { - return view('stylist-theme-admin::administration.users.edit', ['user' => $user->load('roles'), 'roles' => Role::orderBy('name')->pluck('name')]); - } - - public function update(Request $request, User $user) - { - $data = $request->validate(['name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($user)], 'password' => ['nullable', 'string', 'min:8', 'confirmed'], 'roles' => ['array'], 'roles.*' => ['string', 'exists:roles,name']]); - if (blank($data['password'] ?? null)) { - unset($data['password']); - } - $user->update($data); - $user->syncRoles($data['roles'] ?? []); - - return redirect()->route('administration.users.index')->with('status', 'User updated.'); - } - - public function destroy(User $user) - { - abort_if($user->is(auth()->user()), 422, 'You cannot delete your own account.'); - $user->delete(); - - return redirect()->route('administration.users.index')->with('status', 'User deleted.'); - } -} diff --git a/Users/Providers/RouteServiceProvider.php b/Users/Providers/RouteServiceProvider.php deleted file mode 100644 index 89f776a..0000000 --- a/Users/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,17 +0,0 @@ -routes(function (): void { - Route::middleware('web')->group(module_path('Users', 'Routes/web.php')); - }); - } -} diff --git a/Users/Providers/UsersServiceProvider.php b/Users/Providers/UsersServiceProvider.php deleted file mode 100644 index 96446ad..0000000 --- a/Users/Providers/UsersServiceProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -app->register(RouteServiceProvider::class); - $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations')); - $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower()); - } -} diff --git a/Users/Routes/web.php b/Users/Routes/web.php deleted file mode 100644 index d4d0dc8..0000000 --- a/Users/Routes/web.php +++ /dev/null @@ -1,8 +0,0 @@ -prefix('administration/users')->name('administration.users.')->group(function (): void { - Route::resource('', UserController::class)->parameters(['' => 'user'])->except(['show']); -}); diff --git a/Users/Support/ModuleInfo.php b/Users/Support/ModuleInfo.php deleted file mode 100644 index 6122d1c..0000000 --- a/Users/Support/ModuleInfo.php +++ /dev/null @@ -1,16 +0,0 @@ -toBe('Users'); -}); diff --git a/Users/composer.json b/Users/composer.json deleted file mode 100644 index cbcc957..0000000 --- a/Users/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "absdev/users-module", - "description": "Users foundation module for the ABSDEV Laravel application platform.", - "type": "laravel-module", - "autoload": { - "psr-4": { - "Modules\\Users\\": "" - } - } -} diff --git a/Users/module.json b/Users/module.json deleted file mode 100644 index fb2c657..0000000 --- a/Users/module.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Users", - "alias": "users", - "version": "1.0.0", - "description": "Users foundation module for the ABSDEV Laravel application platform.", - "keywords": [ - "laravel", - "modules", - "users" - ], - "priority": 10, - "providers": [ - "Modules\\Users\\Providers\\UsersServiceProvider" - ], - "aliases": {}, - "files": [], - "requires": [ - "Core" - ] -} diff --git a/Workflow/Concerns/HasWorkflow.php b/Workflow/Concerns/HasWorkflow.php deleted file mode 100644 index d018a87..0000000 --- a/Workflow/Concerns/HasWorkflow.php +++ /dev/null @@ -1,19 +0,0 @@ -morphOne(WorkflowInstance::class, 'subject'); - } - - public function workflowLabel(): string - { - return (string) ($this->title ?? $this->name ?? class_basename($this).' #'.$this->getKey()); - } -} diff --git a/Workflow/Config/config.php b/Workflow/Config/config.php deleted file mode 100644 index c61a662..0000000 --- a/Workflow/Config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - 'Workflow', 'default_due_days' => 3]; diff --git a/Workflow/Contracts/Workflowable.php b/Workflow/Contracts/Workflowable.php deleted file mode 100644 index 9d76383..0000000 --- a/Workflow/Contracts/Workflowable.php +++ /dev/null @@ -1,12 +0,0 @@ -id(); - $t->string('name'); - $t->string('slug')->unique(); - $t->text('description')->nullable(); - $t->string('subject_type')->nullable()->index(); - $t->boolean('is_active')->default(true); - $t->timestamps(); - }); - Schema::create('workflow_stages', function (Blueprint $t) { - $t->id(); - $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete(); - $t->string('name'); - $t->string('slug'); - $t->unsignedInteger('position')->default(0); - $t->string('required_permission')->nullable(); - $t->boolean('is_initial')->default(false); - $t->boolean('is_terminal')->default(false); - $t->boolean('allows_rejection')->default(true); - $t->unsignedInteger('due_days')->nullable(); - $t->timestamps(); - $t->unique(['workflow_definition_id', 'slug']); - }); - Schema::create('workflow_transitions', function (Blueprint $t) { - $t->id(); - $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete(); - $t->foreignId('from_stage_id')->constrained('workflow_stages')->cascadeOnDelete(); - $t->foreignId('to_stage_id')->constrained('workflow_stages')->cascadeOnDelete(); - $t->string('name'); - $t->string('action'); - $t->string('required_permission')->nullable(); - $t->boolean('requires_comment')->default(false); - $t->timestamps(); - }); - Schema::create('workflow_instances', function (Blueprint $t) { - $t->id(); - $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete(); - $t->foreignId('workflow_stage_id')->constrained('workflow_stages'); - $t->string('subject_type'); - $t->unsignedBigInteger('subject_id'); - $t->string('status')->default('active'); - $t->foreignId('started_by')->nullable()->constrained('users')->nullOnDelete(); - $t->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete(); - $t->timestamp('due_at')->nullable(); - $t->timestamp('completed_at')->nullable(); - $t->timestamps(); - $t->unique(['subject_type', 'subject_id']); - $t->index(['status', 'workflow_stage_id']); - }); - Schema::create('workflow_history', function (Blueprint $t) { - $t->id(); - $t->foreignId('workflow_instance_id')->constrained()->cascadeOnDelete(); - $t->foreignId('from_stage_id')->nullable()->constrained('workflow_stages')->nullOnDelete(); - $t->foreignId('to_stage_id')->nullable()->constrained('workflow_stages')->nullOnDelete(); - $t->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); - $t->string('action'); - $t->text('comment')->nullable(); - $t->json('metadata')->nullable(); - $t->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('workflow_history'); - Schema::dropIfExists('workflow_instances'); - Schema::dropIfExists('workflow_transitions'); - Schema::dropIfExists('workflow_stages'); - Schema::dropIfExists('workflow_definitions'); - } -}; diff --git a/Workflow/Http/Controllers/WorkflowController.php b/Workflow/Http/Controllers/WorkflowController.php deleted file mode 100644 index 62479b1..0000000 --- a/Workflow/Http/Controllers/WorkflowController.php +++ /dev/null @@ -1,44 +0,0 @@ -user()->can('workflow.view'), 403); - - return view('workflow::admin.index', ['definitions' => WorkflowDefinition::with('stages')->latest()->get(), 'queue' => WorkflowInstance::with(['definition', 'stage', 'subject'])->where('status', 'active')->latest()->paginate(20), 'completed' => WorkflowInstance::where('status', 'completed')->count()]); - } - - public function store(Request $request): RedirectResponse - { - abort_unless($request->user()->can('workflow.manage'), 403); - $data = $request->validate(['name' => 'required|string|max:120', 'description' => 'nullable|string', 'subject_type' => 'nullable|string|max:255']); - $definition = WorkflowDefinition::create($data + ['slug' => Str::slug($data['name']), 'is_active' => true]); - $draft = $definition->stages()->create(['name' => 'Draft', 'slug' => 'draft', 'position' => 0, 'is_initial' => true, 'due_days' => null]); - $review = $definition->stages()->create(['name' => 'Review', 'slug' => 'review', 'position' => 10, 'required_permission' => 'workflow.review', 'due_days' => 3]); - $published = $definition->stages()->create(['name' => 'Approved', 'slug' => 'approved', 'position' => 20, 'is_terminal' => true, 'allows_rejection' => false]); - $definition->transitions()->createMany([['from_stage_id' => $draft->id, 'to_stage_id' => $review->id, 'name' => 'Submit for review', 'action' => 'submit'], ['from_stage_id' => $review->id, 'to_stage_id' => $published->id, 'name' => 'Approve', 'action' => 'approve', 'required_permission' => 'workflow.review'], ['from_stage_id' => $review->id, 'to_stage_id' => $draft->id, 'name' => 'Request changes', 'action' => 'reject', 'required_permission' => 'workflow.review', 'requires_comment' => true]]); - - return back()->with('success', 'Workflow created.'); - } - - public function transition(Request $request, WorkflowInstance $instance, WorkflowManager $manager): RedirectResponse - { - abort_unless($request->user()->can('workflow.review'), 403); - $data = $request->validate(['action' => 'required|string', 'comment' => 'nullable|string|max:2000']); - $manager->transition($instance, $data['action'], $request->user(), $data['comment'] ?? null); - - return back()->with('success', 'Workflow updated.'); - } -} diff --git a/Workflow/Models/WorkflowDefinition.php b/Workflow/Models/WorkflowDefinition.php deleted file mode 100644 index 2316842..0000000 --- a/Workflow/Models/WorkflowDefinition.php +++ /dev/null @@ -1,26 +0,0 @@ - 'boolean']; - } - - public function stages(): HasMany - { - return $this->hasMany(WorkflowStage::class)->orderBy('position'); - } - - public function transitions(): HasMany - { - return $this->hasMany(WorkflowTransition::class); - } -} diff --git a/Workflow/Models/WorkflowHistory.php b/Workflow/Models/WorkflowHistory.php deleted file mode 100644 index 2b8786f..0000000 --- a/Workflow/Models/WorkflowHistory.php +++ /dev/null @@ -1,17 +0,0 @@ - 'array']; - } -} diff --git a/Workflow/Models/WorkflowInstance.php b/Workflow/Models/WorkflowInstance.php deleted file mode 100644 index b3d1af4..0000000 --- a/Workflow/Models/WorkflowInstance.php +++ /dev/null @@ -1,38 +0,0 @@ - 'datetime', 'completed_at' => 'datetime']; - } - - public function definition(): BelongsTo - { - return $this->belongsTo(WorkflowDefinition::class, 'workflow_definition_id'); - } - - public function stage(): BelongsTo - { - return $this->belongsTo(WorkflowStage::class, 'workflow_stage_id'); - } - - public function subject(): MorphTo - { - return $this->morphTo(); - } - - public function history(): HasMany - { - return $this->hasMany(WorkflowHistory::class)->latest(); - } -} diff --git a/Workflow/Models/WorkflowStage.php b/Workflow/Models/WorkflowStage.php deleted file mode 100644 index 3d8a8ad..0000000 --- a/Workflow/Models/WorkflowStage.php +++ /dev/null @@ -1,21 +0,0 @@ - 'boolean', 'is_terminal' => 'boolean', 'allows_rejection' => 'boolean']; - } - - public function definition(): BelongsTo - { - return $this->belongsTo(WorkflowDefinition::class, 'workflow_definition_id'); - } -} diff --git a/Workflow/Models/WorkflowTransition.php b/Workflow/Models/WorkflowTransition.php deleted file mode 100644 index 66bb87f..0000000 --- a/Workflow/Models/WorkflowTransition.php +++ /dev/null @@ -1,15 +0,0 @@ - 'boolean']; - } -} diff --git a/Workflow/Providers/RouteServiceProvider.php b/Workflow/Providers/RouteServiceProvider.php deleted file mode 100644 index 4197f68..0000000 --- a/Workflow/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Workflow', 'Routes/web.php')); - } -} diff --git a/Workflow/Providers/WorkflowServiceProvider.php b/Workflow/Providers/WorkflowServiceProvider.php deleted file mode 100644 index ab10b81..0000000 --- a/Workflow/Providers/WorkflowServiceProvider.php +++ /dev/null @@ -1,22 +0,0 @@ -mergeConfigFrom(module_path('Workflow', 'Config/config.php'), 'workflow'); - $this->app->singleton(WorkflowManager::class); - $this->app->register(RouteServiceProvider::class); - } - - public function boot(): void - { - $this->loadMigrationsFrom(module_path('Workflow', 'Database/Migrations')); - $this->loadViewsFrom(module_path('Workflow', 'Resources/views'), 'workflow'); - } -} diff --git a/Workflow/Resources/views/admin/index.blade.php b/Workflow/Resources/views/admin/index.blade.php deleted file mode 100644 index 673cde3..0000000 --- a/Workflow/Resources/views/admin/index.blade.php +++ /dev/null @@ -1 +0,0 @@ -
WorkflowApproval definitions, active reviews and completed work.
Definitions{{ $definitions->count() }}
Awaiting action{{ $queue->total() }}
Completed{{ $completed }}
Review queue
@forelse($queue as $item)
{{ method_exists($item->subject,'workflowLabel') ? $item->subject->workflowLabel() : class_basename($item->subject_type).' #'.$item->subject_id }}
{{ $item->definition->name }} · {{ $item->stage->name }} @if($item->due_at) · due {{ $item->due_at->diffForHumans() }} @endif
@csrfApprove
@csrfChanges
@emptyNo items are awaiting review.@endforelse
{{ $queue->links() }}
Create workflow
@csrfCreate
@foreach($definitions as $definition)
{{ $definition->name }}
{{ $definition->stages->pluck('name')->join(' → ') }}
@endforeach
diff --git a/Workflow/Routes/web.php b/Workflow/Routes/web.php deleted file mode 100644 index 6653370..0000000 --- a/Workflow/Routes/web.php +++ /dev/null @@ -1,10 +0,0 @@ -prefix('administration/workflows')->name('administration.workflows.')->group(function () { - Route::get('/', [WorkflowController::class, 'index'])->name('index'); - Route::post('/', [WorkflowController::class, 'store'])->name('store'); - Route::post('/{instance}/transition', [WorkflowController::class, 'transition'])->name('transition'); -}); diff --git a/Workflow/Services/WorkflowManager.php b/Workflow/Services/WorkflowManager.php deleted file mode 100644 index 7b8dc1b..0000000 --- a/Workflow/Services/WorkflowManager.php +++ /dev/null @@ -1,46 +0,0 @@ -stages()->where('is_initial', true)->first() ?? $definition->stages()->firstOrFail(); - $instance = WorkflowInstance::updateOrCreate(['subject_type' => $subject->getMorphClass(), 'subject_id' => $subject->getKey()], ['workflow_definition_id' => $definition->getKey(), 'workflow_stage_id' => $stage->getKey(), 'status' => $stage->is_terminal ? 'completed' : 'active', 'started_by' => $user?->getAuthIdentifier(), 'due_at' => $stage->due_days ? now()->addDays($stage->due_days) : null, 'completed_at' => $stage->is_terminal ? now() : null]); - WorkflowHistory::create(['workflow_instance_id' => $instance->id, 'to_stage_id' => $stage->id, 'user_id' => $user?->getAuthIdentifier(), 'action' => 'started']); - - return $instance->fresh(['definition', 'stage']); - }); - } - - public function transition(WorkflowInstance $instance, string $action, ?Authenticatable $user = null, ?string $comment = null): WorkflowInstance - { - return DB::transaction(function () use ($instance, $action, $user, $comment) { - $transition = WorkflowTransition::where('workflow_definition_id', $instance->workflow_definition_id)->where('from_stage_id', $instance->workflow_stage_id)->where('action', $action)->first(); - if (! $transition) { - throw ValidationException::withMessages(['action' => 'This transition is not available.']); - } if ($transition->requires_comment && blank($comment)) { - throw ValidationException::withMessages(['comment' => 'A comment is required.']); - } if ($transition->required_permission && (! $user || ! method_exists($user, 'can') || ! $user->can($transition->required_permission))) { - abort(403); - } $target = WorkflowStage::findOrFail($transition->to_stage_id); - $from = $instance->workflow_stage_id; - $instance->update(['workflow_stage_id' => $target->id, 'status' => $target->is_terminal ? 'completed' : 'active', 'due_at' => $target->due_days ? now()->addDays($target->due_days) : null, 'completed_at' => $target->is_terminal ? now() : null]); - WorkflowHistory::create(['workflow_instance_id' => $instance->id, 'from_stage_id' => $from, 'to_stage_id' => $target->id, 'user_id' => $user?->getAuthIdentifier(), 'action' => $action, 'comment' => $comment]); - - return $instance->fresh(['definition', 'stage', 'subject']); - }); - } -} diff --git a/Workflow/Tests/Feature/ModuleBootTest.php b/Workflow/Tests/Feature/ModuleBootTest.php deleted file mode 100644 index e489747..0000000 --- a/Workflow/Tests/Feature/ModuleBootTest.php +++ /dev/null @@ -1,14 +0,0 @@ -assertTrue(app()->providerIsLoaded(WorkflowServiceProvider::class)); - } -} diff --git a/Workflow/module.json b/Workflow/module.json deleted file mode 100644 index 2a022f0..0000000 --- a/Workflow/module.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "Workflow", - "alias": "workflow", - "description": "Reusable approval workflows, review queues, transitions and audit history.", - "keywords": ["workflow", "approval", "review"], - "priority": 35, - "providers": [ - "Modules\\Workflow\\Providers\\WorkflowServiceProvider" - ], - "files": [] -}