import { useNavigate } from "@tanstack/react-router"; import { AnimatePresence, motion } from "framer-motion"; import { ArrowLeft, Filter, LayoutGrid, LayoutList, X } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { useFormatters } from "../hooks/useFormatters"; import { useGlobalItems } from "../hooks/useGlobalItems"; import { useTags } from "../hooks/useTags"; import { useUIStore } from "../stores/uiStore"; import { GearImage } from "./GearImage"; import { ManualEntryForm } from "./ManualEntryForm"; type ViewMode = "grid" | "list"; export function CatalogSearchOverlay() { const catalogSearchOpen = useUIStore((s) => s.catalogSearchOpen); const catalogSearchMode = useUIStore((s) => s.catalogSearchMode); const closeCatalogSearch = useUIStore((s) => s.closeCatalogSearch); const [searchInput, setSearchInput] = useState(""); const [debouncedQuery, setDebouncedQuery] = useState(""); const [selectedTags, setSelectedTags] = useState([]); const [filterOpen, setFilterOpen] = useState(false); const [viewMode, setViewMode] = useState("grid"); const [manualEntryMode, setManualEntryMode] = useState(false); const [savedItemName, setSavedItemName] = useState(null); const [catalogSubmitted, setCatalogSubmitted] = useState(false); // Range filters (client-side) const [weightMin, setWeightMin] = useState(0); const [weightMax, setWeightMax] = useState(5000); const [priceMin, setPriceMin] = useState(0); const [priceMax, setPriceMax] = useState(100000); // in cents const { weight, price } = useFormatters(); const { data: tags } = useTags(); const { data: rawItems, isLoading } = useGlobalItems( debouncedQuery || undefined, selectedTags.length > 0 ? selectedTags : undefined, ); // Client-side range filtering const hasRangeFilters = weightMin > 0 || weightMax < 5000 || priceMin > 0 || priceMax < 100000; const items = useMemo(() => { if (!rawItems || !hasRangeFilters) return rawItems; return rawItems.filter((item) => { if (item.weightGrams != null) { if (item.weightGrams < weightMin || item.weightGrams > weightMax) return false; } if (item.priceCents != null) { if (item.priceCents < priceMin || item.priceCents > priceMax) return false; } return true; }); }, [rawItems, weightMin, weightMax, priceMin, priceMax, hasRangeFilters]); // Debounce search input useEffect(() => { const timer = setTimeout(() => { setDebouncedQuery(searchInput); }, 300); return () => clearTimeout(timer); }, [searchInput]); // Lock body scroll when overlay is open useEffect(() => { if (catalogSearchOpen) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [catalogSearchOpen]); // Reset state when overlay closes useEffect(() => { if (!catalogSearchOpen) { setSearchInput(""); setDebouncedQuery(""); setSelectedTags([]); setFilterOpen(false); setWeightMin(0); setWeightMax(5000); setPriceMin(0); setPriceMax(100000); setManualEntryMode(false); setSavedItemName(null); setCatalogSubmitted(false); } }, [catalogSearchOpen]); function toggleTag(tagName: string) { setSelectedTags((prev) => prev.includes(tagName) ? prev.filter((t) => t !== tagName) : [...prev, tagName], ); } function removeTag(tagName: string) { setSelectedTags((prev) => prev.filter((t) => t !== tagName)); } function handleEnterManualMode() { setManualEntryMode(true); } function handleManualSuccess(itemName: string) { setSavedItemName(itemName); } function handleAddAnother() { setManualEntryMode(false); setSavedItemName(null); setCatalogSubmitted(false); } const navigate = useNavigate(); function handleCardClick(itemId: number) { closeCatalogSearch(); navigate({ to: "/global-items/$globalItemId", params: { globalItemId: String(itemId) }, }); } function handleAddStub(e: React.MouseEvent) { e.stopPropagation(); // Stub: actual add-to-collection / add-to-thread wired in Phase 22 } const contextText = (() => { if (manualEntryMode && savedItemName) return "Item Added"; if (manualEntryMode) return "Manual Entry"; return catalogSearchMode === "collection" ? "Adding to Collection" : "Starting a Thread"; })(); return ( {catalogSearchOpen && ( {/* Header bar */}
{/* Context text with back arrow */}

{contextText}

{/* Search row — hidden in manual entry mode */} {!manualEntryMode && ( <>
setSearchInput(e.target.value)} placeholder="Search the catalog..." className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent transition-colors" autoFocus />
{/* Filter toggle */} {/* View toggle */}
{/* Active filter pills */} {(selectedTags.length > 0 || hasRangeFilters) && (
{selectedTags.map((tag) => ( ))} {weightMin > 0 && ( ≥{weight(weightMin)} )} {weightMax < 5000 && ( ≤{weight(weightMax)} )} {priceMin > 0 && ( ≥{price(priceMin)} )} {priceMax < 100000 && ( ≤{price(priceMax)} )}
)} )}
{/* Main content area */}
{/* Filter sidebar — hidden in manual entry mode */} {!manualEntryMode && ( {filterOpen && tags && tags.length > 0 && (
{/* Tags */}

Tags

{tags.map((tag) => { const isActive = selectedTags.includes(tag.name); return ( ); })}
{/* Weight range */}

Weight

setWeightMin( Math.min( Number(e.target.value), weightMax - 50, ), ) } className="flex-1 h-1.5 bg-gray-200 rounded-full appearance-none accent-blue-500" />
setWeightMax( Math.max( Number(e.target.value), weightMin + 50, ), ) } className="flex-1 h-1.5 bg-gray-200 rounded-full appearance-none accent-blue-500" />
{weight(weightMin)} {weight(weightMax)}
{/* Price range */}

Price

setPriceMin( Math.min( Number(e.target.value), priceMax - 500, ), ) } className="flex-1 h-1.5 bg-gray-200 rounded-full appearance-none accent-green-500" />
setPriceMax( Math.max( Number(e.target.value), priceMin + 500, ), ) } className="flex-1 h-1.5 bg-gray-200 rounded-full appearance-none accent-green-500" />
{price(priceMin)} {price(priceMax)}
)}
)} {/* Results / Manual Entry */}
{manualEntryMode && savedItemName ? ( /* Success card */

Added {savedItemName} to collection

) : manualEntryMode ? ( /* Manual entry form */ ) : ( /* Normal catalog results */
{isLoading ? ( viewMode === "grid" ? ( ) : ( ) ) : items && items.length > 0 ? ( <> {viewMode === "grid" ? (
{items.map((item) => ( handleCardClick(item.id)} weight={weight} price={price} /> ))}
) : (
{items.map((item) => ( handleCardClick(item.id)} weight={weight} price={price} /> ))}
)} {/* Persistent "Add Manually" link below results */}
) : ( 0} onAddManually={handleEnterManualMode} /> )}
)}
)}
); } // ── Grid Card ────────────────────────────────────────────────────────── interface CardProps { item: { id: number; brand: string; model: string; category: string | null; weightGrams: number | null; priceCents: number | null; imageUrl: string | null; }; onAdd: (e: React.MouseEvent) => void; onCardClick: () => void; weight: (g: number) => string; price: (cents: number) => string; } function GridCard({ item, onAdd, onCardClick, weight, price }: CardProps) { return (
).dominantColor as string) || "#f3f4f6" : undefined, }} > {item.imageUrl ? ( ) : (
)}

