221 lines
11 KiB
JavaScript
221 lines
11 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;
|
|
});
|
|
});
|
|
});
|
|
|
|
/* ---- role gate: parents and "other" won't study, so hide the study-plans block ---- */
|
|
(function () {
|
|
var roleSel = form.querySelector('select[data-name="role"]');
|
|
var plans = form.querySelector('.ev-plans');
|
|
if (!roleSel || !plans) return;
|
|
function syncPlans() {
|
|
var r = roleSel.value;
|
|
plans.hidden = (r === 'parent' || r === 'other');
|
|
}
|
|
roleSel.addEventListener('change', syncPlans);
|
|
syncPlans();
|
|
})();
|
|
|
|
/* ---- 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.closest('[hidden]')) return; // fields hidden by conditional logic aren't required
|
|
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('[hidden]')) return; // skip fields in hidden sections (conditional "Other", study plans for parents/others)
|
|
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' });
|
|
});
|
|
})();
|