5 Commits

Author SHA1 Message Date
deb10ed359 docs(quick-260411-0zq): search UX redesign plan and gitignore tmp/
All checks were successful
CI / ci (push) Successful in 1m10s
CI / e2e (push) Has been skipped
CI / deploy (push) Successful in 13s
2026-04-11 00:50:15 +02:00
c56850954c docs(quick-260411-0zq): complete redesign search UX plan
- Add SUMMARY.md for quick task 260411-0zq
- Update STATE.md with completed quick task entry
2026-04-11 00:49:19 +02:00
467eb8737d chore(quick-260411-0zq): regenerate route tree with updated search params
- Route tree picks up validateSearch for /global-items/ route
- Also adds /setups/ route entry that was missing from previous generation
2026-04-11 00:47:41 +02:00
334bf334f6 feat(quick-260411-0zq): global items page reads query from URL search params
- Add validateSearch with z.object({ q }) to route definition
- Use Route.useSearch() to get q param instead of local state
- Remove duplicate search input UI, debounce state and useEffect
- Show "Showing results for X" label when q is present
- Update empty state text based on whether q param exists
2026-04-11 00:47:23 +02:00
04e32c2017 feat(quick-260411-0zq): convert TopNav search button to real input with navigation
- Replace fake button with real text input and search icon
- Navigate to /global-items?q=query on Enter or icon click
- Clear input after navigation
- Remove openCatalogSearch usage from TopNav (FAB/BottomTabBar flows unchanged)
2026-04-11 00:46:54 +02:00
7 changed files with 267 additions and 73 deletions

6
.gitignore vendored
View File

@@ -233,9 +233,15 @@ e2e/pgdata
test-results/ test-results/
playwright-report/ playwright-report/
# Obsidian
.obsidian/
# Claude Code # Claude Code
.claude/ .claude/
# Scratch / temp files
tmp/
# graphify (cache only — outputs are committed) # graphify (cache only — outputs are committed)
graphify-out/cache/ graphify-out/cache/
graphify-out/cost.json graphify-out/cost.json

View File

@@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-04-09)
Phase: 999.1 Phase: 999.1
Plan: Not started Plan: Not started
Status: Phase complete — ready for verification Status: Phase complete — ready for verification
Last activity: 2026-04-11 - Completed quick task 260411-022: Fix global items search bar layout Last activity: 2026-04-11 - Completed quick task 260411-0zq: Redesign search UX — bigger nav search bar
Progress: [░░░░░░░░░░] 0% Progress: [░░░░░░░░░░] 0%
@@ -91,6 +91,7 @@ None.
| # | Description | Date | Commit | Directory | | # | Description | Date | Commit | Directory |
|---|-------------|------|--------|-----------| |---|-------------|------|--------|-----------|
| 260411-022 | Fix global items search bar layout - too tall and hard to navigate back | 2026-04-10 | ef48891 | [260411-022-fix-global-items-search-bar-layout-too-t](./quick/260411-022-fix-global-items-search-bar-layout-too-t/) | | 260411-022 | Fix global items search bar layout - too tall and hard to navigate back | 2026-04-10 | ef48891 | [260411-022-fix-global-items-search-bar-layout-too-t](./quick/260411-022-fix-global-items-search-bar-layout-too-t/) |
| 260411-0zq | Redesign search UX — real nav search bar navigating to /global-items?q= | 2026-04-10 | 334bf33 | [260411-0zq-redesign-search-ux-bigger-nav-search-bar](./quick/260411-0zq-redesign-search-ux-bigger-nav-search-bar/) |
## Session Continuity ## Session Continuity

View File

