// app.jsx — Everest mission control entry const { useState: useStateA, useEffect: useEffectA } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "fontPairing": "editorial", "accent": "#C9512B", "density": "regular", "showTopo": true }/*EDITMODE-END*/; // Font pairing presets const FONT_PAIRINGS = { editorial: { label: "Editorial", display: '"Instrument Serif", Georgia, serif', sans: '"Geist", ui-sans-serif, system-ui, sans-serif', mono: '"JetBrains Mono", ui-monospace, monospace', google: "Instrument+Serif:ital@0;1|Geist:wght@400;500;600;700|JetBrains+Mono:wght@400;500", }, newsroom: { label: "Newsroom", display: '"Newsreader", Georgia, serif', sans: '"Manrope", ui-sans-serif, system-ui, sans-serif', mono: '"JetBrains Mono", ui-monospace, monospace', google: "Newsreader:ital,opsz,wght@0,6..72,400;1,6..72,400|Manrope:wght@400;500;600;700|JetBrains+Mono:wght@400;500", }, classical: { label: "Classical", display: '"EB Garamond", Georgia, serif', sans: '"Space Grotesk", ui-sans-serif, system-ui, sans-serif', mono: '"JetBrains Mono", ui-monospace, monospace', google: "EB+Garamond:ital,wght@0,400;0,500;1,400|Space+Grotesk:wght@400;500;600;700|JetBrains+Mono:wght@400;500", }, alpine: { label: "Alpine", display: '"Cormorant Garamond", Georgia, serif', sans: '"Inter Tight", ui-sans-serif, system-ui, sans-serif', mono: '"IBM Plex Mono", ui-monospace, monospace', google: "Cormorant+Garamond:ital,wght@0,400;0,500;1,400|Inter+Tight:wght@400;500;600;700|IBM+Plex+Mono:wght@400;500", }, }; function injectGoogleFont(spec) { const id = "gf-everest"; const old = document.getElementById(id); if (old) old.remove(); const link = document.createElement("link"); link.id = id; link.rel = "stylesheet"; const families = spec.split("|").map(f => "family=" + f).join("&"); link.href = "https://fonts.googleapis.com/css2?" + families + "&display=swap"; document.head.appendChild(link); } function App({ onLogout, currentUser }) { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [view, setView] = useStateA("dashboard"); // dashboard | project | notes | receipts | symphony const [activeProjectId, setActiveProjectId] = useStateA(null); const [projects, setProjects] = useStateA([]); const [notes, setNotes] = useStateA([]); const [receipts, setReceipts] = useStateA([]); const [campFiles, setCampFiles] = useStateA([]); // ─── Load data from API on mount ────────────────────────────────── useEffectA(() => { fetch("/api/v1/projects") .then(r => r.json()) .then(data => setProjects(data)) .catch(err => console.error("Failed to load projects:", err)); fetch("/api/v1/notes") .then(r => r.json()) .then(data => setNotes(data)) .catch(err => console.error("Failed to load notes:", err)); fetch("/api/v1/receipts") .then(r => r.json()) .then(data => setReceipts(data)) .catch(err => console.error("Failed to load receipts:", err)); fetch("/api/v1/camp-files") .then(r => r.json()) .then(data => setCampFiles(data)) .catch(err => console.error("Failed to load camp files:", err)); }, []); // ─── Apply font pairing ──────────────────────────────────────────── useEffectA(() => { const p = FONT_PAIRINGS[t.fontPairing] || FONT_PAIRINGS.editorial; injectGoogleFont(p.google); document.documentElement.style.setProperty("--font-display", p.display); document.documentElement.style.setProperty("--font-sans", p.sans); document.documentElement.style.setProperty("--font-mono", p.mono); }, [t.fontPairing]); // ─── Apply accent ────────────────────────────────────────────────── useEffectA(() => { document.documentElement.style.setProperty("--accent", t.accent); }, [t.accent]); // ─── Apply density ───────────────────────────────────────────────── useEffectA(() => { document.documentElement.setAttribute("data-density", t.density); }, [t.density]); // ─── Actions ────────────────────────────────────────────────────── const openProject = (id) => { setActiveProjectId(id); setView("project"); window.scrollTo({ top: 0, behavior: "instant" }); }; const backToBase = () => { setView("dashboard"); window.scrollTo({ top: 0, behavior: "instant" }); }; const switchProject = (id) => { setActiveProjectId(id); window.scrollTo({ top: 0, behavior: "instant" }); }; const markWorked = async (id) => { const today = new Date().toISOString().slice(0, 10); // Optimistic update setProjects(ps => ps.map(p => p.id === id ? { ...p, last_worked: "Worked today", last_worked_at: today } : p)); try { const r = await fetch(`/api/v1/projects/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ last_worked: "Worked today", last_worked_at: today }), }); if (!r.ok) throw new Error("PATCH failed"); const updated = await r.json(); setProjects(ps => ps.map(p => p.id === id ? updated : p)); } catch (err) { console.error("Failed to mark worked:", err); } }; const addNote = async (text, projectId = null) => { try { const r = await fetch("/api/v1/notes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, project_id: projectId }), }); if (!r.ok) throw new Error("POST /api/v1/notes failed"); const note = await r.json(); setNotes(ns => [note, ...ns]); } catch (err) { console.error("Failed to add note:", err); } }; const patchProject = async (id, updates) => { setProjects(ps => ps.map(p => p.id === id ? { ...p, ...updates } : p)); try { const r = await fetch(`/api/v1/projects/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updates), }); if (!r.ok) throw new Error("PATCH failed"); const updated = await r.json(); setProjects(ps => ps.map(p => p.id === id ? updated : p)); } catch (err) { console.error("Failed to patch project:", err); } }; const deleteNote = async (id) => { setNotes(ns => ns.filter(n => n.id !== id)); try { await fetch(`/api/v1/notes/${id}`, { method: "DELETE" }); } catch (err) { console.error("Failed to delete note:", err); } }; const uploadReceipt = async (file, projectId) => { const fd = new FormData(); fd.append("file", file); if (projectId) fd.append("project_id", projectId); try { const r = await fetch("/api/v1/receipts/upload", { method: "POST", body: fd }); if (!r.ok) throw new Error("POST /api/v1/receipts/upload failed"); const receipt = await r.json(); setReceipts(rs => [...rs, receipt]); } catch (err) { console.error("Failed to upload receipt:", err); } }; const uploadCampFile = async (file, projectId) => { const fd = new FormData(); fd.append("file", file); try { const r = await fetch(`/api/v1/projects/${projectId}/files`, { method: "POST", body: fd }); if (!r.ok) throw new Error("POST /api/v1/projects/{id}/files failed"); const cf = await r.json(); setCampFiles(cfs => [...cfs, cf]); } catch (err) { console.error("Failed to upload camp file:", err); } }; const deleteCampFile = async (fileId) => { try { const r = await fetch(`/api/v1/camp-files/${fileId}`, { method: "DELETE" }); if (!r.ok) throw new Error("DELETE /api/v1/camp-files/{id} failed"); setCampFiles(cfs => cfs.filter(f => f.id !== fileId)); } catch (err) { console.error("Failed to delete camp file:", err); } }; const createProject = async ({ name, description, folder }) => { const initials = name.split(" ").map(w => w[0] || "").join("").toUpperCase().slice(0, 2) || "XX"; const seq = String(projects.length + 1).padStart(2, "0"); const elevList = ["8,902","9,843","11,247","12,500","13,114","14,505","15,260","16,730"]; const elevation = elevList[Math.floor(Math.random() * elevList.length)]; const body = { name, description, folder: folder || `~/work/${name.toLowerCase().replace(/\s+/g, "-")}/`, codename: `${initials}–${seq} · ${name.toUpperCase()}`, elevation, coords: "", status: "Ascending", progress: 0.0, }; try { const r = await fetch("/api/v1/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) throw new Error("POST /api/v1/projects failed"); const project = await r.json(); setProjects(ps => [...ps, project]); } catch (err) { console.error("Failed to create project:", err); } }; const activeProject = projects.find(p => p.id === activeProjectId); // ─── Adapt API snake_case fields for project.jsx which uses camelCase ── // project.jsx reads: project.lastWorked, project.endDate, project.startDate // API returns: last_worked, end_date, start_date // We normalise by injecting camelCase aliases when passing down. const normaliseProject = (p) => ({ ...p, lastWorked: p.last_worked, lastWorkedAt: p.last_worked_at, endDate: p.end_date, startDate: p.start_date, docs: p.docs || [], team: p.team || [], notes: p.notes || [], activity: p.activity || [], images: p.images || [], }); const normalisedProjects = projects.map(normaliseProject); const normalisedActive = activeProject ? normaliseProject(activeProject) : null; return ( <>
{ if(v==="dashboard") backToBase(); else setView(v); }} onLogout={onLogout} currentUser={currentUser} /> {view === "dashboard" && ( !n.project_id)} receipts={receipts} onOpen={openProject} onMarkWorked={markWorked} onAddNote={(text) => addNote(text, null)} onUploadReceipt={uploadReceipt} onCreateProject={createProject} onDeleteNote={deleteNote} /> )} {view === "project" && normalisedActive && ( addNote(text, projectId)} onSave={(updates) => patchProject(activeProjectId, updates)} onUploadReceipt={uploadReceipt} onUploadCampFile={uploadCampFile} onDeleteCampFile={deleteCampFile} onDeleteNote={deleteNote} currentUser={currentUser} /> )} {view === "notes" && ( addNote(text, projectId)} onDelete={async (id) => { try { await fetch(`/api/v1/notes/${id}`, { method: "DELETE" }); setNotes(ns => ns.filter(n => n.id !== id)); } catch(e) { console.error("Delete note failed", e); } }} /> )} {view === "receipts" && ( )} {view === "symphony" && }
({ value: k, label: v.label }))} onChange={(v) => setTweak("fontPairing", v)} /> setTweak("accent", v)} /> setTweak("density", v)} /> ); } function Root() { const [auth, setAuth] = useStateA(null); // null=checking, false=login, true=app const [authView, setAuthView] = useStateA("login"); // "login" | "register" const [currentUser, setCurrentUser] = useStateA(null); async function fetchCurrentUser() { try { const res = await fetch("/api/v1/auth/me"); if (res.ok) setCurrentUser(await res.json()); } catch (_) {} } useEffectA(() => { fetch("/api/v1/auth/me") .then(r => { if (r.ok) { r.json().then(u => setCurrentUser(u)); setAuth(true); } else { setAuth(false); } }) .catch(() => setAuth(false)); }, []); const handleLogin = () => { fetchCurrentUser(); setAuth(true); }; const handleLogout = async () => { await fetch("/api/v1/auth/logout", { method: "POST" }); setCurrentUser(null); setAuth(false); setAuthView("login"); }; if (auth === null) return null; if (!auth) { if (authView === "register") { return setAuthView("login")} />; } return setAuthView("register")} />; } return ( <> ); } ReactDOM.createRoot(document.getElementById("root")).render();