// app.jsx — shell: header, footer, routing, tweaks. Mounts the app. const LOGO_SRC = "data:image/svg+xml," + encodeURIComponent( "" + "" + "" + "" + "" + "" + "" + "" + "" + "" ); const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": "oklch(0.60 0.12 195)", "heroLayout": "showcase", "bgTone": "neutral", "lum3d": 0.5, "con3d": 1.25, "sat3d": 1, "rel3d": 1.3, "ang3d": -30, "stageBg": "sombre", "packLum": 0.45, "packCon": 1.45, "ctaX1": 45.87, "ctaY1": 66.42, "ctaZ1": 1, "ctaX2": 62.7, "ctaY2": 53.85, "ctaZ2": 1, "ctaX3": 39.4, "ctaY3": 57.25, "ctaZ3": 1, "ctaX4": 56.53, "ctaY4": 59.41, "ctaZ4": 1, "ctaX5": 50.9, "ctaY5": 59.56, "ctaZ5": 1, "ctaDrag": false }/*EDITMODE-END*/; const ACCENTS = [ "oklch(0.58 0.15 255)", // blue "oklch(0.58 0.15 150)", // green "oklch(0.60 0.12 195)", // teal "oklch(0.55 0.16 285)", // indigo "oklch(0.66 0.15 55)", // orange ]; function Header({ t, lang, setLang, route, go, cartCount }) { return React.createElement( "header", { className: "app-header" }, React.createElement("div", { className: "container app-header__inner" }, React.createElement("div", { className: "brand", onClick: () => go("home") }, React.createElement(window.Logo, { size: 30 }), React.createElement("span", { className: "brand__name" }, "QLEQLE"), ), React.createElement("div", { className: "header-spacer" }), React.createElement("nav", { className: "header-nav header-nav--desktop" }, React.createElement("button", { className: "nav-link" + (route === "home" ? " is-active" : ""), onClick: () => go("home") }, t("nav_home")), React.createElement("button", { className: "nav-link" + (route === "customize" ? " is-active" : ""), onClick: () => go("customize") }, t("nav_create")), ), React.createElement("div", { className: "lang-toggle" }, React.createElement("button", { className: lang === "fr" ? "is-active" : "", onClick: () => setLang("fr") }, "FR"), React.createElement("button", { className: lang === "en" ? "is-active" : "", onClick: () => setLang("en") }, "EN"), ), React.createElement("button", { className: "cart-btn", onClick: () => go("cart"), "aria-label": t("nav_cart") }, React.createElement("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("path", { d: "M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" }), React.createElement("path", { d: "M3 6h18" }), React.createElement("path", { d: "M16 10a4 4 0 0 1-8 0" }), ), cartCount > 0 ? React.createElement("span", { className: "cart-badge" }, cartCount) : null, ), ) ); } function Footer({ t, lang, go, route }) { const [showPrivacy, setShowPrivacy] = React.useState(false); return React.createElement( "footer", { className: "app-footer" }, React.createElement("div", { className: "container app-footer__inner" }, React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 9 } }, React.createElement(window.Logo, { size: 22 }), React.createElement("span", { style: { fontWeight: 600, color: "var(--ink)" } }, "QLEQLE"), ), React.createElement("span", { className: "faint" }, "·"), React.createElement("span", null, "© 2026 · " + t("tagline")), React.createElement("div", { className: "header-spacer" }), React.createElement("button", { className: "nav-link" + (route === "mentions" ? " is-active" : ""), onClick: () => go("mentions"), style: { fontSize: 13 } }, lang === "en" ? "Legal notice" : "Mentions légales"), React.createElement("button", { className: "nav-link" + (route === "cgv" ? " is-active" : ""), onClick: () => go("cgv"), style: { fontSize: 13 } }, "CGV"), React.createElement("button", { className: "nav-link", onClick: () => setShowPrivacy(true), style: { fontSize: 13 } }, t("footer_privacy")), React.createElement("button", { className: "nav-link" + (route === "admin" ? " is-active" : ""), onClick: () => go("admin"), style: { fontSize: 13 } }, t("nav_admin")), ), showPrivacy && React.createElement("div", { className: "modal-overlay", onClick: () => setShowPrivacy(false) }, React.createElement("div", { className: "modal card", style: { maxWidth: 440 }, onClick: (e) => e.stopPropagation() }, React.createElement("div", { className: "modal-head" }, React.createElement("div", { style: { fontWeight: 700, fontSize: 17 } }, t("footer_privacy")), React.createElement("button", { className: "icon-btn", onClick: () => setShowPrivacy(false) }, "✕")), React.createElement("hr", { className: "hr" }), React.createElement("p", { className: "muted", style: { fontSize: 14, lineHeight: 1.6, marginTop: 16 } }, t("privacy_body")), ) ) ); } // Tweaks persistence: the host's save mechanism targets the previewed HTML // file, not this .jsx module — so we mirror tweak values in localStorage and // restore them on load, otherwise slider changes vanish on refresh. function loadSavedTweaks() { try { return { ...TWEAK_DEFAULTS, ...JSON.parse(localStorage.getItem("qbt_tweaks") || "{}") }; } catch (e) { return TWEAK_DEFAULTS; } } function App() { const [tw, setTweakRaw] = window.useTweaks(loadSavedTweaks()); const [ctaSel, setCtaSel] = React.useState(1); const setTweak = React.useCallback((k, v) => { setTweakRaw(k, v); try { const saved = JSON.parse(localStorage.getItem("qbt_tweaks") || "{}"); const edits = typeof k === "object" && k !== null ? k : { [k]: v }; localStorage.setItem("qbt_tweaks", JSON.stringify({ ...saved, ...edits })); } catch (e) { /* storage unavailable — non-fatal */ } }, [setTweakRaw]); const [lang, setLang] = React.useState(() => localStorage.getItem("qbt_lang") || "fr"); React.useEffect(() => { localStorage.setItem("qbt_lang", lang); }, [lang]); const t = React.useMemo(() => window.makeT(lang), [lang]); const [route, setRoute] = React.useState("home"); const [selProduct, setSelProduct] = React.useState(null); const [editing, setEditing] = React.useState(null); const [lastOrder, setLastOrder] = React.useState(null); const cart = window.useCart(); const ordersApi = window.useOrders(); function go(r) { setRoute(r); window.scrollTo({ top: 0, behavior: "auto" }); } function chooseProduct(p) { setSelProduct(p); setEditing(null); go("customize"); } function startCreate() { setSelProduct(null); setEditing(null); go("customize"); } function handleAdd(item) { if (editing) cart.remove(editing.lineId); cart.add(item); setEditing(null); go("cart"); } function editItem(item) { setEditing(item); setSelProduct(window.PRODUCTS.find((p) => p.id === item.productId)); go("customize"); } function handlePaid(d) { const order = { id: window.genOrderId(), date: Date.now(), status: "new", items: cart.items, total: cart.subtotal, customer: d, }; ordersApi.place(order); setLastOrder(order); cart.clear(); go("confirmation"); } // Handle the return from Stripe Checkout: // ?paiement=succes → create the order from the saved cart, clear it, show confirmation // ?paiement=annule → back to the cart const stripeReturnDone = React.useRef(false); React.useEffect(() => { if (stripeReturnDone.current) return; stripeReturnDone.current = true; const params = new URLSearchParams(window.location.search); const p = params.get("paiement"); if (!p) return; if (p === "succes") { let items = []; try { items = JSON.parse(localStorage.getItem("qbt_cart_v1") || "[]"); } catch (e) { items = []; } if (items.length) { // best-effort customer info: email from the first contact keychain let email = ""; for (const it of items) { const u = (it.units || []).find((x) => x.contentType === "contact" && x.email); if (u) { email = u.email; break; } } const order = { id: window.genOrderId(), date: Date.now(), status: "new", items, total: items.reduce((s, i) => s + (i.price || 0) * (i.qty || 1), 0), customer: { email }, paiement: "stripe", }; ordersApi.place(order); setLastOrder(order); cart.clear(); setRoute("confirmation"); } } else if (p === "annule") { setRoute("cart"); } // clean the URL so a refresh doesn't replay the flow params.delete("paiement"); const qs = params.toString(); window.history.replaceState({}, "", window.location.pathname + (qs ? "?" + qs : "")); }, []); // prefill payment from the first contact keychain in the cart const prefill = React.useMemo(() => { let u = null; for (const i of cart.items) { u = (i.units || []).find((x) => x.contentType === "contact"); if (u) break; } if (!u) return null; return { name: `${u.firstName || ""} ${u.lastName || ""}`.trim(), email: u.email || "", address: "", address2: "", zip: "", city: "", country: "France", card: "", exp: "", cvc: "", cardName: "", }; }, [route]); // apply tweaks -> CSS vars on body React.useEffect(() => { document.documentElement.style.setProperty("--accent", tw.accent); if (tw.bgTone === "warm") { document.documentElement.style.setProperty("--bg", "#faf9f6"); document.documentElement.style.setProperty("--surface-2", "#f4f2ec"); } else { document.documentElement.style.setProperty("--bg", "#ffffff"); document.documentElement.style.setProperty("--surface-2", "#f5f5f7"); } }, [tw.accent, tw.bgTone]); let screen; if (route === "home") screen = React.createElement(window.HomeScreen, { t, lang, heroLayout: tw.heroLayout, packLum: tw.packLum, packCon: tw.packCon, ctaFraming: [1, 2, 3, 4, 5].map((n) => ({ x: tw["ctaX" + n], y: tw["ctaY" + n], z: tw["ctaZ" + n] })), onCtaFrame: tw.ctaDrag ? (n, x, y) => setTweak({ ["ctaX" + n]: x, ["ctaY" + n]: y }) : null, onStart: startCreate, onChoose: chooseProduct }); else if (route === "customize") screen = React.createElement(window.CustomizeStudio, { t, lang, product: selProduct, initial: editing || null, brightness3d: tw.lum3d, contrast3d: tw.con3d, saturation3d: tw.sat3d, relief3d: tw.rel3d, angle3d: tw.ang3d, stageBg: tw.stageBg, onBack: () => go("home"), onAdd: handleAdd }); else if (route === "cart") screen = React.createElement(window.CartScreen, { t, lang, cart, onCheckout: () => go("payment"), onContinue: () => go("home"), onEdit: editItem }); else if (route === "payment") screen = React.createElement(window.PaymentScreen, { t, lang, cart, prefill, onPaid: handlePaid, onBack: () => go("cart") }); else if (route === "confirmation") screen = React.createElement(window.ConfirmationScreen, { t, lang, order: lastOrder, onHome: () => go("home") }); else if (route === "admin") screen = React.createElement(window.AdminScreen, { t, lang, orders: ordersApi.orders, setStatus: ordersApi.setStatus, clearAll: ordersApi.clearAll, onBack: () => go("home") }); else if (route === "mentions" || route === "cgv") screen = React.createElement(window.LegalScreen, { t, lang, kind: route, onBack: () => go("home") }); return React.createElement( React.Fragment, null, React.createElement(Header, { t, lang, setLang, route, go, cartCount: cart.count }), React.createElement("main", { key: route, style: { flex: 1 } }, screen), React.createElement(Footer, { t, lang, go, route }), React.createElement(window.TweaksPanel, null, React.createElement(window.TweakSection, { label: lang === "en" ? "Brand" : "Marque" }), React.createElement(window.TweakColor, { label: lang === "en" ? "Accent" : "Accent", value: tw.accent, options: ACCENTS, onChange: (v) => setTweak("accent", v) }), React.createElement(window.TweakRadio, { label: lang === "en" ? "Background" : "Fond", value: tw.bgTone, options: ["neutral", "warm"], onChange: (v) => setTweak("bgTone", v) }), React.createElement(window.TweakSection, { label: lang === "en" ? "Home page" : "Accueil" }), React.createElement(window.TweakRadio, { label: lang === "en" ? "Hero layout" : "Disposition", value: tw.heroLayout, options: ["centered", "split", "showcase"], onChange: (v) => setTweak("heroLayout", v) }), React.createElement(window.TweakSection, { label: lang === "en" ? "CTA photos — framing" : "Photos CTA — cadrage" }), React.createElement(window.TweakToggle, { label: lang === "en" ? "Drag to reframe" : "Recadrage à la souris", value: !!tw.ctaDrag, onChange: (v) => setTweak("ctaDrag", v) }), React.createElement(window.TweakRadio, { label: lang === "en" ? "Photo" : "Photo", value: String(ctaSel), options: ["1", "2", "3", "4"], onChange: (v) => setCtaSel(Number(v)) }), React.createElement(window.TweakSlider, { label: lang === "en" ? "Horizontal" : "Horizontal", value: tw["ctaX" + ctaSel], min: 0, max: 100, step: 1, onChange: (v) => setTweak("ctaX" + ctaSel, v) }), React.createElement(window.TweakSlider, { label: lang === "en" ? "Vertical" : "Vertical", value: tw["ctaY" + ctaSel], min: 0, max: 100, step: 1, onChange: (v) => setTweak("ctaY" + ctaSel, v) }), React.createElement(window.TweakSlider, { label: lang === "en" ? "Zoom" : "Zoom", value: tw["ctaZ" + ctaSel], min: 1, max: 2.5, step: 0.05, onChange: (v) => setTweak("ctaZ" + ctaSel, v) }), React.createElement(window.TweakSection, { label: lang === "en" ? "3D preview" : "Aperçu 3D" }), React.createElement(window.TweakRadio, { label: lang === "en" ? "Preview glass" : "Fond verre", value: tw.stageBg, options: ["aucun", "clair", "sombre"], onChange: (v) => setTweak("stageBg", v) }), React.createElement(window.TweakSlider, { label: lang === "en" ? "Packs — brightness" : "Packs — luminosité", value: tw.packLum, min: 0.4, max: 2.2, step: 0.05, onChange: (v) => setTweak("packLum", v) }), React.createElement(window.TweakSlider, { label: lang === "en" ? "Packs — contrast" : "Packs — contraste", value: tw.packCon, min: 0.5, max: 1.8, step: 0.05, onChange: (v) => setTweak("packCon", v) }), React.createElement(window.TweakSlider, { label: lang === "en" ? "Brightness" : "Luminosité", value: tw.lum3d, min: 0.4, max: 2, step: 0.05, onChange: (v) => setTweak("lum3d", v) }), React.createElement(window.TweakSlider, { label: lang === "en" ? "Contrast" : "Contraste", value: tw.con3d, min: 0.5, max: 1.8, step: 0.05, onChange: (v) => setTweak("con3d", v) }), React.createElement(window.TweakSlider, { label: lang === "en" ? "Saturation" : "Saturation", value: tw.sat3d, min: 0.4, max: 1.8, step: 0.05, onChange: (v) => setTweak("sat3d", v) }), React.createElement(window.TweakSlider, { label: lang === "en" ? "Relief (shadows)" : "Relief (ombres)", value: tw.rel3d, min: 0.2, max: 2, step: 0.05, onChange: (v) => setTweak("rel3d", v) }), React.createElement(window.TweakSlider, { label: lang === "en" ? "Light angle" : "Angle lumière", value: tw.ang3d, min: -90, max: 90, step: 3, onChange: (v) => setTweak("ang3d", v) }), React.createElement(window.TweakButton, { label: lang === "en" ? "Reset lighting" : "Réinitialiser l'éclairage", onClick: () => setTweak({ lum3d: 1, con3d: 1, sat3d: 1, rel3d: 1, ang3d: -27 }) }), ), ); } ReactDOM.createRoot(document.getElementById("root")).render(React.createElement(App));