/* =========================================================== State: hash router + Cart + Quote providers (localStorage) =========================================================== */ const { useState, useEffect, useRef, useCallback, createContext, useContext, useMemo } = React; /* ---------------- hash router ---------------- */ function parseHash() { let h = window.location.hash.replace(/^#/, "") || "/"; const [path, query] = h.split("?"); const params = {}; if (query) query.split("&").forEach(kv => { const [k, v] = kv.split("="); params[k] = decodeURIComponent(v || ""); }); return { path, params }; } const RouterCtx = createContext(null); function RouterProvider({ children }) { const [route, setRoute] = useState(parseHash()); useEffect(() => { const on = () => { setRoute(parseHash()); window.scrollTo(0, 0); const sc = document.querySelector(".app-scroll"); if (sc) sc.scrollTo(0,0); }; window.addEventListener("hashchange", on); return () => window.removeEventListener("hashchange", on); }, []); const navigate = useCallback((to) => { if (("#" + to) === window.location.hash) { window.scrollTo(0,0); const sc=document.querySelector(".app-scroll"); if(sc) sc.scrollTo(0,0); } window.location.hash = to; }, []); return React.createElement(RouterCtx.Provider, { value: { ...route, navigate } }, children); } const useRouter = () => useContext(RouterCtx); function Link({ to, children, className, style, onClick }) { const { navigate } = useRouter(); return React.createElement("a", { href: "#" + to, className, style, onClick: (e) => { e.preventDefault(); onClick && onClick(e); navigate(to); } }, children); } /* ---------------- persistence helper ---------------- */ function usePersist(key, initial) { const [val, setVal] = useState(() => { try { const r = localStorage.getItem(key); return r ? JSON.parse(r) : initial; } catch (e) { return initial; } }); useEffect(() => { try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) {} }, [key, val]); return [val, setVal]; } /* ---------------- Cart ---------------- */ const CartCtx = createContext(null); function CartProvider({ children }) { const [items, setItems] = usePersist("dwt_cart", []); // {variant_id, sku, name, product_slug, quantity, unit_price, core_charge, image} const [reservationExpires, setReservationExpires] = usePersist("dwt_cart_exp", null); const add = useCallback((line) => { setItems(prev => { const i = prev.findIndex(x => x.variant_id === line.variant_id); let next; if (i >= 0) { next = prev.slice(); next[i] = { ...next[i], quantity: next[i].quantity + (line.quantity || 1) }; } else next = [...prev, { ...line, quantity: line.quantity || 1 }]; return next; }); setReservationExpires(Date.now() + 15 * 60 * 1000); }, []); const setQty = useCallback((vid, q) => setItems(prev => prev.map(x => x.variant_id === vid ? { ...x, quantity: Math.max(1, q) } : x)), []); const remove = useCallback((vid) => setItems(prev => prev.filter(x => x.variant_id !== vid)), []); const clear = useCallback(() => setItems([]), []); const count = items.reduce((s, x) => s + x.quantity, 0); const subtotal = items.reduce((s, x) => s + (x.unit_price || 0) * x.quantity, 0); const coreTotal = items.reduce((s, x) => s + (x.core_charge || 0) * x.quantity, 0); return React.createElement(CartCtx.Provider, { value: { items, add, setQty, remove, clear, count, subtotal, coreTotal, reservationExpires } }, children); } const useCart = () => useContext(CartCtx); /* ---------------- Quote list ---------------- */ const QuoteCtx = createContext(null); function QuoteProvider({ children }) { const [items, setItems] = usePersist("dwt_quote", []); // catalog lines const [custom, setCustom] = usePersist("dwt_quote_custom", []); // {description, quantity} const addItem = useCallback((line) => { setItems(prev => { const i = prev.findIndex(x => x.variant_id === line.variant_id); if (i >= 0) { const n = prev.slice(); n[i] = { ...n[i], quantity: n[i].quantity + (line.quantity || 1) }; return n; } return [...prev, { ...line, quantity: line.quantity || 1 }]; }); }, []); const setQty = useCallback((vid, q) => setItems(prev => prev.map(x => x.variant_id === vid ? { ...x, quantity: Math.max(1, q) } : x)), []); const remove = useCallback((vid) => setItems(prev => prev.filter(x => x.variant_id !== vid)), []); const addCustom = useCallback((c) => setCustom(prev => [...prev, { description: "", quantity: 1, ...c, _id: Date.now() + Math.random() }]), []); const setCustomField = useCallback((id, field, v) => setCustom(prev => prev.map(c => c._id === id ? { ...c, [field]: v } : c)), []); const removeCustom = useCallback((id) => setCustom(prev => prev.filter(c => c._id !== id)), []); const clear = useCallback(() => { setItems([]); setCustom([]); }, []); const count = items.reduce((s, x) => s + x.quantity, 0) + custom.length; return React.createElement(QuoteCtx.Provider, { value: { items, custom, addItem, setQty, remove, addCustom, setCustomField, removeCustom, clear, count } }, children); } const useQuote = () => useContext(QuoteCtx); /* ---------------- toast ---------------- */ const ToastCtx = createContext(null); function ToastProvider({ children }) { const [toasts, setToasts] = useState([]); const push = useCallback((msg, opts = {}) => { const id = Date.now() + Math.random(); setToasts(t => [...t, { id, msg, ...opts }]); setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), opts.duration || 2600); }, []); return React.createElement(ToastCtx.Provider, { value: { push } }, children, React.createElement(Toaster, { toasts }) ); } const useToast = () => useContext(ToastCtx); function Toaster({ toasts }) { return React.createElement("div", { className: "toaster" }, toasts.map(t => React.createElement("div", { key: t.id, className: "toast" }, React.createElement(Icon, { name: t.icon || "check", size: 16 }), React.createElement("span", null, t.msg), t.action ? React.createElement(Link, { to: t.action.to, className: "toast-action" }, t.action.label) : null )) ); } /* ---------------- money fmt ---------------- */ function money(n) { if (n === null || n === undefined) return null; return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } /* ---------------- Tweaks context (values provided by TweaksProvider in app.jsx) ---------------- */ const TweaksCtx = createContext(null); function useT() { return useContext(TweaksCtx) || { t: {}, setTweak: () => {} }; } Object.assign(window, { RouterProvider, useRouter, Link, CartProvider, useCart, QuoteProvider, useQuote, ToastProvider, useToast, usePersist, money, TweaksCtx, useT });