// ================= App root ================= const { useState: useStateApp, useEffect: useEffectApp, useMemo: useMemoApp, useCallback: useCallbackApp, useRef: useRefApp } = React; const TWEAK_DEFAULTS = { density: "cozy", accent: "teal", tableStyle: "plain", }; const ACCENT_MAP = { teal: { c: "#0ea5a0", s: "#0fbdb7", w: "rgba(14, 165, 160, 0.14)" }, violet: { c: "#8b5cf6", s: "#a78bfa", w: "rgba(139, 92, 246, 0.14)" }, amber: { c: "#f59e0b", s: "#fbbf24", w: "rgba(245, 158, 11, 0.14)" }, pink: { c: "#ec4899", s: "#f472b6", w: "rgba(236, 72, 153, 0.14)" }, green: { c: "#22c55e", s: "#4ade80", w: "rgba(34, 197, 94, 0.14)" }, }; const DENSITY_MAP = { compact: { rowH: "28px", px: "10px", py: "5px", fm: "12px", fl: "13px" }, cozy: { rowH: "34px", px: "12px", py: "8px", fm: "13px", fl: "14px" }, comfortable: { rowH: "42px", px: "14px", py: "10px", fm: "13.5px", fl: "14.5px" }, }; function App() { const [authState, setAuthState] = useStateApp("checking"); // checking | anon | loading | ready | error const [errorMsg, setErrorMsg] = useStateApp(null); const [, setBump] = useStateApp(0); const bumpRender = () => setBump(x => x + 1); const [view, setView] = useStateApp(() => { try { return JSON.parse(localStorage.getItem("fabcom.view") || "null") || { name: "home" }; } catch { return { name: "home" }; } }); const [theme, setTheme] = useStateApp(() => localStorage.getItem("fabcom.theme") || "dark"); const [alertsOpen, setAlertsOpen] = useStateApp(false); const [paletteOpen, setPaletteOpen] = useStateApp(false); const [ctx, setCtx] = useStateApp(null); const [toastNode, pushToast] = window.useToast(); const [tweaks, setTweaks] = useStateApp(TWEAK_DEFAULTS); const [role, setRole] = useStateApp("Admin"); useEffectApp(() => { document.body.setAttribute("data-theme", theme); localStorage.setItem("fabcom.theme", theme); }, [theme]); useEffectApp(() => { localStorage.setItem("fabcom.view", JSON.stringify(view)); }, [view]); // Apply accent + density useEffectApp(() => { const root = document.documentElement; const a = ACCENT_MAP[tweaks.accent] || ACCENT_MAP.teal; root.style.setProperty("--accent", a.c); root.style.setProperty("--accent-strong", a.s); root.style.setProperty("--accent-weak", a.w); const d = DENSITY_MAP[tweaks.density] || DENSITY_MAP.cozy; root.style.setProperty("--row-h", d.rowH); root.style.setProperty("--cell-px", d.px); root.style.setProperty("--cell-py", d.py); root.style.setProperty("--font-md", d.fm); root.style.setProperty("--font-lg", d.fl); }, [tweaks]); // Command palette shortcut useEffectApp(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setPaletteOpen(true); } }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, []); useEffectApp(() => { window.__openPalette = () => setPaletteOpen(true); window.__openAlerts = () => setAlertsOpen(true); }, []); // ---------- Auth flow ---------- const loadAppData = useCallbackApp(async () => { setAuthState("loading"); try { await window.__fabcomLoadData(); setAuthState("ready"); } catch (e) { if (e.code === 401) { setAuthState("anon"); } else { console.error(e); setErrorMsg(e.message || String(e)); setAuthState("error"); } } }, []); useEffectApp(() => { loadAppData(); }, [loadAppData]); const handleCredential = useCallbackApp(async (idToken) => { setAuthState("loading"); try { const res = await fetch("/auth/google", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ credential: idToken }), }); if (!res.ok) { const msg = (await res.json().catch(() => ({}))).detail || `HTTP ${res.status}`; setErrorMsg(msg); setAuthState("anon"); return; } await loadAppData(); } catch (e) { setErrorMsg(e.message); setAuthState("anon"); } }, [loadAppData]); const handleLogout = useCallbackApp(async () => { await fetch("/auth/logout", { method: "POST", credentials: "include" }); setAuthState("anon"); }, []); const crumbs = useMemoApp(() => { if (authState !== "ready") return [{ label: "Fabcom" }]; const c = [{ label: "Fabcom", onClick: () => setView({ name: "home" }) }]; if (view.name === "home") c.push({ label: "Overview" }); else if (view.name === "brands") c.push({ label: "Brands" }); else if (view.name === "pacing") c.push({ label: "Budget pacing" }); else if (view.name === "nonmedia") c.push({ label: "Non-media spending" }); else if (view.name === "changes") c.push({ label: "Changes" }); else if (view.name === "alerts") c.push({ label: "Alerts" }); else if (view.name?.startsWith("settings_")) c.push({ label: "Settings" }, { label: view.name.split("_")[1] }); else if (view.name === "brand") { c.push({ label: "Brands", onClick: () => setView({ name: "home" }) }); const b = (window.BRANDS || []).find(x => x.code === view.brand); if (b) c.push({ label: b.name }); } else if (view.name === "account") { const b = (window.BRANDS || []).find(x => x.code === view.brand); c.push({ label: "Brands", onClick: () => setView({ name: "home" }) }); if (b) c.push({ label: b.name, onClick: () => setView({ name: "brand", brand: b.code }) }); const a = b?.accounts.find(x => x.id === view.account); if (a) c.push({ label: a.name }); } else if (view.name === "campaign") { const b = (window.BRANDS || []).find(x => x.code === view.brand); c.push({ label: "Brands", onClick: () => setView({ name: "home" }) }); if (b) c.push({ label: b.name, onClick: () => setView({ name: "brand", brand: b.code }) }); const a = b?.accounts.find(x => x.id === view.account) || b?.accounts.find(x => x.campaigns.some(c2 => c2.id === view.campaign)); if (a) c.push({ label: a.name, onClick: () => setView({ name: "account", brand: b.code, account: a.id }) }); const cc = a?.campaigns.find(x => x.id === view.campaign); if (cc) c.push({ label: (cc.name || "").split("_").slice(1).join("_") || cc.name }); } return c; }, [view, authState]); const openContext = (e, entity, kind) => { const items = [ { label: "Open", icon: "external", onClick: () => { if (kind === "brand") setView({ name: "brand", brand: entity.code }); else if (kind === "account") setView({ name: "account", brand: entity.brand_code, account: entity.id }); else if (kind === "campaign") setView({ name: "campaign", brand: entity.brand_code, account: entity.account_id, campaign: entity.id }); }}, { label: "Copy ID", icon: "copy", shortcut: "⌘C", onClick: () => { navigator.clipboard?.writeText(entity.id || entity.code || entity.name); pushToast(`ID copied — ${entity.id || entity.code}`); } }, { label: "Copy shareable URL", icon: "link", onClick: () => { navigator.clipboard?.writeText(`${window.location.origin}/#${kind}/${entity.id || entity.code}`); pushToast("Link copied"); } }, { divider: true }, { label: "Copy BQ query", icon: "bq", onClick: () => { const col = kind === "brand" ? "brand_code" : "campaign_id"; const val = entity.id || entity.code || entity.campaign_id; navigator.clipboard?.writeText(`SELECT * FROM \`fabcom-finance.media_summary.media_summary_internal_only\`\nWHERE ${col} = "${val}"\n AND date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)`); pushToast("BigQuery SELECT copied"); }}, ]; setCtx({ pos: { x: Math.min(e.clientX, window.innerWidth - 240), y: Math.min(e.clientY, window.innerHeight - 380) }, items }); }; // ---------- Render gates ---------- if (authState === "checking") { return
Checking session…
; } if (authState === "anon" || authState === "error") { return <> {toastNode} ; } if (authState === "loading") { return
Loading from BigQuery…
; } // Branch view -> screen const screen = view.name === "home" || view.name === "brands" ? : view.name === "brand" ? : view.name === "account" ? : view.name === "campaign" ? : view.name === "pacing" ? : view.name === "nonmedia" ? : view.name === "changes" ? : view.name === "alerts" ?
Use the alerts drawer — opening now… {(window.__openAlerts?.(), null)}
: view.name?.startsWith("settings_") ? : null; const alertsCount = (window.ALERTS || []).length; return (
{ pushToast("Refreshing from BigQuery…", { icon: "refresh" }); await window.__fabcomLoadData(); bumpRender(); }} lastSynced="just now" role={role} setRole={setRole} />
{screen}
setPaletteOpen(false)} onNav={setView}/> setAlertsOpen(false)} onNav={setView}/> {ctx && setCtx(null)}/>} {toastNode}
); } ReactDOM.createRoot(document.getElementById("root")).render();