chore: checkpoint before block builder refactor
This commit is contained in:
@@ -5,3 +5,12 @@
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Public landing: respect reduced motion for entrance animation */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-preregister-in {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,36 @@ import './bootstrap';
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
/**
|
||||
* Client-side email check (aligned loosely with RFC-style rules; server is authoritative).
|
||||
*/
|
||||
function isValidEmailFormat(value) {
|
||||
const email = String(value).trim().toLowerCase();
|
||||
if (email.length < 5 || email.length > 255) {
|
||||
return false;
|
||||
}
|
||||
if (!email.includes('@')) {
|
||||
return false;
|
||||
}
|
||||
const at = email.lastIndexOf('@');
|
||||
const local = email.slice(0, at);
|
||||
const domain = email.slice(at + 1);
|
||||
if (!local || !domain || local.includes('..') || domain.includes('..')) {
|
||||
return false;
|
||||
}
|
||||
if (domain.startsWith('.') || domain.endsWith('.')) {
|
||||
return false;
|
||||
}
|
||||
if (!/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+$/i.test(local)) {
|
||||
return false;
|
||||
}
|
||||
if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i.test(domain)) {
|
||||
return false;
|
||||
}
|
||||
const tld = domain.split('.').pop();
|
||||
return Boolean(tld && tld.length >= 2);
|
||||
}
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('publicPreregisterPage', (config) => ({
|
||||
phase: config.phase,
|
||||
@@ -12,6 +42,8 @@ document.addEventListener('alpine:init', () => {
|
||||
genericError: config.genericError,
|
||||
labelDay: config.labelDay,
|
||||
labelDays: config.labelDays,
|
||||
invalidEmailMsg: config.invalidEmailMsg,
|
||||
invalidPhoneMsg: config.invalidPhoneMsg,
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
@@ -56,15 +88,36 @@ document.addEventListener('alpine:init', () => {
|
||||
return String(n).padStart(2, '0');
|
||||
},
|
||||
|
||||
validateEmailAndPhone() {
|
||||
this.fieldErrors = {};
|
||||
let ok = true;
|
||||
if (!isValidEmailFormat(this.email)) {
|
||||
this.fieldErrors.email = [this.invalidEmailMsg];
|
||||
ok = false;
|
||||
}
|
||||
if (this.phoneEnabled) {
|
||||
const digits = String(this.phone).replace(/\D/g, '');
|
||||
if (digits.length > 0 && (digits.length < 8 || digits.length > 15)) {
|
||||
this.fieldErrors.phone = [this.invalidPhoneMsg];
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
},
|
||||
|
||||
async submitForm() {
|
||||
this.formError = '';
|
||||
this.fieldErrors = {};
|
||||
if (!this.validateEmailAndPhone()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const form = this.$refs.form;
|
||||
if (form !== undefined && form !== null && !form.checkValidity()) {
|
||||
form.reportValidity();
|
||||
return;
|
||||
}
|
||||
|
||||
this.formError = '';
|
||||
this.fieldErrors = {};
|
||||
this.submitting = true;
|
||||
try {
|
||||
const body = {
|
||||
@@ -262,10 +315,6 @@ document.addEventListener('alpine:init', () => {
|
||||
this.errorMessage = cfg.strings.mapFieldsError;
|
||||
return;
|
||||
}
|
||||
if (this.phoneEnabled && !this.fieldPhone) {
|
||||
this.errorMessage = cfg.strings.mapPhoneError;
|
||||
return;
|
||||
}
|
||||
if (!this.tagField) {
|
||||
this.errorMessage = cfg.strings.tagFieldError;
|
||||
return;
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
'noListsError' => __('No mailing lists were returned. Check your API key or create a list in Mailwizz.'),
|
||||
'selectListError' => __('Select a mailing list.'),
|
||||
'mapFieldsError' => __('Map email, first name, and last name to Mailwizz fields.'),
|
||||
'mapPhoneError' => __('Map the phone field for this page.'),
|
||||
'tagFieldError' => __('Select a checkbox list field for source / tag tracking.'),
|
||||
'tagValueError' => __('Select the tag option that identifies this pre-registration.'),
|
||||
],
|
||||
@@ -183,7 +182,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div x-show="phoneEnabled">
|
||||
<label class="block text-sm font-medium text-slate-700">{{ __('Phone') }}</label>
|
||||
<label class="block text-sm font-medium text-slate-700">{{ __('Phone') }} <span class="font-normal text-slate-500">({{ __('optional') }})</span></label>
|
||||
<select x-model="fieldPhone" class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<option value="">{{ __('Select field…') }}</option>
|
||||
<template x-for="f in phoneFields()" :key="'ph-' + f.tag">
|
||||
|
||||
@@ -27,11 +27,11 @@
|
||||
></div>
|
||||
@endif
|
||||
|
||||
<div class="absolute inset-0 bg-black/50" aria-hidden="true"></div>
|
||||
<div class="absolute inset-0 bg-black/30" aria-hidden="true"></div>
|
||||
|
||||
<div class="relative z-10 flex min-h-screen items-center justify-center px-4 py-10 sm:px-6 sm:py-12">
|
||||
<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="w-full max-w-lg rounded-2xl border border-white/15 bg-white/10 px-6 py-8 shadow-2xl backdrop-blur-md 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_25px_60px_-15px_rgba(0,0,0,0.65)] backdrop-blur-[4px] sm:px-10 sm:py-10"
|
||||
x-cloak
|
||||
x-data="publicPreregisterPage(@js([
|
||||
'phase' => $phase,
|
||||
@@ -42,95 +42,105 @@
|
||||
'genericError' => __('Something went wrong. Please try again.'),
|
||||
'labelDay' => __('day'),
|
||||
'labelDays' => __('days'),
|
||||
'invalidEmailMsg' => __('Please enter a valid email address.'),
|
||||
'invalidPhoneMsg' => __('Please enter a valid phone number (8–15 digits).'),
|
||||
]))"
|
||||
>
|
||||
@if ($logoUrl !== null)
|
||||
<div class="mb-6 flex justify-center">
|
||||
<img
|
||||
src="{{ e($logoUrl) }}"
|
||||
alt=""
|
||||
class="max-h-20 w-auto object-contain object-center"
|
||||
width="320"
|
||||
height="80"
|
||||
>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<h1 class="text-center text-3xl font-semibold leading-tight tracking-tight text-white sm:text-4xl">
|
||||
{{ $page->heading }}
|
||||
</h1>
|
||||
|
||||
{{-- Before start: intro + countdown --}}
|
||||
<div x-show="phase === 'before'" x-cloak class="mt-6 space-y-6">
|
||||
@if (filled($page->intro_text))
|
||||
<div class="whitespace-pre-line text-center text-base leading-relaxed text-white/90">
|
||||
{{ $page->intro_text }}
|
||||
<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
|
||||
|
||||
<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 (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-2 rounded-xl border border-white/20 bg-black/25 px-3 py-4 text-center sm:gap-3 sm:px-4"
|
||||
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-1 text-xs uppercase tracking-wide text-white/60" x-text="days === 1 ? labelDay : labelDays"></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-1 text-xs uppercase tracking-wide text-white/60">{{ __('hrs') }}</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-1 text-xs uppercase tracking-wide text-white/60">{{ __('mins') }}</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-1 text-xs uppercase tracking-wide text-white/60">{{ __('secs') }}</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">
|
||||
<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-lg border border-amber-400/40 bg-amber-500/10 px-3 py-2 text-sm text-amber-100" x-text="formError"></div>
|
||||
<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>
|
||||
<label for="first_name" class="mb-1 block text-sm font-medium text-white/90">{{ __('First name') }}</label>
|
||||
<input
|
||||
id="first_name"
|
||||
type="text"
|
||||
name="first_name"
|
||||
autocomplete="given-name"
|
||||
required
|
||||
maxlength="255"
|
||||
x-model="first_name"
|
||||
class="w-full rounded-lg border border-white/25 bg-white/10 px-3 py-2.5 text-white placeholder-white/40 shadow-sm backdrop-blur-sm focus:border-white/50 focus:outline-none focus:ring-2 focus:ring-white/30"
|
||||
placeholder="{{ __('First name') }}"
|
||||
>
|
||||
<p x-show="fieldErrors.first_name" x-cloak class="mt-1 text-sm text-red-200" x-text="fieldErrors.first_name ? fieldErrors.first_name[0] : ''"></p>
|
||||
<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="last_name" class="mb-1 block text-sm font-medium text-white/90">{{ __('Last name') }}</label>
|
||||
<input
|
||||
id="last_name"
|
||||
type="text"
|
||||
name="last_name"
|
||||
autocomplete="family-name"
|
||||
required
|
||||
maxlength="255"
|
||||
x-model="last_name"
|
||||
class="w-full rounded-lg border border-white/25 bg-white/10 px-3 py-2.5 text-white placeholder-white/40 shadow-sm backdrop-blur-sm focus:border-white/50 focus:outline-none focus:ring-2 focus:ring-white/30"
|
||||
placeholder="{{ __('Last name') }}"
|
||||
>
|
||||
<p x-show="fieldErrors.last_name" x-cloak class="mt-1 text-sm text-red-200" x-text="fieldErrors.last_name ? fieldErrors.last_name[0] : ''"></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="mb-1 block text-sm font-medium text-white/90">{{ __('Email') }}</label>
|
||||
<label for="email" class="sr-only">{{ __('Email') }}</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
@@ -139,31 +149,30 @@
|
||||
required
|
||||
maxlength="255"
|
||||
x-model="email"
|
||||
class="w-full rounded-lg border border-white/25 bg-white/10 px-3 py-2.5 text-white placeholder-white/40 shadow-sm backdrop-blur-sm focus:border-white/50 focus:outline-none focus:ring-2 focus:ring-white/30"
|
||||
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-1 text-sm text-red-200" x-text="fieldErrors.email ? fieldErrors.email[0] : ''"></p>
|
||||
<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="mb-1 block text-sm font-medium text-white/90">{{ __('Phone') }}</label>
|
||||
<label for="phone" class="sr-only">{{ __('Phone (optional)') }}</label>
|
||||
<input
|
||||
id="phone"
|
||||
type="tel"
|
||||
name="phone"
|
||||
autocomplete="tel"
|
||||
:required="phoneEnabled"
|
||||
maxlength="20"
|
||||
x-model="phone"
|
||||
class="w-full rounded-lg border border-white/25 bg-white/10 px-3 py-2.5 text-white placeholder-white/40 shadow-sm backdrop-blur-sm focus:border-white/50 focus:outline-none focus:ring-2 focus:ring-white/30"
|
||||
placeholder="{{ __('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-1 text-sm text-red-200" x-text="fieldErrors.phone ? fieldErrors.phone[0] : ''"></p>
|
||||
<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 w-full rounded-lg bg-white px-4 py-3 text-sm font-semibold text-slate-900 shadow transition hover:bg-white/90 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-slate-900 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
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>
|
||||
@@ -173,25 +182,25 @@
|
||||
</div>
|
||||
|
||||
{{-- Thank you (after successful AJAX) --}}
|
||||
<div x-show="phase === 'thanks'" x-cloak class="mt-8">
|
||||
<p class="whitespace-pre-line text-center text-lg leading-relaxed text-white/95" x-text="thankYouMessage"></p>
|
||||
<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">
|
||||
<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-base leading-relaxed text-white/90">
|
||||
<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-base text-white/90">{{ __('This pre-registration period has ended.') }}</p>
|
||||
<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 items-center justify-center rounded-lg bg-white px-5 py-2.5 text-sm font-semibold text-slate-900 shadow transition hover:bg-white/90 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-slate-900"
|
||||
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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user