msos/event-register.js

207 lines
10 KiB
JavaScript

/* ==========================================================================
Event registration — single-step form logic for the "Meet Student Slovenia"
promo page. No dependencies. Mirrors member-form.js: soft anti-abuse
(honeypot + localStorage cooldown/cap) and a Google Form submission that,
until GOOGLE_FORM.action is filled in, just logs the payload and still shows
the success screen. The success message adapts to how the person will attend
(in person vs live stream).
TODO (future work, deferred 2026-08-20): merge this with an email newsletter
so registrants get a confirmation email that also carries a "cancel
reservation" button (and similar self-service actions). Likely needs a real
backend / Apps Script / email tool rather than the static-site + Google Form
setup used here.
========================================================================== */
(function () {
/* ---- left-hand floating "Register now" CTA: show after the hero, hide near the form/footer ---- */
(function () {
var sticky = document.querySelector('.event-sticky-cta');
if (!sticky) return;
var reg = document.getElementById('register');
var footer = document.querySelector('footer');
function update() {
var past = window.scrollY > 600;
var near = [reg, footer].some(function (el) {
if (!el) return false;
var r = el.getBoundingClientRect();
return r.top < window.innerHeight * 0.9 && r.bottom > 0;
});
if (past && !near) { sticky.hidden = false; sticky.classList.remove('is-hiding'); }
else if (!sticky.hidden) { sticky.classList.add('is-hiding'); }
}
window.addEventListener('scroll', update, { passive: true });
window.addEventListener('resize', update);
update();
})();
var form = document.querySelector('.er-form');
if (!form) return;
var section = form.closest('.msreg') || form.parentNode;
var success = section.querySelector('.er-success');
var alertBox = form.querySelector('.ms-alert');
var submit = form.querySelector('.er-submit');
/* ---- single-select radio groups (role, attendance, how-heard) ---- */
[].slice.call(form.querySelectorAll('.ms-radios')).forEach(function (group) {
[].slice.call(group.querySelectorAll('.ms-radio')).forEach(function (btn) {
btn.addEventListener('click', function () {
[].slice.call(group.querySelectorAll('.ms-radio')).forEach(function (b) { b.classList.remove('is-selected'); });
btn.classList.add('is-selected');
group.setAttribute('data-value', btn.getAttribute('data-value'));
});
});
});
/* ---- dropdowns that reveal a conditional field (e.g. "How did you hear" -> Other) ---- */
[].slice.call(form.querySelectorAll('select[data-name]')).forEach(function (sel) {
sel.addEventListener('change', function () {
[].slice.call(form.querySelectorAll('.ev-cond[data-for="' + sel.getAttribute('data-name') + '"]')).forEach(function (c) {
c.hidden = c.getAttribute('data-when') !== sel.value;
});
});
});
/* ---- multi-select chip groups, if any (kept for compatibility) ---- */
[].slice.call(form.querySelectorAll('.ms-multi')).forEach(function (group) {
[].slice.call(group.querySelectorAll('.ms-chip')).forEach(function (chip) {
chip.addEventListener('click', function () {
chip.classList.toggle('is-selected');
var cond = form.querySelector('.ev-cond[data-when="' + chip.getAttribute('data-value') + '"]');
if (cond) cond.hidden = !chip.classList.contains('is-selected');
});
});
});
function radioValue(name) {
var g = form.querySelector('.ms-radios[data-name="' + name + '"]');
if (!g) return '';
var sel = g.querySelector('.ms-radio.is-selected');
return sel ? sel.getAttribute('data-value') : '';
}
function validate() {
var ok = true;
[].slice.call(form.querySelectorAll('[data-required="1"]')).forEach(function (el) {
if (el.classList.contains('ms-radios')) { if (!el.querySelector('.ms-radio.is-selected')) ok = false; }
else if (el.classList.contains('ms-multi')) { if (!el.querySelector('.ms-chip.is-selected')) ok = false; }
else if (el.type === 'checkbox') { if (!el.checked) ok = false; }
else if (el.type === 'email') { if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(el.value)) ok = false; }
else { if (!el.value || !el.value.trim()) ok = false; }
});
return ok;
}
function collect() {
var d = {};
[].slice.call(form.querySelectorAll('[data-name]')).forEach(function (el) {
var name = el.getAttribute('data-name');
if (el.closest('.ev-cond') && el.closest('.ev-cond').hidden) return; // skip hidden "Other" field
if (el.classList.contains('ms-radios')) { var v = radioValue(name); if (v) d[name] = v; }
else if (el.classList.contains('ms-multi')) {
var sel = [].slice.call(el.querySelectorAll('.ms-chip.is-selected')).map(function (c) { return c.getAttribute('data-value'); });
if (sel.length) d[name] = sel.join(', ');
}
else if (el.type === 'checkbox') { d[name] = el.checked ? 'yes' : 'no'; }
else if (el.value && el.value.trim()) { d[name] = el.value.trim(); }
});
// fold the free-text "other" channel into the heard value so it lands in the same column
if (d.heardOther && d.heard) d.heard = d.heard.replace(/\bother\b/, 'other (' + d.heardOther + ')');
else if (d.heardOther && !d.heard) d.heard = 'other (' + d.heardOther + ')';
return d;
}
/* ---- Google Form wiring ----
Create the Google Form for this event, then paste its formResponse action
and per-question entry IDs below. Until `action` is set, submissions are
only logged to the console and the success screen still shows. */
var GOOGLE_FORM = {
action: 'https://docs.google.com/forms/d/e/1FAIpQLSfUO8LQjcfkYXBcoyl4e4gqk9JjGnR3WVZix_DdL13LNQYKWg/formResponse',
fields: {
fullName: 'entry.1532132852',
city: 'entry.1095973387',
email: 'entry.1967227679',
role: 'entry.1413810066',
attendance: 'entry.362663322',
question: 'entry.294764315',
heard: 'entry.2039184343',
consentData: 'entry.150585529',
consentPhoto: 'entry.156995592',
consentFuture:'entry.1428423095',
// study plans
level: 'entry.1414731533',
startYear: 'entry.1983142302',
cityPrimary: 'entry.2090240003',
citySecondary: 'entry.1911627426',
universityPrimary: 'entry.1057557567',
universitySecondary: 'entry.961330986',
facultyPrimary: 'entry.1947153061',
facultySecondary: 'entry.497532970',
programmePrimary: 'entry.1452861360',
programmeSecondary: 'entry.1113663724',
heardOther: 'entry.1162948550',
summary: 'entry.908347868' // catch-all: the whole payload as one text field
}
};
function sendToForm(d) {
if (!GOOGLE_FORM.action) { console.log('[MSOS event] not wired yet, payload:', d); return; }
var m = {};
Object.keys(GOOGLE_FORM.fields).forEach(function (k) { m[k] = d[k] || ''; });
m.summary = Object.keys(d).map(function (k) { return k + ': ' + d[k]; }).join('\n');
var params = new URLSearchParams();
Object.keys(GOOGLE_FORM.fields).forEach(function (k) {
if (GOOGLE_FORM.fields[k] && m[k]) params.append(GOOGLE_FORM.fields[k], m[k]);
});
try { fetch(GOOGLE_FORM.action, { method: 'POST', mode: 'no-cors', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString() }); } catch (e) { }
}
/* ---- soft anti-abuse: honeypot + localStorage cooldown/cap ---- */
var hp = form.querySelector('.ms-hp input');
var LS_KEY = 'msos_event_submits';
var MIN_GAP_MS = 30 * 1000;
var WINDOW_MS = 24 * 60 * 60 * 1000;
var MAX_IN_WINDOW = 3;
function recentSubmits() {
var now = Date.now(), arr;
try { arr = JSON.parse(localStorage.getItem(LS_KEY) || '[]'); } catch (e) { arr = []; }
if (!Array.isArray(arr)) arr = [];
return arr.filter(function (t) { return typeof t === 'number' && (now - t) < WINDOW_MS; });
}
function coolingDown() {
var arr = recentSubmits(), now = Date.now();
if (arr.length >= MAX_IN_WINDOW) return true;
if (arr.length && (now - Math.max.apply(null, arr)) < MIN_GAP_MS) return true;
return false;
}
function recordSubmit() {
var arr = recentSubmits(); arr.push(Date.now());
try { localStorage.setItem(LS_KEY, JSON.stringify(arr)); } catch (e) { }
}
function showAlert(kind) {
var msg = alertBox.getAttribute(kind === 'cooldown' ? 'data-cooldown-msg' : 'data-required-msg');
if (msg) alertBox.textContent = msg;
alertBox.hidden = false;
}
submit.addEventListener('click', function () {
// Bot trap: honeypot must stay empty. Silently "succeed" (no feedback for bots).
if (hp && hp.value) { form.hidden = true; success.hidden = false; return; }
if (!validate()) { showAlert('required'); return; }
if (coolingDown()) { showAlert('cooldown'); return; }
submit.disabled = true;
var d = collect();
sendToForm(d);
recordSubmit();
// Pick the success copy that matches how they will attend.
var stream = d.attendance === 'stream';
var t = success.querySelector('.ms-success-title');
var p = success.querySelector('.ms-success-text');
if (t) t.textContent = t.getAttribute(stream ? 'data-stream-title' : 'data-venue-title') || t.textContent;
if (p) p.textContent = p.getAttribute(stream ? 'data-stream-text' : 'data-venue-text') || p.textContent;
form.hidden = true;
success.hidden = false;
success.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
})();