Merge branch 'worktree-agent-a00c5cfa' into Develop

# Conflicts:
#	.planning/REQUIREMENTS.md
#	.planning/STATE.md
#	src/client/components/CatalogSearchOverlay.tsx
#	src/client/routes/threads/$threadId.tsx
This commit is contained in:
2026-04-06 15:15:57 +02:00
11 changed files with 203 additions and 136 deletions

View File

@@ -64,7 +64,11 @@ Requirements for this milestone. Each maps to roadmap phases.
- [ ] **DETAIL-02**: Clicking a catalog search result navigates to a public detail page (`/global-items/:id`) with "Add to Collection" button - [ ] **DETAIL-02**: Clicking a catalog search result navigates to a public detail page (`/global-items/:id`) with "Add to Collection" button
- [ ] **DETAIL-03**: Item detail page has edit mode toggle for modifying personal fields (notes, category, quantity, purchase price) - [ ] **DETAIL-03**: Item detail page has edit mode toggle for modifying personal fields (notes, category, quantity, purchase price)
- [x] **DETAIL-04**: Thread candidates navigate to detail pages instead of opening slide-out panels - [x] **DETAIL-04**: Thread candidates navigate to detail pages instead of opening slide-out panels
<<<<<<< HEAD
- [ ] **DETAIL-05**: Slide-out panels for items and candidates are removed from the application - [ ] **DETAIL-05**: Slide-out panels for items and candidates are removed from the application
=======
- [x] **DETAIL-05**: Slide-out panels for items and candidates are removed from the application
>>>>>>> worktree-agent-a00c5cfa
### Tags ### Tags
@@ -185,7 +189,11 @@ Which phases cover which requirements. Updated during roadmap creation.
| DETAIL-02 | Phase 21 | Pending | | DETAIL-02 | Phase 21 | Pending |
| DETAIL-03 | Phase 21 | Pending | | DETAIL-03 | Phase 21 | Pending |
| DETAIL-04 | Phase 21 | Complete | | DETAIL-04 | Phase 21 | Complete |
<<<<<<< HEAD
| DETAIL-05 | Phase 21 | Pending | | DETAIL-05 | Phase 21 | Pending |
=======
| DETAIL-05 | Phase 21 | Complete |
>>>>>>> worktree-agent-a00c5cfa
**Coverage:** **Coverage:**
- v2.0 requirements: 45 total - v2.0 requirements: 45 total

View File

@@ -3,13 +3,13 @@ gsd_state_version: 1.0
milestone: v1.3 milestone: v1.3
milestone_name: Research & Decision Tools milestone_name: Research & Decision Tools
status: planning status: planning
stopped_at: Completed 21-02-PLAN.md stopped_at: Completed 21-03-PLAN.md
last_updated: "2026-04-06T13:03:13.009Z" last_updated: "2026-04-06T13:14:25.653Z"
last_activity: 2026-04-06 last_activity: 2026-04-06
progress: progress:
total_phases: 15 total_phases: 15
completed_phases: 14 completed_phases: 13
total_plans: 39 total_plans: 38
completed_plans: 37 completed_plans: 37
percent: 0 percent: 0
--- ---
@@ -58,8 +58,9 @@ Key decisions made during v2.0 planning:
- [Phase 20]: Created tags table in schema (was missing, needed for GET /api/tags endpoint) - [Phase 20]: Created tags table in schema (was missing, needed for GET /api/tags endpoint)
- [Phase 20]: FAB visible on all authenticated routes, not just collection gear tab - [Phase 20]: FAB visible on all authenticated routes, not just collection gear tab
- [Phase 20]: Add button on catalog search cards is a stub (Phase 21 wires actual flow) - [Phase 20]: Add button on catalog search cards is a stub (Phase 21 wires actual flow)
- [Phase 21]: Candidate data fetched from useThread hook (find in array) not new API endpoint - [Phase 21]: Preserved currentThreadId derivation in __root.tsx for CandidateDeleteDialog
- [Phase 21]: AddCandidateModal inline in thread page, local modal pattern replacing UIStore panel - [Phase 21]: CollectionView empty state Add button rewired to catalog search overlay
- [Phase 21]: ItemForm/CandidateForm decoupled from UIStore with onClose prop pattern
### Pending Todos ### Pending Todos
@@ -70,7 +71,7 @@ None active.
| # | Description | Date | Commit | Directory | | # | Description | Date | Commit | Directory |
|---|-------------|------|--------|-----------| |---|-------------|------|--------|-----------|
| 260406-j44 | Comprehensive dev seed script for bikepacking gear data | 2026-04-06 | — | [260406-j44-comprehensive-dev-seed-script-for-bikepa](./quick/260406-j44-comprehensive-dev-seed-script-for-bikepa/) | | 260406-j44 | Comprehensive dev seed script for bikepacking gear data | 2026-04-06 | — | [260406-j44-comprehensive-dev-seed-script-for-bikepa](./quick/260406-j44-comprehensive-dev-seed-script-for-bikepa/) |
| Phase 21 P02 | 4min | 2 tasks | 2 files | | Phase 21 P03 | 6min | 2 tasks | 10 files |
### Blockers/Concerns ### Blockers/Concerns
@@ -79,6 +80,6 @@ None active.
## Session Continuity ## Session Continuity
Last session: 2026-04-06T13:03:13.007Z Last session: 2026-04-06T13:14:25.651Z
Stopped at: Completed 21-02-PLAN.md Stopped at: Completed 21-03-PLAN.md
Resume file: None Resume file: None

