import './bootstrap'; import Alpine from 'alpinejs'; import Sortable from 'sortablejs'; /** * 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); } function newBlockUid() { return `n_${crypto.randomUUID().replaceAll('-', '')}`; } function deepClone(o) { return JSON.parse(JSON.stringify(o)); } /** Default `content` payloads for admin block editor (Dutch defaults). */ function pageBlockDefaultContent(type) { const defaults = { hero: { headline: '', subheadline: '', eyebrow_text: '', eyebrow_style: 'badge', text_alignment: 'center', }, image: { image: null, link_url: '', alt: '', max_width_px: 320, text_alignment: 'center', }, benefits: { title: 'Waarom voorregistreren?', items: [ { icon: 'ticket', text: 'Exclusieve korting op tickets' }, { icon: 'clock', text: 'Eerder toegang tot de ticketshop' }, ], layout: 'list', max_columns: 2, }, social_proof: { template: 'Al {count} bezoekers aangemeld!', min_count: 10, show_animation: true, style: 'pill', }, form: { title: 'Registreer nu', description: '', button_label: 'Registreer nu!', button_color: '#F47B20', button_text_color: '#FFFFFF', fields: { first_name: { enabled: true, required: true, label: 'Voornaam', placeholder: 'Je voornaam', }, last_name: { enabled: true, required: true, label: 'Achternaam', placeholder: 'Je achternaam', }, email: { enabled: true, required: true, label: 'E-mailadres', placeholder: 'je@email.nl', }, phone: { enabled: false, required: false, label: 'Mobiel', placeholder: '+31 6 12345678', }, }, show_field_icons: true, privacy_text: 'Door je te registreren ga je akkoord met onze privacyverklaring.', privacy_url: '', }, countdown: { target_datetime: new Date(Date.now() + 86400000).toISOString().slice(0, 16), title: 'De pre-registratie opent over:', expired_action: 'reload', expired_message: '', style: 'large', show_labels: true, labels: { days: 'dagen', hours: 'uren', minutes: 'minuten', seconds: 'seconden', }, }, text: { title: '', body: '', text_size: 'base', text_alignment: 'center', }, cta_banner: { text: '', button_label: 'Ga naar de ticketshop', button_url: 'https://', button_color: '#F47B20', style: 'inline', }, divider: { style: 'line', spacing: 'medium', }, }; return deepClone(defaults[type] || { _: '' }); } document.addEventListener('alpine:init', () => { Alpine.data('pageBlockEditor', (config) => ({ blocks: config.initialBlocks || [], blockTypes: config.blockTypes || [], storageBase: typeof config.storageBase === 'string' ? config.storageBase.replace(/\/$/, '') : '', sortable: null, collapsed: {}, confirmDeleteUid: null, _verticalDragLock: null, init() { this.blocks.forEach((b) => { this.collapsed[b.uid] = true; }); this.$nextTick(() => { const el = this.$refs.sortRoot; if (!el) { return; } const lockGhostToVerticalAxis = () => { requestAnimationFrame(() => { const ghost = Sortable.ghost; if (!ghost) { return; } const raw = window.getComputedStyle(ghost).transform; if (!raw || raw === 'none') { return; } try { const m = new DOMMatrix(raw); if (m.m41 !== 0) { m.m41 = 0; ghost.style.transform = m.toString(); } } catch { /* ignore invalid matrix */ } }); }; this.sortable = Sortable.create(el, { handle: '.block-drag-handle', animation: 150, direction: 'vertical', forceFallback: true, fallbackOnBody: true, onStart: () => { this._verticalDragLock = lockGhostToVerticalAxis; document.addEventListener('mousemove', this._verticalDragLock, false); document.addEventListener('touchmove', this._verticalDragLock, { passive: true }); }, onEnd: () => { if (this._verticalDragLock) { document.removeEventListener('mousemove', this._verticalDragLock, false); document.removeEventListener('touchmove', this._verticalDragLock, { passive: true }); this._verticalDragLock = null; } this.syncSortOrderFromDom(); }, }); }); }, syncSortOrderFromDom() { const root = this.$refs.sortRoot; if (!root) { return; } const rows = [...root.querySelectorAll('[data-block-uid]')]; rows.forEach((row, i) => { const uid = row.getAttribute('data-block-uid'); const b = this.blocks.find((x) => x.uid === uid); if (b) { b.sort_order = i; } }); }, hasFormBlock() { return this.blocks.some((b) => b.type === 'form'); }, toggleCollapsed(uid) { this.collapsed[uid] = !this.collapsed[uid]; }, isCollapsed(uid) { return !!this.collapsed[uid]; }, collapseAll() { this.blocks.forEach((b) => { this.collapsed[b.uid] = true; }); }, expandAll() { this.blocks.forEach((b) => { this.collapsed[b.uid] = false; }); }, blockSummary(block) { const c = block.content || {}; const pick = (...keys) => { for (const k of keys) { const v = c[k]; if (typeof v === 'string' && v.trim() !== '') { return v.trim().slice(0, 88); } } return ''; }; switch (block.type) { case 'hero': return pick('headline'); case 'benefits': return pick('title'); case 'form': return pick('title'); case 'countdown': return pick('title'); case 'text': return pick('title') || pick('body'); case 'cta_banner': return pick('text') || pick('button_label'); case 'social_proof': return pick('template'); case 'image': return pick('alt'); default: return ''; } }, addBlock(type) { if (type === 'form' && this.hasFormBlock()) { return; } const nextOrder = this.blocks.length === 0 ? 0 : Math.max(...this.blocks.map((b) => Number(b.sort_order) || 0)) + 1; const uid = newBlockUid(); this.blocks.push({ uid, type, sort_order: nextOrder, is_visible: true, content: pageBlockDefaultContent(type), }); this.collapsed[uid] = false; this.$nextTick(() => this.syncSortOrderFromDom()); }, removeBlock(uid) { this.blocks = this.blocks.filter((b) => b.uid !== uid); delete this.collapsed[uid]; this.confirmDeleteUid = null; this.$nextTick(() => this.syncSortOrderFromDom()); }, requestDelete(uid) { this.confirmDeleteUid = uid; }, cancelDelete() { this.confirmDeleteUid = null; }, addBenefitItem(uid) { const b = this.blocks.find((x) => x.uid === uid); if (!b || b.type !== 'benefits') { return; } if (!Array.isArray(b.content.items)) { b.content.items = []; } b.content.items.push({ icon: 'check', text: '' }); }, removeBenefitItem(uid, idx) { const b = this.blocks.find((x) => x.uid === uid); if (!b || !Array.isArray(b.content.items)) { return; } b.content.items.splice(idx, 1); }, })); Alpine.data('countdownBlock', (cfg) => ({ targetMs: cfg.targetMs, expired: false, days: 0, hours: 0, minutes: 0, seconds: 0, showLabels: cfg.showLabels !== false, labels: cfg.labels || {}, expiredAction: cfg.expiredAction || 'hide', expiredMessage: cfg.expiredMessage || '', timer: null, init() { this.tick(); this.timer = setInterval(() => this.tick(), 1000); }, destroy() { if (this.timer !== null) { clearInterval(this.timer); } }, tick() { const diff = this.targetMs - Date.now(); if (diff <= 0) { if (this.timer !== null) { clearInterval(this.timer); this.timer = null; } this.expired = true; if (this.expiredAction === 'hide' && this.$el) { this.$el.classList.add('hidden'); } if (this.expiredAction === 'reload') { window.location.reload(); } return; } const totalSeconds = Math.floor(diff / 1000); this.days = Math.floor(totalSeconds / 86400); this.hours = Math.floor((totalSeconds % 86400) / 3600); this.minutes = Math.floor((totalSeconds % 3600) / 60); this.seconds = totalSeconds % 60; }, pad(n) { return String(n).padStart(2, '0'); }, })); Alpine.data('publicPreregisterPage', (config) => ({ phase: config.phase, startAtMs: config.startAtMs, phoneEnabled: config.phoneEnabled, phoneRequired: config.phoneRequired === true, subscribeUrl: config.subscribeUrl, csrfToken: config.csrfToken, genericError: config.genericError, labelDay: config.labelDay, labelDays: config.labelDays, invalidEmailMsg: config.invalidEmailMsg, invalidPhoneMsg: config.invalidPhoneMsg, days: 0, hours: 0, minutes: 0, seconds: 0, countdownTimer: null, first_name: '', last_name: '', email: '', phone: '', submitting: false, formError: '', fieldErrors: {}, thankYouMessage: '', formButtonLabel: config.formButtonLabel || '', formButtonColor: config.formButtonColor || '#F47B20', formButtonTextColor: config.formButtonTextColor || '#FFFFFF', pageShareUrl: config.pageShareUrl || '', copyFeedback: '', redirectUrl: config.redirectUrl || '', redirectSecondsLeft: null, redirectTimer: null, strings: config.strings || {}, copyPageLink() { const url = this.pageShareUrl; if (!url) { return; } navigator.clipboard.writeText(url).then(() => { this.copyFeedback = config.strings?.linkCopied || ''; setTimeout(() => { this.copyFeedback = ''; }, 2500); }); }, init() { if (this.phase === 'before') { this.tickCountdown(); this.countdownTimer = setInterval(() => this.tickCountdown(), 1000); } }, destroy() { if (this.countdownTimer !== null) { clearInterval(this.countdownTimer); this.countdownTimer = null; } if (this.redirectTimer !== null) { clearInterval(this.redirectTimer); this.redirectTimer = null; } }, startRedirectCountdownIfNeeded() { if (!this.redirectUrl || String(this.redirectUrl).trim() === '') { return; } this.redirectSecondsLeft = 5; this.redirectTimer = setInterval(() => { this.redirectSecondsLeft--; if (this.redirectSecondsLeft <= 0) { if (this.redirectTimer !== null) { clearInterval(this.redirectTimer); this.redirectTimer = null; } window.location.assign(String(this.redirectUrl)); } }, 1000); }, tickCountdown() { const start = this.startAtMs; const now = Date.now(); const diff = start - now; if (diff <= 0) { if (this.countdownTimer !== null) { clearInterval(this.countdownTimer); this.countdownTimer = null; } this.phase = 'active'; return; } const totalSeconds = Math.floor(diff / 1000); this.days = Math.floor(totalSeconds / 86400); this.hours = Math.floor((totalSeconds % 86400) / 3600); this.minutes = Math.floor((totalSeconds % 3600) / 60); this.seconds = totalSeconds % 60; }, pad(n) { 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 trimmed = String(this.phone).trim(); if (this.phoneRequired && trimmed === '') { this.fieldErrors.phone = [this.invalidPhoneMsg]; ok = false; } else if (trimmed !== '') { const digits = trimmed.replace(/\D/g, ''); if (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.submitting = true; try { const body = { first_name: this.first_name, last_name: this.last_name, email: this.email, }; if (this.phoneEnabled) { body.phone = this.phone; } const res = await fetch(this.subscribeUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-CSRF-TOKEN': this.csrfToken, 'X-Requested-With': 'XMLHttpRequest', }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); if (res.ok && data.success) { this.phase = 'thanks'; this.thankYouMessage = data.message ?? ''; this.startRedirectCountdownIfNeeded(); return; } const hasServerMessage = typeof data.message === 'string' && data.message !== ''; const hasFieldErrors = data.errors !== undefined && data.errors !== null && typeof data.errors === 'object' && Object.keys(data.errors).length > 0; if (hasServerMessage) { this.formError = data.message; } if (hasFieldErrors) { this.fieldErrors = data.errors; } if (!res.ok && !hasServerMessage && !hasFieldErrors) { this.formError = this.genericError; } } catch { this.formError = this.genericError; } finally { this.submitting = false; } }, })); Alpine.data('mailwizzWizard', (cfg) => ({ listsUrl: cfg.listsUrl, fieldsUrl: cfg.fieldsUrl, phoneEnabled: cfg.phoneEnabled, hasExistingConfig: cfg.hasExistingConfig, hasWeeztixIntegration: cfg.hasWeeztixIntegration === true, existing: cfg.existing, csrf: cfg.csrf, step: 1, apiKey: '', lists: [], selectedListUid: '', selectedListName: '', allFields: [], fieldEmail: '', fieldFirstName: '', fieldLastName: '', fieldPhone: '', fieldCouponCode: '', tagField: '', tagValue: '', loading: false, errorMessage: '', init() { if (this.existing) { this.fieldEmail = this.existing.field_email ?? ''; this.fieldFirstName = this.existing.field_first_name ?? ''; this.fieldLastName = this.existing.field_last_name ?? ''; this.fieldPhone = this.existing.field_phone ?? ''; this.fieldCouponCode = this.existing.field_coupon_code ?? ''; this.tagField = this.existing.tag_field ?? ''; this.tagValue = this.existing.tag_value ?? ''; this.selectedListUid = this.existing.list_uid ?? ''; this.selectedListName = this.existing.list_name ?? ''; } }, textFields() { return this.allFields.filter((f) => f.type_identifier === 'text'); }, emailFieldChoices() { const texts = this.textFields(); const tagged = texts.filter( (f) => (f.tag && f.tag.toUpperCase().includes('EMAIL')) || (f.label && f.label.toLowerCase().includes('email')), ); return tagged.length > 0 ? tagged : texts; }, phoneFields() { return this.allFields.filter((f) => f.type_identifier === 'phonenumber'); }, checkboxFields() { return this.allFields.filter((f) => f.type_identifier === 'checkboxlist'); }, tagOptionsList() { const f = this.allFields.find((x) => x.tag === this.tagField); if (!f || !f.options) { return []; } return Object.entries(f.options).map(([key, label]) => ({ key, label: String(label), })); }, async postJson(url, body) { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-CSRF-TOKEN': this.csrf, 'X-Requested-With': 'XMLHttpRequest', }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); return { res, data }; }, async connectLists() { this.errorMessage = ''; if (!this.apiKey.trim()) { this.errorMessage = cfg.strings.apiKeyRequired; return; } this.loading = true; try { const { res, data } = await this.postJson(this.listsUrl, { api_key: this.apiKey }); if (!res.ok) { this.errorMessage = data.message || data.error || cfg.strings.genericError; return; } this.lists = Array.isArray(data.lists) ? data.lists : []; if (this.lists.length === 0) { this.errorMessage = cfg.strings.noListsError; return; } if (this.existing?.list_uid) { const match = this.lists.find((l) => l.list_uid === this.existing.list_uid); if (match) { this.selectedListUid = match.list_uid; this.selectedListName = match.name; } } this.step = 2; } finally { this.loading = false; } }, async loadFieldsAndGoStep3() { this.errorMessage = ''; if (!this.selectedListUid) { this.errorMessage = cfg.strings.selectListError; return; } this.syncListNameFromSelection(); this.loading = true; try { const { res, data } = await this.postJson(this.fieldsUrl, { api_key: this.apiKey, list_uid: this.selectedListUid, }); if (!res.ok) { this.errorMessage = data.message || data.error || cfg.strings.genericError; return; } this.allFields = Array.isArray(data.fields) ? data.fields : []; if (this.existing) { this.fieldEmail = this.existing.field_email || this.fieldEmail; this.fieldFirstName = this.existing.field_first_name || this.fieldFirstName; this.fieldLastName = this.existing.field_last_name || this.fieldLastName; this.fieldPhone = this.existing.field_phone || this.fieldPhone; this.fieldCouponCode = this.existing.field_coupon_code || this.fieldCouponCode; this.tagField = this.existing.tag_field || this.tagField; this.tagValue = this.existing.tag_value || this.tagValue; } this.step = 3; } finally { this.loading = false; } }, syncListNameFromSelection() { const l = this.lists.find((x) => x.list_uid === this.selectedListUid); this.selectedListName = l ? l.name : ''; }, goStep4() { this.errorMessage = ''; if (!this.fieldEmail || !this.fieldFirstName || !this.fieldLastName) { this.errorMessage = cfg.strings.mapFieldsError; return; } if (!this.tagField) { this.errorMessage = cfg.strings.tagFieldError; return; } this.step = 4; }, submitSave() { this.errorMessage = ''; if (!this.tagValue) { this.errorMessage = cfg.strings.tagValueError; return; } if (!this.hasExistingConfig && !this.apiKey.trim()) { this.errorMessage = cfg.strings.apiKeyRequired; return; } this.$refs.saveForm.requestSubmit(); }, })); Alpine.data('weeztixSetup', (cfg) => ({ pageId: cfg.pageId, couponsUrl: cfg.couponsUrl, csrf: cfg.csrf, isConnected: cfg.isConnected === true, callbackUrl: cfg.callbackUrl, errorMessage: '', coupons: [], couponGuid: '', couponName: '', codePrefix: 'PREREG', usageCount: 1, couponsRefreshing: false, strings: cfg.strings || {}, async init() { if (cfg.existing) { this.codePrefix = cfg.existing.code_prefix || 'PREREG'; const uc = cfg.existing.usage_count; if (typeof uc === 'number' && !Number.isNaN(uc)) { this.usageCount = uc; } else if (uc !== null && uc !== undefined && String(uc).trim() !== '') { const parsed = parseInt(String(uc), 10); this.usageCount = Number.isNaN(parsed) ? 1 : parsed; } else { this.usageCount = 1; } this.couponGuid = cfg.existing.coupon_guid || ''; this.couponName = cfg.existing.coupon_name || ''; } if (this.isConnected) { await this.loadCoupons(); } else if (cfg.existing && cfg.existing.coupon_guid) { this.ensureSelectedCouponInList(); } }, async postJson(url, body) { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-CSRF-TOKEN': this.csrf, 'X-Requested-With': 'XMLHttpRequest', }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); return { res, data }; }, syncCouponName() { if (!this.couponGuid) { this.couponName = ''; return; } const c = this.coupons.find((x) => x.guid === this.couponGuid); if (c && typeof c.name === 'string' && c.name.trim() !== '') { this.couponName = c.name.trim(); } }, ensureSelectedCouponInList() { const guid = this.couponGuid; if (!guid || this.coupons.some((x) => x.guid === guid)) { return; } const label = typeof this.couponName === 'string' && this.couponName.trim() !== '' ? this.couponName.trim() : guid; this.coupons = [{ guid, name: label }, ...this.coupons]; }, async loadCoupons() { this.errorMessage = ''; const { res, data } = await this.postJson(this.couponsUrl, { page_id: this.pageId }); if (!res.ok) { this.errorMessage = data.message || this.strings.loadCouponsError; this.ensureSelectedCouponInList(); return; } this.coupons = Array.isArray(data.coupons) ? data.coupons : []; this.ensureSelectedCouponInList(); this.syncCouponName(); }, async refreshCoupons() { if (!this.isConnected) { return; } this.couponsRefreshing = true; try { await this.loadCoupons(); } finally { this.couponsRefreshing = false; } }, })); }); window.Alpine = Alpine; Alpine.start();