@@ -0,0 +1,133 @@
---
phase: quick
plan: 260411-0zq
type: execute
wave: 1
depends_on: []
files_modified:
- src/client/components/TopNav.tsx
- src/client/routes/global-items/index.tsx
autonomous: true
requirements: [search-ux-redesign]
must_haves:
truths:
- "Nav search bar is visually prominent — looks like a real input, not a tiny button"
- "Typing in nav search and pressing Enter navigates to /global-items?q=<query>"
- "Global items page reads q param from URL and pre-fills/uses it for search"
- "Global items page no longer has its own duplicate search input"
- "CatalogSearchOverlay still works for FAB menu flows (Add to Collection, Start Thread)"
artifacts:
- path: "src/client/components/TopNav.tsx"
provides: "Real search input in nav bar that navigates on Enter"
- path: "src/client/routes/global-items/index.tsx"
provides: "Global items page reading query from URL search params"
key_links:
- from: "src/client/components/TopNav.tsx"
to: "/global-items?q=..."
via: "navigate() on Enter/search icon click"
pattern: "navigate.*global-items.*q="
- from: "src/client/routes/global-items/index.tsx"
to: "URL search params"
via: "Route.useSearch() or searchParams"
pattern: "useSearch|searchParams|validateSearch"
---
<objective>
Redesign the TopNav search from a fake button that opens an overlay into a real search input that navigates to the global items catalog page. Remove the duplicate search input from the global items page and have it read the query from URL params instead.
Purpose: Cleaner search UX — one search bar in the nav, navigates to catalog page with query pre-filled.
Output: Updated TopNav.tsx with real input, updated global-items/index.tsx reading from URL params.
</objective>
<execution_context>
@.claude/get-shit-done/workflows/execute-plan.md
@.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@CLAUDE.md
@src/client/components/TopNav.tsx
@src/client/routes/global-items/index.tsx
@src/client/stores/uiStore.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Convert TopNav search button to real input with navigation</name>
<files>src/client/components/TopNav.tsx</files>
<action>
Replace the fake search button (lines 101-111) with a real text input that:
1. Uses local state for the search query (useState).
2. Styled as a proper search bar — wider, taller, with a search icon on the left. Use classes like: `bg-gray-50 border border-gray-200 rounded-lg pl-9 pr-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-gray-300 w-48 lg:w-64 transition-colors`. Keep `hidden md:flex` for desktop-only.
3. On Enter keypress OR clicking the search icon: navigate to `/global-items?q=<encodeURIComponent(query)>` using TanStack Router's `useNavigate()`. Only navigate if query is non-empty (trimmed). After navigation, clear the input.
4. The search icon (LucideIcon name="search") sits inside the input container as an absolute-positioned clickable element on the left side.
5. Remove the `openCatalogSearch` import from uiStore IF it is no longer used in this file (it will no longer be used since the button is replaced). Keep the import of `useUIStore` if still needed for `openAuthPrompt`.
Do NOT touch CatalogSearchOverlay or its usage from FabMenu/BottomTabBar/CollectionView — those flows remain unchanged.
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -20</automated>
</verify>
<done>TopNav shows a real search input. Typing and pressing Enter navigates to /global-items?q=query. Search icon is clickable. Input clears after navigation.</done>
</task>
<task type="auto">
<name>Task 2: Global items page reads query from URL search params</name>
<files>src/client/routes/global-items/index.tsx</files>
<action>
Update the global items route to read the search query from URL params instead of having its own search input:
1. Add search param validation to the route using TanStack Router's `validateSearch`:
```ts
import { z } from "zod";
export const Route = createFileRoute("/global-items/")({
component: GlobalItemsCatalog,
validateSearch: z.object({
q: z.string().optional().catch(undefined),
}),
});
```
2. In GlobalItemsCatalog, use `const { q } = Route.useSearch()` to get the query param.
3. Remove the local `searchInput` state and the `debouncedQuery` state + debounce useEffect. Instead, use `q` directly as the search query passed to `useGlobalItems(q || undefined)`. The debounce is no longer needed since the user submits from the nav bar (no keystroke-by-keystroke filtering).
4. Remove the entire search input UI (the `<div className="relative w-full sm:w-auto sm:max-w-xs">` block with the SVG icon and input, lines 44-86). Keep the title row but simplify it — just the "Global Gear Catalog" heading, no search input beside it.
5. Keep the back link to Discover.
6. Show a small text below the title indicating the current search, e.g., if `q` exists: `<p className="text-sm text-gray-500 mb-4">Showing results for "<strong>{q}</strong>"</p>`. If no `q`, show nothing extra (the page shows all items).
7. Update the empty state text: if `q` exists, "No items found matching your search"; if no `q`, "No items in the global catalog yet" (same logic as before, but using `q` instead of `debouncedQuery`).
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -20</automated>
</verify>
<done>Global items page reads ?q= from URL. No duplicate search input on the page. Results filter based on URL query. Page works with and without q param.</done>
</task>
</tasks>
<verification>
1. `bun run lint` passes with no errors
2. `bun run build` completes successfully
3. Manual check: Navigate to app, type in nav search bar, press Enter — goes to /global-items?q=query
4. Manual check: /global-items page shows results filtered by q param, no search input on the page
5. Manual check: FAB menu "Add to Collection" and "Start Thread" still open the CatalogSearchOverlay as before
</verification>
<success_criteria>
- Nav search bar is visually a real input (not a fake button)
- Enter/search icon navigates to /global-items?q=query
- Global items page reads q from URL, no duplicate search input
- CatalogSearchOverlay unaffected for FAB/BottomTabBar flows
- Lint and build pass
</success_criteria>
<output>
After completion, create `.planning/quick/260411-0zq-redesign-search-ux-bigger-nav-search-bar/260411-0zq-SUMMARY.md`
</output>