View File

@@ -0,0 +1,143 @@
---
phase: 21-item-catalog-detail-pages
plan: 03
subsystem: ui
tags: [react, tanstack-router, navigation, panel-removal, uistore]
requires:
- phase: 21-item-catalog-detail-pages
provides: "Item detail page at /items/:id (Plan 01), Candidate detail page at /threads/:threadId/candidates/:candidateId (Plan 02)"
provides:
- "All card components navigate to detail pages instead of opening panels"
- "Slide-out panels removed from root layout"
- "UIStore cleaned of all panel-related state"
affects: [phase-22]
tech-stack:
added: []
patterns:
- "Card click navigation via useNavigate instead of UIStore panel state"
- "Catalog search card click closes overlay then navigates to detail page"
key-files:
created: []
modified:
- src/client/components/ItemCard.tsx
- src/client/components/CandidateCard.tsx
- src/client/components/CandidateListItem.tsx
- src/client/components/CatalogSearchOverlay.tsx
- src/client/routes/__root.tsx
- src/client/stores/uiStore.ts
- src/client/components/CollectionView.tsx
- src/client/components/ItemForm.tsx
- src/client/components/CandidateForm.tsx
- src/client/routes/threads/$threadId.tsx
key-decisions:
- "Preserved currentThreadId derivation in __root.tsx for CandidateDeleteDialog dependency"
- "CollectionView empty state Add button now opens catalog search instead of removed add panel"
- "ItemForm and CandidateForm migrated to onClose prop pattern instead of UIStore panel close"
patterns-established:
- "Navigation-first pattern: card clicks navigate to detail pages, no more slide-out panels"
- "Form onClose prop: forms accept optional onClose callback instead of coupling to UIStore"
requirements-completed: [DETAIL-04, DETAIL-05]
duration: 6min
completed: 2026-04-06
---
# Phase 21 Plan 03: Card Navigation Rewire & Panel Removal Summary
**All card components rewired from slide-out panels to detail page navigation, panels removed from root layout, UIStore cleaned of panel state**
## Performance
- **Duration:** 6 min
- **Started:** 2026-04-06T13:04:59Z
- **Completed:** 2026-04-06T13:11:00Z
- **Tasks:** 2
- **Files modified:** 10
## Accomplishments
- ItemCard, CandidateCard, CandidateListItem all navigate to detail pages on click using TanStack Router useNavigate
- CatalogSearchOverlay cards navigate to /global-items/:id (closing overlay first), with Add button using stopPropagation
- Slide-out panel JSX and imports completely removed from __root.tsx
- UIStore cleaned of 11 panel-related properties/actions while preserving all dialog, FAB, and catalog search state
- All downstream references (CollectionView, ItemForm, CandidateForm, thread page) updated to remove dead panel references
## Task Commits
Each task was committed atomically:
1. **Task 1: Rewire card click handlers to navigate to detail pages** - `1f79c5c` (feat)
2. **Task 2: Remove slide-out panels from root layout and clean UIStore** - `4c79735` (feat)
## Files Created/Modified
- `src/client/components/ItemCard.tsx` - Navigate to /items/$itemId instead of openEditPanel
- `src/client/components/CandidateCard.tsx` - Navigate to /threads/$threadId/candidates/$candidateId
- `src/client/components/CandidateListItem.tsx` - Navigate to candidate detail page
- `src/client/components/CatalogSearchOverlay.tsx` - Card click navigates to /global-items/$globalItemId
- `src/client/routes/__root.tsx` - Removed both SlideOutPanel instances and all panel state reads
- `src/client/stores/uiStore.ts` - Removed panel state/actions, kept dialogs and other state
- `src/client/components/CollectionView.tsx` - Empty state button uses catalog search instead of add panel
- `src/client/components/ItemForm.tsx` - Replaced closePanel with onClose prop
- `src/client/components/CandidateForm.tsx` - Replaced closeCandidatePanel with onClose prop, removed UIStore import
- `src/client/routes/threads/$threadId.tsx` - Replaced openCandidateAddPanel with local state
## Decisions Made
- Preserved currentThreadId derivation block in __root.tsx because CandidateDeleteDialog depends on it (checker flagged this)
- CollectionView empty state Add button rewired to open catalog search overlay rather than the removed add panel
- ItemForm and CandidateForm given onClose prop to decouple from UIStore panel actions (forms still exist on disk per plan requirement)
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Updated CollectionView.tsx to remove dead openAddPanel reference**
- **Found during:** Task 2 (panel cleanup)
- **Issue:** CollectionView referenced openAddPanel which was removed from UIStore
- **Fix:** Replaced with openCatalogSearch("collection") to maintain "Add Item" functionality via catalog
- **Files modified:** src/client/components/CollectionView.tsx
- **Verification:** grep confirms no dead references remain
- **Committed in:** 4c79735 (Task 2 commit)
**2. [Rule 3 - Blocking] Updated threads/$threadId.tsx to remove dead openCandidateAddPanel reference**
- **Found during:** Task 2 (panel cleanup)
- **Issue:** Thread page referenced openCandidateAddPanel which was removed from UIStore
- **Fix:** Replaced with local useState (Plan 02 already restructured this file with a proper modal)
- **Files modified:** src/client/routes/threads/$threadId.tsx
- **Verification:** grep confirms no dead references remain
- **Committed in:** 4c79735 (Task 2 commit)
**3. [Rule 3 - Blocking] Updated ItemForm.tsx and CandidateForm.tsx to remove dead panel close references**
- **Found during:** Task 2 (panel cleanup)
- **Issue:** Forms referenced closePanel/closeCandidatePanel which were removed from UIStore
- **Fix:** Added onClose prop to both forms, replaced store calls with onClose?.()
- **Files modified:** src/client/components/ItemForm.tsx, src/client/components/CandidateForm.tsx
- **Verification:** grep confirms no dead references remain
- **Committed in:** 4c79735 (Task 2 commit)
---
**Total deviations:** 3 auto-fixed (3 blocking)
**Impact on plan:** All auto-fixes necessary to prevent broken references after panel state removal. No scope creep.
## Issues Encountered
None
## User Setup Required
None - no external service configuration required.
## Known Stubs
None - all navigation targets exist (created by Plans 01 and 02), no placeholder data.
## Next Phase Readiness
- All cards navigate to detail pages, panel-to-page migration complete
- Phase 21 complete: item detail pages, candidate detail pages, and navigation rewiring all done
- Ready for Phase 22 (next milestone work)
---
*Phase: 21-item-catalog-detail-pages*
*Completed: 2026-04-06*

