diff --git a/src/client/components/CandidateCard.tsx b/src/client/components/CandidateCard.tsx index 4d25272..3cea32d 100644 --- a/src/client/components/CandidateCard.tsx +++ b/src/client/components/CandidateCard.tsx @@ -3,6 +3,7 @@ import { useWeightUnit } from "../hooks/useWeightUnit"; import { formatPrice, formatWeight } from "../lib/formatters"; import { LucideIcon } from "../lib/iconData"; import { useUIStore } from "../stores/uiStore"; +import { RankBadge } from "./CandidateListItem"; import { StatusBadge } from "./StatusBadge"; interface CandidateCardProps { @@ -20,6 +21,7 @@ interface CandidateCardProps { onStatusChange: (status: "researching" | "ordered" | "arrived") => void; pros?: string | null; cons?: string | null; + rank?: number; } export function CandidateCard({ @@ -37,6 +39,7 @@ export function CandidateCard({ onStatusChange, pros, cons, + rank, }: CandidateCardProps) { const unit = useWeightUnit(); const currency = useCurrency(); @@ -159,6 +162,7 @@ export function CandidateCard({ {name}
+ {rank != null && } {weightGrams != null && ( {formatWeight(weightGrams, unit)} diff --git a/src/client/routes/threads/$threadId.tsx b/src/client/routes/threads/$threadId.tsx index 758dcdd..72c936a 100644 --- a/src/client/routes/threads/$threadId.tsx +++ b/src/client/routes/threads/$threadId.tsx @@ -1,6 +1,12 @@ import { createFileRoute, Link } from "@tanstack/react-router"; +import { Reorder } from "framer-motion"; +import { useEffect, useState } from "react"; import { CandidateCard } from "../../components/CandidateCard"; -import { useUpdateCandidate } from "../../hooks/useCandidates"; +import { CandidateListItem } from "../../components/CandidateListItem"; +import { + useReorderCandidates, + useUpdateCandidate, +} from "../../hooks/useCandidates"; import { useThread } from "../../hooks/useThreads"; import { LucideIcon } from "../../lib/iconData"; import { useUIStore } from "../../stores/uiStore"; @@ -14,7 +20,21 @@ function ThreadDetailPage() { const threadId = Number(threadIdParam); const { data: thread, isLoading, isError } = useThread(threadId); const openCandidateAddPanel = useUIStore((s) => s.openCandidateAddPanel); + const candidateViewMode = useUIStore((s) => s.candidateViewMode); + const setCandidateViewMode = useUIStore((s) => s.setCandidateViewMode); const updateCandidate = useUpdateCandidate(threadId); + const reorderMutation = useReorderCandidates(threadId); + + const [tempItems, setTempItems] = + useState( + null, + ); + + // Clear tempItems when server data changes (biome-ignore: thread?.candidates is intentional dep) + // biome-ignore lint/correctness/useExhaustiveDependencies: thread?.candidates is the intended trigger + useEffect(() => { + setTempItems(null); + }, [thread?.candidates]); if (isLoading) { return ( @@ -53,6 +73,16 @@ function ThreadDetailPage() { ? thread.candidates.find((c) => c.id === thread.resolvedCandidateId) : null; + const displayItems = tempItems ?? thread.candidates; + + function handleDragEnd() { + if (!tempItems) return; + reorderMutation.mutate( + { orderedIds: tempItems.map((c) => c.id) }, + { onSettled: () => setTempItems(null) }, + ); + } + return (
{/* Header */} @@ -88,9 +118,9 @@ function ThreadDetailPage() {
)} - {/* Add candidate button */} - {isActive && ( -
+ {/* Toolbar: Add candidate + view toggle */} +
+ {isActive && ( -
- )} + )} + {thread.candidates.length > 0 && ( +
+ + +
+ )} +
- {/* Candidate grid */} + {/* Candidates */} {thread.candidates.length === 0 ? (
@@ -131,9 +189,50 @@ function ThreadDetailPage() { Add your first candidate to start comparing.

+ ) : candidateViewMode === "list" ? ( + isActive ? ( + + {displayItems.map((candidate, index) => ( + + updateCandidate.mutate({ + candidateId: candidate.id, + status: newStatus, + }) + } + /> + ))} + + ) : ( +
+ {displayItems.map((candidate, index) => ( + + updateCandidate.mutate({ + candidateId: candidate.id, + status: newStatus, + }) + } + /> + ))} +
+ ) ) : (
- {thread.candidates.map((candidate) => ( + {thread.candidates.map((candidate, index) => ( ))}