// 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