// Entry point: sign-in gate, app shell, routing, overlays, mobile tab bar. import { html, render, useState, useEffect, useRef } from '../vendor/preact-htm.mjs'; import { api, connectEvents } from './api.js'; import { state, update, useStore, navigate, loadBootstrap, applyTheme } from './store.js'; import { Icon, PopoverHost, ModalHost, Spinner, useMobile } from './ui.js'; import { Sidebar } from './sidebar.js'; import { BoardPage } from './board.js'; import { HomePage, MyWorkPage, InboxPage, SearchPage, PeoplePage, AdminPage, WorkspacePage } from './pages.js'; applyTheme(); function App() { const s = useStore(); const [phase, setPhase] = useState('loading'); // loading | signin | ready useEffect(() => { loadBootstrap().then(() => { setPhase('ready'); connectEvents(); }).catch(() => setPhase('signin')); const out = () => setPhase('signin'); window.addEventListener('wb:signed-out', out); return () => window.removeEventListener('wb:signed-out', out); }, []); // Keyboard: "/" or Cmd/Ctrl+K opens search useEffect(() => { const fn = (e) => { if (/INPUT|TEXTAREA|SELECT/.test(document.activeElement?.tagName) || document.activeElement?.isContentEditable) return; if ((e.key === 'k' && (e.metaKey || e.ctrlKey)) || e.key === '/') { e.preventDefault(); navigate('/search'); } }; document.addEventListener('keydown', fn); return () => document.removeEventListener('keydown', fn); }, []); if (phase === 'loading') return html`
`; if (phase === 'signin') return html`<${SignIn} onDone=${async () => { await loadBootstrap(); connectEvents(); setPhase('ready'); }} />`; return html`
<${Sidebar} />
<${Route} />
<${MobileTabs} /> <${PopoverHost} /> <${ModalHost} /> <${Toasts} />
`; } function Route() { const r = useStore().route; useEffect(() => { const b = r.boardId && state.boards.find((x) => x.id === r.boardId); document.title = b ? `${b.name} · Waters Boards` : 'Waters Boards'; }, [r.name, r.boardId, state.boards.length]); switch (r.name) { case 'board': return html`<${BoardPage} key=${r.boardId} boardId=${r.boardId} viewId=${r.viewId} itemId=${r.itemId} />`; case 'my-work': return html`<${MyWorkPage} key=${r.q?.user || 'me'} />`; case 'inbox': return html`<${InboxPage} />`; case 'search': return html`<${SearchPage} />`; case 'people': return html`<${PeoplePage} />`; case 'admin': return html`<${AdminPage} />`; case 'workspace': return html`<${WorkspacePage} workspaceId=${r.workspaceId} />`; default: return html`<${HomePage} />`; } } function MobileTabs() { const s = useStore(); const mobile = useMobile(); if (!mobile || s.route.itemId) return null; const r = s.route.name; const Tab = ({ icon, label, to, active, badge, onClick }) => html``; return html``; } function Toasts() { const s = useStore(); return html`
${s.toasts.map((t) => html`
${t.text} ${t.action ? html`` : null}
`)}
`; } // ─── Sign in ───────────────────────────────────────────── function SignIn({ onDone }) { const [cfg, setCfg] = useState(null); const [err, setErr] = useState(''); const [mode, setMode] = useState('google'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const btn = useRef(); useEffect(() => { api.get('/api/auth/config').then(setCfg).catch(() => setCfg({})); }, []); useEffect(() => { if (!cfg?.googleClientId || mode !== 'google') return; let tries = 0; const init = () => { if (!window.google?.accounts?.id) { if (tries++ < 50) setTimeout(init, 100); else setErr("Couldn't load Google sign-in. Check your connection."); return; } window.google.accounts.id.initialize({ client_id: cfg.googleClientId, callback: async (resp) => { setErr(''); try { await api.post('/api/auth/google', { credential: resp.credential }); onDone(); } catch (e) { setErr(e.message); } }, hd: cfg.allowedDomains?.length === 1 ? cfg.allowedDomains[0] : undefined, ux_mode: 'popup', }); window.google.accounts.id.renderButton(btn.current, { theme: 'outline', size: 'large', text: 'continue_with', shape: 'pill', width: 280 }); }; init(); }, [cfg, mode]); const submitPassword = async (e) => { e.preventDefault(); setErr(''); try { await api.post(mode === 'dev' ? '/api/auth/dev' : '/api/auth/password', { email, password, name: email.split('@')[0] }); onDone(); } catch (e2) { setErr(e2.message); } }; if (!cfg) return html`
<${Spinner} />
`; return html`