Implemented a block editor for changing the layout of the page

This commit is contained in:
2026-04-04 01:17:05 +02:00
parent 0800f7664f
commit ff58e82497
41 changed files with 2706 additions and 298 deletions

View File

@@ -0,0 +1,485 @@
@php
use Illuminate\Support\Carbon;
$blockEditorState = $page->blocks->map(function ($b) {
$content = $b->content ?? [];
if ($b->type === 'countdown' && ! empty($content['target_datetime'])) {
try {
$content['target_datetime'] = Carbon::parse($content['target_datetime'])
->timezone(config('app.timezone'))
->format('Y-m-d\TH:i');
} catch (\Throwable) {
}
}
return [
'uid' => 'b'.$b->id,
'type' => $b->type,
'sort_order' => $b->sort_order,
'is_visible' => $b->is_visible,
'content' => $content,
];
})->values()->all();
$blockTypesMeta = [
['type' => 'hero', 'label' => __('Hero-sectie'), 'hint' => __('Kop & subkop')],
['type' => 'image', 'label' => __('Afbeelding'), 'hint' => __('Logo of beeld, optionele link')],
['type' => 'benefits', 'label' => __('Voordelen'), 'hint' => __('Lijst met USPs')],
['type' => 'social_proof', 'label' => __('Social proof'), 'hint' => __('Telleraanmeldingen')],
['type' => 'form', 'label' => __('Registratieformulier'), 'hint' => __('Aanmeldformulier')],
['type' => 'countdown', 'label' => __('Afteltimer'), 'hint' => __('Aftellen naar datum')],
['type' => 'text', 'label' => __('Tekst'), 'hint' => __('Vrije tekst')],
['type' => 'cta_banner', 'label' => __('CTA-banner'), 'hint' => __('Knop + link')],
['type' => 'divider', 'label' => __('Scheiding'), 'hint' => __('Lijn of ruimte')],
];
$benefitIcons = ['ticket', 'clock', 'mail', 'users', 'star', 'heart', 'gift', 'music', 'shield', 'check'];
@endphp
<div
x-data="pageBlockEditor({
initialBlocks: @js($blockEditorState),
blockTypes: @js($blockTypesMeta),
storageBase: @js(asset('storage'))
})"
>
<h2 id="page-blocks-heading" class="text-lg font-semibold text-slate-900">{{ __('Pagina-inhoud (blokken)') }}</h2>
<p class="mt-1 text-sm text-slate-600">{{ __('Sleep blokken om de volgorde te wijzigen. Klik op een blok om het te openen of te sluiten. Het oog-icoon verbergt een blok op de publieke pagina.') }}</p>
<div class="mt-4 flex flex-wrap items-center gap-2">
<div class="relative" x-data="{ open: false }">
<button
type="button"
class="inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 shadow-sm hover:bg-slate-50"
@click="open = !open"
>
{{ __('+ Blok toevoegen') }}
<span class="text-slate-400" x-text="open ? '▲' : '▼'"></span>
</button>
<div
x-show="open"
x-cloak
@click.outside="open = false"
class="absolute left-0 z-20 mt-2 w-full max-w-md rounded-lg border border-slate-200 bg-white py-1 shadow-lg"
>
<template x-for="bt in blockTypes" :key="bt.type">
<button
type="button"
class="flex w-full flex-col items-start gap-0.5 px-4 py-3 text-left text-sm hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50"
@click="addBlock(bt.type); open = false"
x-bind:disabled="bt.type === 'form' && hasFormBlock()"
>
<span class="font-medium text-slate-900" x-text="bt.label"></span>
<span class="text-xs text-slate-500" x-text="bt.hint"></span>
</button>
</template>
</div>
</div>
<button type="button" class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50" @click="expandAll()">
{{ __('Alles uitklappen') }}
</button>
<button type="button" class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50" @click="collapseAll()">
{{ __('Alles inklappen') }}
</button>
</div>
<div class="mt-6 flex flex-col gap-4" x-ref="sortRoot">
<template x-for="block in blocks" :key="block.uid">
<div
class="overflow-hidden rounded-xl border border-slate-200 bg-slate-50/80 shadow-sm"
:class="{ 'opacity-50': !block.is_visible }"
:data-block-uid="block.uid"
>
<input type="hidden" :name="`blocks[${block.uid}][type]`" :value="block.type" />
<input type="hidden" :name="`blocks[${block.uid}][sort_order]`" :value="block.sort_order" />
<input type="hidden" :name="`blocks[${block.uid}][is_visible]`" :value="block.is_visible ? 1 : 0" />
<div
class="flex items-start gap-2 bg-white px-3 py-2"
:class="isCollapsed(block.uid) ? 'rounded-xl' : 'rounded-t-xl border-b border-slate-200'"
>
<button type="button" class="block-drag-handle mt-2 cursor-grab touch-pan-y px-1 text-slate-400 hover:text-slate-600" title="{{ __('Sleep') }}"></button>
<button
type="button"
class="min-w-0 flex-1 rounded-lg py-0.5 text-left text-sm font-semibold text-slate-800 hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
@click="toggleCollapsed(block.uid)"
:aria-expanded="!isCollapsed(block.uid)"
>
<span class="flex items-start gap-2">
<span class="mt-0.5 w-4 shrink-0 text-center text-xs text-slate-500" x-text="isCollapsed(block.uid) ? '▶' : '▼'" aria-hidden="true"></span>
<span class="min-w-0 flex-1">
<span class="block" x-text="blockTypes.find(t => t.type === block.type)?.label || block.type"></span>
<span
x-show="isCollapsed(block.uid) && blockSummary(block) !== ''"
x-cloak
class="mt-0.5 block max-w-full truncate text-xs font-normal text-slate-500"
x-text="blockSummary(block)"
></span>
</span>
</span>
</button>
<div class="flex shrink-0 items-center gap-0.5 self-center">
<button type="button" class="p-1 text-slate-500 hover:text-slate-800" :title="block.is_visible ? '{{ __('Verbergen op publieke pagina') }}' : '{{ __('Tonen') }}'" @click="block.is_visible = !block.is_visible">
<span x-text="block.is_visible ? '👁' : '🚫'"></span>
</button>
<button type="button" class="p-1 text-red-600 hover:text-red-800" @click="requestDelete(block.uid)"></button>
</div>
</div>
<div x-show="!isCollapsed(block.uid)" class="space-y-4 p-4">
{{-- Hero --}}
<div x-show="block.type === 'hero'" class="grid gap-4">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Kop (headline)') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][headline]`" x-model="block.content.headline" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" maxlength="255" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Subkop') }}</label>
<textarea :name="`blocks[${block.uid}][content][subheadline]`" x-model="block.content.subheadline" rows="3" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm"></textarea>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Eyebrow / label') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][eyebrow_text]`" x-model="block.content.eyebrow_text" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Eyebrow-stijl') }}</label>
<select :name="`blocks[${block.uid}][content][eyebrow_style]`" x-model="block.content.eyebrow_style" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="badge">badge</option>
<option value="text">text</option>
<option value="none">none</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Tekstuitlijning') }}</label>
<select :name="`blocks[${block.uid}][content][text_alignment]`" x-model="block.content.text_alignment" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="center">center</option>
<option value="left">left</option>
<option value="right">right</option>
</select>
</div>
</div>
{{-- Image --}}
<div x-show="block.type === 'image'" class="grid gap-4">
<template x-if="block.content.image && block.content.image !== ''">
<div class="flex flex-wrap items-center gap-3">
<img :src="storageBase + '/' + block.content.image" alt="" class="h-16 w-auto max-w-[12rem] rounded border border-slate-200 bg-white object-contain p-1" />
<label class="inline-flex items-center gap-2 text-sm text-red-700">
<input type="checkbox" :name="`blocks[${block.uid}][remove_block_image]`" value="1" />
{{ __('Afbeelding verwijderen') }}
</label>
</div>
</template>
<input type="hidden" :name="`blocks[${block.uid}][content][image]`" :value="block.content.image || ''" />
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Afbeelding uploaden') }}</label>
<input type="file" :name="`blocks[${block.uid}][block_image]`" accept="image/jpeg,image/png,image/webp,image/svg+xml,.svg" class="mt-1 block w-full text-sm text-slate-600" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Link-URL (optioneel, bij klik)') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][link_url]`" x-model="block.content.link_url" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" placeholder="https://…" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Alt-tekst') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][alt]`" x-model="block.content.alt" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Max. breedte (px)') }}</label>
<input type="number" min="48" max="800" :name="`blocks[${block.uid}][content][max_width_px]`" x-model.number="block.content.max_width_px" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Uitlijning') }}</label>
<select :name="`blocks[${block.uid}][content][text_alignment]`" x-model="block.content.text_alignment" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="center">center</option>
<option value="left">left</option>
<option value="right">right</option>
</select>
</div>
</div>
</div>
{{-- Benefits --}}
<div x-show="block.type === 'benefits'" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Titel') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][title]`" x-model="block.content.title" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Layout') }}</label>
<select :name="`blocks[${block.uid}][content][layout]`" x-model="block.content.layout" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="list">list</option>
<option value="grid">grid</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Max. kolommen') }}</label>
<select :name="`blocks[${block.uid}][content][max_columns]`" x-model.number="block.content.max_columns" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</div>
</div>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-slate-700">{{ __('Items') }}</span>
<button type="button" class="text-sm font-medium text-indigo-600 hover:text-indigo-500" @click="addBenefitItem(block.uid)">+ {{ __('Voordeel') }}</button>
</div>
<template x-for="(item, idx) in block.content.items" :key="idx">
<div class="flex flex-wrap items-end gap-2 rounded-lg border border-slate-200 bg-white p-3">
<div class="min-w-[8rem] flex-1">
<label class="text-xs text-slate-500">{{ __('Icoon') }}</label>
<select :name="`blocks[${block.uid}][content][items][${idx}][icon]`" x-model="item.icon" class="mt-0.5 block w-full rounded border-slate-300 text-sm">
@foreach ($benefitIcons as $ic)
<option value="{{ $ic }}">{{ $ic }}</option>
@endforeach
</select>
</div>
<div class="min-w-[12rem] flex-[2]">
<label class="text-xs text-slate-500">{{ __('Tekst') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][items][${idx}][text]`" x-model="item.text" class="mt-0.5 block w-full rounded border-slate-300 text-sm" />
</div>
<button type="button" class="mb-0.5 text-sm text-red-600 hover:text-red-800" @click="removeBenefitItem(block.uid, idx)">{{ __('Verwijderen') }}</button>
</div>
</template>
</div>
{{-- Social proof --}}
<div x-show="block.type === 'social_proof'" class="grid gap-4">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Sjabloon (gebruik {count})') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][template]`" x-model="block.content.template" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Minimum aantal (alleen tonen als ≥)') }}</label>
<input type="number" min="0" :name="`blocks[${block.uid}][content][min_count]`" x-model.number="block.content.min_count" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<label class="inline-flex items-center gap-2 text-sm text-slate-700">
<input type="hidden" :name="`blocks[${block.uid}][content][show_animation]`" :value="block.content.show_animation ? 1 : 0" />
<input type="checkbox" @change="block.content.show_animation = $event.target.checked" :checked="block.content.show_animation" />
{{ __('Animatie') }}
</label>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Stijl') }}</label>
<select :name="`blocks[${block.uid}][content][style]`" x-model="block.content.style" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="pill">pill</option>
<option value="badge">badge</option>
<option value="plain">plain</option>
</select>
</div>
</div>
{{-- Form --}}
<div x-show="block.type === 'form'" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Titel boven formulier') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][title]`" x-model="block.content.title" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Beschrijving') }}</label>
<textarea :name="`blocks[${block.uid}][content][description]`" x-model="block.content.description" rows="2" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm"></textarea>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Knoptekst') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][button_label]`" x-model="block.content.button_label" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Knopkleur') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][button_color]`" x-model="block.content.button_color" class="mt-1 block w-full rounded-lg border-slate-300 font-mono text-sm shadow-sm" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Knoptekstkleur') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][button_text_color]`" x-model="block.content.button_text_color" class="mt-1 block w-full rounded-lg border-slate-300 font-mono text-sm shadow-sm" />
</div>
<label class="inline-flex items-center gap-2 text-sm text-slate-700">
<input type="hidden" :name="`blocks[${block.uid}][content][show_field_icons]`" :value="block.content.show_field_icons ? 1 : 0" />
<input type="checkbox" @change="block.content.show_field_icons = $event.target.checked" :checked="block.content.show_field_icons" />
{{ __('Iconen bij velden') }}
</label>
<p class="text-xs text-slate-500">{{ __('Voornaam, achternaam en e-mail blijven verplicht ingeschakeld voor de database.') }}</p>
<template x-for="fk in ['first_name','last_name','email','phone']" :key="fk">
<div class="rounded-lg border border-slate-200 bg-white p-3">
<p class="text-sm font-medium capitalize text-slate-800" x-text="fk.replace('_',' ')"></p>
<template x-if="fk === 'phone'">
<label class="mt-2 inline-flex items-center gap-2 text-sm text-slate-700">
<input type="hidden" :name="`blocks[${block.uid}][content][fields][phone][enabled]`" :value="block.content.fields.phone.enabled ? 1 : 0" />
<input type="checkbox" @change="block.content.fields.phone.enabled = $event.target.checked" :checked="block.content.fields.phone.enabled" />
{{ __('Telefoonveld tonen') }}
</label>
</template>
<template x-if="fk !== 'phone'">
<input type="hidden" :name="`blocks[${block.uid}][content][fields][${fk}][enabled]`" value="1" />
<input type="hidden" :name="`blocks[${block.uid}][content][fields][${fk}][required]`" value="1" />
</template>
<div class="mt-2 grid gap-2 sm:grid-cols-2">
<div>
<label class="text-xs text-slate-500">{{ __('Label') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][fields][${fk}][label]`" x-model="block.content.fields[fk].label" class="mt-0.5 block w-full rounded border-slate-300 text-sm" />
</div>
<div>
<label class="text-xs text-slate-500">{{ __('Placeholder') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][fields][${fk}][placeholder]`" x-model="block.content.fields[fk].placeholder" class="mt-0.5 block w-full rounded border-slate-300 text-sm" />
</div>
</div>
<template x-if="fk === 'phone'">
<label class="mt-2 inline-flex items-center gap-2 text-sm text-slate-700">
<input type="hidden" :name="`blocks[${block.uid}][content][fields][phone][required]`" :value="block.content.fields.phone.required ? 1 : 0" />
<input type="checkbox" @change="block.content.fields.phone.required = $event.target.checked" :checked="block.content.fields.phone.required" />
{{ __('Telefoon verplicht') }}
</label>
</template>
</div>
</template>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Privacytekst') }}</label>
<textarea :name="`blocks[${block.uid}][content][privacy_text]`" x-model="block.content.privacy_text" rows="2" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Privacy-URL') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][privacy_url]`" x-model="block.content.privacy_url" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" placeholder="https://…" />
</div>
</div>
{{-- Countdown --}}
<div x-show="block.type === 'countdown'" class="grid gap-4">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Doeldatum / -tijd') }}</label>
<input type="datetime-local" :name="`blocks[${block.uid}][content][target_datetime]`" x-model="block.content.target_datetime" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Titel') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][title]`" x-model="block.content.title" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Na afloop') }}</label>
<select :name="`blocks[${block.uid}][content][expired_action]`" x-model="block.content.expired_action" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="hide">hide</option>
<option value="show_message">show_message</option>
<option value="reload">reload</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Bericht (bij show_message)') }}</label>
<textarea :name="`blocks[${block.uid}][content][expired_message]`" x-model="block.content.expired_message" rows="2" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Stijl') }}</label>
<select :name="`blocks[${block.uid}][content][style]`" x-model="block.content.style" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="large">large</option>
<option value="compact">compact</option>
</select>
</div>
<label class="inline-flex items-center gap-2 text-sm text-slate-700">
<input type="hidden" :name="`blocks[${block.uid}][content][show_labels]`" :value="block.content.show_labels ? 1 : 0" />
<input type="checkbox" @change="block.content.show_labels = $event.target.checked" :checked="block.content.show_labels" />
{{ __('Labels tonen') }}
</label>
@foreach (['days' => 'dagen', 'hours' => 'uren', 'minutes' => 'minuten', 'seconds' => 'seconden'] as $lk => $lab)
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Label :key', ['key' => $lk]) }}</label>
<input
type="text"
:name="`blocks[${block.uid}][content][labels][{{ $lk }}]`"
x-model="block.content.labels['{{ $lk }}']"
class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm"
/>
</div>
@endforeach
</div>
{{-- Text --}}
<div x-show="block.type === 'text'" class="grid gap-2">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Titel') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][title]`" x-model="block.content.title" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Tekst') }}</label>
<textarea :name="`blocks[${block.uid}][content][body]`" x-model="block.content.body" rows="6" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Tekstgrootte') }}</label>
<select :name="`blocks[${block.uid}][content][text_size]`" x-model="block.content.text_size" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="sm">sm</option>
<option value="base">base</option>
<option value="lg">lg</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Uitlijning') }}</label>
<select :name="`blocks[${block.uid}][content][text_alignment]`" x-model="block.content.text_alignment" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="center">center</option>
<option value="left">left</option>
<option value="right">right</option>
</select>
</div>
</div>
{{-- CTA --}}
<div x-show="block.type === 'cta_banner'" class="grid gap-4">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Tekst') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][text]`" x-model="block.content.text" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Knoptekst') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][button_label]`" x-model="block.content.button_label" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Knop-URL') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][button_url]`" x-model="block.content.button_url" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Knopkleur') }}</label>
<input type="text" :name="`blocks[${block.uid}][content][button_color]`" x-model="block.content.button_color" class="mt-1 block w-full rounded-lg border-slate-300 font-mono text-sm shadow-sm" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Stijl') }}</label>
<select :name="`blocks[${block.uid}][content][style]`" x-model="block.content.style" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="inline">inline</option>
<option value="stacked">stacked</option>
</select>
</div>
</div>
{{-- Divider --}}
<div x-show="block.type === 'divider'" class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Stijl') }}</label>
<select :name="`blocks[${block.uid}][content][style]`" x-model="block.content.style" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="line">line</option>
<option value="dots">dots</option>
<option value="space_only">space_only</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">{{ __('Ruimte') }}</label>
<select :name="`blocks[${block.uid}][content][spacing]`" x-model="block.content.spacing" class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm">
<option value="small">small</option>
<option value="medium">medium</option>
<option value="large">large</option>
</select>
</div>
</div>
</div>
<div
x-show="confirmDeleteUid === block.uid"
x-cloak
class="border-t border-red-100 bg-red-50 px-4 py-3 text-sm text-red-900"
>
<p>{{ __('Dit blok verwijderen?') }}</p>
<div class="mt-2 flex gap-2">
<button type="button" class="rounded bg-red-600 px-3 py-1.5 font-medium text-white hover:bg-red-500" @click="removeBlock(block.uid)">{{ __('Verwijderen') }}</button>
<button type="button" class="rounded border border-slate-300 bg-white px-3 py-1.5 font-medium text-slate-700 hover:bg-slate-50" @click="cancelDelete()">{{ __('Annuleren') }}</button>
</div>
</div>
</div>
</template>
</div>
</div>

