feat: optional fixed viewport background on public pages

Adds background_fixed column, admin checkbox, fixed-position layers on the public layout, Dutch strings, and store tests.

Made-with: Cursor
This commit is contained in:
2026-04-04 13:36:26 +02:00
parent 2603288881
commit 5a67827c23
7 changed files with 72 additions and 4 deletions

View File

@@ -23,6 +23,7 @@ trait ValidatesPreregistrationPageInput
'post_submit_redirect_url' => ['nullable', 'string', 'url:http,https', 'max:500'], 'post_submit_redirect_url' => ['nullable', 'string', 'url:http,https', 'max:500'],
'background_overlay_color' => ['nullable', 'string', 'regex:/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/'], 'background_overlay_color' => ['nullable', 'string', 'regex:/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/'],
'background_overlay_opacity' => ['nullable', 'integer', 'min:0', 'max:100'], 'background_overlay_opacity' => ['nullable', 'integer', 'min:0', 'max:100'],
'background_fixed' => ['sometimes', 'boolean'],
'page_background' => ['nullable', 'file', 'image', 'mimes:jpeg,png,jpg,webp', 'max:5120'], 'page_background' => ['nullable', 'file', 'image', 'mimes:jpeg,png,jpg,webp', 'max:5120'],
'remove_page_background' => ['sometimes', 'boolean'], 'remove_page_background' => ['sometimes', 'boolean'],
'start_date' => ['required', 'date'], 'start_date' => ['required', 'date'],
@@ -69,6 +70,7 @@ trait ValidatesPreregistrationPageInput
$this->merge([ $this->merge([
'is_active' => $this->boolean('is_active'), 'is_active' => $this->boolean('is_active'),
'background_fixed' => $this->boolean('background_fixed'),
'remove_page_background' => $this->boolean('remove_page_background'), 'remove_page_background' => $this->boolean('remove_page_background'),
'ticketshop_url' => $ticketshopNormalized, 'ticketshop_url' => $ticketshopNormalized,
'post_submit_redirect_url' => $redirectNormalized, 'post_submit_redirect_url' => $redirectNormalized,

View File

@@ -32,6 +32,7 @@ class PreregistrationPage extends Model
'background_image', 'background_image',
'background_overlay_color', 'background_overlay_color',
'background_overlay_opacity', 'background_overlay_opacity',
'background_fixed',
'logo_image', 'logo_image',
'is_active', 'is_active',
]; ];
@@ -42,6 +43,7 @@ class PreregistrationPage extends Model
'start_date' => 'datetime', 'start_date' => 'datetime',
'end_date' => 'datetime', 'end_date' => 'datetime',
'phone_enabled' => 'boolean', 'phone_enabled' => 'boolean',
'background_fixed' => 'boolean',
'is_active' => 'boolean', 'is_active' => 'boolean',
]; ];
} }

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
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::table('preregistration_pages', function (Blueprint $table): void {
$table->boolean('background_fixed')->default(false)->after('background_overlay_opacity');
});
}
public function down(): void
{
Schema::table('preregistration_pages', function (Blueprint $table): void {
$table->dropColumn('background_fixed');
});
}
};

View File

@@ -22,5 +22,7 @@
"Subscriber removed.": "Abonnee verwijderd.", "Subscriber removed.": "Abonnee verwijderd.",
"Delete this subscriber? This cannot be undone.": "Deze abonnee verwijderen? Dit kan niet ongedaan worden gemaakt.", "Delete this subscriber? This cannot be undone.": "Deze abonnee verwijderen? Dit kan niet ongedaan worden gemaakt.",
"Remove": "Verwijderen", "Remove": "Verwijderen",
"Actions": "Acties" "Actions": "Acties",
"Fix background to viewport": "Achtergrond vastzetten op het scherm",
"When enabled, the background image and overlay stay fixed while visitors scroll long content.": "Als dit aan staat, blijven de achtergrondafbeelding en de overlay stilstaan terwijl bezoekers door lange inhoud scrollen."
} }

View File

