// 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" }} />
{!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 => ( ))}
Recent
{recent.map(r => ( ))}
Trending nearby
{trending.map(r => ( ))}
{/* Browse by store — horizontal scroller on all widths */}
Or browse by store
{STORES.map(s => ( ))}
)} {results && (
{results.stores.length > 0 && (
Stores
{results.stores.map(s => ( ))}
)}
{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 => ( ))}
)} {/* 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;