Files
GearBox/src/client/components/CandidateCard.tsx
Jean-Luc Makiola 9bcdcc7168 style: replace blue accent with gray and mute card badge colors
Switch all interactive UI elements (buttons, focus rings, active tabs,
FAB, links, spinners) from blue to gray to match icon colors for a
more cohesive look. Mute card badge text colors to pastels (blue-400,
green-500, purple-500) to keep the focus on card content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 22:42:38 +01:00

137 lines
3.8 KiB
TypeScript

import { formatPrice, formatWeight } from "../lib/formatters";
import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
interface CandidateCardProps {
id: number;
name: string;
weightGrams: number | null;
priceCents: number | null;
categoryName: string;
categoryIcon: string;
imageFilename: string | null;
productUrl?: string | null;
threadId: number;
isActive: boolean;
}
export function CandidateCard({
id,
name,
weightGrams,
priceCents,
categoryName,
categoryIcon,
imageFilename,
productUrl,
threadId,
isActive,
}: CandidateCardProps) {
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
const openConfirmDeleteCandidate = useUIStore(
(s) => s.openConfirmDeleteCandidate,
);
const openResolveDialog = useUIStore((s) => s.openResolveDialog);
const openExternalLink = useUIStore((s) => s.openExternalLink);
return (
<div className="relative bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all overflow-hidden group">
{productUrl && (
<span
role="button"
tabIndex={0}
onClick={() => openExternalLink(productUrl)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
openExternalLink(productUrl);
}
}}
className="absolute top-2 right-2 z-10 w-6 h-6 flex items-center justify-center rounded-full bg-gray-100/80 text-gray-400 hover:bg-gray-200 hover:text-gray-600 opacity-0 group-hover:opacity-100 transition-all cursor-pointer"
title="Open product link"
>
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3"
/>
</svg>
</span>
)}
<div className="aspect-[4/3] bg-gray-50">
{imageFilename ? (
<img
src={`/uploads/${imageFilename}`}
alt={name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex flex-col items-center justify-center">
<LucideIcon
name={categoryIcon}
size={36}
className="text-gray-400"
/>
</div>
)}
</div>
<div className="p-4">
<h3 className="text-sm font-semibold text-gray-900 mb-2 truncate">
{name}
</h3>
<div className="flex flex-wrap gap-1.5 mb-3">
{weightGrams != null && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400">
{formatWeight(weightGrams)}
</span>
)}
{priceCents != null && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-500">
{formatPrice(priceCents)}
</span>
)}
<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={14}
className="inline-block mr-1 text-gray-500"
/>{" "}
{categoryName}
</span>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => openCandidateEditPanel(id)}
className="text-xs text-gray-500 hover:text-gray-700 transition-colors"
>
Edit
</button>
<button
type="button"
onClick={() => openConfirmDeleteCandidate(id)}
className="text-xs text-gray-500 hover:text-red-600 transition-colors"
>
Delete
</button>
{isActive && (
<button
type="button"
onClick={() => openResolveDialog(threadId, id)}
className="ml-auto text-xs font-medium text-amber-600 hover:text-amber-700 transition-colors"
>
Pick Winner
</button>
)}
</div>
</div>
</div>
);
}