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, startAtMs: config.startAtMs, phoneEnabled: config.phoneEnabled, 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: '', init() { if (this.phase === 'before') { this.tickCountdown(); this.countdownTimer = setInterval(() => this.tickCountdown(), 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 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.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 ?? ''; return; } if (typeof data.message === 'string' && data.message !== '') { this.formError = data.message; } if (data.errors !== undefined && data.errors !== null && typeof data.errors === 'object') { this.fieldErrors = data.errors; } } 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, existing: cfg.existing, csrf: cfg.csrf, step: 1, apiKey: '', lists: [], selectedListUid: '', selectedListName: '', allFields: [], fieldEmail: '', fieldFirstName: '', fieldLastName: '', fieldPhone: '', 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.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.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(); }, })); }); window.Alpine = Alpine; Alpine.start();