/* Shared storefront chrome for the product and category pages. */ (function () { const NS = window.LemonaSportswearDesignSystem_c8f307; const { Header, Footer, Drawer, MobileMenu, Button, EmptyState, CartItem, OrderSummary } = NS; /* ---- PHASE 9: server-authoritative bag pricing (shared Chrome) ---- The same useBagQuote + BagSummary pattern already proven on index.html and product.html, kept local to shop.jsx. It renders ONLY what ?r=checkout_quote returns: no discount, percentage, coupon saving or total is computed in this file. OPTION B: checkout.html runs its own quote flow, so the quote here is DISABLED on that page — opening the drawer there must not fire a second checkout_quote request. The drawer still works there, falling back to the plain client-side subtotal placeholder. */ function onCheckoutPage() { try { return /(^|\/)checkout\.html$/i.test(location.pathname); } catch (e) { return false; } } function useBagQuote(cart, open) { const [quote, setQuote] = React.useState(null); const [offers, setOffers] = React.useState({ coupons: [], sales: [] }); /* React state only, for this page session; the server re-validates on every quote and again at checkout, so it stays authoritative. */ const [coupon, setCoupon] = React.useState(null); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(null); const enabled = !onCheckoutPage(); const fetchQuote = React.useCallback(async (code) => { const S = window.LemonaStore; const lines = S && typeof S.getCart === "function" ? (S.getCart() || []) : []; const items = lines.map(function (l) { return { product_id: l.product_id !== undefined ? l.product_id : l.id, variant_id: l.variant_id || null, size: l.size || null, color: l.color || l.color_name || null, quantity: l.quantity, }; }).filter(function (l) { return l.product_id; }); if (!items.length) { setQuote(null); setOffers({ coupons: [], sales: [] }); return null; } setBusy(true); setErr(null); try { const res = await fetch("/api/index.php?r=checkout_quote", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ items: items, coupon_code: code || null, payment_method: "razorpay" }), }); const d = await res.json().catch(function () { return {}; }); if (!res.ok || d.ok === false) { setErr((d && d.message) || "Could not refresh your bag total."); return null; } setQuote(d.quote || null); setOffers((d.offers && (d.offers.coupons || d.offers.sales)) ? d.offers : { coupons: [], sales: [] }); // Applied state follows the QUOTE, not the click: the server drops a code it will not honour. const applied = d.quote && d.quote.coupon ? d.quote.coupon.code : null; setCoupon(applied); /* Hand the SERVER-CONFIRMED code to checkout.html. Only the code travels; checkout submits it to checkout_quote and build_quote() re-decides the discount and total. */ if (window.LemonaStore && window.LemonaStore.setCheckoutCoupon) window.LemonaStore.setCheckoutCoupon(applied); if (code && !applied) setErr("That code could not be applied."); return d.quote || null; } catch (e) { setErr("Could not refresh your bag total."); return null; } finally { setBusy(false); } }, []); React.useEffect(() => { if (!open || !enabled) return; fetchQuote(coupon); }, [open, cart, enabled, fetchQuote]); // eslint-disable-line react-hooks/exhaustive-deps return { enabled: enabled, quote: quote, offers: offers, coupon: coupon, busy: busy, err: err, apply: function (code) { return fetchQuote(String(code || "").trim().toUpperCase() || null); }, remove: function () { return fetchQuote(null); }, }; } /* Drawer footer. Renders ONLY server values; the checkout button is the existing shared Chrome anchor, unchanged. */ function BagSummary({ bag, cart }) { const q = bag.enabled ? bag.quote : null; const money = function (n) { return "\u20B9" + Number(n || 0).toLocaleString("en-IN", { maximumFractionDigits: 0 }); }; const sale = q ? Number(q.sale_discount || 0) : 0; const coup = q ? Number(q.coupon_discount || 0) : 0; const activeSale = (bag.offers.sales || [])[0] || null; const row = { display: "flex", justifyContent: "space-between", gap: 8 }; return (
{activeSale && sale > 0 ? (
{activeSale.name || activeSale.label || "Sale"} −{money(sale)}
) : null} {q ? (
Subtotal{money(q.items_subtotal)}
{sale > 0 ?
Sale discount−{money(sale)}
: null} {coup > 0 ?
Coupon {q.coupon ? q.coupon.code : ""}−{money(coup)}
: null}
Delivery{Number(q.shipping_amount || 0) > 0 ? money(q.shipping_amount) : "Free"}
Total{money(q.total_amount)}
) : ( )} {bag.enabled && bag.coupon ? (
{bag.coupon} applied
) : bag.enabled && (bag.offers.coupons || []).length ? (
{(bag.offers.coupons || []).map(function (c) { return (
{c.code}{c.saving ? " \u00B7 save " + money(c.saving) : (c.name ? " \u00B7 " + c.name : "")}
); })}
) : null} {bag.err ? {bag.err} : null}
); } function Chrome({ children }) { const nav = window.useLemonaNav(); const [menuOpen, setMenuOpen] = React.useState(false); const [cartOpen, setCartOpen] = React.useState(false); const [cart, setCart] = React.useState([]); const [counts, setCounts] = React.useState({ cart: 0, wish: 0 }); const bag = useBagQuote(cart, cartOpen); // Header badges read the shared cart and the customer's saved wishlist, so they are // correct on every page rather than hardcoded to zero. // // The Store object is looked up at CALL time, not captured once when Chrome mounts, and // every method is feature-checked before it is called. A stale or partially loaded // store.js therefore degrades to a zero badge instead of throwing inside the effect and // taking the whole page down with it. As soon as the real API is present the badges // populate normally on the next sync. React.useEffect(() => { const sync = () => { const S = window.LemonaStore; const cart = S && typeof S.getCartCount === "function" ? Number(S.getCartCount()) || 0 : 0; let wish = 0; if (S && typeof S.getWishlist === "function") { const list = S.getWishlist(); wish = Array.isArray(list) ? list.length : 0; } setCounts({ cart: cart, wish: wish }); // Keep the drawer's own line list in step with the shared cart. if (S && typeof S.getCart === "function") { try { setCart(S.getCart() || []); } catch (e) { /* leave the last known list */ } } }; sync(); const S = window.LemonaStore; if (S && typeof S.loadWishlist === "function") { // Wrapped in try/catch as well as the promise catch: a synchronous throw from an // older implementation must not stop the listeners below from being attached. try { Promise.resolve(S.loadWishlist()).then(sync).catch(function () {}); } catch (e) { /* wishlist stays at its current value */ } } window.addEventListener("lemona:cart-changed", sync); window.addEventListener("lemona:wishlist-changed", sync); return () => { window.removeEventListener("lemona:cart-changed", sync); window.removeEventListener("lemona:wishlist-changed", sync); }; }, []); return (
{window.StoreNotice ? : null}
{ location.href = "account.html"; }} onWishlistClick={() => { location.href = "account.html#wishlist"; }} onCartClick={() => setCartOpen(true)} onMenuClick={() => setMenuOpen(true)} />
{children}
); } /** Bag subtotal from the shared cart lines. Mirrors the drawer maths already used on the * homepage and product page; the server remains the authority at checkout. */ function cartSubtotal(lines) { return (lines || []).reduce(function (n, l) { return n + (Number(l.price) || 0) * (Number(l.quantity) || 0); }, 0); } /** Slug from /product/, /category/ or ?slug= when rewrites are off. */ function slugFromUrl() { const q = new URLSearchParams(location.search).get("slug"); if (q) return q; const parts = location.pathname.split("/").filter(Boolean); const last = parts[parts.length - 1] || ""; return last.endsWith(".html") ? "" : last; } Object.assign(window, { Chrome, slugFromUrl }); })();