feat: Phase 3 - public registration pages and Mailwizz config
This commit is contained in:
@@ -2,6 +2,290 @@ import './bootstrap';
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
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,
|
||||
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');
|
||||
},
|
||||
|
||||
async submitForm() {
|
||||
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 = {
|
||||
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.phoneEnabled && !this.fieldPhone) {
|
||||
this.errorMessage = cfg.strings.mapPhoneError;
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user