View File

@@ -1,6 +1,11 @@
@php
/** @var \App\Models\PreregistrationPage|null $page */
use Illuminate\Support\Facades\Storage;
$page = $page ?? null;
$pageBgUrl = $page !== null && filled($page->background_image)
? Storage::disk('public')->url($page->background_image)
: null;
@endphp
<div class="grid max-w-3xl gap-6">
@@ -13,24 +18,6 @@
@enderror
</div>
<div>
<label for="heading" class="block text-sm font-medium text-slate-700">{{ __('Heading') }}</label>
<input type="text" name="heading" id="heading" value="{{ old('heading', $page?->heading) }}" required maxlength="255"
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
@error('heading')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="intro_text" class="block text-sm font-medium text-slate-700">{{ __('Intro text') }}</label>
<textarea name="intro_text" id="intro_text" rows="4"
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">{{ old('intro_text', $page?->intro_text) }}</textarea>
@error('intro_text')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="thank_you_message" class="block text-sm font-medium text-slate-700">{{ __('Thank you message') }}</label>
<textarea name="thank_you_message" id="thank_you_message" rows="3"
@@ -54,11 +41,65 @@
<input type="text" name="ticketshop_url" id="ticketshop_url" inputmode="url" autocomplete="url"
value="{{ old('ticketshop_url', $page?->ticketshop_url) }}"
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="https://…" />
<p class="mt-1 text-xs text-slate-500">{{ __('Shown as the main button when the registration period has ended (and as the CTA link for the CTA banner block in that state).') }}</p>
@error('ticketshop_url')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="post_submit_redirect_url" class="block text-sm font-medium text-slate-700">{{ __('Redirect URL (after registration)') }}</label>
<input type="text" name="post_submit_redirect_url" id="post_submit_redirect_url" inputmode="url" autocomplete="url"
value="{{ old('post_submit_redirect_url', $page?->post_submit_redirect_url) }}"
class="mt-1 block w-full rounded-lg border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="https://…" />
<p class="mt-1 text-xs text-slate-500">{{ __('Optional. Visitors are sent here 5 seconds after a successful registration.') }}</p>
@error('post_submit_redirect_url')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="rounded-xl border border-slate-200 bg-slate-50/80 p-4">
<p class="text-sm font-semibold text-slate-800">{{ __('Full-page background') }}</p>
<p class="mt-1 text-xs text-slate-600">{{ __('Cover image behind the card on the public page.') }}</p>
@if ($pageBgUrl !== null)
<div class="mt-3 flex flex-wrap items-center gap-3">
<img src="{{ e($pageBgUrl) }}" alt="" class="h-20 w-32 rounded border border-slate-200 object-cover" />
<label class="inline-flex items-center gap-2 text-sm text-red-700">
<input type="checkbox" name="remove_page_background" value="1" @checked(old('remove_page_background')) />
{{ __('Remove background image') }}
</label>
</div>
@endif
<div class="mt-3">
<label for="page_background" class="block text-sm font-medium text-slate-700">{{ __('Upload background') }}</label>
<input type="file" name="page_background" id="page_background" accept="image/jpeg,image/png,image/webp"
class="mt-1 block w-full text-sm text-slate-600" />
@error('page_background')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="mt-4 grid gap-4 sm:grid-cols-2">
<div>
<label for="background_overlay_color" class="block text-sm font-medium text-slate-700">{{ __('Overlay colour') }}</label>
<input type="text" name="background_overlay_color" id="background_overlay_color"
value="{{ old('background_overlay_color', $page?->background_overlay_color ?? '#000000') }}"
class="mt-1 block w-full rounded-lg border-slate-300 font-mono text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
@error('background_overlay_color')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="background_overlay_opacity" class="block text-sm font-medium text-slate-700">{{ __('Overlay opacity (%)') }}</label>
<input type="number" name="background_overlay_opacity" id="background_overlay_opacity" min="0" max="100"
value="{{ old('background_overlay_opacity', $page?->background_overlay_opacity ?? 50) }}"
class="mt-1 block w-full rounded-lg border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
@error('background_overlay_opacity')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
</div>
<div class="grid gap-6 sm:grid-cols-2">
<div>
<label for="start_date" class="block text-sm font-medium text-slate-700">{{ __('Start date') }}</label>
@@ -80,42 +121,11 @@
</div>
</div>
<div class="flex flex-wrap gap-6">
<label class="inline-flex items-center gap-2">
<input type="checkbox" name="phone_enabled" value="1" class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
@checked(old('phone_enabled', $page?->phone_enabled ?? false)) />
<span class="text-sm font-medium text-slate-700">{{ __('Phone enabled') }}</span>
</label>
<div>
<label class="inline-flex items-center gap-2">
<input type="checkbox" name="is_active" value="1" class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
@checked(old('is_active', $page?->is_active ?? true)) />
<span class="text-sm font-medium text-slate-700">{{ __('Active') }}</span>
</label>
</div>
<div>
<label for="background_image" class="block text-sm font-medium text-slate-700">{{ __('Background image') }}</label>
<p class="mt-0.5 text-xs text-slate-500">{{ __('JPG, PNG or WebP. Max 5 MB.') }}</p>
<input type="file" name="background_image" id="background_image" accept="image/jpeg,image/png,image/webp"
class="mt-1 block w-full text-sm text-slate-600 file:mr-4 file:rounded-lg file:border-0 file:bg-slate-100 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-slate-700 hover:file:bg-slate-200" />
@error('background_image')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
@if ($page?->background_image)
<p class="mt-2 text-xs text-slate-600">{{ __('Current file:') }} <a href="/storage/{{ $page->background_image }}" class="text-indigo-600 hover:underline" target="_blank" rel="noopener">{{ __('View') }}</a></p>
@endif
</div>
<div>
<label for="logo_image" class="block text-sm font-medium text-slate-700">{{ __('Logo image') }}</label>
<p class="mt-0.5 text-xs text-slate-500">{{ __('JPG, PNG, WebP or SVG. Max 2 MB.') }}</p>
<input type="file" name="logo_image" id="logo_image" accept="image/jpeg,image/png,image/webp,image/svg+xml,.svg"
class="mt-1 block w-full text-sm text-slate-600 file:mr-4 file:rounded-lg file:border-0 file:bg-slate-100 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-slate-700 hover:file:bg-slate-200" />
@error('logo_image')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
@if ($page?->logo_image)
<p class="mt-2 text-xs text-slate-600">{{ __('Current file:') }} <a href="/storage/{{ $page->logo_image }}" class="text-indigo-600 hover:underline" target="_blank" rel="noopener">{{ __('View') }}</a></p>
@endif
</div>
</div>