@@ -98,6 +98,19 @@
@enderror @enderror
</div> </div>
</div> </div>
<div class="mt-4">
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-slate-800">
<input
type="checkbox"
name="background_fixed"
value="1"
class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
@checked(old('background_fixed', $page?->background_fixed ?? false))
/>
{{ __('Fix background to viewport') }}
</label>
<p class="mt-1 text-xs text-slate-600">{{ __('When enabled, the background image and overlay stay fixed while visitors scroll long content.') }}</p>
</div>
</div> </div>
<div class="grid gap-6 sm:grid-cols-2"> <div class="grid gap-6 sm:grid-cols-2">

View File

@@ -20,6 +20,9 @@
$formButtonLabel = (string) (data_get($formContent, 'button_label') ?: __('public.register_button')); $formButtonLabel = (string) (data_get($formContent, 'button_label') ?: __('public.register_button'));
$formButtonColor = (string) data_get($formContent, 'button_color', '#F47B20'); $formButtonColor = (string) data_get($formContent, 'button_color', '#F47B20');
$formButtonTextColor = (string) data_get($formContent, 'button_text_color', '#FFFFFF'); $formButtonTextColor = (string) data_get($formContent, 'button_text_color', '#FFFFFF');
$bgFixed = $page->background_fixed;
$bgLayerPosition = $bgFixed ? 'fixed inset-0 pointer-events-none z-0' : 'absolute inset-0';
$overlayPosition = $bgFixed ? 'fixed inset-0 pointer-events-none z-[1]' : 'absolute inset-0';
@endphp @endphp
@extends('layouts.public') @extends('layouts.public')
@@ -28,19 +31,19 @@
<div class="relative min-h-screen w-full overflow-x-hidden"> <div class="relative min-h-screen w-full overflow-x-hidden">
@if ($bgUrl !== null) @if ($bgUrl !== null)
<div <div
class="absolute inset-0 bg-cover bg-center bg-no-repeat" class="{{ $bgLayerPosition }} bg-cover bg-center bg-no-repeat"
style="background-image: url('{{ e($bgUrl) }}')" style="background-image: url('{{ e($bgUrl) }}')"
aria-hidden="true" aria-hidden="true"
></div> ></div>
@else @else
<div <div
class="absolute inset-0 bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950" class="{{ $bgLayerPosition }} bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950"
aria-hidden="true" aria-hidden="true"
></div> ></div>
@endif @endif
<div <div
class="absolute inset-0" class="{{ $overlayPosition }}"
style="background-color: {{ e($overlayColor) }}; opacity: {{ $overlayOpacity }}" style="background-color: {{ e($overlayColor) }}; opacity: {{ $overlayOpacity }}"
aria-hidden="true" aria-hidden="true"
></div> ></div>

View File

@@ -32,9 +32,31 @@ class StorePreregistrationPageTest extends TestCase
$page = PreregistrationPage::query()->first(); $page = PreregistrationPage::query()->first();
$response->assertRedirect(route('admin.pages.edit', $page)); $response->assertRedirect(route('admin.pages.edit', $page));
$this->assertSame('Summer Fest', $page?->title); $this->assertSame('Summer Fest', $page?->title);
$this->assertFalse($page?->background_fixed);
$this->assertGreaterThanOrEqual(4, PageBlock::query()->where('preregistration_page_id', $page?->id)->count()); $this->assertGreaterThanOrEqual(4, PageBlock::query()->where('preregistration_page_id', $page?->id)->count());
} }
public function test_store_can_enable_fixed_background(): void
{
$user = User::factory()->create(['role' => 'user']);
$response = $this->actingAs($user)->post(route('admin.pages.store'), [
'title' => 'Winter Fest',
'thank_you_message' => null,
'expired_message' => null,
'ticketshop_url' => null,
'start_date' => '2026-06-01T10:00',
'end_date' => '2026-06-30T18:00',
'is_active' => true,
'background_fixed' => true,
]);
$page = PreregistrationPage::query()->where('title', 'Winter Fest')->first();
$response->assertRedirect(route('admin.pages.edit', $page));
$this->assertNotNull($page);
$this->assertTrue($page->background_fixed);
}
public function test_validation_failure_redirects_back_with_input(): void public function test_validation_failure_redirects_back_with_input(): void
{ {
$user = User::factory()->create(['role' => 'user']); $user = User::factory()->create(['role' => 'user']);