From f0e1cf4b9be5608b8cad8ad0847fabec355c355c Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 6 Apr 2026 17:56:41 +0200 Subject: [PATCH] feat(23-01): wire ManualEntryForm into CatalogSearchOverlay - Add manualEntryMode and savedItemName local state with resets on overlay close - Back arrow is context-sensitive: returns to search when in manual mode, closes overlay otherwise - Header text updates to 'Manual Entry' or 'Item Added' in manual entry mode - Search input, filters, and view toggles hidden when in manual entry mode - EmptyState now accepts onAddManually callback with context-sensitive link text - Persistent 'Can't find it? Add manually' link shown below search results - ManualEntryForm rendered inline when manualEntryMode is active - Success card shown after save with 'Submit to Catalog?' toast-only button - 'Add Another' resets to search, 'Done' closes overlay --- .../components/CatalogSearchOverlay.tsx | 703 +++++++++++------- 1 file changed, 416 insertions(+), 287 deletions(-) diff --git a/src/client/components/CatalogSearchOverlay.tsx b/src/client/components/CatalogSearchOverlay.tsx index 7b6f2eb..d3fc01d 100644 --- a/src/client/components/CatalogSearchOverlay.tsx +++ b/src/client/components/CatalogSearchOverlay.tsx @@ -2,10 +2,12 @@ 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 { ManualEntryForm } from "./ManualEntryForm"; type ViewMode = "grid" | "list"; @@ -19,6 +21,8 @@ export function CatalogSearchOverlay() { const [selectedTags, setSelectedTags] = useState([]); const [filterOpen, setFilterOpen] = useState(false); const [viewMode, setViewMode] = useState("grid"); + const [manualEntryMode, setManualEntryMode] = useState(false); + const [savedItemName, setSavedItemName] = useState(null); // Range filters (client-side) const [weightMin, setWeightMin] = useState(0); @@ -83,6 +87,8 @@ export function CatalogSearchOverlay() { setWeightMax(5000); setPriceMin(0); setPriceMax(100000); + setManualEntryMode(false); + setSavedItemName(null); } }, [catalogSearchOpen]); @@ -98,6 +104,19 @@ export function CatalogSearchOverlay() { setSelectedTags((prev) => prev.filter((t) => t !== tagName)); } + function handleEnterManualMode() { + setManualEntryMode(true); + } + + function handleManualSuccess(itemName: string) { + setSavedItemName(itemName); + } + + function handleAddAnother() { + setManualEntryMode(false); + setSavedItemName(null); + } + const navigate = useNavigate(); function handleCardClick(itemId: number) { @@ -113,10 +132,13 @@ export function CatalogSearchOverlay() { // Stub: actual add-to-collection / add-to-thread wired in Phase 22 } - const contextText = - catalogSearchMode === "collection" + const contextText = (() => { + if (manualEntryMode && savedItemName) return "Item Added"; + if (manualEntryMode) return "Manual Entry"; + return catalogSearchMode === "collection" ? "Adding to Collection" : "Starting a Thread"; + })(); return ( @@ -135,7 +157,14 @@ export function CatalogSearchOverlay() {
- {/* Search row */} -
-
- 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 - /> -
+ {/* 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) => ( + {/* Filter toggle */} - ))} - {weightMin > 0 && ( - - ≥{weight(weightMin)} - - - )} - {weightMax < 5000 && ( - - ≤{weight(weightMax)} - - - )} - {priceMin > 0 && ( - - ≥{price(priceMin)} -
+
+ + {/* 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)} + + + )} + - +
)} - {priceMax < 100000 && ( - - ≤{price(priceMax)} - - - )} - - + )} {/* Main content area */}
- {/* Filter sidebar */} - - {filterOpen && tags && tags.length > 0 && ( - -
- {/* Tags */} -
-

- Tags -

-
- {tags.map((tag) => { - const isActive = selectedTags.includes(tag.name); - return ( - - ); - })} + {/* 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" - /> + {/* 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)} +
-
- - 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)} +
- - {/* 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 */} -
-
- {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} - /> - ))} -
- ) - ) : ( - 0} - /> + )} -
+ + )} + + {/* Results / Manual Entry */} +
+ {manualEntryMode && savedItemName ? ( + /* Success card */ +
+
+ + + +
+

+ Added {savedItemName} to collection +

+ +
+ + +
+
+ ) : manualEntryMode ? ( + /* Manual entry form */ + { + setManualEntryMode(false); + setSavedItemName(null); + }} + /> + ) : ( + /* 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} + /> + )} +
+ )}
@@ -664,7 +780,13 @@ function SkeletonList() { // ── Empty State ──────────────────────────────────────────────────────── -function EmptyState({ hasQuery }: { hasQuery: boolean }) { +function EmptyState({ + hasQuery, + onAddManually, +}: { + hasQuery: boolean; + onAddManually: () => void; +}) { return (
-

+

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

+
); }