40) {
showError(groupId, errorId, `${label} must be 40 characters or fewer.`);
return false;
}
// No digits
if (/[0-9]/.test(val)) {
showError(groupId, errorId, `${label} must not contain numbers.`);
return false;
}
// No emojis (covers most emoji ranges)
if (/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}]/u.test(val)) {
showError(groupId, errorId, `${label} must not contain emojis.`);
return false;
}
// Allowed characters: Unicode letters, apostrophe, hyphen, single spaces
// Rejects symbols, special chars not in that set
if (!/^[\p{L}'\- ]+$/u.test(val)) {
showError(groupId, errorId, `${label} may only contain letters, hyphens, apostrophes, and spaces.`);
return false;
}
// No consecutive spaces
if (/ /.test(val)) {
showError(groupId, errorId, `${label} must not contain consecutive spaces.`);
return false;
}
// No consecutive hyphens or apostrophes
if (/--|''/.test(val)) {
showError(groupId, errorId, `${label} must not contain consecutive hyphens or apostrophes.`);
return false;
}
// Reject fake / placeholder values
if (FAKE_NAME_VALUES.has(val.toLowerCase())) {
showError(groupId, errorId, `Please enter your real ${label.toLowerCase()}.`);
return false;
}
// Reject repeated single characters: aaaa, bbbb, etc.
if (/^(.)\1{3,}$/.test(val)) {
showError(groupId, errorId, `Please enter your real ${label.toLowerCase()}.`);
return false;
}
showSuccess(groupId, errorId);
return true;
}
// ═════════════════════════════════════════════
// SECTION 4: EMAIL VALIDATION
// Production-grade: RFC structure + security +
// disposable blocking + gibberish domain detection
// ═════════════════════════════════════════════
// ── 4a. Known-good provider whitelist ────────
// Domains on this list bypass gibberish scoring.
// Add any real provider your users actually use.
const TRUSTED_EMAIL_DOMAINS = new Set([
// Global / major
'gmail.com', 'googlemail.com', 'yahoo.com', 'yahoo.com.au',
'yahoo.co.uk', 'yahoo.co.in', 'yahoo.ca', 'yahoo.fr',
'yahoo.de', 'yahoo.it', 'yahoo.es', 'yahoo.com.br',
'hotmail.com', 'hotmail.co.uk', 'hotmail.com.au',
'hotmail.fr', 'hotmail.de', 'hotmail.it', 'hotmail.es',
'outlook.com', 'outlook.com.au', 'outlook.co.uk',
'live.com', 'live.com.au', 'live.co.uk',
'msn.com', 'icloud.com', 'me.com', 'mac.com',
'proton.me', 'protonmail.com', 'protonmail.ch',
'pm.me', 'tutanota.com', 'tutamail.com',
'fastmail.com', 'fastmail.fm', 'fastmail.net',
'zoho.com', 'zohomail.com',
'aol.com', 'aim.com',
// Australia
'bigpond.com', 'bigpond.net.au', 'telstra.com',
'optusnet.com.au', 'tpg.com.au', 'iinet.net.au',
'internode.on.net', 'westnet.com.au', 'aapt.net.au',
'dodo.com.au', 'adam.com.au', 'aussiebb.com.au',
// India
'rediffmail.com', 'indiatimes.com',
// UK
'btinternet.com', 'sky.com', 'virginmedia.com',
'talk21.com', 'ntlworld.com',
// General ISP / edu patterns handled separately
// ISKCON / religious orgs
'iskcon.net', 'iskcon.net.au', 'iskcon.org',
'harekrishna.com',
// Common business suffixes — not domains but handled via SLD scoring
]);
// ── 4b. Disposable / burner providers ────────
const DISPOSABLE_DOMAINS = new Set([
'mailinator.com', 'mailinator.net', 'mailinator.org',
'tempmail.com', 'tempmail.net', 'temp-mail.org', 'temp-mail.ru',
'10minutemail.com', '10minutemail.net', '10minutemail.org',
'10minutemail.de', '10minutemail.co.uk',
'guerrillamail.com', 'guerrillamail.net', 'guerrillamail.org',
'guerrillamail.biz', 'guerrillamail.de', 'guerrillamail.info',
'yopmail.com', 'yopmail.fr', 'yopmail.net',
'trashmail.com', 'trashmail.me', 'trashmail.net',
'trashmail.org', 'trashmail.at', 'trashmail.io',
'maildrop.cc', 'mailsac.com',
'spamgourmet.com', 'spamgourmet.net', 'spamgourmet.org',
'throwam.com', 'throwaway.email', 'dispostable.com',
'getnada.com', 'sharklasers.com', 'spam4.me',
'fakeinbox.com', 'mailnull.com', 'spamherelots.com',
'discard.email', 'crapmail.org', 'binkmail.com',
'safetymail.info', 'tempr.email', 'tmpmail.org', 'tmpmail.net',
'burnermail.io', 'inboxbear.com', 'anonaddy.com',
'mailnesia.com', 'spamgourmet.com', 'emailondeck.com',
'getairmail.com', 'meltmail.com', 'mt2015.com',
'nwytg.net', 'objectmail.com', 'ownmail.net',
'petml.com', 'put2.net', 'qq.com.cn',
'quickinbox.com', 'rcpt.at', 'rppkn.com',
's0ny.net', 'splytre.com', 'supermailer.jp',
'suremail.info', 'teewars.org', 'tempalias.com',
'tempe-mail.com', 'tempinbox.com', 'tempsky.com',
'tempymail.com', 'thanksnospam.info', 'thisisnotmyrealemail.com',
'throwam.com', 'tmail.com', 'trbvm.com',
'trillianpro.com', 'tunxis.edu', 'twinmail.de',
'uggsrock.com', 'uroid.com', 'veryrealemail.com',
'viditag.com', 'vomoto.com', 'wuzupmail.net',
'xagloo.com', 'xoxy.net', 'yep.it',
'zerotohero.com', 'zippymail.info', 'zoemail.net',
'zomg.info'
]);
// ── 4c. Blocked fake / placeholder domains ───
const FAKE_DOMAINS = new Set([
'localhost', 'localdomain', 'local',
'test.com', 'test.net', 'test.org',
'example.com', 'example.org', 'example.net',
'sample.com', 'invalid.com', 'invalid.org',
'domain.com', 'email.com', 'noreply.com',
'nowhere.com', 'fake.com', 'fake.net',
'dummy.com', 'dummy.org', 'nomail.com',
'notreal.com', 'noemail.com', 'mailme.com',
'test123.com', 'abc.com', 'xyz.com',
'qwerty.com', 'asdf.com', 'zxcv.com',
'aaa.com', 'bbb.com', 'ccc.com', 'ddd.com',
'eee.com', 'fff.com', 'zzz.com',
'mail.com', // too generic and commonly misused
'webmail.com',
'placeholder.com', 'something.com',
'company.com', 'mycompany.com', 'yourcompany.com'
]);
// ── 4d. Gibberish domain detection ───────────
// Catches randomly-typed domains like jjfjhgu.com,
// dhfjjgsjg.net, qpzrmlx.org — real domains don't
// look like that. We score the domain SLD (second-
// level domain, e.g. "jjfjhgu" from "jjfjhgu.com")
// on several heuristics.
/**
* Returns true if the domain's SLD looks like gibberish.
* Only applied when the full domain is not in TRUSTED_EMAIL_DOMAINS.
* @param {string} sld — e.g. "jjfjhgu", "gmail", "iskcon"
* @returns {boolean}
*/
function isGibberishDomain(sld) {
if (!sld || sld.length < 2) return true;
const s = sld.toLowerCase();
const len = s.length;
// Very short SLDs that are not in trusted list are suspect
// (single-char already blocked upstream; 2-char only for known ccTLDs)
if (len <= 2) return false; // let it through — could be a real 2-char SLD
// ── Heuristic 1: consonant cluster ratio ────
// Real words/brand names have vowels interspersed.
// Count consonant runs of 4+ in a row.
const consonantRunMatch = s.match(/[^aeiou]{4,}/g);
if (consonantRunMatch) {
// Sum total chars in long consonant runs
const clusterChars = consonantRunMatch.reduce((a, m) => a + m.length, 0);
const clusterRatio = clusterChars / len;
if (clusterRatio > 0.75) return true;
}
// ── Heuristic 2: vowel ratio ─────────────────
// Real brand/company names are typically 20–60% vowels.
// Gibberish typed on a keyboard often has very few.
const vowelCount = (s.match(/[aeiou]/g) || []).length;
const vowelRatio = vowelCount / len;
if (len >= 6 && vowelRatio < 0.1) return true; // almost no vowels
// ── Heuristic 3: bigram frequency score ──────
// Score how "English-like" the character pairs are.
// Common bigrams in real domain names vs keyboard mash.
const COMMON_BIGRAMS = new Set([
'th','he','in','er','an','re','on','en','at','ou',
'ed','ha','to','or','it','is','hi','es','ng','ne',
'al','si','ar','co','me','de','ro','le','ic','la',
'li','nd','ma','se','ce','ld','ea','ri','gh','ch',
'di','be','ti','el','st','so','nt','io','ot','tr',
'un','ca','te','pr','pe','ex','ab','om','ad','ll',
'ac','ge','oo','we','ni','ho','fi','mi','rt','ss',
'no','go','po','pl','cl','fl','br','gr','cr','dr',
'bl','gl','sk','sp','sw','sm','sl','sn','sc','sh',
'wh','ph','qu','ck','ff','tt','pp','mm','nn','rr',
'ly','ry','ey','ay','ty','ny','my','by','gy','ky',
'ow','ew','aw','iw','yw', 'ai','au','ei','eu','ui',
'ia','ie','io','iu','ua','ue','uo','oo','ee','aa'
]);
if (len >= 5) {
let knownBigrams = 0;
for (let i = 0; i < len - 1; i++) {
if (COMMON_BIGRAMS.has(s[i] + s[i + 1])) knownBigrams++;
}
const bigramScore = knownBigrams / (len - 1);
// Very low bigram match = keyboard mash
if (bigramScore < 0.1 && len >= 7) return true;
}
// ── Heuristic 4: repeated/alternating pattern ─
// e.g. "jjfjhgu", "ababab", "xyzxyz"
if (/(.)\1{3,}/.test(s)) return true; // 4+ same char in a row
// ── Heuristic 5: no vowels at all (for len >= 5) ─
if (len >= 5 && vowelCount === 0) return true;
return false;
}
/**
* Full production email validator.
* Normalises → structure checks → security →
* domain quality → disposable/fake → gibberish.
*/
function validateEmail() {
const raw = emailInput.value;
const val = raw.trim().toLowerCase();
// ── Required ─────────────────────────────────
if (!val) {
showError('emailGroup', 'emailError', 'Email address is required.');
return false;
}
// ── Max total length (RFC 5321) ───────────────
if (val.length > 254) {
showError('emailGroup', 'emailError', 'Email is too long (maximum 254 characters).');
return false;
}
// ── Security: HTML / JS / SQL injection ──────
if (containsMalicious(val)) {
showError('emailGroup', 'emailError', 'Email contains invalid characters.');
return false;
}
// ── Exactly one @ ─────────────────────────────
const atCount = (val.match(/@/g) || []).length;
if (atCount !== 1) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
const [local, domain] = val.split('@');
// ── Local part checks ─────────────────────────
// Minimum 2 characters in local part (rejects a@b.com style)
if (local.length < 2) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
// Max 64 chars
if (local.length > 64) {
showError('emailGroup', 'emailError', 'Email is too long (local part exceeds 64 characters).');
return false;
}
// No leading/trailing dot
if (local.startsWith('.') || local.endsWith('.')) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
// No consecutive dots
if (/\.\./.test(local)) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
// Permitted characters only (RFC 5321 unquoted local)
if (!/^[a-z0-9!#$%&'*+/=?^_`{|}~.\-]+$/.test(local)) {
showError('emailGroup', 'emailError', 'Email contains invalid characters.');
return false;
}
// Reject local parts that are pure gibberish keyboard mash
// (only the alphabetic portion of local, strip numbers/symbols first)
const localAlpha = local.replace(/[^a-z]/g, '');
if (localAlpha.length >= 6 && isGibberishDomain(localAlpha)) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
// ── Domain checks ─────────────────────────────
if (!domain || !domain.includes('.')) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
// No leading/trailing dot or hyphen
if (/^[.\-]|[.\-]$/.test(domain)) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
// No consecutive dots
if (/\.\./.test(domain)) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
// Domain characters
if (!/^[a-z0-9.\-]+$/.test(domain)) {
showError('emailGroup', 'emailError', 'Email contains invalid characters.');
return false;
}
const domainParts = domain.split('.');
// Each domain label must be non-empty and not start/end with hyphen
for (const part of domainParts) {
if (!part || /^-|-$/.test(part)) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
}
// TLD must be 2–10 alpha chars
const tld = domainParts[domainParts.length - 1];
if (!/^[a-z]{2,10}$/.test(tld)) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
// ── Blocked fake / placeholder domains ────────
if (FAKE_DOMAINS.has(domain)) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
// ── Disposable provider check ─────────────────
// Check full domain and root domain (sub.mailinator.com → mailinator.com)
const rootDomain = domainParts.slice(-2).join('.');
if (DISPOSABLE_DOMAINS.has(domain) || DISPOSABLE_DOMAINS.has(rootDomain)) {
showError('emailGroup', 'emailError', 'Disposable email addresses are not allowed.');
return false;
}
// ── Gibberish domain detection ────────────────
// Only applied when domain is NOT in our trusted whitelist.
// Extract the SLD: for "mail.iskcon.net.au" → "iskcon"
// for "gmail.com" → "gmail" (trusted, skipped)
// for "jjfjhgu.com" → "jjfjhgu"
if (!TRUSTED_EMAIL_DOMAINS.has(domain)) {
// SLD is always the second-to-last label
const sld = domainParts[domainParts.length - 2];
if (isGibberishDomain(sld)) {
showError('emailGroup', 'emailError', 'Please enter a valid email address.');
return false;
}
}
showSuccess('emailGroup', 'emailError');
return true;
}
// ═════════════════════════════════════════════
// SECTION 5: PHONE VALIDATION
// Country-aware, with formatting + paste handling
// ═════════════════════════════════════════════
function getCurrentMaxLength(country, currentVal) {
const rule = countryRules[country];
if (!rule) return 15;
if (country === '+64') {
return currentVal.startsWith('0') ? 10 : 9;
}
return currentVal.startsWith('0') ? rule.maxLengthWithZero : rule.maxLengthWithoutZero;
}
function updatePhoneUI() {
const country = countrySelect.value;
const rule = countryRules[country];
if (!rule) return;
phoneInput.placeholder = rule.placeholder;
phoneHelp.textContent = rule.help;
const cleaned = phoneInput.value.replace(/\D/g, '');
phoneInput.maxLength = getCurrentMaxLength(country, cleaned);
}
function validatePhone() {
const country = countrySelect.value;
const rule = countryRules[country];
const val = phoneInput.value.replace(/\D/g, '');
if (!val) {
showError('phoneGroup', 'phoneError', 'Mobile number is required.');
return false;
}
if (rule) {
if (!new RegExp(rule.pattern).test(val)) {
showError('phoneGroup', 'phoneError', rule.errorMsg);
return false;
}
}
showSuccess('phoneGroup', 'phoneError');
return true;
}
// Prevent non-numeric keystrokes and enforce max length
phoneInput.addEventListener('keydown', function (e) {
const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End'];
if (allowedKeys.includes(e.key)) return;
if ((e.ctrlKey || e.metaKey) && ['a', 'c', 'v', 'x'].includes(e.key.toLowerCase())) return;
if (!/^[0-9]$/.test(e.key)) {
e.preventDefault();
return;
}
const country = countrySelect.value;
const rule = countryRules[country];
if (!rule) return;
const cleaned = this.value.replace(/\D/g, '');
const prefix = country.replace(/\D/g, '');
let isPrefixed = cleaned.startsWith(prefix);
if (cleaned.length === 0 && e.key === prefix[0]) isPrefixed = true;
if (cleaned.length === 1 && cleaned === prefix[0] && e.key === prefix[1]) isPrefixed = true;
let maxLen = getCurrentMaxLength(country, cleaned);
if (isPrefixed) maxLen = getCurrentMaxLength(country, cleaned) + prefix.length;
if (cleaned.length >= maxLen) e.preventDefault();
});
// Strip non-digits on input (handles paste), enforce max length, strip country prefix
phoneInput.addEventListener('input', function () {
const country = countrySelect.value;
const rule = countryRules[country];
let cleaned = this.value.replace(/\D/g, '');
if (rule) {
const prefix = country.replace(/\D/g, '');
const isPrefixed = cleaned.startsWith(prefix);
let maxLen = getCurrentMaxLength(country, cleaned);
if (isPrefixed) maxLen += prefix.length;
if (cleaned.length > maxLen) cleaned = cleaned.substring(0, maxLen);
// Auto-strip country prefix if user pastes it in
if (isPrefixed) {
const rest = cleaned.substring(prefix.length);
const patterns = {
'+61': /^0?4[0-9]{8}$/,
'+1': /^[2-9][0-9]{9}$/,
'+44': /^0?7[0-9]{9}$/,
'+91': /^[6-9][0-9]{9}$/,
'+64': /^0?2[0-9]{7,9}$/
};
if (patterns[country] && patterns[country].test(rest)) cleaned = rest;
}
}
this.value = cleaned;
updatePhoneUI();
validatePhone();
});
countrySelect.addEventListener('change', function () {
phoneInput.value = '';
updatePhoneUI();
clearState('phoneGroup', 'phoneError');
});
// ═════════════════════════════════════════════
// SECTION 6: SUBURB VALIDATION
// ═════════════════════════════════════════════
const FAKE_SUBURB_VALUES = new Set([
'test', 'testing', 'asdf', 'xxxxx', 'admin',
'sample', 'suburb', 'location', 'place', 'city',
'town', 'none', 'n/a', 'na', 'here', 'there',
'abc', 'abcd', 'xyz', 'foo', 'bar', 'baz'
]);
function validateSuburb() {
const raw = suburbInput.value;
const val = raw.trim();
if (!val) {
showError('suburbGroup', 'suburbError', 'Suburb is required.');
return false;
}
if (containsMalicious(val)) {
showError('suburbGroup', 'suburbError', 'Suburb contains invalid characters.');
return false;
}
if (val.length < 2) {
showError('suburbGroup', 'suburbError', 'Suburb must be at least 2 characters.');
return false;
}
if (val.length > 60) {
showError('suburbGroup', 'suburbError', 'Suburb must be 60 characters or fewer.');
return false;
}
// Must contain at least one letter (reject purely numeric input)
if (!/[\p{L}]/u.test(val)) {
showError('suburbGroup', 'suburbError', 'Suburb must contain letters.');
return false;
}
// Allowed: Unicode letters, spaces, hyphens, apostrophes
if (!/^[\p{L}'\- ]+$/u.test(val)) {
showError('suburbGroup', 'suburbError', 'Suburb may only contain letters, spaces, hyphens, and apostrophes.');
return false;
}
if (FAKE_SUBURB_VALUES.has(val.toLowerCase())) {
showError('suburbGroup', 'suburbError', 'Please enter your real suburb.');
return false;
}
// Repeated single character
if (/^(.)\1{3,}$/.test(val)) {
showError('suburbGroup', 'suburbError', 'Please enter your real suburb.');
return false;
}
showSuccess('suburbGroup', 'suburbError');
return true;
}
// ═════════════════════════════════════════════
// SECTION 7: GUESTS VALIDATION
// ═════════════════════════════════════════════
function validateGuests() {
const raw = guestsInput.value.trim();
if (!raw) {
showError('guestsGroup', 'guestsError', 'Number of guests is required.');
return false;
}
if (containsMalicious(raw)) {
showError('guestsGroup', 'guestsError', 'Invalid input detected.');
return false;
}
// Reject scientific notation, decimals, letters, symbols
if (!/^[0-9]+$/.test(raw)) {
showError('guestsGroup', 'guestsError', 'Please enter between 1 and 20 guests.');
return false;
}
const num = parseInt(raw, 10);
if (isNaN(num) || num < 1 || num > 20) {
showError('guestsGroup', 'guestsError', 'Please enter between 1 and 20 guests.');
return false;
}
showSuccess('guestsGroup', 'guestsError');
return true;
}
// ═════════════════════════════════════════════
// SECTION 8: TERMS VALIDATION
// ═════════════════════════════════════════════
function validateTerms() {
if (!termsInput.checked) {
showError('termsGroup', 'termsError', 'You must agree to the terms and conditions to proceed.');
return false;
}
showSuccess('termsGroup', 'termsError');
return true;
}
// ═════════════════════════════════════════════
// SECTION 9: EVENT BINDINGS — realtime + blur
// ═════════════════════════════════════════════
// First Name
firstName.addEventListener('input', () => validateName(firstName, 'firstNameGroup', 'firstNameError', 'First name'));
firstName.addEventListener('blur', () => validateName(firstName, 'firstNameGroup', 'firstNameError', 'First name'));
// Last Name
lastName.addEventListener('input', () => validateName(lastName, 'lastNameGroup', 'lastNameError', 'Last name'));
lastName.addEventListener('blur', () => validateName(lastName, 'lastNameGroup', 'lastNameError', 'Last name'));
// Email
emailInput.addEventListener('input', validateEmail);
emailInput.addEventListener('blur', validateEmail);
// Suburb
suburbInput.addEventListener('input', validateSuburb);
suburbInput.addEventListener('blur', validateSuburb);
// Guests
guestsInput.addEventListener('input', validateGuests);
guestsInput.addEventListener('blur', validateGuests);
// Terms — clear error immediately on check
termsInput.addEventListener('change', validateTerms);
// ═════════════════════════════════════════════
// SECTION 10: FORM SUBMIT
// ═════════════════════════════════════════════
document.getElementById('ktRegistrationForm').addEventListener('submit', function (e) {
e.preventDefault();
const valid = [
validateName(firstName, 'firstNameGroup', 'firstNameError', 'First name'),
validateName(lastName, 'lastNameGroup', 'lastNameError', 'Last name'),
validateEmail(),
validatePhone(),
validateSuburb(),
validateGuests(),
validateTerms()
];
if (valid.includes(false)) {
// Focus the first invalid field for accessibility
const firstInvalid = document.querySelector('.kt-form-group.has-error input, .kt-form-group.has-error select');
if (firstInvalid) firstInvalid.focus();
return;
}
// ── Backend submission — unchanged from original ──────────────
Swal.fire({
title: 'Submitting Registration...',
text: 'Please wait while we process your request.',
allowOutsideClick: false,
didOpen: () => { Swal.showLoading(); }
});
const formData = new FormData(this);
fetch(BACKEND_PATH + 'event_live.php?event=' + encodeURIComponent(currentEventSlug), {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(err => { throw new Error(err.error || 'Server error occurred.'); });
}
return response.json();
})
.then(data => {
if (data.success) {
Swal.fire({
title: 'Registration Successful!',
text: data.message || 'Your registration has been submitted and a confirmation email has been sent.',
icon: 'success',
confirmButtonColor: '#043651'
}).then(() => {
document.getElementById('ktRegistrationForm').reset();
document.querySelectorAll('.kt-form-group').forEach(g => g.classList.remove('has-success', 'has-error'));
updatePhoneUI();
});
} else {
Swal.fire({
title: 'Submission Failed',
text: data.error || 'Something went wrong.',
icon: 'error',
confirmButtonColor: '#e98b76'
});
}
})
.catch(error => {
Swal.fire({
title: 'Error',
text: error.message || 'Unable to connect to the server. Please try again later.',
icon: 'error',
confirmButtonColor: '#e98b76'
});
});
});
// ═════════════════════════════════════════════
// SECTION 11: EVENT LOADER — unchanged from original
// ═════════════════════════════════════════════
function showEventError(title, desc) {
document.getElementById('eventTitle').textContent = title;
document.getElementById('eventMeta').textContent = '';
document.getElementById('eventDesc').textContent = desc;
document.getElementById('eventMediaContainer').style.display = 'none';
document.getElementById('formCard').style.display = 'none';
}
fetch(BACKEND_PATH + 'events.json?v=' + Date.now())
.then(res => res.json())
.then(events => {
const activeEvents = events.filter(ev => (ev.status || 'enabled') === 'enabled' && ev.registration === 'yes');
let selected = null;
if (eventSlug) {
selected = activeEvents.find(ev => ev.slug === eventSlug);
} else if (activeEvents.length > 0) {
selected = activeEvents[0];
}
if (selected) {
currentEventSlug = selected.slug;
document.getElementById('eventTitle').textContent = selected.name;
const dateStr = selected.date
? selected.date + (selected.time ? ' @ ' + selected.time : '')
: 'Date & Time TBA';
document.getElementById('eventMeta').textContent = dateStr;
document.getElementById('eventDesc').innerHTML = selected.description || '';
const imgContainer = document.getElementById('eventMediaContainer');
if (selected.media) {
const imgUrl = selected.media.startsWith('http') ? selected.media : BACKEND_PATH + selected.media;
if (imgUrl.endsWith('.mp4') || imgUrl.endsWith('.webm')) {
imgContainer.innerHTML = `
`;
} else {
imgContainer.innerHTML = `

`;
}
imgContainer.style.display = 'block';
} else {
imgContainer.style.display = 'none';
}
document.getElementById('formCard').style.display = 'block';
} else if (eventSlug) {
showEventError('Event Not Found', 'The event you are looking for does not exist or is no longer active.');
} else if (activeEvents.length === 0) {
showEventError('No Active Registrations', 'There are no events open for registration at this time.');
}
})
.catch(err => {
console.warn('Backend events.json could not be loaded.', err);
showEventError('Connection Error', 'Unable to fetch real-time registration data at this moment.');
});
// Initialise phone UI on load
updatePhoneUI();
})();