// cart.jsx — Cart, Checkout, Confirmation. Multi-product-ready. // Loaded after sections-*.jsx, before main.jsx. Exposes CartProvider, useCart, CartButton, // CartDrawer, CheckoutOverlay, OrderConfirmation, PRODUCTS, PICKUP_SLOTS, PAYMENT_METHODS. const { createContext: rdbCreateContext, useContext: rdbUseContext } = React; // ─── CONFIG — edit here to add products, slots, payment methods ────────────── // To add a new product later, append an entry. tiers[] is the size/price ladder // for that product. The cart understands any tier from any product. const PRODUCTS = { 'real-date-bar': { id: 'real-date-bar', name: 'Real Date Bar', blurb: 'Organic dates + organic cashews. Two ingredients.', image: 'uploads/DSC02390.jpeg', tiers: [ { id: 'single', label: 'Single bar', qty: 1, price: 5 }, { id: 'five', label: '5-pack', qty: 5, price: 22, featured: true }, { id: 'ten', label: '10-pack', qty: 10, price: 40 }, ], }, // FUTURE PRODUCT example — uncomment & rename to add a 2nd bar: // 'chocolate-date-bar': { // id: 'chocolate-date-bar', name: 'Chocolate Date Bar', // blurb: 'Dates + cashews dipped in dark chocolate.', // image: 'uploads/chocolate.jpeg', // tiers: [ // { id: 'single', label: 'Single bar', qty: 1, price: 6 }, // { id: 'five', label: '5-pack', qty: 5, price: 26, featured: true }, // ], // }, }; // Weekly recurring pickup windows. Ela picks the actual date with each customer. // To add/remove slots, edit this list and re-upload cart.jsx. const PICKUP_SLOTS = [ { id: 'sat-am', label: 'Saturday morning · 9–11 AM' }, { id: 'sat-pm', label: 'Saturday afternoon · 1–3 PM' }, { id: 'sat-eve', label: 'Saturday evening · 4–6 PM' }, { id: 'sun-am', label: 'Sunday morning · 10 AM–12 PM' }, { id: 'sun-pm', label: 'Sunday afternoon · 2–4 PM' }, { id: 'flexible', label: "I'll work out a time with Ela" }, ]; const PAYMENT_METHODS = ['Venmo', 'Zelle', 'Apple Pay', 'PayPal', 'Cash']; // ─── Order number generator ────────────────────────────────────────────────── // RDB-YYYYMMDD-XXXX where XXXX is 4 unambiguous uppercase chars (no 0/O/1/I). function makeOrderNumber() { const d = new Date(); const ymd = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`; const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; let suffix = ''; for (let i = 0; i < 4; i++) suffix += chars[Math.floor(Math.random() * chars.length)]; return `RDB-${ymd}-${suffix}`; } // ─── Cart context ──────────────────────────────────────────────────────────── const CART_KEY = 'rdb_cart_v1'; const CartContext = rdbCreateContext(null); function CartProvider({ children }) { const [items, setItems] = React.useState(() => { try { return JSON.parse(localStorage.getItem(CART_KEY) || '[]') || []; } catch { return []; } }); const [drawerOpen, setDrawerOpen] = React.useState(false); const [checkoutOpen, setCheckoutOpen] = React.useState(false); const [confirmation, setConfirmation] = React.useState(null); React.useEffect(() => { try { localStorage.setItem(CART_KEY, JSON.stringify(items)); } catch {} }, [items]); // Sanitize: drop any line that no longer matches a known product/tier React.useEffect(() => { const filtered = items.filter(it => { const p = PRODUCTS[it.productId]; return !!(p && p.tiers.some(t => t.id === it.tierId)); }); if (filtered.length !== items.length) setItems(filtered); }, []); // run once on mount const addItem = (productId, tierId, qty = 1) => { setItems(prev => { const idx = prev.findIndex(it => it.productId === productId && it.tierId === tierId); if (idx >= 0) { const next = [...prev]; next[idx] = { ...next[idx], qty: next[idx].qty + qty }; return next; } return [...prev, { productId, tierId, qty }]; }); setDrawerOpen(true); }; const setItemQty = (productId, tierId, qty) => { setItems(prev => { if (qty <= 0) return prev.filter(it => !(it.productId === productId && it.tierId === tierId)); return prev.map(it => (it.productId === productId && it.tierId === tierId) ? { ...it, qty } : it ); }); }; const removeItem = (productId, tierId) => setItemQty(productId, tierId, 0); const clearCart = () => setItems([]); // Hydrate with product/tier details const lines = items.map(it => { const product = PRODUCTS[it.productId]; const tier = product && product.tiers.find(t => t.id === it.tierId); if (!product || !tier) return null; return { productId: it.productId, tierId: it.tierId, qty: it.qty, productName: product.name, tierLabel: tier.label, unitPrice: tier.price, barsPerUnit: tier.qty, lineTotal: tier.price * it.qty, lineBars: tier.qty * it.qty, }; }).filter(Boolean); const itemCount = lines.reduce((s, l) => s + l.qty, 0); const totalBars = lines.reduce((s, l) => s + l.lineBars, 0); const subtotal = lines.reduce((s, l) => s + l.lineTotal, 0); return ( {children} ); } const useCart = () => rdbUseContext(CartContext); // ─── Cart icon button (for Nav) ────────────────────────────────────────────── function CartButton() { const cart = useCart(); if (!cart) return null; const { itemCount, setDrawerOpen } = cart; return ( ); } // ─── Cart drawer ───────────────────────────────────────────────────────────── const qtyBtnStyle = { width: 34, height: 34, padding: 0, border: 0, background: 'transparent', color: 'var(--ink)', fontFamily: 'var(--display)', fontSize: 18, cursor: 'pointer', }; function CartDrawer() { const cart = useCart(); if (!cart) return null; const { drawerOpen, setDrawerOpen, lines, subtotal, totalBars, setItemQty, removeItem, setCheckoutOpen } = cart; React.useEffect(() => { if (!drawerOpen) return; const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, [drawerOpen]); React.useEffect(() => { if (!drawerOpen) return; const on = (e) => { if (e.key === 'Escape') setDrawerOpen(false); }; window.addEventListener('keydown', on); return () => window.removeEventListener('keydown', on); }, [drawerOpen, setDrawerOpen]); if (!drawerOpen) return null; const goToCheckout = () => { setDrawerOpen(false); setCheckoutOpen(true); if (window.location.hash !== '#checkout') { history.pushState({ checkout: true }, '', '#checkout'); } }; return (
setDrawerOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 90, background: 'rgba(20, 12, 4, 0.42)', backdropFilter: 'blur(4px)', WebkitBackdropFilter: 'blur(4px)', animation: 'rdbFadeIn .18s ease', }}>
); } // ─── Checkout overlay ──────────────────────────────────────────────────────── function CheckoutOverlay() { const cart = useCart(); if (!cart) return null; const { checkoutOpen, setCheckoutOpen, lines, subtotal, totalBars, clearCart, confirmation, setConfirmation } = cart; const [name, setName] = React.useState(''); const [email, setEmail] = React.useState(''); const [slot, setSlot] = React.useState(''); const [pay, setPay] = React.useState('Venmo'); const [notes, setNotes] = React.useState(''); const [submitting, setSubmitting] = React.useState(false); const [err, setErr] = React.useState(''); const closeCheckout = React.useCallback(() => { setCheckoutOpen(false); setConfirmation(null); setErr(''); if (window.location.hash === '#checkout') { history.replaceState(null, '', window.location.pathname + window.location.search); } }, [setCheckoutOpen, setConfirmation]); // Esc closes React.useEffect(() => { if (!checkoutOpen) return; const on = (e) => { if (e.key === 'Escape' && !submitting) closeCheckout(); }; window.addEventListener('keydown', on); return () => window.removeEventListener('keydown', on); }, [checkoutOpen, submitting, closeCheckout]); // Lock body scroll React.useEffect(() => { if (!checkoutOpen) return; const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, [checkoutOpen]); // Back button exits checkout but preserves cart React.useEffect(() => { if (!checkoutOpen) return; const on = () => closeCheckout(); window.addEventListener('popstate', on); return () => window.removeEventListener('popstate', on); }, [checkoutOpen, closeCheckout]); if (!checkoutOpen) return null; // If we have a confirmation, render that instead of the form. if (confirmation) { return ; } // No items? Bail back to homepage. if (lines.length === 0) { return (
Checkout

