Desktop is a GSAP/Lenis/d3 animated experience that doesn't hold up on phones. Rather than retrofitting media queries across 1200+ lines of scroll-trigger code, add a completely isolated static mobile tree: - protected/mobile/index.html — one-page static flow covering the intro, 12 timeline events, hero, 4 capability cards, Bifrost reveal, 3 participation stops, and Join CTA. All copy duplicated from the desktop HTML on purpose — a shared data module would re-couple the two trees. - protected/mobile/mobile.css — paper/ink palette, all m-prefixed, zero cascade overlap with the desktop CSS. - protected/mobile/mobile.js — 60-line client: /auth/me check, /api/bifrost-join POST + panel swap, /auth/logout. No GSAP, no Lenis, no d3. Routing (server.js): - GET /timeline now UA-dispatches via MOBILE_UA_RE. Phone UAs get the mobile page; everything else gets the desktop page. - ?view=mobile and ?view=desktop query overrides take precedence over the UA sniff — for bad guesses or previewing the other version. - Gating is unchanged: protected/mobile/ is inside protected/ so the existing requireAuth + express.static gate covers it. Docs: - CLAUDE.md §routing now lists the UA dispatch as step 4. - PROJECT.md gets a new "Mobile view" section explaining the isolation rules (no shared JS/CSS, content duplicated manually). - CHECKLIST.md gains section H0 with dispatch curl checks, render verification on a phone, and an isolation audit that fails if mobile classes leak into the desktop HTML or vice versa. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
84 lines
3.1 KiB
JavaScript
84 lines
3.1 KiB
JavaScript
// ─────────────────────────────────────────────────────────────
|
|
// protected/mobile/mobile.js — minimal client for the mobile view.
|
|
//
|
|
// Three behaviours, nothing else:
|
|
// 1. Confirm the session is still valid on page load. If the
|
|
// session expired since the server rendered the HTML, bounce
|
|
// to "/" so the user doesn't read gated content without a
|
|
// session cookie (defensive — requireAuth already gates the
|
|
// page request itself).
|
|
// 2. POST /api/bifrost-join on CTA click; swap CTA panel →
|
|
// confirmation panel on success.
|
|
// 3. POST /auth/logout on log-out button; navigate to "/".
|
|
//
|
|
// No GSAP, no Lenis, no d3. No sharing of globals with the desktop
|
|
// timeline/bifrost scripts — this file is only loaded by
|
|
// protected/mobile/index.html and never by the desktop view.
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
(async function checkSession() {
|
|
try {
|
|
const res = await fetch('/auth/me', { credentials: 'same-origin' });
|
|
if (!res.ok) {
|
|
window.location.href = '/';
|
|
}
|
|
} catch {
|
|
// Network error — do not boot the user out; desktop behaviour is
|
|
// the same. If the next action actually needs the server, we'll
|
|
// surface the error there.
|
|
}
|
|
})();
|
|
|
|
const joinBtn = document.getElementById('m-join-btn');
|
|
const joinCta = document.getElementById('m-join-cta');
|
|
const joinConfirm = document.getElementById('m-join-confirm');
|
|
|
|
if (joinBtn && joinCta && joinConfirm) {
|
|
joinBtn.addEventListener('click', async () => {
|
|
joinBtn.disabled = true;
|
|
try {
|
|
const res = await fetch('/api/bifrost-join', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'content-type': 'application/json' },
|
|
// Server reads email + sessionId from the session cookie, body
|
|
// just needs to be parseable JSON for express.json() to keep
|
|
// its rhythm.
|
|
body: '{}',
|
|
});
|
|
if (res.status === 401) {
|
|
window.location.href = '/';
|
|
return;
|
|
}
|
|
if (!res.ok) {
|
|
joinBtn.disabled = false;
|
|
joinBtn.textContent = 'Try again';
|
|
return;
|
|
}
|
|
joinCta.hidden = true;
|
|
joinConfirm.hidden = false;
|
|
joinConfirm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
} catch {
|
|
joinBtn.disabled = false;
|
|
joinBtn.textContent = 'Try again';
|
|
}
|
|
});
|
|
}
|
|
|
|
const logoutBtn = document.getElementById('m-logout');
|
|
if (logoutBtn) {
|
|
logoutBtn.addEventListener('click', async () => {
|
|
try {
|
|
await fetch('/auth/logout', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
});
|
|
} catch {
|
|
// If logout POST fails, still navigate home — the user's
|
|
// intent is "leave". The server-side session will still be
|
|
// valid until it expires, but the cookie on this device
|
|
// will be cleared by the navigation away.
|
|
}
|
|
window.location.href = '/';
|
|
});
|
|
}
|