View File

@@ -0,0 +1,13 @@
@if (session('status'))
<div
class="mb-6 flex items-start gap-3 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 shadow-sm"
role="status"
>
<span class="mt-0.5 shrink-0 text-emerald-600" aria-hidden="true">
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</span>
<p class="min-w-0 flex-1 font-medium leading-snug">{{ session('status') }}</p>
</div>
@endif

View File

@@ -12,10 +12,12 @@
<p class="mt-1 text-sm text-slate-600">{{ __('After saving, use the pages list to copy the public URL.') }}</p>
</div>
<form action="{{ route('admin.pages.store') }}" method="post" enctype="multipart/form-data" class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8" novalidate>
@include('admin.pages._save_flash')
<form action="{{ route('admin.pages.store') }}" method="post" enctype="multipart/form-data" class="space-y-8" novalidate>
@csrf
@if ($errors->any())
<div class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" role="alert">
<div class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" role="alert">
<p class="font-medium">{{ __('Please fix the following:') }}</p>
<ul class="mt-2 list-inside list-disc space-y-1">
@foreach ($errors->all() as $message)
@@ -24,12 +26,20 @@
</ul>
</div>
@endif
@include('admin.pages._form', ['page' => null])
<div class="mt-8 flex gap-3">
<section class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8" aria-labelledby="page-settings-heading">
<h2 id="page-settings-heading" class="text-lg font-semibold text-slate-900">{{ __('Page settings') }}</h2>
<p class="mt-1 text-sm text-slate-600">{{ __('Title, dates, messages, and background for this pre-registration page.') }}</p>
<div class="mt-6">
@include('admin.pages._form', ['page' => null])
</div>
</section>
<div class="flex gap-3">
<button type="submit" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
{{ __('Create page') }}
</button>
<a href="{{ route('admin.pages.index') }}" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50">{{ __('Cancel') }}</a>
<a href="{{ route('admin.pages.index') }}" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 shadow-sm hover:bg-slate-50">{{ __('Cancel') }}</a>
</div>
</form>
</div>

