themes how part of site settings

This commit is contained in:
Mark Gregory
2026-07-28 12:43:19 +01:00
parent eafc5e8063
commit f834328044
12 changed files with 144 additions and 18 deletions
@@ -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'));
});