Files
GearBox/src/client/routes/threads/$threadId.tsx
Jean-Luc Makiola 7d043a8585 feat(02-02): add thread detail page with candidate CRUD and resolution flow
- Create CandidateCard with edit, delete, and pick winner actions
- Create CandidateForm with same fields as ItemForm for candidate add/edit
- Build thread detail page with candidate grid and resolution banner
- Update root layout with candidate panel, delete dialog, and resolve dialog
- Hide FAB on thread detail pages, keep it for gear tab
- Resolution navigates back to planning tab after success
2026-03-15 11:46:19 +01:00

148 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createFileRoute, Link } from "@tanstack/react-router";
import { useThread } from "../../hooks/useThreads";
import { CandidateCard } from "../../components/CandidateCard";
import { useUIStore } from "../../stores/uiStore";
export const Route = createFileRoute("/threads/$threadId")({
component: ThreadDetailPage,
});
function ThreadDetailPage() {
const { threadId: threadIdParam } = Route.useParams();
const threadId = Number(threadIdParam);
const { data: thread, isLoading, isError } = useThread(threadId);
const openCandidateAddPanel = useUIStore((s) => s.openCandidateAddPanel);
if (isLoading) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="animate-pulse space-y-6">
<div className="h-6 bg-gray-200 rounded w-48" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<div key={i} className="h-40 bg-gray-200 rounded-xl" />
))}
</div>
</div>
</div>
);
}
if (isError || !thread) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Thread not found
</h2>
<Link
to="/"
search={{ tab: "planning" }}
className="text-sm text-blue-600 hover:text-blue-700"
>
Back to planning
</Link>
</div>
);
}
const isActive = thread.status === "active";
const winningCandidate = thread.resolvedCandidateId
? thread.candidates.find((c) => c.id === thread.resolvedCandidateId)
: null;
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{/* Header */}
<div className="mb-6">
<Link
to="/"
search={{ tab: "planning" }}
className="text-sm text-gray-500 hover:text-gray-700 mb-2 inline-block"
>
&larr; Back to planning
</Link>
<div className="flex items-center gap-3">
<h1 className="text-xl font-semibold text-gray-900">
{thread.name}
</h1>
<span
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
isActive
? "bg-blue-50 text-blue-700"
: "bg-gray-100 text-gray-500"
}`}
>
{isActive ? "Active" : "Resolved"}
</span>
</div>
</div>
{/* Resolution banner */}
{!isActive && winningCandidate && (
<div className="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-xl">
<p className="text-sm text-amber-800">
<span className="font-medium">{winningCandidate.name}</span> was
picked as the winner and added to your collection.
</p>
</div>
)}
{/* Add candidate button */}
{isActive && (
<div className="mb-6">
<button
type="button"
onClick={openCandidateAddPanel}
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
Add Candidate
</button>
</div>
)}
{/* Candidate grid */}
{thread.candidates.length === 0 ? (
<div className="py-12 text-center">
<div className="text-4xl mb-3">🏷</div>
<h3 className="text-lg font-semibold text-gray-900 mb-1">
No candidates yet
</h3>
<p className="text-sm text-gray-500">
Add your first candidate to start comparing.
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{thread.candidates.map((candidate) => (
<CandidateCard
key={candidate.id}
id={candidate.id}
name={candidate.name}
weightGrams={candidate.weightGrams}
priceCents={candidate.priceCents}
categoryName={candidate.categoryName}
categoryEmoji={candidate.categoryEmoji}
imageFilename={candidate.imageFilename}
threadId={threadId}
isActive={isActive}
/>
))}
</div>
)}
</div>
);
}