// src/app.jsx — Root component. Holds nav + cart state, routes between screens, wires Tweaks. const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "density": "comfy", "addPattern": "morph", "categoryNav": "aisle", "customizePattern": "inline", "dark": true, "navyHex": "#0A1F44" }/*EDITMODE-END*/; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [nav, setNav] = React.useState({ screen: "home" }); const [cart, setCart] = React.useState({ items: [] }); const [toast, setToast] = React.useState(null); const [address, setAddress] = React.useState({ id: "home", label: "98 Orchard St" }); const [addrPickerOpen, setAddrPickerOpen] = React.useState(false); const [activeOrder, setActiveOrder] = React.useState(null); const toastTimer = React.useRef(null); const placeOrder = () => { const items = cart.items; if (!items.length) return; const storeId = items[0].storeId; const subtotalNow = items.reduce((s, i) => s + (PRODUCT_INDEX[i.productId]?.price || 0) * i.qty, 0); const fee = 1.99 + Math.max(0.99, subtotalNow * 0.05); const total = subtotalNow + fee + subtotalNow * 0.15; const order = { storeId, number: String(4800 + Math.floor(Math.random() * 200)), itemsCount: items.reduce((s, i) => s + i.qty, 0), // Snapshot the actual line items so Tracking and Orders can show them. items: items.map(i => ({ id: i.productId, name: PRODUCT_INDEX[i.productId]?.name || "Item", qty: i.qty, unit: PRODUCT_INDEX[i.productId]?.unit || "", price: PRODUCT_INDEX[i.productId]?.price || 0, })), total, placedAt: Date.now(), etaSeconds: 18 * 60, }; setActiveOrder(order); setCart({ items: [] }); onNav({ screen: "tracking" }); }; const showToast = (msg) => { setToast(msg); clearTimeout(toastTimer.current); toastTimer.current = setTimeout(() => setToast(null), 1800); }; // cart helpers const addToCart = (storeId, productId, delta = 1) => { setCart(c => { const existing = c.items.find(i => i.productId === productId); if (existing) { const q = existing.qty + delta; if (q <= 0) return { items: c.items.filter(i => i.productId !== productId) }; return { items: c.items.map(i => i.productId === productId ? { ...i, qty: q } : i) }; } if (delta <= 0) return c; return { items: [...c.items, { storeId, productId, qty: delta }] }; }); }; const addMany = (storeId, ids) => { ids.forEach(id => addToCart(storeId, id, 1)); showToast(`Added ${ids.length} items`); }; const clear = () => setCart({ items: [] }); // subtotal memo const subtotal = React.useMemo(() => cart.items.reduce((s, i) => s + (PRODUCT_INDEX[i.productId]?.price || 0) * i.qty, 0), [cart]); const cartFull = { ...cart, subtotal }; const onNav = (target) => { setNav(target); window.scrollTo({ top: 0, behavior: "instant" }); }; // Navy tint derived from tweak React.useEffect(() => { document.documentElement.style.setProperty("--navy", t.navyHex); }, [t.navyHex]); // Hydrate dark from localStorage on first mount so the user's choice persists. // useTweaks itself round-trips via postMessage to the design host — not useful // in the deployed demo — so we own persistence here instead. const hydratedDark = React.useRef(false); React.useEffect(() => { if (hydratedDark.current) return; hydratedDark.current = true; try { const saved = localStorage.getItem("opa.dark"); if (saved === "1" && !t.dark) setTweak("dark", true); if (saved === "0" && t.dark) setTweak("dark", false); } catch (_) {} }, []); React.useEffect(() => { document.documentElement.dataset.dark = t.dark ? "1" : "0"; if (hydratedDark.current) { try { localStorage.setItem("opa.dark", t.dark ? "1" : "0"); } catch (_) {} } }, [t.dark]); // Global ⌘K / Ctrl+K → open cross-store search. Ignored when the user is // already typing in a text field so we don't hijack their input. React.useEffect(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { const el = document.activeElement; const tag = el?.tagName?.toLowerCase(); if (tag === "input" || tag === "textarea" || el?.isContentEditable) return; e.preventDefault(); setNav({ screen: "search" }); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const screenContent = (() => { switch (nav.screen) { case "home": return ; case "search": return ; case "store": return ; case "category": return ; case "product": return ; case "cart": return ; case "tracking": return ; case "orders": return ; case "account": return ; default: return ; } })(); const cartCount = cart.items.reduce((s, i) => s + i.qty, 0); const onStoreOrCategory = nav.screen === "store" || nav.screen === "category"; return (
onNav({ screen: "cart" })} onOpenSearch={() => onNav({ screen: "search" })} address={address} onOpenAddress={() => setAddrPickerOpen(true)} isDark={t.dark} onToggleDark={() => setTweak("dark", !t.dark)} />
{screenContent}
onNav({ screen: "cart" })} /> onNav({ screen: "home" })} /> setAddrPickerOpen(false)} onPick={(a) => { setAddress(a); showToast(`Delivering to ${a.label}`); }} /> setTweak("density", v)} /> setTweak("categoryNav", v)} /> setTweak("navyHex", v)} /> setTweak("dark", v)} />
{[ { id: "home", label: "Home" }, { id: "store", label: "Store" }, { id: "product", label: "Sandwich" }, { id: "cart", label: "Cart" }, ].map(s => ( ))}
); } ReactDOM.createRoot(document.getElementById("app")).render();