/* app.jsx — root: state, cart logic, toast, tweaks */ const HERO_MAP = { "Editorial": "editorial", "Vollbild": "fullbleed", "Zentriert": "centered" }; const MENU_MAP = { "Foto oben": "default", "Horizontal": "horizontal", "Bild-Overlay": "overlay" }; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "hero": "Editorial", "menu": "Foto oben", "accent": "#8a6d4b", "rounded": true }/*EDITMODE-END*/; const ACCENTS = ["#8a6d4b", "#9c5a4d", "#6b7256", "#c8923f", "#7a6a8a"]; function darken(hex, amt) { const h = hex.replace("#", ""); let r = parseInt(h.slice(0,2),16), g = parseInt(h.slice(2,4),16), b = parseInt(h.slice(4,6),16); r = Math.max(0, Math.round(r * (1 - amt))); g = Math.max(0, Math.round(g * (1 - amt))); b = Math.max(0, Math.round(b * (1 - amt))); return `#${[r,g,b].map((x) => x.toString(16).padStart(2,"0")).join("")}`; } function toRgb(hex) { const h = hex.replace("#", ""); return `${parseInt(h.slice(0,2),16)},${parseInt(h.slice(2,4),16)},${parseInt(h.slice(4,6),16)}`; } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [cart, setCart] = React.useState([]); const [cartOpen, setCartOpen] = React.useState(false); const [checkout, setCheckout] = React.useState(false); const [favs, setFavs] = React.useState([]); const [toast, setToast] = React.useState(null); const [scrolled, setScrolled] = React.useState(false); const [pop, setPop] = React.useState(false); const toastTimer = React.useRef(null); const popTimer = React.useRef(null); const SOUPS = window.SOUPS; const soupsById = React.useMemo(() => Object.fromEntries(SOUPS.map((s) => [s.id, s])), [SOUPS]); // apply accent + radius tweaks to CSS vars React.useEffect(() => { const root = document.documentElement; root.style.setProperty("--accent", t.accent); root.style.setProperty("--accent-deep", darken(t.accent, 0.22)); root.style.setProperty("--accent-rgb", toRgb(t.accent)); if (!t.rounded) { root.style.setProperty("--r-sm", "3px"); root.style.setProperty("--r", "5px"); root.style.setProperty("--r-lg", "6px"); root.style.setProperty("--r-xl", "8px"); } else { root.style.setProperty("--r-sm", "10px"); root.style.setProperty("--r", "16px"); root.style.setProperty("--r-lg", "24px"); root.style.setProperty("--r-xl", "32px"); } }, [t.accent, t.rounded]); React.useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 12); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); const count = cart.reduce((s, it) => s + it.qty, 0); const bumpPop = () => { setPop(true); clearTimeout(popTimer.current); popTimer.current = setTimeout(() => setPop(false), 280); }; const showToast = (msg, action) => { setToast({ msg, action }); clearTimeout(toastTimer.current); toastTimer.current = setTimeout(() => setToast(null), 3200); }; const addToCart = (soup, qty = 1) => { setCart((c) => { const ex = c.find((x) => x.id === soup.id); if (ex) return c.map((x) => x.id === soup.id ? { ...x, qty: x.qty + qty } : x); return [...c, { id: soup.id, qty, price: soup.price, name: soup.name, size: soup.size, tint: soup.tint }]; }); bumpPop(); showToast(`${soup.name} hinzugefügt`, { label: "Warenkorb", fn: () => { setToast(null); setCartOpen(true); } }); }; const choosePlan = (plan) => { setCart((c) => { const id = "plan-" + plan.id; const ex = c.find((x) => x.id === id); if (ex) return c.map((x) => x.id === id ? { ...x, qty: x.qty + 1 } : x); return [...c, { id, qty: 1, price: plan.price, name: plan.name + " (Abo)", size: plan.sub, tint: "#6b7256" }]; }); bumpPop(); showToast(`Abo „${plan.name}“ hinzugefügt`, { label: "Warenkorb", fn: () => { setToast(null); setCartOpen(true); } }); }; const inc = (id) => setCart((c) => c.map((x) => x.id === id ? { ...x, qty: x.qty + 1 } : x)); const dec = (id) => setCart((c) => c.map((x) => x.id === id ? { ...x, qty: Math.max(1, x.qty - 1) } : x)); const remove = (id) => setCart((c) => c.filter((x) => x.id !== id)); const toggleFav = (id) => setFavs((f) => f.includes(id) ? f.filter((x) => x !== id) : [...f, id]); // build a lookup that also knows about plan line-items (so cart/checkout can render them) const lineLookup = React.useMemo(() => { const m = { ...soupsById }; cart.forEach((it) => { if (!m[it.id]) m[it.id] = it; }); return m; }, [soupsById, cart]); const goNav = (id) => { setCartOpen(false); if (id === "top") { window.scrollTo({ top: 0, behavior: "smooth" }); return; } const el = document.getElementById(id); if (el) window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 60, behavior: "smooth" }); }; const openCheckout = () => { setCartOpen(false); setCheckout(true); document.body.style.overflow = "hidden"; window.scrollTo(0, 0); }; const closeCheckout = () => { setCheckout(false); document.body.style.overflow = ""; }; const heroVariant = HERO_MAP[t.hero] || "editorial"; const menuLayout = MENU_MAP[t.menu] || "default"; return ( setCartOpen(true)} onNav={goNav} /> goNav("menu")} onAbo={() => goNav("abo")} /> setCartOpen(false)} onInc={inc} onDec={dec} onRemove={remove} onCheckout={openCheckout} /> {checkout && ( { closeCheckout(); }} onDone={() => { setCart([]); }} /> )} {/* toast */} {toast && ( {toast.msg} {toast.action && {toast.action.label}} )} {/* Tweaks */} setTweak("hero", v)} /> setTweak("menu", v)} /> setTweak("accent", v)} /> setTweak("rounded", v)} /> ); } ReactDOM.createRoot(document.getElementById("root")).render();