/* ============================================================ APP — estado global, carta, scrollspy, backend de tienda, montaje ============================================================ */ const { CATS: APP_CATS, FEATURE: APP_FEATURE, RICE: APP_RICE, DRINKS: APP_DRINKS, HOURS: APP_HOURS } = window; function scrollToId(id, offset = 110) { const el = document.getElementById(id); if (!el) return; const y = el.getBoundingClientRect().top + window.scrollY - offset; window.scrollTo({ top: Math.max(0, y), behavior: "smooth" }); } function TopBar({ onOrder, scrolled, isOpen, onPdf }) { return (
{ e.preventDefault(); scrollToId("top", 0); }}> Dos Océanos
{isOpen ? "Abierto" : "Cerrado"}
); } /* ---- Plato estrella destacado ---- */ function FeatureBlock({ onOpen, store }) { const f = APP_FEATURE; const cat = APP_CATS.find(c => c.id === f.catId); const av = itemAvailable(store, f.n); return (
{!av && Agotado}
{f.tag}

{f.n}

{f.d}

); } function App() { const [cart, setCart] = useState(() => { try { const raw = JSON.parse(localStorage.getItem("do_cart") || "[]"); // migra líneas guardadas antes de agregar tamaños/precio por rango, // reconstruyendo from/to/weights desde la carta actual por nombre de plato. return raw.map(l => { const match = (l.name === APP_FEATURE.n) ? APP_FEATURE : APP_CATS.reduce((found, c) => found || c.items.find(it => it.n === l.name), null); return { ...l, from: l.from != null ? l.from : (match ? match.from : (l.ranged ? l.unitPrice - (l.drinkExtra || 0) : undefined)), to: l.to != null ? l.to : (match ? match.to : undefined), weights: l.weights != null ? l.weights : (match ? (match.weights || null) : null), drinkExtra: l.drinkExtra != null ? l.drinkExtra : 0, size: l.size != null ? l.size : null, }; }); } catch (e) { return []; } }); const [store, setStore] = useState(loadStore); const [modal, setModal] = useState(null); // {item, cat} const [drawer, setDrawer] = useState(false); const [admin, setAdmin] = useState(false); const [pdf, setPdf] = useState(false); const [active, setActive] = useState(APP_CATS[0].id); const [scrolled, setScrolled] = useState(false); const special = useMemo(() => getActiveSpecial("auto"), []); const promo = useMemo(() => getWeeklyPromo(), []); const isOpen = isStoreOpen(store); useEffect(() => { localStorage.setItem("do_cart", JSON.stringify(cart)); }, [cart]); useEffect(() => { localStorage.setItem("do_store", JSON.stringify(store)); }, [store]); // abre el panel admin con #admin en la URL useEffect(() => { if (location.hash === "#admin") setAdmin(true); }, []); // scrolled state for topbar useEffect(() => { const onS = () => setScrolled(window.scrollY > 80); window.addEventListener("scroll", onS, { passive: true }); return () => window.removeEventListener("scroll", onS); }, []); // scrollspy useEffect(() => { const obs = new IntersectionObserver((es) => { es.forEach(e => { if (e.isIntersecting) setActive(e.target.id); }); }, { rootMargin: "-45% 0px -50% 0px" }); APP_CATS.forEach(c => { const el = document.getElementById(c.id); if (el) obs.observe(el); }); return () => obs.disconnect(); }, []); const openModal = (item, cat) => setModal({ item, cat }); const addToCart = (line) => setCart(c => [...c, line]); const order = () => { if (cart.length) setDrawer(true); else scrollToId("menu"); }; const openItem = (name) => { const f = findItem(name); if (f) openModal(f.item, f.cat); }; // agregado rápido (cross-sell) con opciones por defecto const quickAdd = (item, cat) => { const inc = item.includes || cat.includes || {}; addToCart({ uid: "L" + Date.now() + Math.random().toString(36).slice(2, 6), name: item.n, catName: cat.name, unitPrice: item.from, ranged: !!(item.to && item.to !== item.from), from: item.from, to: item.to, drinkExtra: 0, weights: item.weights || null, size: null, qty: 1, rice: inc.rice ? APP_RICE[0].label : null, drink: inc.drink ? APP_DRINKS[0].label : null, notes: "", }); }; const onPromoCta = (p) => { if (p.itemName) openItem(p.itemName); else scrollToId(p.catId || "menu"); }; // "Quiero probarlo": agrega automáticamente los platos de la experiencia // (el plato estrella + las sugerencias del chef) y abre el carrito. const tryExperience = () => { const picks = []; const fcat = APP_CATS.find(c => c.id === APP_FEATURE.catId); if (itemAvailable(store, APP_FEATURE.n)) picks.push({ item: APP_FEATURE, cat: fcat }); APP_CATS.forEach(c => c.items.forEach(it => { if (it.suggested && itemAvailable(store, it.n)) picks.push({ item: it, cat: c }); })); const inCart = new Set(cart.map(l => l.name)); const toAdd = picks.filter(p => !inCart.has(p.item.n)); toAdd.forEach(({ item, cat }) => quickAdd(item, cat)); setDrawer(true); }; return ( <> setPdf(true)} /> scrollToId("menu")} onScrollTo={scrollToId} onOpenItem={openItem} onPdf={() => setPdf(true)} isOpen={isOpen} /> {promo && } scrollToId((special && special.target) || "menu")} />
scrollToId(id, 112)} />
{APP_CATS.map((c, i) => (
{c.kicker}

{c.name}

{c.note &&

{c.note}

}
{c.items.map((it, k) => )}
{c.id === "marineros" && }
))}