// src/store.jsx — Store menu (the hero screen).
// Supports 4 category-nav patterns via tweak: 'tabs' | 'rail' | 'sheet' | 'aisle'.
// 'aisle' drops the nav chrome entirely for a long-scroll walk-the-store feel.
// Always: sticky storefront header, delivery/pickup toggle, category sections, density-aware product grid.
// Store-specific mock content — distinct flavor per store so the editorial lede
// and orders-near ticker don't feel generic. Keyed by store.id.
const STORE_VOICE = {
kalymnos: {
lede: {
pull: "The same 8 AM pastry shipment for thirty years. That's not tradition, it's a handshake.",
body: "Maria's been running Kalymnos since her father shoved a knife into her hand in '92. If it's on the shelf, it sold last week.",
author: "Staff of Kalymnos",
},
orders: { count: 142, window: "in the last hour", recent: ["Modelo 12pk", "BEC on a roll", "Topo Chico"] },
},
liu: {
lede: {
pull: "The sign says 24hr. The sign has always said 24hr.",
body: "Liu's is the block's power grid. If the lights are out somewhere else, they're on here.",
author: "Liu's 24hr",
},
orders: { count: 58, window: "in the last hour", recent: ["Liquid Death", "Red Bull 12oz", "Advil"] },
},
arbor: {
lede: {
pull: "Real hammers. Real level. No fruit.",
body: "Arbor has been the hardware answer for three generations of Greenpoint renters. They know what drywall you have before you do.",
author: "Arbor Hardware",
},
orders: { count: 17, window: "this morning", recent: ["Picture hooks", "Drywall anchors", "Painter's tape"] },
},
fig: {
lede: {
pull: "A wall of under-$22 bottles worth fighting about.",
body: "Cobble Hill's wine counter where the staff will argue with you about the Pinot you just pulled off the shelf, and be right.",
author: "Fig & Thistle",
},
orders: { count: 34, window: "in the last hour", recent: ["Meiomi Pinot", "Whispering Angel", "White Claw 12"] },
},
elmo: {
lede: {
pull: "We brined 200 thighs this morning. You are not going to eat them all.",
body: "Elmo's Chicken & Fixins runs on a Tuesday dinner rush that has to be seen to be believed. The fryer doesn't stop until the thighs do.",
author: "Elmo's",
},
orders: { count: 96, window: "in the last hour", recent: ["Fried chicken plate", "Collards", "Sweet tea"] },
},
verde: {
lede: {
pull: "Orchard to your bag in 14 hours. We timed it.",
body: "Verde pulls from three Hudson Valley farms. The produce you pick today was on a branch yesterday.",
author: "Verde Corner Produce",
},
orders: { count: 41, window: "in the last hour", recent: ["Honeycrisp apples", "Vine tomatoes", "Baby spinach"] },
},
};
// Editorial pull-quote — distinct shape from product cards: no image, big serif text
// with a vertical accent rule and small attribution.
function EditorialLede({ voice }) {
if (!voice) return null;
return (
“{voice.pull}”
{voice.body}
— {voice.author}
);
}
// Horizontal "who's ordering right now" ticker — dots + recent item names.
// Distinct shape from the pill toggles and cards: a thin rail with pulsing dot.
function OrdersNearby({ voice }) {
if (!voice) return null;
return (
{voice.count} orders{voice.window}·
{voice.recent.map(item => (
{item}
))}
);
}
// Deals strip — small horizontal ticket-style cards, visually distinct from the
// larger ProductCard grid that dominates the rest of the page. One deal per
// scroll-column. Container respects its parent column width (no negative
// bleed margins) so it can't push the page into horizontal scroll.
function DealsStrip({ deals, onOpen, onAdd, getQty }) {
if (!deals.length) return null;
return (
Deals this week
{deals.length} on
{deals.map(p => (
{p.deal}
{p.name}
{money(p.price)}
))}
);
}
// Large prominent pickup-ready banner — visible when mode is "pickup". Different
// shape from the PickupBanner on home: full-bleed green stripe with timer dot.
function PickupReadyChip({ store }) {
return (
{store.pickupEta || "Ready in 8 min"}
We'll text you when your bag's packed. Show your name at the counter.
{/* Orders ticker — social proof that this store is alive right now */}
{voice && }
{/* Prominent pickup-ready chip when user has switched to pickup */}
{mode === "pickup" && }
{/* Lede + Deals — stacked on mobile, 40/60 side-by-side on desktop.
Both cells get min-width:0 so the deals scroller can't push the
lede column out past the container on wide screens. */}
{CATEGORIES.map((c, i) => renderSection(c, i))}
{/* End-of-store warm close — distinct shape from HomeFarewell:
this one's a warm postcard/note style with the store owner's
voice, not the navy gradient hero the home page uses. */}
{/* Mobile bottom-sheet nav */}
{tweaks.categoryNav === "sheet" && isMobile && (
)}
)}
>
);
}
// ─── StoreFarewell ─────────────────────────────────────────────────────────
// End-of-store close. Warm postcard/hand-drawn feel with a tiny store map
// pin glyph, the store's own voice, and two CTAs: Ask Opa and Try nearby.
// Intentionally quieter than the HomeFarewell navy gradient — this is a
// pause, not a pitch.
function StoreFarewell({ store, voice, onNav, isMobile }) {
const nearby = STORES.filter(s => s.id !== store.id).slice(0, 3);
const firstName = (voice?.lede?.author || store.name).split(/[,\s]/)[0];
return (
{/* Left: tactile "aisle end" stamp band */}
{/* Right: store voice + CTAs */}
Didn't see what you came for? {firstName} keeps more behind the counter than what's on the shelves.
Tell Opa what you need — or hop to a nearby store that stocks it.