Implemented a block editor for changing the layout of the page
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
'listsUrl' => route('admin.mailwizz.lists'),
|
||||
'fieldsUrl' => route('admin.mailwizz.fields'),
|
||||
'csrf' => csrf_token(),
|
||||
'phoneEnabled' => (bool) $page->phone_enabled,
|
||||
'phoneEnabled' => $page->isPhoneFieldEnabledForSubscribers(),
|
||||
'hasExistingConfig' => $config !== null,
|
||||
'existing' => $existing,
|
||||
'strings' => [
|
||||
|
||||
485
resources/views/admin/pages/_blocks_editor.blade.php
Normal file
485
resources/views/admin/pages/_blocks_editor.blade.php
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
13
resources/views/admin/pages/_save_flash.blade.php
Normal file
13
resources/views/admin/pages/_save_flash.blade.php
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('First name') }}</th>
|
||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Last name') }}</th>
|
||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Email') }}</th>
|
||||
@if ($page->phone_enabled)
|
||||
@if ($page->isPhoneFieldEnabledForSubscribers())
|
||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Phone') }}</th>
|
||||
@endif
|
||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Registered at') }}</th>
|
||||
@@ -62,7 +62,7 @@
|
||||
<td class="px-4 py-3 text-slate-900">{{ $subscriber->first_name }}</td>
|
||||
<td class="px-4 py-3 text-slate-900">{{ $subscriber->last_name }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ $subscriber->email }}</td>
|
||||
@if ($page->phone_enabled)
|
||||
@if ($page->isPhoneFieldEnabledForSubscribers())
|
||||
<td class="px-4 py-3 text-slate-600">{{ $subscriber->phone ?? '—' }}</td>
|
||||
@endif
|
||||
<td class="whitespace-nowrap px-4 py-3 text-slate-600">{{ $subscriber->created_at->timezone(config('app.timezone'))->format('Y-m-d H:i') }}</td>
|
||||
@@ -80,7 +80,7 @@
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ $page->phone_enabled ? 6 : 5 }}" class="px-4 py-12 text-center text-slate-500">
|
||||
<td colspan="{{ $page->isPhoneFieldEnabledForSubscribers() ? 6 : 5 }}" class="px-4 py-12 text-center text-slate-500">
|
||||
{{ __('No subscribers match your criteria.') }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
36
resources/views/components/blocks/benefits.blade.php
Normal file
36
resources/views/components/blocks/benefits.blade.php
Normal file
@@ -0,0 +1,36 @@
|
||||
@props(['block'])
|
||||
|
||||
@php
|
||||
/** @var \App\Models\PageBlock $block */
|
||||
$c = $block->content ?? [];
|
||||
$title = data_get($c, 'title');
|
||||
$items = data_get($c, 'items', []);
|
||||
$layout = (string) data_get($c, 'layout', 'list');
|
||||
$cols = (int) data_get($c, 'max_columns', 2);
|
||||
$gridClass = $layout === 'grid'
|
||||
? match ($cols) {
|
||||
3 => 'sm:grid-cols-3',
|
||||
1 => 'sm:grid-cols-1',
|
||||
default => 'sm:grid-cols-2',
|
||||
}
|
||||
: '';
|
||||
@endphp
|
||||
|
||||
<div class="w-full space-y-4" x-show="phase !== 'thanks'" x-cloak>
|
||||
@if (filled($title))
|
||||
<h2 class="text-center text-lg font-semibold text-white sm:text-xl">{{ $title }}</h2>
|
||||
@endif
|
||||
|
||||
@if (is_array($items) && $items !== [])
|
||||
<ul class="{{ $layout === 'grid' ? 'grid grid-cols-1 gap-3 '.$gridClass : 'space-y-3' }} w-full text-left">
|
||||
@foreach ($items as $item)
|
||||
@if (is_array($item))
|
||||
<li class="flex gap-3 rounded-xl border border-white/15 bg-black/25 px-4 py-3">
|
||||
<x-blocks.icon :name="(string) ($item['icon'] ?? 'check')" class="mt-0.5 text-festival" />
|
||||
<span class="text-sm leading-snug text-white/95 sm:text-[15px]">{{ $item['text'] ?? '' }}</span>
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
82
resources/views/components/blocks/countdown.blade.php
Normal file
82
resources/views/components/blocks/countdown.blade.php
Normal file
@@ -0,0 +1,82 @@
|
||||
@props(['block'])
|
||||
|
||||
@php
|
||||
/** @var \App\Models\PageBlock $block */
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
$c = $block->content ?? [];
|
||||
$rawTarget = data_get($c, 'target_datetime');
|
||||
try {
|
||||
$targetMs = $rawTarget ? (int) (Carbon::parse($rawTarget)->getTimestamp() * 1000) : 0;
|
||||
} catch (\Throwable) {
|
||||
$targetMs = 0;
|
||||
}
|
||||
$title = data_get($c, 'title');
|
||||
$expiredAction = (string) data_get($c, 'expired_action', 'hide');
|
||||
$expiredMessage = (string) data_get($c, 'expired_message', '');
|
||||
$style = (string) data_get($c, 'style', 'large');
|
||||
$showLabels = filter_var(data_get($c, 'show_labels', true), FILTER_VALIDATE_BOOLEAN);
|
||||
$labels = data_get($c, 'labels', []);
|
||||
$labelsArr = is_array($labels) ? $labels : [];
|
||||
$gridClass = $style === 'compact' ? 'gap-2 px-2 py-3 sm:gap-3 sm:px-3' : 'gap-3 px-3 py-4 sm:gap-4 sm:px-4 sm:py-5';
|
||||
$numClass = $style === 'compact' ? 'text-xl sm:text-2xl' : 'text-2xl sm:text-3xl';
|
||||
@endphp
|
||||
|
||||
@if ($targetMs > 0)
|
||||
<div
|
||||
class="w-full"
|
||||
x-show="phase !== 'thanks'"
|
||||
x-cloak
|
||||
x-data="countdownBlock(@js([
|
||||
'targetMs' => $targetMs,
|
||||
'showLabels' => $showLabels,
|
||||
'labels' => [
|
||||
'days' => (string) ($labelsArr['days'] ?? __('day')),
|
||||
'hours' => (string) ($labelsArr['hours'] ?? __('hrs')),
|
||||
'minutes' => (string) ($labelsArr['minutes'] ?? __('mins')),
|
||||
'seconds' => (string) ($labelsArr['seconds'] ?? __('secs')),
|
||||
],
|
||||
'expiredAction' => $expiredAction,
|
||||
'expiredMessage' => $expiredMessage,
|
||||
]))"
|
||||
>
|
||||
<div x-show="!expired" x-cloak>
|
||||
@if (filled($title))
|
||||
<p class="mb-3 text-center text-sm font-medium text-white/90 sm:text-base">{{ $title }}</p>
|
||||
@endif
|
||||
<div
|
||||
class="grid grid-cols-4 rounded-2xl border border-white/15 bg-black/35 text-center shadow-inner {{ $gridClass }}"
|
||||
role="timer"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div>
|
||||
<div class="font-mono font-semibold tabular-nums text-white {{ $numClass }}" x-text="pad(days)"></div>
|
||||
@if ($showLabels)
|
||||
<div class="mt-2 text-[10px] uppercase tracking-wider text-white/65 sm:text-xs" x-text="labels.days"></div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-mono font-semibold tabular-nums text-white {{ $numClass }}" x-text="pad(hours)"></div>
|
||||
@if ($showLabels)
|
||||
<div class="mt-2 text-[10px] uppercase tracking-wider text-white/65 sm:text-xs" x-text="labels.hours"></div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-mono font-semibold tabular-nums text-white {{ $numClass }}" x-text="pad(minutes)"></div>
|
||||
@if ($showLabels)
|
||||
<div class="mt-2 text-[10px] uppercase tracking-wider text-white/65 sm:text-xs" x-text="labels.minutes"></div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-mono font-semibold tabular-nums text-white {{ $numClass }}" x-text="pad(seconds)"></div>
|
||||
@if ($showLabels)
|
||||
<div class="mt-2 text-[10px] uppercase tracking-wider text-white/65 sm:text-xs" x-text="labels.seconds"></div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="expired && expiredAction === 'show_message'" x-cloak>
|
||||
<p class="text-center text-sm text-white/90" x-text="expiredMessage"></p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
37
resources/views/components/blocks/cta-banner.blade.php
Normal file
37
resources/views/components/blocks/cta-banner.blade.php
Normal file
@@ -0,0 +1,37 @@
|
||||
@props([
|
||||
'block',
|
||||
'page' => null,
|
||||
'pageState' => 'active',
|
||||
])
|
||||
|
||||
@php
|
||||
/** @var \App\Models\PageBlock $block */
|
||||
/** @var \App\Models\PreregistrationPage|null $page */
|
||||
$c = $block->content ?? [];
|
||||
$url = (string) data_get($c, 'button_url', '#');
|
||||
if ($pageState === 'expired' && $page !== null && filled($page->ticketshop_url)) {
|
||||
$url = $page->ticketshop_url;
|
||||
}
|
||||
$btnColor = (string) data_get($c, 'button_color', '#F47B20');
|
||||
$style = (string) data_get($c, 'style', 'inline');
|
||||
$flexClass = $style === 'stacked' ? 'flex-col gap-3' : 'flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4';
|
||||
@endphp
|
||||
|
||||
<div class="w-full rounded-2xl border border-white/15 bg-black/35 px-4 py-4 sm:px-5" x-show="phase !== 'thanks'" x-cloak>
|
||||
<div class="flex {{ $flexClass }}">
|
||||
@if (filled(data_get($c, 'text')))
|
||||
<p class="text-center text-sm font-medium text-white sm:text-left sm:text-base">{{ data_get($c, 'text') }}</p>
|
||||
@endif
|
||||
<div class="flex justify-center sm:justify-end">
|
||||
<a
|
||||
href="{{ e($url) }}"
|
||||
class="inline-flex min-h-[48px] items-center justify-center rounded-xl px-6 py-3 text-sm font-bold text-white shadow-lg transition hover:scale-[1.02] hover:brightness-110 focus:outline-none focus:ring-2 focus:ring-white/40"
|
||||
style="background-color: {{ e($btnColor) }};"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ data_get($c, 'button_label') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
27
resources/views/components/blocks/divider.blade.php
Normal file
27
resources/views/components/blocks/divider.blade.php
Normal file
@@ -0,0 +1,27 @@
|
||||
@props(['block'])
|
||||
|
||||
@php
|
||||
/** @var \App\Models\PageBlock $block */
|
||||
$c = $block->content ?? [];
|
||||
$style = (string) data_get($c, 'style', 'line');
|
||||
$spacing = (string) data_get($c, 'spacing', 'medium');
|
||||
$py = match ($spacing) {
|
||||
'small' => 'py-2',
|
||||
'large' => 'py-6',
|
||||
default => 'py-4',
|
||||
};
|
||||
@endphp
|
||||
|
||||
<div class="w-full {{ $py }}" x-show="phase !== 'thanks'" x-cloak>
|
||||
@if ($style === 'space_only')
|
||||
<div class="h-2" aria-hidden="true"></div>
|
||||
@elseif ($style === 'dots')
|
||||
<div class="flex justify-center gap-2" aria-hidden="true">
|
||||
@foreach (range(1, 5) as $_)
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-white/35"></span>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="h-px w-full bg-gradient-to-r from-transparent via-white/25 to-transparent" aria-hidden="true"></div>
|
||||
@endif
|
||||
</div>
|
||||
177
resources/views/components/blocks/form.blade.php
Normal file
177
resources/views/components/blocks/form.blade.php
Normal file
@@ -0,0 +1,177 @@
|
||||
@props([
|
||||
'block',
|
||||
'page',
|
||||
])
|
||||
|
||||
@php
|
||||
/** @var \App\Models\PageBlock $block */
|
||||
/** @var \App\Models\PreregistrationPage $page */
|
||||
$c = $block->content ?? [];
|
||||
$fields = data_get($c, 'fields', []);
|
||||
$showIcons = filter_var(data_get($c, 'show_field_icons', true), FILTER_VALIDATE_BOOLEAN);
|
||||
$btnColor = (string) data_get($c, 'button_color', '#F47B20');
|
||||
$btnText = (string) data_get($c, 'button_text_color', '#FFFFFF');
|
||||
$privacyText = data_get($c, 'privacy_text');
|
||||
$privacyUrl = data_get($c, 'privacy_url');
|
||||
@endphp
|
||||
|
||||
<div class="w-full space-y-4">
|
||||
@if (filled(data_get($c, 'title')))
|
||||
<h2 class="text-center text-xl font-semibold text-white sm:text-2xl">{{ data_get($c, 'title') }}</h2>
|
||||
@endif
|
||||
|
||||
@if (filled(data_get($c, 'description')))
|
||||
<p class="text-center text-sm leading-relaxed text-white/85 sm:text-[15px]">{{ data_get($c, 'description') }}</p>
|
||||
@endif
|
||||
|
||||
<div x-show="phase === 'active'" x-cloak>
|
||||
<form x-ref="form" class="space-y-4" @submit.prevent="submitForm()">
|
||||
<div x-show="formError !== ''" x-cloak class="rounded-xl border border-amber-400/50 bg-amber-500/15 px-4 py-3 text-sm leading-snug text-amber-50" x-text="formError"></div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
@foreach (['first_name', 'last_name'] as $fk)
|
||||
@php
|
||||
$fc = is_array($fields[$fk] ?? null) ? $fields[$fk] : [];
|
||||
$enabled = filter_var($fc['enabled'] ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||
$req = filter_var($fc['required'] ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||
@endphp
|
||||
@if ($enabled)
|
||||
<div>
|
||||
<label for="pf-{{ $fk }}" class="sr-only">{{ $fc['label'] ?? $fk }}</label>
|
||||
<div class="relative">
|
||||
@if ($showIcons)
|
||||
<span class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-white/40" aria-hidden="true">
|
||||
@if ($fk === 'first_name')
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /></svg>
|
||||
@else
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0z" /></svg>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
<input
|
||||
id="pf-{{ $fk }}"
|
||||
type="text"
|
||||
name="{{ $fk }}"
|
||||
autocomplete="{{ $fk === 'first_name' ? 'given-name' : 'family-name' }}"
|
||||
@if ($req) required @endif
|
||||
maxlength="255"
|
||||
x-model="{{ $fk }}"
|
||||
class="min-h-[48px] w-full rounded-xl border border-white/35 bg-white/15 py-3 text-[15px] text-white placeholder-white/55 shadow-sm transition duration-200 ease-out focus:border-festival focus:outline-none focus:ring-2 focus:ring-festival/45 {{ $showIcons ? 'pl-11 pr-4' : 'px-4' }}"
|
||||
placeholder="{{ $fc['placeholder'] ?? $fc['label'] ?? '' }}"
|
||||
>
|
||||
</div>
|
||||
<p x-show="fieldErrors.{{ $fk }}" x-cloak class="mt-2 text-sm text-red-200" x-text="fieldErrors.{{ $fk }} ? fieldErrors.{{ $fk }}[0] : ''"></p>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@php
|
||||
$emailFc = is_array($fields['email'] ?? null) ? $fields['email'] : [];
|
||||
$emailOn = filter_var($emailFc['enabled'] ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||
@endphp
|
||||
@if ($emailOn)
|
||||
<div>
|
||||
<label for="pf-email" class="sr-only">{{ $emailFc['label'] ?? __('Email') }}</label>
|
||||
<div class="relative">
|
||||
@if ($showIcons)
|
||||
<span class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-white/40" 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="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" /></svg>
|
||||
</span>
|
||||
@endif
|
||||
<input
|
||||
id="pf-email"
|
||||
type="email"
|
||||
name="email"
|
||||
autocomplete="email"
|
||||
required
|
||||
maxlength="255"
|
||||
x-model="email"
|
||||
class="min-h-[48px] w-full rounded-xl border border-white/35 bg-white/15 py-3 text-[15px] text-white placeholder-white/55 shadow-sm transition duration-200 ease-out focus:border-festival focus:outline-none focus:ring-2 focus:ring-festival/45 {{ $showIcons ? 'pl-11 pr-4' : 'px-4' }}"
|
||||
placeholder="{{ $emailFc['placeholder'] ?? $emailFc['label'] ?? '' }}"
|
||||
>
|
||||
</div>
|
||||
<p x-show="fieldErrors.email" x-cloak class="mt-2 text-sm text-red-200" x-text="fieldErrors.email ? fieldErrors.email[0] : ''"></p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$phoneFc = is_array($fields['phone'] ?? null) ? $fields['phone'] : [];
|
||||
$phoneOn = filter_var($phoneFc['enabled'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
$phoneReq = filter_var($phoneFc['required'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
@endphp
|
||||
@if ($phoneOn)
|
||||
<div>
|
||||
<label for="pf-phone" class="sr-only">{{ $phoneFc['label'] ?? __('Phone') }}</label>
|
||||
<div class="relative">
|
||||
@if ($showIcons)
|
||||
<span class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-white/40" 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="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.608-1.25.608H9.75a2.25 2.25 0 01-2.25-2.25V9.75c0-.481.232-.968.608-1.25l1.293-.97c.362-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z" /></svg>
|
||||
</span>
|
||||
@endif
|
||||
<input
|
||||
id="pf-phone"
|
||||
type="tel"
|
||||
name="phone"
|
||||
autocomplete="tel"
|
||||
maxlength="20"
|
||||
@if ($phoneReq) required @endif
|
||||
x-model="phone"
|
||||
class="min-h-[48px] w-full rounded-xl border border-white/35 bg-white/15 py-3 text-[15px] text-white placeholder-white/55 shadow-sm transition duration-200 ease-out focus:border-festival focus:outline-none focus:ring-2 focus:ring-festival/45 {{ $showIcons ? 'pl-11 pr-4' : 'px-4' }}"
|
||||
placeholder="{{ $phoneFc['placeholder'] ?? $phoneFc['label'] ?? '' }}"
|
||||
>
|
||||
</div>
|
||||
<p x-show="fieldErrors.phone" x-cloak class="mt-2 text-sm text-red-200" x-text="fieldErrors.phone ? fieldErrors.phone[0] : ''"></p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (filled($privacyText))
|
||||
<p class="text-center text-xs leading-relaxed text-white/60">
|
||||
@if (filled($privacyUrl))
|
||||
<a href="{{ e($privacyUrl) }}" class="underline decoration-white/30 underline-offset-2 hover:text-white" target="_blank" rel="noopener noreferrer">{{ $privacyText }}</a>
|
||||
@else
|
||||
{{ $privacyText }}
|
||||
@endif
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="mt-2 min-h-[52px] w-full rounded-xl px-6 py-3.5 text-base font-bold tracking-wide shadow-lg transition duration-200 ease-out hover:scale-[1.02] hover:brightness-110 focus:outline-none focus:ring-2 focus:ring-festival focus:ring-offset-2 focus:ring-offset-black/80 active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:scale-100 sm:min-h-[56px] sm:text-lg"
|
||||
style="background-color: {{ e($btnColor) }}; color: {{ e($btnText) }};"
|
||||
:disabled="submitting"
|
||||
>
|
||||
<span x-show="!submitting" x-text="formButtonLabel"></span>
|
||||
<span x-show="submitting" x-cloak>{{ __('Sending…') }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div x-show="phase === 'thanks'" x-cloak class="space-y-6">
|
||||
<div class="flex justify-center">
|
||||
<div class="animate-preregister-in flex h-16 w-16 items-center justify-center rounded-full bg-emerald-500/20 text-emerald-300 ring-2 ring-emerald-400/50">
|
||||
<svg class="h-9 w-9" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="whitespace-pre-line text-center text-base leading-relaxed text-white/95 sm:text-lg" x-text="thankYouMessage"></p>
|
||||
<p
|
||||
x-show="redirectSecondsLeft !== null && redirectSecondsLeft > 0"
|
||||
x-cloak
|
||||
class="text-center text-sm text-white/75"
|
||||
x-text="(strings.redirectCountdown || '').replace(':seconds', String(redirectSecondsLeft))"
|
||||
></p>
|
||||
<div class="rounded-xl border border-white/15 bg-black/30 px-4 py-4 text-center">
|
||||
<p class="text-sm font-medium text-white/90">{{ __('Deel deze pagina') }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 inline-flex min-h-[44px] w-full items-center justify-center rounded-lg border border-white/25 bg-white/10 px-4 py-2 text-sm font-semibold text-white hover:bg-white/15"
|
||||
@click="copyPageLink()"
|
||||
>
|
||||
{{ __('Link kopiëren') }}
|
||||
</button>
|
||||
<p x-show="copyFeedback !== ''" x-cloak class="mt-2 text-xs text-emerald-300" x-text="copyFeedback"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
43
resources/views/components/blocks/hero.blade.php
Normal file
43
resources/views/components/blocks/hero.blade.php
Normal file
@@ -0,0 +1,43 @@
|
||||
@props([
|
||||
'block',
|
||||
'page',
|
||||
'pageState' => 'active',
|
||||
])
|
||||
|
||||
@php
|
||||
/** @var \App\Models\PageBlock $block */
|
||||
/** @var \App\Models\PreregistrationPage $page */
|
||||
$c = $block->content ?? [];
|
||||
$align = (string) data_get($c, 'text_alignment', 'center');
|
||||
$alignClass = match ($align) {
|
||||
'left' => 'items-start text-left',
|
||||
'right' => 'items-end text-right',
|
||||
default => 'items-center text-center',
|
||||
};
|
||||
$eyebrow = data_get($c, 'eyebrow_text');
|
||||
$eyebrowStyle = (string) data_get($c, 'eyebrow_style', 'badge');
|
||||
@endphp
|
||||
|
||||
<div class="flex w-full flex-col {{ $alignClass }} space-y-4">
|
||||
@if (filled($eyebrow) && $eyebrowStyle !== 'none')
|
||||
@if ($eyebrowStyle === 'badge')
|
||||
<span class="inline-flex rounded-full border border-white/25 bg-white/10 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-white/90">
|
||||
{{ $eyebrow }}
|
||||
</span>
|
||||
@else
|
||||
<p class="text-sm font-medium text-white/80">{{ $eyebrow }}</p>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if (filled(data_get($c, 'headline')))
|
||||
<h1 class="w-full max-w-none text-balance text-2xl font-bold leading-snug tracking-tight text-festival sm:text-3xl">
|
||||
{{ data_get($c, 'headline') }}
|
||||
</h1>
|
||||
@endif
|
||||
|
||||
@if ($pageState !== 'expired' && filled(data_get($c, 'subheadline')))
|
||||
<div class="w-full max-w-none whitespace-pre-line text-[15px] leading-[1.65] text-white sm:text-base sm:leading-relaxed">
|
||||
{{ trim((string) data_get($c, 'subheadline')) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
37
resources/views/components/blocks/icon.blade.php
Normal file
37
resources/views/components/blocks/icon.blade.php
Normal file
@@ -0,0 +1,37 @@
|
||||
@props(['name' => 'check'])
|
||||
|
||||
@php
|
||||
$common = 'h-6 w-6 shrink-0 text-festival';
|
||||
@endphp
|
||||
|
||||
@switch($name)
|
||||
@case('ticket')
|
||||
<svg {{ $attributes->merge(['class' => $common]) }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 6v.75l1.5 1.5M9 6.75V9m0 0v3.75m0-3.75h3.75m-3.75 0H9m9 3.75V9m0 0V6.75m0 3.75h-3.75m3.75 0H15M4.5 19.5l15-15M4.5 4.5l15 15" /></svg>
|
||||
@break
|
||||
@case('clock')
|
||||
<svg {{ $attributes->merge(['class' => $common]) }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
@break
|
||||
@case('mail')
|
||||
<svg {{ $attributes->merge(['class' => $common]) }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" /></svg>
|
||||
@break
|
||||
@case('users')
|
||||
<svg {{ $attributes->merge(['class' => $common]) }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" /></svg>
|
||||
@break
|
||||
@case('star')
|
||||
<svg {{ $attributes->merge(['class' => $common]) }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.873a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.385a.562.562 0 00-.182-.557l-4.204-3.602a.562.562 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" /></svg>
|
||||
@break
|
||||
@case('heart')
|
||||
<svg {{ $attributes->merge(['class' => $common]) }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" /></svg>
|
||||
@break
|
||||
@case('gift')
|
||||
<svg {{ $attributes->merge(['class' => $common]) }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m-8.25 3.75h15" /></svg>
|
||||
@break
|
||||
@case('music')
|
||||
<svg {{ $attributes->merge(['class' => $common]) }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M9 9l10.5-3m0 6.553v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 11-.99-3.467l2.31-.66a2.25 2.25 0 001.632-2.163zm0 0V2.25L9 5.25v10.303m0 0v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 01-.99-3.467l2.31-.66A2.25 2.25 0 009 15.553z" /></svg>
|
||||
@break
|
||||
@case('shield')
|
||||
<svg {{ $attributes->merge(['class' => $common]) }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" /></svg>
|
||||
@break
|
||||
@default
|
||||
<svg {{ $attributes->merge(['class' => $common]) }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
|
||||
@endswitch
|
||||
49
resources/views/components/blocks/image.blade.php
Normal file
49
resources/views/components/blocks/image.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
@props([
|
||||
'block',
|
||||
])
|
||||
|
||||
@php
|
||||
/** @var \App\Models\PageBlock $block */
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
$c = $block->content ?? [];
|
||||
$path = data_get($c, 'image');
|
||||
$src = is_string($path) && $path !== '' ? Storage::disk('public')->url($path) : null;
|
||||
$rawLink = data_get($c, 'link_url');
|
||||
$linkUrl = null;
|
||||
if (is_string($rawLink) && preg_match('/\Ahttps?:\/\//i', trim($rawLink)) === 1) {
|
||||
$linkUrl = trim($rawLink);
|
||||
}
|
||||
$alt = is_string(data_get($c, 'alt')) ? (string) data_get($c, 'alt') : '';
|
||||
$maxW = max(48, min(800, (int) data_get($c, 'max_width_px', 320)));
|
||||
$align = (string) data_get($c, 'text_alignment', 'center');
|
||||
// text-align + inline-block centers reliably; flex + max-w-full often yields full-width flex items.
|
||||
$textAlignClass = match ($align) {
|
||||
'left' => 'text-left',
|
||||
'right' => 'text-right',
|
||||
default => 'text-center',
|
||||
};
|
||||
@endphp
|
||||
|
||||
@if ($src !== null)
|
||||
<div class="w-full {{ $textAlignClass }}">
|
||||
@if ($linkUrl !== null)
|
||||
<a
|
||||
href="{{ e($linkUrl) }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-block max-w-full align-middle"
|
||||
>
|
||||
@endif
|
||||
<img
|
||||
src="{{ e($src) }}"
|
||||
alt="{{ e($alt) }}"
|
||||
class="inline-block h-auto w-auto max-w-full align-middle object-contain drop-shadow-[0_8px_32px_rgba(0,0,0,0.35)]"
|
||||
style="max-width: min(100%, {{ $maxW }}px)"
|
||||
loading="lazy"
|
||||
>
|
||||
@if ($linkUrl !== null)
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
33
resources/views/components/blocks/social-proof.blade.php
Normal file
33
resources/views/components/blocks/social-proof.blade.php
Normal file
@@ -0,0 +1,33 @@
|
||||
@props([
|
||||
'block',
|
||||
'subscriberCount' => 0,
|
||||
])
|
||||
|
||||
@php
|
||||
/** @var \App\Models\PageBlock $block */
|
||||
$c = $block->content ?? [];
|
||||
$min = (int) data_get($c, 'min_count', 0);
|
||||
$template = (string) data_get($c, 'template', '');
|
||||
$style = (string) data_get($c, 'style', 'pill');
|
||||
$showAnim = filter_var(data_get($c, 'show_animation', true), FILTER_VALIDATE_BOOLEAN);
|
||||
@endphp
|
||||
|
||||
@if ($subscriberCount >= $min && str_contains($template, '{count}'))
|
||||
<div
|
||||
class="w-full text-center"
|
||||
x-show="phase !== 'thanks'"
|
||||
x-cloak
|
||||
>
|
||||
@php
|
||||
$text = str_replace('{count}', number_format($subscriberCount, 0, ',', '.'), $template);
|
||||
$wrapClass = match ($style) {
|
||||
'badge' => 'inline-flex rounded-lg border border-white/20 bg-white/10 px-4 py-2 text-sm text-white',
|
||||
'plain' => 'text-sm text-white/90',
|
||||
default => 'inline-flex rounded-full border border-festival/40 bg-festival/15 px-5 py-2 text-sm font-medium text-white shadow-inner',
|
||||
};
|
||||
@endphp
|
||||
<p @class([$wrapClass, 'transition-transform duration-500' => $showAnim]) x-data="{ shown: false }" x-init="setTimeout(() => shown = true, 100)" :class="shown ? 'scale-100 opacity-100' : 'scale-95 opacity-0'">
|
||||
{{ $text }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
30
resources/views/components/blocks/text.blade.php
Normal file
30
resources/views/components/blocks/text.blade.php
Normal file
@@ -0,0 +1,30 @@
|
||||
@props(['block'])
|
||||
|
||||
@php
|
||||
/** @var \App\Models\PageBlock $block */
|
||||
$c = $block->content ?? [];
|
||||
$size = (string) data_get($c, 'text_size', 'base');
|
||||
$align = (string) data_get($c, 'text_alignment', 'center');
|
||||
$sizeClass = match ($size) {
|
||||
'sm' => 'text-sm sm:text-[15px]',
|
||||
'lg' => 'text-lg sm:text-xl',
|
||||
default => 'text-[15px] sm:text-base',
|
||||
};
|
||||
$alignClass = match ($align) {
|
||||
'left' => 'text-left',
|
||||
'right' => 'text-right',
|
||||
default => 'text-center',
|
||||
};
|
||||
$bodyRaw = data_get($c, 'body');
|
||||
$body = is_string($bodyRaw) ? trim($bodyRaw) : '';
|
||||
@endphp
|
||||
|
||||
{{-- Body must be on one line inside the div: whitespace-pre-line turns indentation/newlines around {{ $body }} into visible gaps. --}}
|
||||
<div class="w-full space-y-3" x-show="phase !== 'thanks'" x-cloak>
|
||||
@if (filled(data_get($c, 'title')))
|
||||
<h2 class="{{ $alignClass }} text-lg font-semibold text-white sm:text-xl">{{ data_get($c, 'title') }}</h2>
|
||||
@endif
|
||||
@if ($body !== '')
|
||||
<div class="{{ $alignClass }} {{ $sizeClass }} whitespace-pre-line leading-relaxed text-white/90">{{ $body }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -16,14 +16,16 @@
|
||||
@php
|
||||
$adminFlashSuccess = session('status');
|
||||
$adminFlashError = session('error');
|
||||
$inlinePageFlash = request()->routeIs(['admin.pages.edit', 'admin.pages.create']);
|
||||
$showSuccessToast = $adminFlashSuccess !== null && ! $inlinePageFlash;
|
||||
@endphp
|
||||
@if ($adminFlashSuccess !== null || $adminFlashError !== null)
|
||||
@if ($showSuccessToast || $adminFlashError !== null)
|
||||
<div
|
||||
class="pointer-events-none fixed bottom-4 right-4 z-[100] flex w-full max-w-sm flex-col gap-2 px-4 sm:px-0"
|
||||
aria-live="polite"
|
||||
>
|
||||
@foreach (array_filter([
|
||||
$adminFlashSuccess !== null ? ['type' => 'success', 'message' => $adminFlashSuccess] : null,
|
||||
$showSuccessToast ? ['type' => 'success', 'message' => $adminFlashSuccess] : null,
|
||||
$adminFlashError !== null ? ['type' => 'error', 'message' => $adminFlashError] : null,
|
||||
]) as $toast)
|
||||
<div
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ $title ?? $page->heading }} — {{ config('app.name', 'PreRegister') }}</title>
|
||||
<title>{{ $title ?? $page->headlineForMeta() }} — {{ config('app.name', 'PreRegister') }}</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet">
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
@php
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
$bgUrl = $page->background_image !== null
|
||||
? Storage::disk('public')->url($page->background_image)
|
||||
: null;
|
||||
$logoUrl = $page->logo_image !== null
|
||||
? Storage::disk('public')->url($page->logo_image)
|
||||
: null;
|
||||
$phase = $page->isBeforeStart() ? 'before' : ($page->isExpired() ? 'expired' : 'active');
|
||||
/** @var \Illuminate\Support\Collection<int, \App\Models\PageBlock> $blocksToRender */
|
||||
$bgPath = $page->background_image;
|
||||
$bgUrl = is_string($bgPath) && $bgPath !== '' ? Storage::disk('public')->url($bgPath) : null;
|
||||
$overlayColor = (string) ($page->background_overlay_color ?: '#000000');
|
||||
$overlayOpacity = max(0, min(100, (int) ($page->background_overlay_opacity ?? 50))) / 100;
|
||||
$hasExpiredCtaBlock = $blocksToRender->contains(fn (\App\Models\PageBlock $b): bool => $b->type === 'cta_banner');
|
||||
$redirectAfterSubmit = $page->post_submit_redirect_url;
|
||||
|
||||
$alpinePhase = match ($pageState) {
|
||||
'countdown' => 'before',
|
||||
'expired' => 'expired',
|
||||
default => 'active',
|
||||
};
|
||||
|
||||
$formBlock = $page->getFormBlock();
|
||||
$formContent = $formBlock?->content ?? [];
|
||||
$formButtonLabel = (string) (data_get($formContent, 'button_label') ?: __('public.register_button'));
|
||||
$formButtonColor = (string) data_get($formContent, 'button_color', '#F47B20');
|
||||
$formButtonTextColor = (string) data_get($formContent, 'button_text_color', '#FFFFFF');
|
||||
@endphp
|
||||
|
||||
@extends('layouts.public')
|
||||
@@ -27,16 +39,20 @@
|
||||
></div>
|
||||
@endif
|
||||
|
||||
<div class="absolute inset-0 bg-black/30" aria-hidden="true"></div>
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
style="background-color: {{ e($overlayColor) }}; opacity: {{ $overlayOpacity }}"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<div class="relative z-10 flex min-h-screen items-center justify-center px-4 py-6 sm:px-6 sm:py-10">
|
||||
<div
|
||||
class="animate-preregister-in w-full max-w-3xl rounded-3xl border border-white/20 bg-black/60 px-5 py-8 shadow-[0_25px_60px_-15px_rgba(0,0,0,0.65)] backdrop-blur-[4px] sm:px-10 sm:py-10"
|
||||
class="animate-preregister-in w-full max-w-3xl rounded-3xl border border-white/20 bg-black/60 px-5 py-8 shadow-[0_0_0_1px_rgba(0,0,0,0.2),0_28px_90px_-4px_rgba(0,0,0,0.28),0_48px_140px_2px_rgba(0,0,0,0.18),0_72px_200px_16px_rgba(0,0,0,0.12)] backdrop-blur-[4px] sm:px-10 sm:py-10"
|
||||
x-cloak
|
||||
x-data="publicPreregisterPage(@js([
|
||||
'phase' => $phase,
|
||||
'phase' => $alpinePhase,
|
||||
'startAtMs' => $page->start_date->getTimestamp() * 1000,
|
||||
'phoneEnabled' => (bool) $page->phone_enabled,
|
||||
'phoneEnabled' => $page->isPhoneFieldEnabledForSubscribers(),
|
||||
'subscribeUrl' => route('public.subscribe', ['publicPage' => $page]),
|
||||
'csrfToken' => csrf_token(),
|
||||
'genericError' => __('Something went wrong. Please try again.'),
|
||||
@@ -44,169 +60,51 @@
|
||||
'labelDays' => __('days'),
|
||||
'invalidEmailMsg' => __('Please enter a valid email address.'),
|
||||
'invalidPhoneMsg' => __('Please enter a valid phone number (8–15 digits).'),
|
||||
'formButtonLabel' => $formButtonLabel,
|
||||
'formButtonColor' => $formButtonColor,
|
||||
'formButtonTextColor' => $formButtonTextColor,
|
||||
'pageShareUrl' => url()->current(),
|
||||
'redirectUrl' => filled($redirectAfterSubmit) ? $redirectAfterSubmit : null,
|
||||
'strings' => [
|
||||
'linkCopied' => __('Link gekopieerd!'),
|
||||
'redirectCountdown' => __('You will be redirected in :seconds s…'),
|
||||
],
|
||||
]))"
|
||||
>
|
||||
<div class="flex flex-col items-center text-center">
|
||||
@if ($logoUrl !== null)
|
||||
<div class="mb-4 flex w-full justify-center sm:mb-5">
|
||||
<img
|
||||
src="{{ e($logoUrl) }}"
|
||||
alt=""
|
||||
class="max-h-32 w-auto object-contain object-center drop-shadow-[0_8px_32px_rgba(0,0,0,0.45)] sm:max-h-44 md:max-h-48"
|
||||
width="384"
|
||||
height="192"
|
||||
>
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-col items-stretch space-y-4">
|
||||
@foreach ($blocksToRender as $block)
|
||||
<x-dynamic-component
|
||||
:component="$block->bladeComponentName()"
|
||||
:block="$block"
|
||||
:page="$page"
|
||||
:page-state="$pageState"
|
||||
:subscriber-count="$subscriberCount"
|
||||
/>
|
||||
@endforeach
|
||||
|
||||
<h1 class="w-full max-w-none text-balance text-2xl font-bold leading-snug tracking-tight text-festival sm:text-3xl">
|
||||
{{ $page->heading }}
|
||||
</h1>
|
||||
@if ($pageState === 'expired')
|
||||
<div class="space-y-6">
|
||||
@if (filled($page->expired_message))
|
||||
<div class="whitespace-pre-line text-center text-[15px] leading-[1.65] text-white/92 sm:text-base sm:leading-relaxed">
|
||||
{{ $page->expired_message }}
|
||||
</div>
|
||||
@else
|
||||
<p class="text-center text-[15px] leading-relaxed text-white/92 sm:text-base">{{ __('This pre-registration period has ended.') }}</p>
|
||||
@endif
|
||||
|
||||
@if (filled($page->intro_text))
|
||||
<div
|
||||
x-show="phase === 'before' || phase === 'active'"
|
||||
x-cloak
|
||||
class="mt-0 w-full max-w-none whitespace-pre-line text-[15px] leading-[1.65] text-white sm:text-base sm:leading-relaxed"
|
||||
>
|
||||
{{ trim($page->intro_text) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Before start: countdown --}}
|
||||
<div x-show="phase === 'before'" x-cloak class="mt-8 space-y-6 sm:mt-10">
|
||||
<div
|
||||
class="grid grid-cols-4 gap-3 rounded-2xl border border-white/15 bg-black/35 px-3 py-4 text-center shadow-inner sm:gap-4 sm:px-4 sm:py-5"
|
||||
role="timer"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div>
|
||||
<div class="font-mono text-2xl font-semibold tabular-nums text-white sm:text-3xl" x-text="pad(days)"></div>
|
||||
<div class="mt-2 text-[10px] uppercase tracking-wider text-white/65 sm:text-xs" x-text="days === 1 ? labelDay : labelDays"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-mono text-2xl font-semibold tabular-nums text-white sm:text-3xl" x-text="pad(hours)"></div>
|
||||
<div class="mt-2 text-[10px] uppercase tracking-wider text-white/65 sm:text-xs">{{ __('hrs') }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-mono text-2xl font-semibold tabular-nums text-white sm:text-3xl" x-text="pad(minutes)"></div>
|
||||
<div class="mt-2 text-[10px] uppercase tracking-wider text-white/65 sm:text-xs">{{ __('mins') }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-mono text-2xl font-semibold tabular-nums text-white sm:text-3xl" x-text="pad(seconds)"></div>
|
||||
<div class="mt-2 text-[10px] uppercase tracking-wider text-white/65 sm:text-xs">{{ __('secs') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Active: registration form --}}
|
||||
<div x-show="phase === 'active'" x-cloak class="mt-8 sm:mt-10">
|
||||
<form x-ref="form" class="space-y-4" @submit.prevent="submitForm()">
|
||||
<div x-show="formError !== ''" x-cloak class="rounded-xl border border-amber-400/50 bg-amber-500/15 px-4 py-3 text-sm leading-snug text-amber-50" x-text="formError"></div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="first_name" class="sr-only">{{ __('First name') }}</label>
|
||||
<input
|
||||
id="first_name"
|
||||
type="text"
|
||||
name="first_name"
|
||||
autocomplete="given-name"
|
||||
required
|
||||
maxlength="255"
|
||||
x-model="first_name"
|
||||
class="min-h-[48px] w-full rounded-xl border border-white/35 bg-white/15 px-4 py-3 text-[15px] text-white placeholder-white/55 shadow-sm transition duration-200 ease-out focus:border-festival focus:outline-none focus:ring-2 focus:ring-festival/45"
|
||||
placeholder="{{ __('First name') }}"
|
||||
>
|
||||
<p x-show="fieldErrors.first_name" x-cloak class="mt-2 text-sm text-red-200" x-text="fieldErrors.first_name ? fieldErrors.first_name[0] : ''"></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="last_name" class="sr-only">{{ __('Last name') }}</label>
|
||||
<input
|
||||
id="last_name"
|
||||
type="text"
|
||||
name="last_name"
|
||||
autocomplete="family-name"
|
||||
required
|
||||
maxlength="255"
|
||||
x-model="last_name"
|
||||
class="min-h-[48px] w-full rounded-xl border border-white/35 bg-white/15 px-4 py-3 text-[15px] text-white placeholder-white/55 shadow-sm transition duration-200 ease-out focus:border-festival focus:outline-none focus:ring-2 focus:ring-festival/45"
|
||||
placeholder="{{ __('Last name') }}"
|
||||
>
|
||||
<p x-show="fieldErrors.last_name" x-cloak class="mt-2 text-sm text-red-200" x-text="fieldErrors.last_name ? fieldErrors.last_name[0] : ''"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="sr-only">{{ __('Email') }}</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autocomplete="email"
|
||||
required
|
||||
maxlength="255"
|
||||
x-model="email"
|
||||
class="min-h-[48px] w-full rounded-xl border border-white/35 bg-white/15 px-4 py-3 text-[15px] text-white placeholder-white/55 shadow-sm transition duration-200 ease-out focus:border-festival focus:outline-none focus:ring-2 focus:ring-festival/45"
|
||||
placeholder="{{ __('Email') }}"
|
||||
>
|
||||
<p x-show="fieldErrors.email" x-cloak class="mt-2 text-sm text-red-200" x-text="fieldErrors.email ? fieldErrors.email[0] : ''"></p>
|
||||
</div>
|
||||
|
||||
<div x-show="phoneEnabled">
|
||||
<label for="phone" class="sr-only">{{ __('Phone (optional)') }}</label>
|
||||
<input
|
||||
id="phone"
|
||||
type="tel"
|
||||
name="phone"
|
||||
autocomplete="tel"
|
||||
maxlength="20"
|
||||
x-model="phone"
|
||||
class="min-h-[48px] w-full rounded-xl border border-white/35 bg-white/15 px-4 py-3 text-[15px] text-white placeholder-white/55 shadow-sm transition duration-200 ease-out focus:border-festival focus:outline-none focus:ring-2 focus:ring-festival/45"
|
||||
placeholder="{{ __('Phone (optional)') }}"
|
||||
>
|
||||
<p x-show="fieldErrors.phone" x-cloak class="mt-2 text-sm text-red-200" x-text="fieldErrors.phone ? fieldErrors.phone[0] : ''"></p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="mt-2 min-h-[52px] w-full rounded-xl bg-festival px-6 py-3.5 text-base font-bold tracking-wide text-white shadow-lg shadow-festival/30 transition duration-200 ease-out hover:scale-[1.02] hover:brightness-110 focus:outline-none focus:ring-2 focus:ring-festival focus:ring-offset-2 focus:ring-offset-black/80 active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:scale-100 sm:min-h-[56px] sm:text-lg"
|
||||
:disabled="submitting"
|
||||
>
|
||||
<span x-show="!submitting">{{ __('public.register_button') }}</span>
|
||||
<span x-show="submitting" x-cloak>{{ __('Sending…') }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Thank you (after successful AJAX) --}}
|
||||
<div x-show="phase === 'thanks'" x-cloak class="mt-8 sm:mt-10">
|
||||
<p class="whitespace-pre-line text-center text-base leading-relaxed text-white/95 sm:text-lg" x-text="thankYouMessage"></p>
|
||||
</div>
|
||||
|
||||
{{-- Expired --}}
|
||||
<div x-show="phase === 'expired'" x-cloak class="mt-8 space-y-6 sm:mt-10">
|
||||
@if (filled($page->expired_message))
|
||||
<div class="whitespace-pre-line text-center text-[15px] leading-[1.65] text-white/92 sm:text-base sm:leading-relaxed">
|
||||
{{ $page->expired_message }}
|
||||
</div>
|
||||
@else
|
||||
<p class="text-center text-[15px] leading-relaxed text-white/92 sm:text-base">{{ __('This pre-registration period has ended.') }}</p>
|
||||
@endif
|
||||
|
||||
@if (filled($page->ticketshop_url))
|
||||
<div class="text-center">
|
||||
<a
|
||||
href="{{ e($page->ticketshop_url) }}"
|
||||
class="inline-flex min-h-[52px] items-center justify-center rounded-xl bg-festival px-8 py-3.5 text-base font-bold tracking-wide text-white shadow-lg shadow-festival/30 transition duration-200 ease-out hover:scale-[1.02] hover:brightness-110 focus:outline-none focus:ring-2 focus:ring-festival focus:ring-offset-2 focus:ring-offset-black/80 active:scale-[0.99]"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ __('Visit ticket shop') }}
|
||||
<span class="ml-1" aria-hidden="true">→</span>
|
||||
</a>
|
||||
@if (filled($page->ticketshop_url) && ! $hasExpiredCtaBlock)
|
||||
<div class="text-center">
|
||||
<a
|
||||
href="{{ e($page->ticketshop_url) }}"
|
||||
class="inline-flex min-h-[52px] items-center justify-center rounded-xl bg-festival px-8 py-3.5 text-base font-bold tracking-wide text-white shadow-lg shadow-festival/30 transition duration-200 ease-out hover:scale-[1.02] hover:brightness-110 focus:outline-none focus:ring-2 focus:ring-festival focus:ring-offset-2 focus:ring-offset-black/80 active:scale-[0.99]"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ __('Visit ticket shop') }}
|
||||
<span class="ml-1" aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user