Fix mobile nav/TOC/overflow on article pages + harden membership form
Mobile fixes (article, guide, get-to-know, projects pages): - Remove duplicate main.js include site-wide. footer.js emitted a <script src=main.js> inside the footer block while pages also load one before </body>, so DOMContentLoaded ran twice and every tap toggled the mobile menu / article TOC twice (net no-op). Menu and TOC only worked on the homepage. Now exactly one main.js per page, before </body>. - Kill horizontal overflow: add overflow-x: clip to html/body (clips the off-canvas mobile nav panel without breaking position: sticky, unlike overflow-x: hidden). - Reset align-items to stretch in the mobile .article-layout-container rule; the desktop flex-start made .article-container size to max-content width in a column flexbox and overflow the viewport. Membership form hardening (Become a Member): - Honeypot field + localStorage cooldown (30s gap, max 3/24h) + stronger email regex; trilingual cooldown alert message. - Apps Script (setup doc): per-email auto-reply dedupe + daily send cap; applicant reply is a plain thank-you that never echoes submitted answers. - Draft HTML welcome email (email-welcome-preview.html) for later use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7bc249e8a4
commit
efb3c6ea39
196 files changed
+83
-203
No files matched your search
+38
-2
@@ -117,7 +117,7 @@
|
||||
}
|
||||
|
||||
next.addEventListener('click', function () {
|
||||
if (!validate()) { alertBox.hidden = false; return; }
|
||||
if (!validate()) { alertBox.textContent = alertBox.getAttribute('data-required-msg') || alertBox.textContent; alertBox.hidden = false; return; }
|
||||
if (state.idx < state.order.length - 1) { state.idx++; render(); form.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
|
||||
});
|
||||
back.addEventListener('click', function () {
|
||||
@@ -172,10 +172,46 @@
|
||||
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 ----
|
||||
A static site cannot rate-limit by IP (no backend). These are client-side
|
||||
soft limits that stop casual flooding and bots; the Apps Script adds a
|
||||
server-side per-email dedupe and daily cap as the real safety net. */
|
||||
var hp = form.querySelector('.ms-hp input');
|
||||
var LS_KEY = 'msos_member_submits';
|
||||
var MIN_GAP_MS = 30 * 1000; // at least 30s between submissions
|
||||
var WINDOW_MS = 24 * 60 * 60 * 1000; // rolling 24h window
|
||||
var MAX_IN_WINDOW = 3; // max 3 submissions / 24h from this browser
|
||||
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 () {
|
||||
if (!validate()) { alertBox.hidden = false; return; }
|
||||
// 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();
|
||||
form.hidden = true;
|
||||
var t = success.querySelector('.ms-success-title'), p = success.querySelector('.ms-success-text');
|
||||
t.textContent = d.type === 'student' ? t.getAttribute('data-student-title') : t.getAttribute('data-supporter-title');
|
||||
|
||||
Reference in new issue
Block a user