{item.brand}

{item.model}

{item.weightGrams != null && ( {weight(item.weightGrams)} )} {item.priceCents != null && ( {price(item.priceCents)} )} {item.category && ( {item.category} )}
); } // ── List Row ─────────────────────────────────────────────────────────── function ListRow({ item, onAdd, onCardClick, weight, price }: CardProps) { return (
{/* Thumbnail */}
).dominantColor as string) || "#f3f4f6" : undefined, }} > {item.imageUrl ? ( ) : (
)}
{/* Info */}

{item.brand} {item.model}

{item.weightGrams != null && ( {weight(item.weightGrams)} )} {item.priceCents != null && ( {price(item.priceCents)} )} {item.category && ( {item.category} )}
{/* Add button */}
); } // ── Skeletons ────────────────────────────────────────────────────────── function SkeletonGrid() { return (
{[1, 2, 3, 4, 5, 6].map((id) => (
))}
); } function SkeletonList() { return (
{[1, 2, 3, 4, 5, 6].map((id) => (
))}
); } // ── Empty State ──────────────────────────────────────────────────────── function EmptyState({ hasQuery, onAddManually, }: { hasQuery: boolean; onAddManually: () => void; }) { return (

{hasQuery ? "No items found matching your search" : "Search the catalog to find gear"}

); }