View File

@@ -5,7 +5,7 @@
@section('mobile_title', __('Edit page'))
@section('content')
<div class="mx-auto max-w-3xl">
<div class="mx-auto max-w-4xl">
<div class="mb-8">
<a href="{{ route('admin.pages.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500"> {{ __('Back to pages') }}</a>
<h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Edit page') }}</h1>
@@ -19,11 +19,13 @@
@endcan
</div>
<form action="{{ route('admin.pages.update', $page) }}" method="post" enctype="multipart/form-data" class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8" novalidate>
@include('admin.pages._save_flash')
<form action="{{ route('admin.pages.update', $page) }}" method="post" enctype="multipart/form-data" class="space-y-8" novalidate>
@csrf
@method('PUT')
@if ($errors->any())
<div class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" role="alert">
<div class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" role="alert">
<p class="font-medium">{{ __('Please fix the following:') }}</p>
<ul class="mt-2 list-inside list-disc space-y-1">
@foreach ($errors->all() as $message)
@@ -32,12 +34,24 @@
</ul>
</div>
@endif
@include('admin.pages._form', ['page' => $page])
<div class="mt-8 flex gap-3">
<section class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8" aria-labelledby="page-settings-heading">
<h2 id="page-settings-heading" class="text-lg font-semibold text-slate-900">{{ __('Page settings') }}</h2>
<p class="mt-1 text-sm text-slate-600">{{ __('Title, dates, messages, and background for this pre-registration page.') }}</p>
<div class="mt-6">
@include('admin.pages._form', ['page' => $page])
</div>
</section>
<section class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8" aria-labelledby="page-blocks-heading">
@include('admin.pages._blocks_editor', ['page' => $page])
</section>
<div class="flex gap-3">
<button type="submit" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
{{ __('Save changes') }}
</button>
<a href="{{ route('admin.pages.index') }}" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50">{{ __('Cancel') }}</a>
<a href="{{ route('admin.pages.index') }}" class="rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 shadow-sm hover:bg-slate-50">{{ __('Cancel') }}</a>
</div>
</form>
</div>