// src/search.jsx — cross-store search with live-filter results, suggestion chips, recent queries.
function Search({ onNav, cart, addToCart }) {
const isMobile = useMedia("(max-width: 720px)");
const [q, setQ] = React.useState("");
const inputRef = React.useRef(null);
React.useEffect(() => { setTimeout(() => inputRef.current?.focus(), 50); }, []);
const results = React.useMemo(() => {
if (!q.trim()) return null;
const needle = q.toLowerCase();
const ps = PRODUCTS.filter(p =>
p.name.toLowerCase().includes(needle) ||
p.category.toLowerCase().includes(needle) ||
(p.subcategory || "").toLowerCase().includes(needle)
).slice(0, 24);
const ss = STORES.filter(s => s.name.toLowerCase().includes(needle) ||
(s.tags || []).some(t => t.toLowerCase().includes(needle))).slice(0, 4);
return { products: ps, stores: ss };
}, [q]);
const recent = ["modelo", "advil", "turkey sandwich", "oat milk", "lime"];
const trending = ["Honeycrisp apples", "Cold brew", "Gatorade", "Topo Chico", "Martin's potato rolls"];
return (
setQ(e.target.value)}
placeholder='Search "Coke", or ask "what helps a cough?"'
style={{ flex: 1, height: 32, border: 0, background: "transparent", outline: 0, color: "var(--ink)",
fontSize: 16, fontFamily: "inherit" }} />
onNav({ screen: "home" })}
style={{ color: "var(--ink-3)", fontSize: 13, padding: "6px 10px" }}>Cancel
{!q.trim() && (
{/* Top picks block: try-this prompts in a grid */}
Try a question
{[
{ q: "what helps a cough?", hint: "Cross-store · 6 results", tone: "navy" },
{ q: "modelo 12-pack near me", hint: "Beer · 4 stores stock it" },
{ q: "everything for tacos", hint: "Bundle · ~18 items" },
{ q: "lactose-free milk", hint: "Pantry · 2 stores" },
{ q: "a sandwich under $10", hint: "Deli · 14 results" },
{ q: "something for a headache", hint: "OTC · 3 picks" },
].map(p => (
setQ(p.q)}
style={{ display: "flex", flexDirection: "column", alignItems: "flex-start",
gap: 6, padding: "12px 14px", borderRadius: 14,
background: p.tone === "navy" ? "var(--navy-tint)" : "var(--surface)",
border: "1px solid " + (p.tone === "navy" ? "transparent" : "var(--line)"),
textAlign: "left" }}>
"{p.q}"
{p.hint}
))}
Recent
{recent.map(r => (
setQ(r)}
style={{ padding: "8px 14px", borderRadius: 999, background: "var(--surface-2)",
border: "1px solid var(--line)", fontSize: 13, color: "var(--ink-2)" }}>
{r}
))}
Trending nearby
{trending.map(r => (
setQ(r)}
style={{ padding: "8px 14px", borderRadius: 999, background: "var(--navy-tint)",
fontSize: 13, color: "var(--navy)", fontWeight: 500 }}>
{r}
))}
{/* Browse by store — horizontal scroller on all widths */}
Or browse by store
{STORES.map(s => (
onNav({ screen: "store", storeId: s.id })}
style={{ display: "flex", alignItems: "center", gap: 10,
padding: "10px 12px", borderRadius: 12,
background: "var(--surface)", border: "1px solid var(--line)",
textAlign: "left", scrollSnapAlign: "start" }}>
))}
)}
{results && (
{results.stores.length > 0 && (
Stores
{results.stores.map(s => (
onNav({ screen: "store", storeId: s.id })}
style={{ display: "flex", alignItems: "center", gap: 14, padding: 10,
background: "var(--surface)", border: "1px solid var(--line)",
borderRadius: 14, textAlign: "left" }}>
{s.name}
{s.tagline} · {s.eta}
))}
)}
{results.products.length} items across {new Set(results.products.map(p => storeForProduct(p).id)).size || 1} stores
{results.products.length === 0 ? (
) : (
{results.products.map(p => {
const src = storeForProduct(p);
return (
onNav({ screen: "product", storeId: src.id, productId: p.id })}
qty={cart.items.find(i => i.productId === p.id)?.qty || 0}
onAdd={() => addToCart(src.id, p.id, 1)}
onRemove={() => addToCart(src.id, p.id, -1)} />
);
})}
)}
)}
);
}
window.Search = Search;
// ─── SearchNoResults ───────────────────────────────────────────────────────
// When the live filter returns 0 items, fill the void with: re-frame as a
// query for Opa, suggest fuzzy near-matches, and offer to request the item.
function SearchNoResults({ q, setQ, onNav }) {
// crude near-match: products whose name shares any 3-char prefix with the query word(s)
const tokens = q.toLowerCase().split(/\s+/).filter(t => t.length >= 3);
const near = PRODUCTS.filter(p => {
const n = p.name.toLowerCase();
return tokens.some(tok => n.includes(tok.slice(0, 3)));
}).slice(0, 6);
return (
{/* Top: re-frame as Opa question */}
Nothing matched on the shelves.
Want Opa to figure out "{q}"?
Sometimes what you mean isn't a product name. Opa will ask the closest 3 stores, build a cart, and show you what they propose.
}
onClick={() => onNav({ screen: "home" })}>
Ask Opa
{/* Middle: closest near-matches if any */}
{near.length > 0 && (
Closest matches we did find
{near.map(p => (
onNav({ screen: "product", storeId: "kalymnos", productId: p.id })}
style={{ display: "inline-flex", alignItems: "center", gap: 8,
padding: "8px 12px 8px 8px", borderRadius: 99,
background: "var(--surface-2)", border: "1px solid var(--line)",
fontSize: 13, color: "var(--ink-2)" }}>
{p.name}
{money(p.price)}
))}
)}
{/* Bottom: shortcuts / request stocking */}
Or skip the search.
}
onClick={() => onNav({ screen: "store", storeId: "kalymnos" })}>
Browse Kalymnos
}
onClick={() => alert(`Request to stock "${q}" sent to nearby store managers.`)}>
Request "{q}"
setQ("")}>Clear
);
}
window.SearchNoResults = SearchNoResults;