customer-presentation/protected/mobile/mobile.js
Arlind Ukshini 72590b08bc add mobile view at protected/mobile/ (UA-dispatched)
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>
2026-04-24 10:03:13 +02:00

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 = '/';
});
}