Files
GearBox/src/client/components/ThreadCard.tsx
Jean-Luc Makiola e9d33e59e9 refactor: add useFormatters hook to reduce boilerplate across 14 components
Created useFormatters() combining useWeightUnit + useCurrency + formatWeight/formatPrice
into a single hook returning weight(grams) and price(cents) bound functions plus
raw unit and currency values. Updated all 14 consumer files to use the new hook,
removing the repeated 4-import + 2-hook-call pattern from each.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:49:16 +02:00

93 lines
2.6 KiB
TypeScript

import { useNavigate } from "@tanstack/react-router";
import { useFormatters } from "../hooks/useFormatters";
import { LucideIcon } from "../lib/iconData";
interface ThreadCardProps {
id: number;
name: string;
candidateCount: number;
minPriceCents: number | null;
maxPriceCents: number | null;
createdAt: string;
status: "active" | "resolved";
categoryName: string;
categoryIcon: string;
}
function formatDate(iso: string): string {
const d = new Date(iso);
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
export function ThreadCard({
id,
name,
candidateCount,
minPriceCents,
maxPriceCents,
createdAt,
status,
categoryName,
categoryIcon,
}: ThreadCardProps) {
const navigate = useNavigate();
const { price } = useFormatters();
function formatPriceRange(
min: number | null,
max: number | null,
): string | null {
if (min == null && max == null) return null;
if (min === max) return price(min);
return `${price(min)} - ${price(max)}`;
}
const isResolved = status === "resolved";
const priceRange = formatPriceRange(minPriceCents, maxPriceCents);
return (
<button
type="button"
onClick={() =>
navigate({
to: "/threads/$threadId",
params: { threadId: String(id) },
})
}
className={`w-full text-left bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all p-4 ${
isResolved ? "opacity-60" : ""
}`}
>
<div className="flex items-start justify-between mb-2">
<h3 className="text-sm font-semibold text-gray-900 truncate">{name}</h3>
{isResolved && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500 shrink-0">
Resolved
</span>
)}
</div>
<div className="flex flex-wrap gap-1.5">
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">
<LucideIcon
name={categoryIcon}
size={16}
className="inline-block mr-1 text-gray-500"
/>{" "}
{categoryName}
</span>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-50 text-purple-500">
{candidateCount} {candidateCount === 1 ? "candidate" : "candidates"}
</span>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">
{formatDate(createdAt)}
</span>
{priceRange && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-500">
{priceRange}
</span>
)}
</div>
</button>
);
}