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) => (
))}