Your cart is empty.

Add a pack first, then come back.

); } const submit = (e) => { e.preventDefault(); if (!slot) { setErr('Please pick a pickup window.'); return; } setSubmitting(true); setErr(''); const orderNumber = makeOrderNumber(); const slotLabel = (PICKUP_SLOTS.find(s => s.id === slot) || {}).label || slot; const packLine = lines.map(l => `${l.qty}× ${l.tierLabel}`).join(' + '); const payload = { orderNumber, // legacy fields preserved for back-compat with old order.php name, contact: email, carrier: '', pack: packLine, price: String(subtotal), payment: pay, pickup: slotLabel, notes: notes || '—', // new structured fields items: lines.map(l => ({ productId: l.productId, productName: l.productName, tierId: l.tierId, tierLabel: l.tierLabel, qty: l.qty, unitPrice: l.unitPrice, lineTotal: l.lineTotal, bars: l.lineBars, })), subtotal, totalBars, pickupSlot: { id: slot, label: slotLabel }, }; fetch('/order.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) .then(r => r.json()) .then(d => { if (d.success) { setConfirmation({ orderNumber, lines: [...lines], subtotal, totalBars, slotLabel, payment: pay, name, email, }); clearCart(); setSubmitting(false); } else { setErr('Something went wrong. Please email ela@realdatebar.com directly.'); setSubmitting(false); } }) .catch(() => { setErr('Network error. Please email ela@realdatebar.com directly.'); setSubmitting(false); }); }; const inputStyle = { width: '100%', padding: '14px 16px', borderRadius: 10, border: '1px solid var(--line-strong)', background: 'var(--paper)', fontFamily: 'var(--sans)', fontSize: 15, color: 'var(--ink)', outline: 'none', }; const labelStyle = { fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '.14em', textTransform: 'uppercase', color: 'var(--ink-mute)', display: 'block', marginBottom: 8, }; return (
Real Date Bar · Checkout
{/* Order summary */}
Order summary

{totalBars} bar{totalBars === 1 ? '' : 's'} · ${subtotal}

    {lines.map(l => (
  • {l.productName}
    {l.qty}× {l.tierLabel}
    ${l.lineTotal}
  • ))}
Subtotal
${subtotal}

Pickup only · 13030 Dolomite Drive, Frisco TX · Pay on pickup

{/* Form */}
Your details

Tell Ela where to find you.

setName(e.target.value)} required placeholder="Jordan Reeves" autoComplete="name" />
setEmail(e.target.value)} required placeholder="you@example.com" autoComplete="email" />

We'll send your order receipt here.

{PICKUP_SLOTS.map(s => ( ))}

Ela will confirm the exact date by email.

{PAYMENT_METHODS.map(p => ( ))}

You'll pay Ela on pickup — or send in advance, your choice.