View File

@@ -0,0 +1,59 @@
---
phase: quick
plan: 260411-0zq
subsystem: client-ui
tags: [search, navigation, topnav, global-items, ux]
dependency_graph:
requires: []
provides: [nav-search-bar, url-driven-catalog-search]
affects: [TopNav, global-items-route]
tech_stack:
added: []
patterns: [TanStack Router validateSearch, useNavigate, URL-driven state]
key_files:
created: []
modified:
- src/client/components/TopNav.tsx
- src/client/routes/global-items/index.tsx
- src/client/routeTree.gen.ts
decisions:
- No debounce needed on catalog page since query is submitted via Enter/icon click from nav bar
- openCatalogSearch removed from TopNav (FAB/BottomTabBar flows still use it unchanged)
- routeTree.gen.ts regenerated to pick up validateSearch on /global-items/ and pre-existing /setups/ entry
metrics:
duration: ~10 minutes
completed: 2026-04-10
---
# Quick Task 260411-0zq: Redesign Search UX — Bigger Nav Search Bar
**One-liner:** Real search input in TopNav navigates to /global-items?q=query; catalog page reads q from URL, removing the duplicate on-page search input.
## Tasks Completed
| # | Task | Commit | Files |
|---|------|--------|-------|
| 1 | Convert TopNav search button to real input with navigation | 04e32c2 | src/client/components/TopNav.tsx |
| 2 | Global items page reads query from URL search params | 334bf33 | src/client/routes/global-items/index.tsx |
| - | Regenerate route tree | 467eb87 | src/client/routeTree.gen.ts |
## What Was Built
**TopNav search bar:** Replaced the fake button (`openCatalogSearch("collection")`) with a real `<input>` element. The input has a left-aligned clickable search icon and is styled as a proper search bar (`w-48 lg:w-64`). On Enter or icon click, `useNavigate()` routes to `/global-items` with the query as a `q` search param. Input clears after navigation. Desktop-only (`hidden md:flex`).
**Global items catalog page:** Route now declares `validateSearch: z.object({ q: z.string().optional().catch(undefined) })`. The component reads `{ q } = Route.useSearch()` and passes it directly to `useGlobalItems(q || undefined)`. The local `searchInput` state, `debouncedQuery` state, debounce `useEffect`, and the entire search input UI block have been removed. When `q` is present, a "Showing results for X" label appears below the title. Empty state message switches based on whether `q` is set.
## Deviations from Plan
None - plan executed exactly as written.
## Known Stubs
None.
## Self-Check
- [x] `src/client/components/TopNav.tsx` - modified, committed 04e32c2
- [x] `src/client/routes/global-items/index.tsx` - modified, committed 334bf33
- [x] `bun run lint` passes with no errors
- [x] `bun run build` completes successfully (540ms)

