// src/category.jsx — Category deep-dive: filter + sort + bigger grid for a single aisle.
function Category({ storeId, category, onNav, cart, addToCart, tweaks }) {
const isMobile = useMedia("(max-width: 720px)");
const store = STORES.find(s => s.id === storeId) || STORES[0];
const cat = CATEGORIES.find(c => c.id === category) || CATEGORIES[1];
const [sort, setSort] = React.useState("popular");
const [sub, setSub] = React.useState("all");
const all = PRODUCTS.filter(p => p.category === cat.id);
const subs = ["all", ...Array.from(new Set(all.map(p => p.subcategory).filter(Boolean)))];
let list = sub === "all" ? all : all.filter(p => p.subcategory === sub);
if (sort === "priceAsc") list = [...list].sort((a,b) => a.price - b.price);
if (sort === "priceDesc") list = [...list].sort((a,b) => b.price - a.price);
if (sort === "popular") list = [...list].sort((a,b) => (b.popular?1:0) - (a.popular?1:0));
const cols = isMobile ? 2 : tweaks.density === "compact" ? 6 : tweaks.density === "comfy" ? 4 : 5;
const getQty = (pid) => cart.items.find(i => i.productId === pid)?.qty || 0;
const Icon = Icons[cat.icon];
return (
{/* breadcrumb */}
onNav({ screen: "home" })}>Home
onNav({ screen: "store", storeId })}>{store.name}
{cat.label}
{all.length} items
{cat.label}
{[
{ id: "popular", label: "Popular" },
{ id: "priceAsc", label: "Price ↑" },
{ id: "priceDesc", label: "Price ↓" },
].map(s => (
setSort(s.id)}
style={{ padding: "8px 14px", borderRadius: 999,
background: sort === s.id ? "var(--navy-fill)" : "transparent",
color: sort === s.id ? "#fff" : "var(--ink-2)",
fontSize: 12.5, fontWeight: 500 }}>{s.label}
))}
{/* Subcategory chips */}
{subs.length > 2 && (
{subs.map(s => (
setSub(s)}
style={{ padding: "7px 14px", borderRadius: 999, whiteSpace: "nowrap",
background: sub === s ? "var(--navy-tint)" : "var(--surface)",
color: sub === s ? "var(--navy)" : "var(--ink-2)",
border: "1px solid " + (sub === s ? "transparent" : "var(--line)"),
fontSize: 12.5, fontWeight: 500 }}>
{s === "all" ? "All" : s}
))}
)}
{list.length === 0 ? (
setSub("all")} subActive={sub} />
) : (
{list.map(p => (
onNav({ screen: "product", storeId, productId: p.id })}
onAdd={() => p.customizable
? onNav({ screen: "product", storeId, productId: p.id })
: addToCart(storeId, p.id, 1)}
onRemove={() => addToCart(storeId, p.id, -1)}
pattern={tweaks.addPattern}
density={tweaks.density} />
))}
)}
);
}
// Empty-state for a category aisle — either the store doesn't stock this
// category or the active sub-filter is too narrow. Offers three ways out:
// clear the filter, Ask Opa, or jump to a nearby store that does stock it.
function CategoryEmpty({ cat, store, onNav, onClearFilter, subActive }) {
const alts = STORES.filter(s => s.id !== store.id).slice(0, 3);
const Icon = Icons[cat.icon];
const filtered = subActive !== "all";
return (
{filtered ? `Nothing matches "${subActive}" here.` : `${store.name} doesn't stock ${cat.label} today.`}
{filtered
? "Clear the filter to see everything in this aisle, or ask Opa to find it across stores."
: `Three stores nearby carry ${cat.label.toLowerCase()}. One of them is probably 6 minutes closer than you think.`}
{filtered && (
Clear filter
)}
}
onClick={() => onNav({ screen: "home" })}>
Ask Opa
{!filtered && alts.length > 0 && (
Try nearby
{alts.map(s => (
onNav({ screen: "store", storeId: s.id, category: cat.id })}
style={{ padding: "7px 13px", borderRadius: 999,
background: "var(--surface-2)", border: "1px solid var(--line)",
fontSize: 12.5, fontWeight: 500, color: "var(--ink-2)",
display: "inline-flex", alignItems: "center", gap: 6 }}>
{s.name}
· {s.eta}
))}
)}
);
}
window.Category = Category;