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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
143
.planning/phases/21-item-catalog-detail-pages/21-03-SUMMARY.md
Normal file
143
.planning/phases/21-item-catalog-detail-pages/21-03-SUMMARY.md
Normal 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*
|
||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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?.() },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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) },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?.() },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user