Compare commits
5 Commits
e9d8ddc418
...
deb10ed359
| Author | SHA1 | Date | |
|---|---|---|---|
| deb10ed359 | |||
| c56850954c | |||
| 467eb8737d | |||
| 334bf334f6 | |||
| 04e32c2017 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user