View File

@@ -1,4 +1,5 @@
import { Link, useMatchRoute } from "@tanstack/react-router"; import { Link, useMatchRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore"; import { useUIStore } from "../stores/uiStore";
@@ -43,14 +44,22 @@ function NavLinkOrButton({
export function TopNav() { export function TopNav() {
const { data: auth } = useAuth(); const { data: auth } = useAuth();
const isAuthenticated = !!auth?.user; const isAuthenticated = !!auth?.user;
const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
const openAuthPrompt = useUIStore((s) => s.openAuthPrompt); const openAuthPrompt = useUIStore((s) => s.openAuthPrompt);
const matchRoute = useMatchRoute(); const matchRoute = useMatchRoute();
const navigate = useNavigate();
const [searchQuery, setSearchQuery] = useState("");
const isHome = !!matchRoute({ to: "/" }); const isHome = !!matchRoute({ to: "/" });
const isCollection = !!matchRoute({ to: "/collection", fuzzy: true }); const isCollection = !!matchRoute({ to: "/collection", fuzzy: true });
const isSetups = !!matchRoute({ to: "/setups", fuzzy: true }); const isSetups = !!matchRoute({ to: "/setups", fuzzy: true });
function handleSearch() {
const trimmed = searchQuery.trim();
if (!trimmed) return;
navigate({ to: "/global-items", search: { q: trimmed } });
setSearchQuery("");
}
return ( return (
<div className="sticky top-0 z-10 bg-white border-b border-gray-100"> <div className="sticky top-0 z-10 bg-white border-b border-gray-100">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
@@ -98,17 +107,27 @@ export function TopNav() {
{/* Right: Search bar (desktop only) + User section */} {/* Right: Search bar (desktop only) + User section */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* Search bar — desktop only */} {/* Search bar — desktop only */}
<div className="relative hidden md:flex items-center">
<button <button
type="button" type="button"
onClick={() => openCatalogSearch("collection")} onClick={handleSearch}
onKeyDown={(e) => { className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
if (e.key === "Enter") openCatalogSearch("collection"); tabIndex={-1}
}} aria-label="Search"
className="hidden md:flex items-center gap-2 bg-gray-50 border border-gray-200 rounded-lg px-3 py-1.5 text-sm text-gray-400 cursor-pointer hover:border-gray-300 transition-colors"
> >
<LucideIcon name="search" size={16} /> <LucideIcon name="search" size={16} />
<span className="hidden lg:inline">Search catalog...</span>
</button> </button>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleSearch();
}}
placeholder="Search catalog..."
className="bg-gray-50 border border-gray-200 rounded-lg pl-9 pr-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-gray-300 w-48 lg:w-64 transition-colors"
/>
</div>
{/* User section */} {/* User section */}
{isAuthenticated ? ( {isAuthenticated ? (

View File

@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as SettingsRouteImport } from './routes/settings' import { Route as SettingsRouteImport } from './routes/settings'
import { Route as LoginRouteImport } from './routes/login' import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as SetupsIndexRouteImport } from './routes/setups/index'
import { Route as GlobalItemsIndexRouteImport } from './routes/global-items/index' import { Route as GlobalItemsIndexRouteImport } from './routes/global-items/index'
import { Route as CollectionIndexRouteImport } from './routes/collection/index' import { Route as CollectionIndexRouteImport } from './routes/collection/index'
import { Route as UsersUserIdRouteImport } from './routes/users/$userId' import { Route as UsersUserIdRouteImport } from './routes/users/$userId'
@@ -36,6 +37,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const SetupsIndexRoute = SetupsIndexRouteImport.update({
id: '/setups/',
path: '/setups/',
getParentRoute: () => rootRouteImport,
} as any)
const GlobalItemsIndexRoute = GlobalItemsIndexRouteImport.update({ const GlobalItemsIndexRoute = GlobalItemsIndexRouteImport.update({
id: '/global-items/', id: '/global-items/',
path: '/global-items/', path: '/global-items/',
@@ -88,6 +94,7 @@ export interface FileRoutesByFullPath {
'/users/$userId': typeof UsersUserIdRoute '/users/$userId': typeof UsersUserIdRoute
'/collection/': typeof CollectionIndexRoute '/collection/': typeof CollectionIndexRoute
'/global-items/': typeof GlobalItemsIndexRoute '/global-items/': typeof GlobalItemsIndexRoute
'/setups/': typeof SetupsIndexRoute
'/threads/$threadId/': typeof ThreadsThreadIdIndexRoute '/threads/$threadId/': typeof ThreadsThreadIdIndexRoute
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute '/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
} }
@@ -101,6 +108,7 @@ export interface FileRoutesByTo {
'/users/$userId': typeof UsersUserIdRoute '/users/$userId': typeof UsersUserIdRoute
'/collection': typeof CollectionIndexRoute '/collection': typeof CollectionIndexRoute
'/global-items': typeof GlobalItemsIndexRoute '/global-items': typeof GlobalItemsIndexRoute
'/setups': typeof SetupsIndexRoute
'/threads/$threadId': typeof ThreadsThreadIdIndexRoute '/threads/$threadId': typeof ThreadsThreadIdIndexRoute
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute '/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
} }
@@ -115,6 +123,7 @@ export interface FileRoutesById {
'/users/$userId': typeof UsersUserIdRoute '/users/$userId': typeof UsersUserIdRoute
'/collection/': typeof CollectionIndexRoute '/collection/': typeof CollectionIndexRoute
'/global-items/': typeof GlobalItemsIndexRoute '/global-items/': typeof GlobalItemsIndexRoute
'/setups/': typeof SetupsIndexRoute
'/threads/$threadId/': typeof ThreadsThreadIdIndexRoute '/threads/$threadId/': typeof ThreadsThreadIdIndexRoute
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute '/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
} }
@@ -130,6 +139,7 @@ export interface FileRouteTypes {
| '/users/$userId' | '/users/$userId'
| '/collection/' | '/collection/'
| '/global-items/' | '/global-items/'
| '/setups/'
| '/threads/$threadId/' | '/threads/$threadId/'
| '/threads/$threadId/candidates/$candidateId' | '/threads/$threadId/candidates/$candidateId'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
@@ -143,6 +153,7 @@ export interface FileRouteTypes {
| '/users/$userId' | '/users/$userId'
| '/collection' | '/collection'
| '/global-items' | '/global-items'
| '/setups'
| '/threads/$threadId' | '/threads/$threadId'
| '/threads/$threadId/candidates/$candidateId' | '/threads/$threadId/candidates/$candidateId'
id: id:
@@ -156,6 +167,7 @@ export interface FileRouteTypes {
| '/users/$userId' | '/users/$userId'
| '/collection/' | '/collection/'
| '/global-items/' | '/global-items/'
| '/setups/'
| '/threads/$threadId/' | '/threads/$threadId/'
| '/threads/$threadId/candidates/$candidateId' | '/threads/$threadId/candidates/$candidateId'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
@@ -170,6 +182,7 @@ export interface RootRouteChildren {
UsersUserIdRoute: typeof UsersUserIdRoute UsersUserIdRoute: typeof UsersUserIdRoute
CollectionIndexRoute: typeof CollectionIndexRoute CollectionIndexRoute: typeof CollectionIndexRoute
GlobalItemsIndexRoute: typeof GlobalItemsIndexRoute GlobalItemsIndexRoute: typeof GlobalItemsIndexRoute
SetupsIndexRoute: typeof SetupsIndexRoute
ThreadsThreadIdIndexRoute: typeof ThreadsThreadIdIndexRoute ThreadsThreadIdIndexRoute: typeof ThreadsThreadIdIndexRoute
ThreadsThreadIdCandidatesCandidateIdRoute: typeof ThreadsThreadIdCandidatesCandidateIdRoute ThreadsThreadIdCandidatesCandidateIdRoute: typeof ThreadsThreadIdCandidatesCandidateIdRoute
} }
@@ -197,6 +210,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/setups/': {
id: '/setups/'
path: '/setups'
fullPath: '/setups/'
preLoaderRoute: typeof SetupsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/global-items/': { '/global-items/': {
id: '/global-items/' id: '/global-items/'
path: '/global-items' path: '/global-items'
@@ -266,6 +286,7 @@ const rootRouteChildren: RootRouteChildren = {
UsersUserIdRoute: UsersUserIdRoute, UsersUserIdRoute: UsersUserIdRoute,
CollectionIndexRoute: CollectionIndexRoute, CollectionIndexRoute: CollectionIndexRoute,
GlobalItemsIndexRoute: GlobalItemsIndexRoute, GlobalItemsIndexRoute: GlobalItemsIndexRoute,
SetupsIndexRoute: SetupsIndexRoute,
ThreadsThreadIdIndexRoute: ThreadsThreadIdIndexRoute, ThreadsThreadIdIndexRoute: ThreadsThreadIdIndexRoute,
ThreadsThreadIdCandidatesCandidateIdRoute: ThreadsThreadIdCandidatesCandidateIdRoute:
ThreadsThreadIdCandidatesCandidateIdRoute, ThreadsThreadIdCandidatesCandidateIdRoute,

View File

@@ -1,27 +1,20 @@
import { createFileRoute, Link } from "@tanstack/react-router"; import { createFileRoute, Link } from "@tanstack/react-router";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { useEffect, useState } from "react"; import { z } from "zod";
import { GlobalItemCard } from "../../components/GlobalItemCard"; import { GlobalItemCard } from "../../components/GlobalItemCard";
import { useGlobalItems } from "../../hooks/useGlobalItems"; import { useGlobalItems } from "../../hooks/useGlobalItems";
export const Route = createFileRoute("/global-items/")({ export const Route = createFileRoute("/global-items/")({
component: GlobalItemsCatalog, component: GlobalItemsCatalog,
validateSearch: z.object({
q: z.string().optional().catch(undefined),
}),
}); });
function GlobalItemsCatalog() { function GlobalItemsCatalog() {
const [searchInput, setSearchInput] = useState(""); const { q } = Route.useSearch();
const [debouncedQuery, setDebouncedQuery] = useState("");
useEffect(() => { const { data: items, isLoading } = useGlobalItems(q || undefined);
const timer = setTimeout(() => {
setDebouncedQuery(searchInput);
}, 300);
return () => clearTimeout(timer);
}, [searchInput]);
const { data: items, isLoading } = useGlobalItems(
debouncedQuery || undefined,
);
return ( return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
@@ -36,55 +29,17 @@ function GlobalItemsCatalog() {
</Link> </Link>
</div> </div>
{/* Title + search row */} {/* Title row */}
<div className="flex flex-wrap items-center justify-between gap-4 mb-4"> <div className="mb-4">
<h1 className="text-lg font-semibold text-gray-900"> <h1 className="text-lg font-semibold text-gray-900">
Global Gear Catalog Global Gear Catalog
</h1> </h1>
<div className="relative w-full sm:w-auto sm:max-w-xs"> {q && (
<svg <p className="text-sm text-gray-500 mt-1">
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" Showing results for "<strong>{q}</strong>"
fill="none" </p>
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<input
type="text"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
placeholder="Search gear by brand or model..."
className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-gray-300 transition-colors"
/>
{searchInput && (
<button
type="button"
onClick={() => setSearchInput("")}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)} )}
</div> </div>
</div>
{/* Results */} {/* Results */}
{isLoading ? ( {isLoading ? (
@@ -128,7 +83,7 @@ function GlobalItemsCatalog() {
/> />
</svg> </svg>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{debouncedQuery {q
? "No items found matching your search" ? "No items found matching your search"
: "No items in the global catalog yet"} : "No items in the global catalog yet"}
</p> </p>