/* ========================================================== YAPPY. — data, store, router Anything `null` is UNCONFIRMED and renders as "TBC". ========================================================== */ const PRODUCTS = { brb: { id: 'brb', name: 'BRB', pct: '5%', status: 'Be right back', hue: '#D8D42F', ink: '#0E2A17', tone2: '#0E2A17', cut: 'assets/cut-brb.png', photo: 'assets/pouch-brb.jpg', line: 'Set your status to BRB.', blurb: 'Made for the in-between moments.', onDark: false, }, dnd: { id: 'dnd', name: 'DND', pct: '8.5%', status: 'Do not disturb', hue: '#F26A3C', ink: '#0E2A17', tone2: '#F7A57F', cut: 'assets/cut-dnd.png', photo: 'assets/pouch-dnd.jpg', line: 'Mute the group chat.', blurb: 'Step away from the noise.', onDark: false, }, offline: { id: 'offline', name: 'OFFLINE', pct: '12.5%', status: 'Status: elsewhere', hue: '#0E2A17', ink: '#F3E3C7', tone2: '#6F8F63', cut: 'assets/cut-offline.png', photo: 'assets/pouch-offline.jpg', line: "OFFLINE. For when you're done.", blurb: 'Your quiet, on your terms.', onDark: true, }, }; const SKU_ORDER = ['brb', 'dnd', 'offline']; /* From the packaging reference (Brand Guidelines Ed. 01). Confirm against final label. */ const PACK = { descriptor: 'Vijaya leaf extract gummies', count: '10 gummies', format: 'Stand-up resealable pouch', basis: 'Plant based formulation', }; /* Pricing — null until confirmed by the business. */ const PRICING = { product: { brb: null, dnd: null, offline: null }, shipping: null, currency: '₹', }; const fmtPrice = (v) => (v === null || v === undefined) ? null : `${PRICING.currency}${v.toLocaleString('en-IN')}`; function cartTotal(cart) { let sum = 0; for (const it of cart) { const p = PRICING.product[it.id]; if (typeof p !== 'number') return null; sum += p * it.qty; } if (typeof PRICING.shipping !== 'number') return null; return sum + PRICING.shipping; } const NOISE_WORDS = ['NOTIFICATIONS','DEADLINES','GROUP CHAT','47 UNREAD','REPLY ALL','TRENDING NOW','BREAKING','REMINDER','PING','NEW FOLLOWER','INBOX ZERO?','MEETING IN 5',"DON'T MISS OUT",'SCROLL','REFRESH','UPDATES AVAILABLE','URGENT','PER MY LAST EMAIL','LIVE NOW','3 MISSED CALLS']; /* Prescription states on an order */ const RX = { awaiting: { label: 'Prescription needed', cls: 's-warn' }, verifying: { label: 'Checking prescription', cls: 's-prog' }, verified: { label: 'Prescription verified', cls: 's-ok' }, rejected: { label: 'Prescription not accepted', cls: 's-stop' }, }; const SHIP = { held: { label: 'On hold', cls: 's-lock' }, packed: { label: 'Packed', cls: 's-prog' }, shipped: { label: 'Shipped', cls: 's-prog' }, delivered: { label: 'Delivered', cls: 's-ok' }, cancelled: { label: 'Cancelled · refund due', cls: 's-stop' }, }; /* ---------------- store (non-sensitive demo state) ---------------- */ const STORE_KEY = 'yappy.demo.v2'; const INITIAL = { cart: [], orders: [], consults: [] }; function loadState() { try { return { ...INITIAL, ...(JSON.parse(localStorage.getItem(STORE_KEY)) || {}) }; } catch (e) { return { ...INITIAL }; } } const StoreCtx = React.createContext(null); function StoreProvider({ children }) { const [state, setState] = React.useState(loadState); const [cartOpen, setCartOpen] = React.useState(false); React.useEffect(() => { localStorage.setItem(STORE_KEY, JSON.stringify(state)); }, [state]); // keep storefront + admin tabs in sync React.useEffect(() => { const on = (e) => { if (e.key === STORE_KEY) setState(loadState()); }; window.addEventListener('storage', on); return () => window.removeEventListener('storage', on); }, []); const set = React.useCallback((patch) => setState((s) => ({ ...s, ...(typeof patch === 'function' ? patch(s) : patch) })), []); const api = React.useMemo(() => ({ state, set, cartOpen, setCartOpen, reset: () => setState({ ...INITIAL }), add: (id, qty = 1) => setState((s) => { const ex = s.cart.find((x) => x.id === id); return { ...s, cart: ex ? s.cart.map((x) => x.id === id ? { ...x, qty: Math.min(9, x.qty + qty) } : x) : [...s.cart, { id, qty }] }; }), setQty: (id, qty) => setState((s) => ({ ...s, cart: qty <= 0 ? s.cart.filter((x) => x.id !== id) : s.cart.map((x) => x.id === id ? { ...x, qty: Math.min(9, qty) } : x) })), updateOrder: (ref, patch) => setState((s) => ({ ...s, orders: s.orders.map((o) => o.ref === ref ? { ...o, ...(typeof patch === 'function' ? patch(o) : patch) } : o) })), updateConsult: (id, patch) => setState((s) => ({ ...s, consults: s.consults.map((c) => c.id === id ? { ...c, ...(typeof patch === 'function' ? patch(c) : patch) } : c) })), }), [state, set, cartOpen]); return {children}; } const useStore = () => React.useContext(StoreCtx); const cartCount = (cart) => cart.reduce((n, x) => n + x.qty, 0); /* ---------------- router (hash) ---------------- */ function getPath() { return (location.hash.replace(/^#/, '') || '/').split('?')[0]; } function getQuery() { return new URLSearchParams((location.hash.split('?')[1]) || ''); } function useRoute() { const [path, setPath] = React.useState(getPath); React.useEffect(() => { const on = () => { setPath(getPath()); window.scrollTo(0, 0); }; window.addEventListener('hashchange', on); return () => window.removeEventListener('hashchange', on); }, []); return path; } const go = (p) => { if (location.hash.replace(/^#/, '') === p) window.scrollTo(0, 0); location.hash = p; }; function useReveal() { React.useEffect(() => { const els = document.querySelectorAll('.reveal:not(.in)'); const io = new IntersectionObserver((es) => es.forEach((e) => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }), { threshold: 0.12 }); els.forEach((el) => io.observe(el)); return () => io.disconnect(); }); } Object.assign(window, { PRODUCTS, SKU_ORDER, PACK, PRICING, fmtPrice, cartTotal, NOISE_WORDS, RX, SHIP, StoreProvider, useStore, cartCount, useRoute, go, getPath, getQuery, useReveal, INITIAL, });