// East Quabbin Health — main app // Orchestrates state, routing between screens, tweaks, edit mode const STORAGE_KEY = 'eq-health-intake-v1'; const TWEAKS_KEY = 'eq-health-tweaks-v1'; const EqApp = () => { // ── Tweaks (defaults + persistence) ──────────────────────── const TWEAK_DEFAULS = /*EDITMODE-BEGIN*/{ "palette": "sage", "density": "default", "language": "en", "illustrations": true, "tone": "warm" }/*EDITMODE-END*/; const [tweaks, setTweaks] = React.useState(() => { try { const v = localStorage.getItem(TWEAKS_KEY); if (v) return { ...TWEAK_DEFAULS, ...JSON.parse(v) }; } catch (e) {} return TWEAK_DEFAULS; }); const [tweaksVisible, setTweaksVisible] = React.useState(false); const patchTweaks = (patch) => { const next = { ...tweaks, ...patch }; setTweaks(next); try { localStorage.setItem(TWEAKS_KEY, JSON.stringify(next)); } catch (e) {} try { window.parent.postMessage({ type: '__edit_mode_set_keys', edits: patch }, '*'); } catch (e) {} }; // ── Edit mode protocol ───────────────────────────────────── React.useEffect(() => { const onMsg = (e) => { if (e.data?.type === '__activate_edit_mode') setTweaksVisible(true); if (e.data?.type === '__deactivate_edit_mode') setTweaksVisible(false); }; window.addEventListener('message', onMsg); try { window.parent.postMessage({ type: '__edit_mode_available' }, '*'); } catch (e) {} return () => window.removeEventListener('message', onMsg); }, []); // Apply tweaks to document root for CSS variable cascading React.useEffect(() => { const root = document.documentElement; root.setAttribute('data-palette', tweaks.palette); root.setAttribute('data-density', tweaks.density); }, [tweaks.palette, tweaks.density]); // ── App state (persisted) ────────────────────────────────── const [state, setState] = React.useState(() => { try { const v = localStorage.getItem(STORAGE_KEY); if (v) return JSON.parse(v); } catch (e) {} return { route: 'launch', // launch | coord | wizard | done | resume step: 0, mode: 'self', // self | coord coord: { name: '', location: '' }, data: {}, resumeCode: '', }; }); React.useEffect(() => { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} }, [state]); const [savedFlash, setSavedFlash] = React.useState(false); const flashSaved = () => { setSavedFlash(true); setTimeout(() => setSavedFlash(false), 1600); }; // ── Derived ──────────────────────────────────────────────── const t = EQ_COPY[tweaks.language] || EQ_COPY.en; const steps = t.steps; const currentStep = steps[state.step]; const totalSteps = steps.length; const setField = (k, v) => setState(s => ({ ...s, data: { ...s.data, [k]: v } })); const next = () => setState(s => ({ ...s, step: Math.min(s.step + 1, totalSteps - 1) })); const back = () => { if (state.step === 0) { setState(s => ({ ...s, route: s.mode === 'coord' ? 'coord' : 'launch' })); } else { setState(s => ({ ...s, step: s.step - 1 })); } }; const beginSelf = () => setState(s => ({ ...s, route: 'wizard', step: 0, mode: 'self', data: {} })); const beginCoord = () => setState(s => ({ ...s, route: 'coord', mode: 'coord' })); const coordBegin = () => setState(s => ({ ...s, route: 'wizard', step: 0, data: {} })); const submit = () => { const code = 'EQH-' + Math.floor(1000 + Math.random() * 9000) + '-' + Math.random().toString(36).slice(2, 5).toUpperCase(); setState(s => ({ ...s, route: 'done', data: { ...s.data, code } })); }; const restart = () => { try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} setState({ route: 'launch', step: 0, mode: 'self', coord: { name: '', location: '' }, data: {}, resumeCode: '' }); }; // ── Validation per step ──────────────────────────────────── const canAdvance = () => { const d = state.data; switch (currentStep?.id) { case 'about': return d.firstName && d.lastName && d.dob; case 'contact': return d.address && d.phone && d.preferredContact; case 'house': return true; case 'daily': return true; case 'sdoh': return true; case 'health': return true; case 'access': return d.transport; case 'services':return true; case 'consent': { const req = t.consent.items.filter(i => i.required).map(i => i.key); return req.every(k => (d.consents || {})[k]); } case 'sign': return d.hasSig; default: return true; } }; // ── Wizard step renderer ─────────────────────────────────── const renderStep = () => { if (!currentStep) return null; const props = { t, data: state.data, set: setField }; switch (currentStep.id) { case 'about': return ; case 'contact': return ; case 'house': return ; case 'daily': return ; case 'sdoh': return ; case 'health': return ; case 'access': return ; case 'services': return ; case 'consent': return ; case 'sign': return ; default: return null; } }; // ── Main body ────────────────────────────────────────────── const body = (() => { if (state.route === 'launch') { return alert('Resume: enter your 8-character code (demo).')} />; } if (state.route === 'coord') { return setState(s => ({ ...s, coord: c }))} onBack={() => setState(s => ({ ...s, route: 'launch', mode: 'self' }))} onBegin={coordBegin} />; } if (state.route === 'done') { return window.print()} />; } // wizard const isLast = state.step === totalSteps - 1; return ( <> {state.mode === 'coord' && (
Coordinator: {state.coord.name || 'Staff'} · {state.coord.location || 'Town hall'}
)}
{renderStep()}
); })(); // ── Phone frame ──────────────────────────────────────────── return ( <>
{body}
{tweaksVisible && (

Tweaks

Live-tune the intake.
Palette patchTweaks({ palette: v })} options={[{ v: 'sage', l: 'Sage' }, { v: 'clinical', l: 'Clinical' }, { v: 'earthy', l: 'Earthy' }]} />
Density patchTweaks({ density: v })} options={[{ v: 'compact', l: 'Compact' }, { v: 'default', l: 'Default' }, { v: 'roomy', l: 'Roomy' }]} />
Language patchTweaks({ language: v })} options={[{ v: 'en', l: 'English' }, { v: 'es', l: 'Español' }]} />
Jump to screen
)} ); }; window.EqApp = EqApp;