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,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>

View 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

View 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>

View 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>

View 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>

View 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>

View 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

View 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

View 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

View 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>