View File

@@ -1,3 +1,4 @@
import { useNavigate } from "@tanstack/react-router";
import { useFormatters } from "../hooks/useFormatters"; import { useFormatters } from "../hooks/useFormatters";
import type { CandidateDelta } from "../hooks/useImpactDeltas"; import type { CandidateDelta } from "../hooks/useImpactDeltas";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
@@ -46,7 +47,7 @@ export function CandidateCard({
delta, delta,
}: CandidateCardProps) { }: CandidateCardProps) {
const { weight, price } = useFormatters(); const { weight, price } = useFormatters();
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel); const navigate = useNavigate();
const openConfirmDeleteCandidate = useUIStore( const openConfirmDeleteCandidate = useUIStore(
(s) => s.openConfirmDeleteCandidate, (s) => s.openConfirmDeleteCandidate,
); );
@@ -56,7 +57,12 @@ export function CandidateCard({
return ( return (
<button <button
type="button" type="button"
onClick={() => openCandidateEditPanel(id)} onClick={() =>
navigate({
to: "/threads/$threadId/candidates/$candidateId",
params: { threadId: String(threadId), candidateId: String(id) },
})
}
className="relative w-full text-left bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all overflow-hidden group" className="relative w-full text-left bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all overflow-hidden group"
> >
{/* Hover-reveal action buttons */} {/* Hover-reveal action buttons */}

View File

@@ -1,7 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useCreateCandidate, useUpdateCandidate } from "../hooks/useCandidates"; import { useCreateCandidate, useUpdateCandidate } from "../hooks/useCandidates";
import { useThread } from "../hooks/useThreads"; import { useThread } from "../hooks/useThreads";
import { useUIStore } from "../stores/uiStore";
import { CategoryPicker } from "./CategoryPicker"; import { CategoryPicker } from "./CategoryPicker";
import { ImageUpload } from "./ImageUpload"; import { ImageUpload } from "./ImageUpload";
@@ -9,6 +8,7 @@ interface CandidateFormProps {
mode: "add" | "edit"; mode: "add" | "edit";
threadId: number; threadId: number;
candidateId?: number | null; candidateId?: number | null;
onClose?: () => void;
} }
interface FormData { interface FormData {
@@ -39,11 +39,11 @@ export function CandidateForm({
mode, mode,
threadId, threadId,
candidateId, candidateId,
onClose,
}: CandidateFormProps) { }: CandidateFormProps) {
const { data: thread } = useThread(threadId); const { data: thread } = useThread(threadId);
const createCandidate = useCreateCandidate(threadId); const createCandidate = useCreateCandidate(threadId);
const updateCandidate = useUpdateCandidate(threadId); const updateCandidate = useUpdateCandidate(threadId);
const closeCandidatePanel = useUIStore((s) => s.closeCandidatePanel);
const [form, setForm] = useState<FormData>(INITIAL_FORM); const [form, setForm] = useState<FormData>(INITIAL_FORM);
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
@@ -124,13 +124,13 @@ export function CandidateForm({
createCandidate.mutate(payload, { createCandidate.mutate(payload, {
onSuccess: () => { onSuccess: () => {
setForm(INITIAL_FORM); setForm(INITIAL_FORM);
closeCandidatePanel(); onClose?.();
}, },
}); });
} else if (candidateId != null) { } else if (candidateId != null) {
updateCandidate.mutate( updateCandidate.mutate(
{ candidateId, ...payload }, { candidateId, ...payload },
{ onSuccess: () => closeCandidatePanel() }, { onSuccess: () => onClose?.() },
); );
} }
} }

View File

@@ -1,3 +1,4 @@
import { useNavigate } from "@tanstack/react-router";
import { Reorder } from "framer-motion"; import { Reorder } from "framer-motion";
import { useRef } from "react"; import { useRef } from "react";
import { useFormatters } from "../hooks/useFormatters"; import { useFormatters } from "../hooks/useFormatters";
@@ -60,7 +61,7 @@ export function CandidateListItem({
}: CandidateListItemProps) { }: CandidateListItemProps) {
const isDragging = useRef(false); const isDragging = useRef(false);
const { weight, price } = useFormatters(); const { weight, price } = useFormatters();
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel); const navigate = useNavigate();
const openConfirmDeleteCandidate = useUIStore( const openConfirmDeleteCandidate = useUIStore(
(s) => s.openConfirmDeleteCandidate, (s) => s.openConfirmDeleteCandidate,
); );
@@ -104,7 +105,13 @@ export function CandidateListItem({
type="button" type="button"
onClick={() => { onClick={() => {
if (isDragging.current) return; if (isDragging.current) return;
openCandidateEditPanel(candidate.id); navigate({
to: "/threads/$threadId/candidates/$candidateId",
params: {
threadId: String(candidate.threadId),
candidateId: String(candidate.id),
},
});
}} }}
className="flex-1 min-w-0 text-left" className="flex-1 min-w-0 text-left"
> >

View File

@@ -1,3 +1,4 @@
import { useNavigate } from "@tanstack/react-router";
import { useFormatters } from "../hooks/useFormatters"; import { useFormatters } from "../hooks/useFormatters";
import { useDuplicateItem } from "../hooks/useItems"; import { useDuplicateItem } from "../hooks/useItems";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";
@@ -36,14 +37,16 @@ export function ItemCard({
onClassificationCycle, onClassificationCycle,
}: ItemCardProps) { }: ItemCardProps) {
const { weight, price } = useFormatters(); const { weight, price } = useFormatters();
const openEditPanel = useUIStore((s) => s.openEditPanel); const navigate = useNavigate();
const openExternalLink = useUIStore((s) => s.openExternalLink); const openExternalLink = useUIStore((s) => s.openExternalLink);
const duplicateItem = useDuplicateItem(); const duplicateItem = useDuplicateItem();
return ( return (
<button <button
type="button" type="button"
onClick={() => openEditPanel(id)} onClick={() =>
navigate({ to: "/items/$itemId", params: { itemId: String(id) } })
}
className="relative w-full text-left bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all overflow-hidden group" className="relative w-full text-left bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all overflow-hidden group"
> >
{!onRemove && ( {!onRemove && (
@@ -54,7 +57,10 @@ export function ItemCard({
e.stopPropagation(); e.stopPropagation();
duplicateItem.mutate(id, { duplicateItem.mutate(id, {
onSuccess: (newItem) => { onSuccess: (newItem) => {
openEditPanel(newItem.id); navigate({
to: "/items/$itemId",
params: { itemId: String(newItem.id) },
});
}, },
}); });
}} }}
@@ -63,7 +69,10 @@ export function ItemCard({
e.stopPropagation(); e.stopPropagation();
duplicateItem.mutate(id, { duplicateItem.mutate(id, {
onSuccess: (newItem) => { onSuccess: (newItem) => {
openEditPanel(newItem.id); navigate({
to: "/items/$itemId",
params: { itemId: String(newItem.id) },
});
}, },
}); });
} }

View File

@@ -7,6 +7,7 @@ import { ImageUpload } from "./ImageUpload";
interface ItemFormProps { interface ItemFormProps {
mode: "add" | "edit"; mode: "add" | "edit";
itemId?: number | null; itemId?: number | null;
onClose?: () => void;
} }
interface FormData { interface FormData {
@@ -31,11 +32,10 @@ const INITIAL_FORM: FormData = {
imageFilename: null, imageFilename: null,
}; };
export function ItemForm({ mode, itemId }: ItemFormProps) { export function ItemForm({ mode, itemId, onClose }: ItemFormProps) {
const { data: items } = useItems(); const { data: items } = useItems();
const createItem = useCreateItem(); const createItem = useCreateItem();
const updateItem = useUpdateItem(); const updateItem = useUpdateItem();
const closePanel = useUIStore((s) => s.closePanel);
const openConfirmDelete = useUIStore((s) => s.openConfirmDelete); const openConfirmDelete = useUIStore((s) => s.openConfirmDelete);
const [form, setForm] = useState<FormData>(INITIAL_FORM); const [form, setForm] = useState<FormData>(INITIAL_FORM);
@@ -112,13 +112,13 @@ export function ItemForm({ mode, itemId }: ItemFormProps) {
createItem.mutate(payload, { createItem.mutate(payload, {
onSuccess: () => { onSuccess: () => {
setForm(INITIAL_FORM); setForm(INITIAL_FORM);
closePanel(); onClose?.();
}, },
}); });
} else if (itemId != null) { } else if (itemId != null) {
updateItem.mutate( updateItem.mutate(
{ id: itemId, ...payload }, { id: itemId, ...payload },
{ onSuccess: () => closePanel() }, { onSuccess: () => onClose?.() },
); );
} }
} }

View File

@@ -15,7 +15,6 @@ import { Route as IndexRouteImport } from './routes/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'
import { Route as ThreadsThreadIdRouteImport } from './routes/threads/$threadId'
import { Route as SetupsSetupIdRouteImport } from './routes/setups/$setupId' import { Route as SetupsSetupIdRouteImport } from './routes/setups/$setupId'
import { Route as ItemsItemIdRouteImport } from './routes/items/$itemId' import { Route as ItemsItemIdRouteImport } from './routes/items/$itemId'
import { Route as GlobalItemsGlobalItemIdRouteImport } from './routes/global-items/$globalItemId' import { Route as GlobalItemsGlobalItemIdRouteImport } from './routes/global-items/$globalItemId'
@@ -52,11 +51,6 @@ const UsersUserIdRoute = UsersUserIdRouteImport.update({
path: '/users/$userId', path: '/users/$userId',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ThreadsThreadIdRoute = ThreadsThreadIdRouteImport.update({
id: '/threads/$threadId',
path: '/threads/$threadId',
getParentRoute: () => rootRouteImport,
} as any)
const SetupsSetupIdRoute = SetupsSetupIdRouteImport.update({ const SetupsSetupIdRoute = SetupsSetupIdRouteImport.update({
id: '/setups/$setupId', id: '/setups/$setupId',
path: '/setups/$setupId', path: '/setups/$setupId',
@@ -91,7 +85,6 @@ export interface FileRoutesByFullPath {
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute '/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
'/items/$itemId': typeof ItemsItemIdRoute '/items/$itemId': typeof ItemsItemIdRoute
'/setups/$setupId': typeof SetupsSetupIdRoute '/setups/$setupId': typeof SetupsSetupIdRoute
'/threads/$threadId': typeof ThreadsThreadIdRouteWithChildren
'/users/$userId': typeof UsersUserIdRoute '/users/$userId': typeof UsersUserIdRoute
'/collection/': typeof CollectionIndexRoute '/collection/': typeof CollectionIndexRoute
'/global-items/': typeof GlobalItemsIndexRoute '/global-items/': typeof GlobalItemsIndexRoute
@@ -119,7 +112,6 @@ export interface FileRoutesById {
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute '/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
'/items/$itemId': typeof ItemsItemIdRoute '/items/$itemId': typeof ItemsItemIdRoute
'/setups/$setupId': typeof SetupsSetupIdRoute '/setups/$setupId': typeof SetupsSetupIdRoute
'/threads/$threadId': typeof ThreadsThreadIdRouteWithChildren
'/users/$userId': typeof UsersUserIdRoute '/users/$userId': typeof UsersUserIdRoute
'/collection/': typeof CollectionIndexRoute '/collection/': typeof CollectionIndexRoute
'/global-items/': typeof GlobalItemsIndexRoute '/global-items/': typeof GlobalItemsIndexRoute
@@ -135,7 +127,6 @@ export interface FileRouteTypes {
| '/global-items/$globalItemId' | '/global-items/$globalItemId'
| '/items/$itemId' | '/items/$itemId'
| '/setups/$setupId' | '/setups/$setupId'
| '/threads/$threadId'
| '/users/$userId' | '/users/$userId'
| '/collection/' | '/collection/'
| '/global-items/' | '/global-items/'
@@ -162,7 +153,6 @@ export interface FileRouteTypes {
| '/global-items/$globalItemId' | '/global-items/$globalItemId'
| '/items/$itemId' | '/items/$itemId'
| '/setups/$setupId' | '/setups/$setupId'
| '/threads/$threadId'
| '/users/$userId' | '/users/$userId'
| '/collection/' | '/collection/'
| '/global-items/' | '/global-items/'
@@ -177,7 +167,6 @@ export interface RootRouteChildren {
GlobalItemsGlobalItemIdRoute: typeof GlobalItemsGlobalItemIdRoute GlobalItemsGlobalItemIdRoute: typeof GlobalItemsGlobalItemIdRoute
ItemsItemIdRoute: typeof ItemsItemIdRoute ItemsItemIdRoute: typeof ItemsItemIdRoute
SetupsSetupIdRoute: typeof SetupsSetupIdRoute SetupsSetupIdRoute: typeof SetupsSetupIdRoute
ThreadsThreadIdRoute: typeof ThreadsThreadIdRouteWithChildren
UsersUserIdRoute: typeof UsersUserIdRoute UsersUserIdRoute: typeof UsersUserIdRoute
CollectionIndexRoute: typeof CollectionIndexRoute CollectionIndexRoute: typeof CollectionIndexRoute
GlobalItemsIndexRoute: typeof GlobalItemsIndexRoute GlobalItemsIndexRoute: typeof GlobalItemsIndexRoute
@@ -227,13 +216,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof UsersUserIdRouteImport preLoaderRoute: typeof UsersUserIdRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/threads/$threadId': {
id: '/threads/$threadId'
path: '/threads/$threadId'
fullPath: '/threads/$threadId'
preLoaderRoute: typeof ThreadsThreadIdRouteImport
parentRoute: typeof rootRouteImport
}
'/setups/$setupId': { '/setups/$setupId': {
id: '/setups/$setupId' id: '/setups/$setupId'
path: '/setups/$setupId' path: '/setups/$setupId'
@@ -272,21 +254,6 @@ declare module '@tanstack/react-router' {
} }
} }
interface ThreadsThreadIdRouteChildren {
ThreadsThreadIdIndexRoute: typeof ThreadsThreadIdIndexRoute
ThreadsThreadIdCandidatesCandidateIdRoute: typeof ThreadsThreadIdCandidatesCandidateIdRoute
}
const ThreadsThreadIdRouteChildren: ThreadsThreadIdRouteChildren = {
ThreadsThreadIdIndexRoute: ThreadsThreadIdIndexRoute,
ThreadsThreadIdCandidatesCandidateIdRoute:
ThreadsThreadIdCandidatesCandidateIdRoute,
}
const ThreadsThreadIdRouteWithChildren = ThreadsThreadIdRoute._addFileChildren(
ThreadsThreadIdRouteChildren,
)
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
LoginRoute: LoginRoute, LoginRoute: LoginRoute,
@@ -294,7 +261,6 @@ const rootRouteChildren: RootRouteChildren = {
GlobalItemsGlobalItemIdRoute: GlobalItemsGlobalItemIdRoute, GlobalItemsGlobalItemIdRoute: GlobalItemsGlobalItemIdRoute,
ItemsItemIdRoute: ItemsItemIdRoute, ItemsItemIdRoute: ItemsItemIdRoute,
SetupsSetupIdRoute: SetupsSetupIdRoute, SetupsSetupIdRoute: SetupsSetupIdRoute,
ThreadsThreadIdRoute: ThreadsThreadIdRouteWithChildren,
UsersUserIdRoute: UsersUserIdRoute, UsersUserIdRoute: UsersUserIdRoute,
CollectionIndexRoute: CollectionIndexRoute, CollectionIndexRoute: CollectionIndexRoute,
GlobalItemsIndexRoute: GlobalItemsIndexRoute, GlobalItemsIndexRoute: GlobalItemsIndexRoute,

View File

@@ -9,14 +9,11 @@ import {
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
import "../app.css"; import "../app.css";
import { CandidateForm } from "../components/CandidateForm";
import { CatalogSearchOverlay } from "../components/CatalogSearchOverlay"; import { CatalogSearchOverlay } from "../components/CatalogSearchOverlay";
import { ConfirmDialog } from "../components/ConfirmDialog"; import { ConfirmDialog } from "../components/ConfirmDialog";
import { ExternalLinkDialog } from "../components/ExternalLinkDialog"; import { ExternalLinkDialog } from "../components/ExternalLinkDialog";
import { FabMenu } from "../components/FabMenu"; import { FabMenu } from "../components/FabMenu";
import { ItemForm } from "../components/ItemForm";
import { OnboardingWizard } from "../components/OnboardingWizard"; import { OnboardingWizard } from "../components/OnboardingWizard";
import { SlideOutPanel } from "../components/SlideOutPanel";
import { TotalsBar } from "../components/TotalsBar"; import { TotalsBar } from "../components/TotalsBar";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import { useDeleteCandidate } from "../hooks/useCandidates"; import { useDeleteCandidate } from "../hooks/useCandidates";
@@ -79,16 +76,6 @@ function RootLayout() {
const { data: auth, isLoading: authLoading } = useAuth(); const { data: auth, isLoading: authLoading } = useAuth();
const isAuthenticated = !!auth?.user; const isAuthenticated = !!auth?.user;
// Item panel state
const panelMode = useUIStore((s) => s.panelMode);
const editingItemId = useUIStore((s) => s.editingItemId);
const closePanel = useUIStore((s) => s.closePanel);
// Candidate panel state
const candidatePanelMode = useUIStore((s) => s.candidatePanelMode);
const editingCandidateId = useUIStore((s) => s.editingCandidateId);
const closeCandidatePanel = useUIStore((s) => s.closeCandidatePanel);
// Candidate delete state // Candidate delete state
const confirmDeleteCandidateId = useUIStore( const confirmDeleteCandidateId = useUIStore(
(s) => s.confirmDeleteCandidateId, (s) => s.confirmDeleteCandidateId,
@@ -114,9 +101,6 @@ function RootLayout() {
!wizardDismissed && !wizardDismissed &&
isAuthenticated; isAuthenticated;
const isItemPanelOpen = panelMode !== "closed";
const isCandidatePanelOpen = candidatePanelMode !== "closed";
// Route matching for contextual behavior // Route matching for contextual behavior
const matchRoute = useMatchRoute(); const matchRoute = useMatchRoute();
@@ -186,40 +170,6 @@ function RootLayout() {
<TotalsBar {...finalTotalsProps} /> <TotalsBar {...finalTotalsProps} />
<Outlet /> <Outlet />
{/* Item Slide-out Panel */}
<SlideOutPanel
isOpen={isItemPanelOpen}
onClose={closePanel}
title={panelMode === "add" ? "Add Item" : "Edit Item"}
>
{panelMode === "add" && <ItemForm mode="add" />}
{panelMode === "edit" && (
<ItemForm mode="edit" itemId={editingItemId} />
)}
</SlideOutPanel>
{/* Candidate Slide-out Panel */}
{currentThreadId != null && (
<SlideOutPanel
isOpen={isCandidatePanelOpen}
onClose={closeCandidatePanel}
title={
candidatePanelMode === "add" ? "Add Candidate" : "Edit Candidate"
}
>
{candidatePanelMode === "add" && (
<CandidateForm mode="add" threadId={currentThreadId} />
)}
{candidatePanelMode === "edit" && (
<CandidateForm
mode="edit"
threadId={currentThreadId}
candidateId={editingCandidateId}
/>
)}
</SlideOutPanel>
)}
{/* Item Confirm Delete Dialog */} {/* Item Confirm Delete Dialog */}
<ConfirmDialog /> <ConfirmDialog />

View File

@@ -1,25 +1,15 @@
import { create } from "zustand"; import { create } from "zustand";
interface UIState { interface UIState {
// Item panel state // Item delete state
panelMode: "closed" | "add" | "edit";
editingItemId: number | null;
confirmDeleteItemId: number | null; confirmDeleteItemId: number | null;
openAddPanel: () => void;
openEditPanel: (itemId: number) => void;
closePanel: () => void;
openConfirmDelete: (itemId: number) => void; openConfirmDelete: (itemId: number) => void;
closeConfirmDelete: () => void; closeConfirmDelete: () => void;
// Candidate panel state // Candidate delete state
candidatePanelMode: "closed" | "add" | "edit";
editingCandidateId: number | null;
confirmDeleteCandidateId: number | null; confirmDeleteCandidateId: number | null;
openCandidateAddPanel: () => void;
openCandidateEditPanel: (id: number) => void;
closeCandidatePanel: () => void;
openConfirmDeleteCandidate: (id: number) => void; openConfirmDeleteCandidate: (id: number) => void;
closeConfirmDeleteCandidate: () => void; closeConfirmDeleteCandidate: () => void;
@@ -70,28 +60,15 @@ interface UIState {
} }
export const useUIStore = create<UIState>((set) => ({ export const useUIStore = create<UIState>((set) => ({
// Item panel // Item delete
panelMode: "closed",
editingItemId: null,
confirmDeleteItemId: null, confirmDeleteItemId: null,
openAddPanel: () => set({ panelMode: "add", editingItemId: null }),
openEditPanel: (itemId) => set({ panelMode: "edit", editingItemId: itemId }),
closePanel: () => set({ panelMode: "closed", editingItemId: null }),
openConfirmDelete: (itemId) => set({ confirmDeleteItemId: itemId }), openConfirmDelete: (itemId) => set({ confirmDeleteItemId: itemId }),
closeConfirmDelete: () => set({ confirmDeleteItemId: null }), closeConfirmDelete: () => set({ confirmDeleteItemId: null }),
// Candidate panel // Candidate delete
candidatePanelMode: "closed",
editingCandidateId: null,
confirmDeleteCandidateId: null, confirmDeleteCandidateId: null,
openCandidateAddPanel: () =>
set({ candidatePanelMode: "add", editingCandidateId: null }),
openCandidateEditPanel: (id) =>
set({ candidatePanelMode: "edit", editingCandidateId: id }),
closeCandidatePanel: () =>
set({ candidatePanelMode: "closed", editingCandidateId: null }),
openConfirmDeleteCandidate: (id) => set({ confirmDeleteCandidateId: id }), openConfirmDeleteCandidate: (id) => set({ confirmDeleteCandidateId: id }),
closeConfirmDeleteCandidate: () => set({ confirmDeleteCandidateId: null }), closeConfirmDeleteCandidate: () => set({ confirmDeleteCandidateId: null }),