themes how part of site settings
This commit is contained in:
@@ -15,7 +15,7 @@ final class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
return view('stylist-theme-admin::dashboard', [
|
||||
return stylist_backend_view(stylist()->backend()->entryView(), [
|
||||
'userCount' => User::count(),
|
||||
'roleCount' => Schema::hasTable('roles') ? Role::count() : 0,
|
||||
'mediaCount' => Schema::hasTable('media') ? Media::count() : 0,
|
||||
|
||||
@@ -5,23 +5,25 @@ namespace Modules\Settings\Http\Controllers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Settings\Services\SettingsRepository;
|
||||
use Modules\Stylist\Theme\Loader;
|
||||
|
||||
final class SettingController extends Controller
|
||||
{
|
||||
public function index(SettingsRepository $settings): View
|
||||
public function index(SettingsRepository $settings, Loader $themes): View
|
||||
{
|
||||
return view('settings::index', [
|
||||
'values' => $settings->site(),
|
||||
'sections' => $this->sections(),
|
||||
'sections' => $this->sections($themes),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, SettingsRepository $settings): RedirectResponse
|
||||
public function update(Request $request, SettingsRepository $settings, Loader $themes): RedirectResponse
|
||||
{
|
||||
$rules = [];
|
||||
foreach ($this->sections() as $fields) {
|
||||
foreach ($this->sections($themes) as $fields) {
|
||||
foreach ($fields as $key => $definition) {
|
||||
$rules["settings.$key"] = $definition['rules'];
|
||||
}
|
||||
@@ -32,17 +34,43 @@ final class SettingController extends Controller
|
||||
$settings->put($key, $value, 'site', 'string', true);
|
||||
}
|
||||
|
||||
return back()->with('status', 'Site settings saved. The frontend has been updated everywhere.');
|
||||
return back()->with('status', 'Site settings saved. The active frontend and backend themes have been updated.');
|
||||
}
|
||||
|
||||
/** @return array<string, array<string, array<string, mixed>>> */
|
||||
private function sections(): 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'],
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
@if (! empty($field['help']))<span class="mt-1 block text-xs text-zinc-500">{{ $field['help'] }}</span>@endif
|
||||
@if (($field['type'] ?? 'text') === 'textarea')
|
||||
<textarea name="settings[{{ $key }}]" rows="4" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">{{ old("settings.$key", $values[$key] ?? '') }}</textarea>
|
||||
@elseif (($field['type'] ?? 'text') === 'select')
|
||||
<select name="settings[{{ $key }}]" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">
|
||||
@foreach (($field['options'] ?? []) as $optionValue => $optionLabel)
|
||||
<option value="{{ $optionValue }}" @selected(old("settings.$key", $values[$key] ?? '') === $optionValue)>{{ $optionLabel }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
<input type="{{ $field['type'] ?? 'text' }}" name="settings[{{ $key }}]" value="{{ old("settings.$key", $values[$key] ?? '') }}" placeholder="{{ $field['placeholder'] ?? '' }}" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">
|
||||
@endif
|
||||
|
||||
@@ -14,17 +14,21 @@ final class SettingsRepository
|
||||
return $default;
|
||||
}
|
||||
|
||||
/** @var array{value: ?string, type: string}|null $setting */
|
||||
$setting = Cache::rememberForever(
|
||||
"settings.$group.$key",
|
||||
fn (): ?Setting => Setting::query()
|
||||
"settings.value.v2.$group.$key",
|
||||
fn (): ?array => Setting::query()
|
||||
->where([
|
||||
'group' => $group,
|
||||
'key' => $key,
|
||||
])
|
||||
->first()
|
||||
->first(['value', 'type'])
|
||||
?->only(['value', 'type'])
|
||||
);
|
||||
|
||||
return $setting ? $this->decode($setting->value, $setting->type) : $default;
|
||||
return $setting !== null
|
||||
? $this->decode($setting['value'], $setting['type'])
|
||||
: $default;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
@@ -70,6 +74,8 @@ final class SettingsRepository
|
||||
'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'));
|
||||
@@ -86,7 +92,9 @@ final class SettingsRepository
|
||||
]
|
||||
);
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Modules\Settings\Services\SettingsRepository;
|
||||
use Modules\Stylist\Theme\Stylist;
|
||||
|
||||
it('stores separate active frontend and backend themes in site settings', function (): void {
|
||||
$settings = app(SettingsRepository::class);
|
||||
|
||||
$settings->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'));
|
||||
});
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
return [
|
||||
'default_theme' => 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.
|
||||
|
||||
@@ -35,6 +35,20 @@ if (! function_exists('stylist_theme_view')) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
namespace Modules\Stylist\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Modules\Settings\Services\SettingsRepository;
|
||||
|
||||
final class TenantThemeResolver
|
||||
{
|
||||
public function resolve(): string
|
||||
public function __construct(private readonly SettingsRepository $settings) {}
|
||||
|
||||
public function resolve(?string $context = null): string
|
||||
{
|
||||
$context ??= $this->context();
|
||||
$column = config('stylist.tenant_theme_column', 'theme_key');
|
||||
$user = Auth::user();
|
||||
$company = $user?->company ?? null;
|
||||
@@ -23,7 +27,26 @@ final class TenantThemeResolver
|
||||
}
|
||||
}
|
||||
|
||||
return config('stylist.default_theme', 'default');
|
||||
$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
|
||||
|
||||
@@ -10,13 +10,15 @@ final class Stylist
|
||||
{
|
||||
public function __construct(private readonly Loader $loader, private readonly TenantThemeResolver $resolver, private readonly ThemeSettings $settings) {}
|
||||
|
||||
public function current(): Theme
|
||||
public function current(?string $context = null): Theme
|
||||
{
|
||||
$requested = $this->resolver->resolve();
|
||||
$requested = $this->resolver->resolve($context);
|
||||
try {
|
||||
return $this->loader->find($requested);
|
||||
} catch (ThemeNotFoundException) {
|
||||
$fallback = (string) config('stylist.default_theme', 'default');
|
||||
$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);
|
||||
}
|
||||
@@ -24,6 +26,16 @@ final class Stylist
|
||||
}
|
||||
}
|
||||
|
||||
public function frontend(): Theme
|
||||
{
|
||||
return $this->current('frontend');
|
||||
}
|
||||
|
||||
public function backend(): Theme
|
||||
{
|
||||
return $this->current('backend');
|
||||
}
|
||||
|
||||
public function variables(): array
|
||||
{
|
||||
$theme = $this->current();
|
||||
|
||||
@@ -64,6 +64,11 @@ final class Theme
|
||||
return (string) ($this->meta['description'] ?? '');
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return (string) ($this->meta['type'] ?? 'frontend');
|
||||
}
|
||||
|
||||
public function author(): string
|
||||
{
|
||||
return (string) ($this->meta['author'] ?? '');
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
"name": "Corporate",
|
||||
"slug": "corporate",
|
||||
"version": "1.0.0",
|
||||
"parent": "common"
|
||||
"parent": "common",
|
||||
"type": "backend"
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ use Illuminate\Support\Facades\Route;
|
||||
use Modules\Administration\Http\Controllers\DashboardController;
|
||||
|
||||
Route::get('/', function () {
|
||||
return stylist_theme_view('saasland', 'home');
|
||||
return stylist_frontend_view(stylist()->frontend()->entryView());
|
||||
})->name('home');
|
||||
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
|
||||
Reference in New Issue
Block a user