phase 7
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
namespace Modules\Blog\Models;
|
||||
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Modules\Authors\Models\Author; use Modules\Categories\Models\Category; use Modules\Tags\Models\Tag; use Modules\Workflow\Concerns\HasWorkflow; use Modules\Localization\Concerns\HasTranslations;
|
||||
final class Blog extends Model { use HasTranslations, HasWorkflow; protected $table='blog_posts'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'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())); } }
|
||||
use Modules\Sites\Concerns\BelongsToSites;
|
||||
final class Blog extends Model { use BelongsToSites, HasTranslations, HasWorkflow; protected $table='blog_posts'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'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())); } }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
namespace Modules\News\Models;
|
||||
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Modules\Authors\Models\Author; use Modules\Categories\Models\Category; use Modules\Tags\Models\Tag; use Modules\Workflow\Concerns\HasWorkflow; use Modules\Localization\Concerns\HasTranslations;
|
||||
final class News extends Model { use HasTranslations, HasWorkflow; protected $table='news_articles'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'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())); } }
|
||||
use Modules\Sites\Concerns\BelongsToSites;
|
||||
final class News extends Model { use BelongsToSites, HasTranslations, HasWorkflow; protected $table='news_articles'; protected $fillable=['author_id','category_id','title','slug','excerpt','content','featured_image','status','is_featured','published_at','expires_at','meta_title','meta_description']; protected function casts(): array { return ['is_featured'=>'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())); } }
|
||||
|
||||
@@ -7,10 +7,11 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Modules\Workflow\Concerns\HasWorkflow;
|
||||
use Modules\Localization\Concerns\HasTranslations;
|
||||
use Modules\Sites\Concerns\BelongsToSites;
|
||||
|
||||
final class Page extends Model
|
||||
{
|
||||
use HasFactory, HasTranslations, HasWorkflow, SoftDeletes;
|
||||
use BelongsToSites, HasFactory, HasTranslations, HasWorkflow, SoftDeletes;
|
||||
protected $table = 'pages';
|
||||
protected $fillable = ['title', 'slug', 'summary', 'content', 'status', 'published_at', 'is_home'];
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ final class RolePermissionSeeder extends Seeder
|
||||
'workflow.review',
|
||||
'localization.manage',
|
||||
'localization.translate',
|
||||
'sites.view',
|
||||
'sites.manage',
|
||||
'sites.assign',
|
||||
];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Concerns;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Modules\Sites\Models\Site;
|
||||
use Modules\Sites\Services\SiteManager;
|
||||
trait BelongsToSites
|
||||
{
|
||||
public function sites(): MorphToMany
|
||||
{
|
||||
return $this->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]]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
return [
|
||||
'fallback_to_default' => true,
|
||||
'session_key' => 'platform_site_id',
|
||||
'central_domains' => array_values(array_filter(explode(',', (string) env('CENTRAL_DOMAINS', '')))),
|
||||
'content_types' => [
|
||||
'pages' => Modules\Pages\Models\Page::class,
|
||||
'blog' => Modules\Blog\Models\Blog::class,
|
||||
'news' => Modules\News\Models\News::class,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Contracts;
|
||||
interface SiteAwareModule
|
||||
{
|
||||
public function supportsMultisite(): bool;
|
||||
public function sharedAcrossSites(): bool;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sites', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Database\Seeders;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Modules\Sites\Models\Site;
|
||||
final class SiteSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$host = parse_url((string) config('app.url'), PHP_URL_HOST) ?: 'localhost';
|
||||
$site = Site::query()->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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Http\Controllers;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Sites\Models\Site;
|
||||
use Modules\Sites\Models\SiteDomain;
|
||||
final class DomainController extends Controller
|
||||
{
|
||||
public function store(Request $request, Site $site): RedirectResponse
|
||||
{
|
||||
$data=$request->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.'); }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Http\Controllers;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Sites\Models\Site;
|
||||
final class SiteController extends Controller
|
||||
{
|
||||
public function index(): View { return view('sites::admin.sites.index', ['sites'=>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')];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Http\Middleware;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Sites\Services\SiteManager;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
final class ResolveSite
|
||||
{
|
||||
public function __construct(private readonly SiteManager $sites) {}
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$site = $this->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Models;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
final class Site extends Model
|
||||
{
|
||||
protected $fillable = ['name','key','status','theme','default_locale','timezone','is_default'];
|
||||
protected function casts(): array { return ['is_default' => '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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
final class SiteContent extends Model
|
||||
{
|
||||
protected $table = 'site_content';
|
||||
protected $fillable = ['site_id','content_type','content_id','is_primary'];
|
||||
protected function casts(): array { return ['is_primary'=>'boolean']; }
|
||||
public function site(): BelongsTo { return $this->belongsTo(Site::class); }
|
||||
public function content(): MorphTo { return $this->morphTo(__FUNCTION__, 'content_type', 'content_id'); }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
final class SiteDomain extends Model
|
||||
{
|
||||
protected $fillable = ['site_id','domain','is_primary','redirect_to_primary','force_https','is_active'];
|
||||
protected function casts(): array { return ['is_primary'=>'boolean','redirect_to_primary'=>'boolean','force_https'=>'boolean','is_active'=>'boolean']; }
|
||||
public function site(): BelongsTo { return $this->belongsTo(Site::class); }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
final class SiteSetting extends Model
|
||||
{
|
||||
protected $fillable = ['site_id','group','key','value','type'];
|
||||
public function site(): BelongsTo { return $this->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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Providers;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void { parent::boot(); Route::middleware('web')->group(module_path('Sites', 'Routes/web.php')); }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Providers;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Sites\Services\SiteManager;
|
||||
final class SitesServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="$site->exists ? __('Edit site') : __('New site')"><div class="max-w-5xl space-y-6"><div><flux:heading size="xl">{{ $site->exists ? 'Edit site' : 'New site' }}</flux:heading><flux:text>Configure the site identity, theme and regional defaults.</flux:text></div><form method="POST" action="{{ $site->exists ? route('administration.sites.update',$site) : route('administration.sites.store') }}" class="space-y-5 rounded-xl border border-zinc-200 p-6 dark:border-zinc-700">@csrf @if($site->exists) @method('PUT') @endif<div class="grid gap-4 md:grid-cols-2"><flux:input name="name" label="Name" value="{{ old('name',$site->name) }}" required/><flux:input name="key" label="Key" value="{{ old('key',$site->key) }}" required/><flux:select name="status" label="Status"><option value="active" @selected(old('status',$site->status)==='active')>Active</option><option value="inactive" @selected(old('status',$site->status)==='inactive')>Inactive</option><option value="maintenance" @selected(old('status',$site->status)==='maintenance')>Maintenance</option></flux:select><flux:input name="theme" label="Theme" value="{{ old('theme',$site->theme) }}"/><flux:input name="default_locale" label="Default locale" value="{{ old('default_locale',$site->default_locale ?: 'en') }}" required/><flux:input name="timezone" label="Timezone" value="{{ old('timezone',$site->timezone ?: config('app.timezone')) }}" required/></div><flux:checkbox name="is_default" value="1" :checked="old('is_default',$site->is_default)">Default site</flux:checkbox><div class="flex gap-2"><flux:button type="submit" variant="primary">Save site</flux:button><flux:button :href="route('administration.sites.index')" wire:navigate>Cancel</flux:button></div></form>@if($site->exists)<div class="rounded-xl border border-zinc-200 p-6 dark:border-zinc-700"><flux:heading size="lg">Domains</flux:heading><div class="mt-4 space-y-3">@foreach($site->domains as $domain)<div class="flex items-center justify-between rounded-lg border border-zinc-200 p-3 dark:border-zinc-700"><div><span class="font-medium">{{ $domain->domain }}</span> @if($domain->is_primary)<flux:badge size="sm">Primary</flux:badge>@endif</div><form method="POST" action="{{ route('administration.sites.domains.destroy',[$site,$domain]) }}">@csrf @method('DELETE')<flux:button type="submit" size="sm" variant="danger">Remove</flux:button></form></div>@endforeach</div><form method="POST" action="{{ route('administration.sites.domains.store',$site) }}" class="mt-5 grid gap-3 md:grid-cols-2">@csrf<flux:input name="domain" label="Domain" placeholder="example.com" required/><div class="flex flex-wrap items-end gap-4"><flux:checkbox name="is_primary" value="1">Primary</flux:checkbox><flux:checkbox name="force_https" value="1" checked>Force HTTPS</flux:checkbox><flux:checkbox name="is_active" value="1" checked>Active</flux:checkbox></div><div><flux:button type="submit">Add domain</flux:button></div></form></div>@endif</div></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Sites')"><div class="flex h-full w-full flex-1 flex-col gap-6 rounded-xl"><div class="flex items-center justify-between"><div><flux:heading size="xl">Sites</flux:heading><flux:text>Manage websites, domains, themes and regional defaults.</flux:text></div><flux:button variant="primary" icon="plus" :href="route('administration.sites.create')" wire:navigate>New site</flux:button></div><div class="overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-700"><table class="w-full text-sm"><thead class="bg-zinc-50 dark:bg-zinc-900"><tr><th class="p-3 text-left">Site</th><th class="p-3 text-left">Domains</th><th class="p-3 text-left">Theme</th><th class="p-3 text-left">Status</th><th class="p-3"></th></tr></thead><tbody>@foreach($sites as $site)<tr class="border-t border-zinc-200 dark:border-zinc-700"><td class="p-3 font-medium">{{ $site->name }} @if($site->is_default)<flux:badge size="sm">Default</flux:badge>@endif</td><td class="p-3">{{ $site->domains->pluck('domain')->join(', ') ?: 'No domains' }}</td><td class="p-3">{{ $site->theme ?: 'Inherited' }}</td><td class="p-3"><flux:badge :color="$site->status === 'active' ? 'green' : 'zinc'">{{ ucfirst($site->status) }}</flux:badge></td><td class="p-3 text-right"><flux:button size="sm" :href="route('administration.sites.edit',$site)" wire:navigate>Edit</flux:button></td></tr>@endforeach</tbody></table></div>{{ $sites->links() }}</div></x-layouts.app>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Sites\Http\Controllers\DomainController;
|
||||
use Modules\Sites\Http\Controllers\SiteController;
|
||||
Route::middleware(['auth','verified'])->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');
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Services;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Modules\Sites\Models\Site;
|
||||
final class SiteManager
|
||||
{
|
||||
private ?Site $current = null;
|
||||
public function current(): ?Site { return $this->current; }
|
||||
public function setCurrent(?Site $site): void { $this->current = $site; if ($site) { app()->instance(Site::class, $site); } }
|
||||
public function all(): Collection { return Site::query()->active()->orderByDesc('is_default')->orderBy('name')->get(); }
|
||||
public function resolve(Request $request): ?Site
|
||||
{
|
||||
$host = strtolower(preg_replace('/:\d+$/', '', $request->getHost()));
|
||||
$site = Site::query()->with(['domains','settings'])->whereHas('domains', fn ($q) => $q->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace Modules\Sites\Tests\Feature;
|
||||
use Tests\TestCase;
|
||||
final class SitesModuleBootTest extends TestCase
|
||||
{
|
||||
public function test_sites_module_routes_are_registered(): void
|
||||
{
|
||||
$this->assertTrue(app('router')->has('administration.sites.index'));
|
||||
$this->assertTrue(app('router')->has('administration.sites.domains.store'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
+1
-1
@@ -12,7 +12,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->web(append: [\Modules\Localization\Http\Middleware\SetLocale::class]);
|
||||
$middleware->web(append: [\Modules\Sites\Http\Middleware\ResolveSite::class, \Modules\Localization\Http\Middleware\SetLocale::class]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
$exceptions->shouldRenderJsonWhen(
|
||||
|
||||
@@ -32,5 +32,6 @@
|
||||
"Careers": true,
|
||||
"Locations": true,
|
||||
"Workflow": true,
|
||||
"Localization": true
|
||||
"Localization": true,
|
||||
"Sites": true
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<flux:sidebar.item icon="user-group" :href="route('administration.team.index')" :current="request()->routeIs('administration.team.*')" wire:navigate>{{ __('Team') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="chat-bubble-left-right" :href="route('administration.testimonials.index')" :current="request()->routeIs('administration.testimonials.*')" wire:navigate>{{ __('Testimonials') }}</flux:sidebar.item>
|
||||
</flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Platform')" class="grid"><flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:sidebar.item><flux:sidebar.item icon="squares-2x2" :href="route('administration.dashboard')" :current="request()->routeIs('administration.dashboard')" wire:navigate>{{ __('Administration') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Platform')" class="grid"><flux:sidebar.item icon="globe-alt" :href="route('administration.sites.index')" :current="request()->routeIs('administration.sites.*')" wire:navigate>{{ __('Sites') }}</flux:sidebar.item><flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:sidebar.item><flux:sidebar.item icon="squares-2x2" :href="route('administration.dashboard')" :current="request()->routeIs('administration.dashboard')" wire:navigate>{{ __('Administration') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Workflow')" class="grid"><flux:sidebar.item icon="arrows-right-left" :href="route('administration.workflows.index')" :current="request()->routeIs('administration.workflows.*')" wire:navigate>{{ __('Review Queue') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Management')" class="grid"><flux:sidebar.item icon="users" :href="route('administration.users.index')" :current="request()->routeIs('administration.users.*')" wire:navigate>{{ __('Users') }}</flux:sidebar.item><flux:sidebar.item icon="shield-check" :href="route('administration.roles.index')" :current="request()->routeIs('administration.roles.*')" wire:navigate>{{ __('Roles & Permissions') }}</flux:sidebar.item><flux:sidebar.item icon="photo" :href="route('administration.media.index')" :current="request()->routeIs('administration.media.*')" wire:navigate>{{ __('Media') }}</flux:sidebar.item><flux:sidebar.item icon="magnifying-glass" :href="route('administration.search.index')" :current="request()->routeIs('administration.search.*')" wire:navigate>{{ __('Search') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Configuration')" class="grid"><flux:sidebar.item icon="language" :href="route('administration.localization.index')" :current="request()->routeIs('administration.localization.*')" wire:navigate>{{ __('Localisation') }}</flux:sidebar.item><flux:sidebar.item icon="globe-alt" :href="route('administration.seo.index')" :current="request()->routeIs('administration.seo.*')" wire:navigate>{{ __('SEO') }}</flux:sidebar.item><flux:sidebar.item icon="cog-6-tooth" :href="route('administration.settings.index')" :current="request()->routeIs('administration.settings.*')" wire:navigate>{{ __('Settings') }}</flux:sidebar.item><flux:sidebar.item icon="cube" :href="route('administration.modules.index')" :current="request()->routeIs('administration.modules.*')" wire:navigate>{{ __('Modules') }}</flux:sidebar.item><flux:sidebar.item icon="clipboard-document-list" :href="route('administration.audit.index')" :current="request()->routeIs('administration.audit.*')" wire:navigate>{{ __('Audit Log') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
|
||||
Reference in New Issue
Block a user