From 25956ed3eeeca7e4045614617a006b0684ffab30 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 16 Mar 2026 14:12:02 +0100 Subject: [PATCH] feat(08-01): create StatusBadge component and wire into CandidateCard - StatusBadge: clickable pill badge with popup menu (researching/ordered/arrived) - Muted gray styling, LucideIcon per status, click-outside dismiss, Escape key support - CandidateCard: status + onStatusChange props, StatusBadge in pill row after category - Thread detail page: passes candidate.status and useUpdateCandidate for onStatusChange - Fix Biome formatting for candidateStatusSchema enum Co-Authored-By: Claude Opus 4.6 --- src/client/components/CandidateCard.tsx | 6 ++ src/client/components/StatusBadge.tsx | 103 ++++++++++++++++++++++++ src/client/routes/threads/$threadId.tsx | 9 +++ src/shared/schemas.ts | 6 +- 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/client/components/StatusBadge.tsx diff --git a/src/client/components/CandidateCard.tsx b/src/client/components/CandidateCard.tsx index f994e1b..254d483 100644 --- a/src/client/components/CandidateCard.tsx +++ b/src/client/components/CandidateCard.tsx @@ -2,6 +2,7 @@ import { useWeightUnit } from "../hooks/useWeightUnit"; import { formatPrice, formatWeight } from "../lib/formatters"; import { LucideIcon } from "../lib/iconData"; import { useUIStore } from "../stores/uiStore"; +import { StatusBadge } from "./StatusBadge"; interface CandidateCardProps { id: number; @@ -14,6 +15,8 @@ interface CandidateCardProps { productUrl?: string | null; threadId: number; isActive: boolean; + status: "researching" | "ordered" | "arrived"; + onStatusChange: (status: "researching" | "ordered" | "arrived") => void; } export function CandidateCard({ @@ -27,6 +30,8 @@ export function CandidateCard({ productUrl, threadId, isActive, + status, + onStatusChange, }: CandidateCardProps) { const unit = useWeightUnit(); const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel); @@ -106,6 +111,7 @@ export function CandidateCard({ />{" "} {categoryName} +
+ + {isOpen && ( +
+ {(Object.keys(STATUS_CONFIG) as CandidateStatus[]).map((key) => { + const option = STATUS_CONFIG[key]; + const isActive = key === status; + return ( + + ); + })} +
+ )} +
+ ); +} diff --git a/src/client/routes/threads/$threadId.tsx b/src/client/routes/threads/$threadId.tsx index d455d21..e607cce 100644 --- a/src/client/routes/threads/$threadId.tsx +++ b/src/client/routes/threads/$threadId.tsx @@ -1,5 +1,6 @@ import { createFileRoute, Link } from "@tanstack/react-router"; import { CandidateCard } from "../../components/CandidateCard"; +import { useUpdateCandidate } from "../../hooks/useCandidates"; import { useThread } from "../../hooks/useThreads"; import { LucideIcon } from "../../lib/iconData"; import { useUIStore } from "../../stores/uiStore"; @@ -13,6 +14,7 @@ function ThreadDetailPage() { const threadId = Number(threadIdParam); const { data: thread, isLoading, isError } = useThread(threadId); const openCandidateAddPanel = useUIStore((s) => s.openCandidateAddPanel); + const updateCandidate = useUpdateCandidate(threadId); if (isLoading) { return ( @@ -144,6 +146,13 @@ function ThreadDetailPage() { productUrl={candidate.productUrl} threadId={threadId} isActive={isActive} + status={candidate.status} + onStatusChange={(newStatus) => + updateCandidate.mutate({ + candidateId: candidate.id, + status: newStatus, + }) + } /> ))} diff --git a/src/shared/schemas.ts b/src/shared/schemas.ts index 2c1bba6..cf9e4d6 100644 --- a/src/shared/schemas.ts +++ b/src/shared/schemas.ts @@ -37,7 +37,11 @@ export const updateThreadSchema = z.object({ }); // Candidate status -export const candidateStatusSchema = z.enum(["researching", "ordered", "arrived"]); +export const candidateStatusSchema = z.enum([ + "researching", + "ordered", + "arrived", +]); // Candidate schemas (same fields as items) export const createCandidateSchema = z.object({