feat(29-02): update CandidateCard and CandidateListItem to use GearImage

Replace object-cover with GearImage for fit-within rendering on
candidate cards and list items.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 19:59:44 +02:00
parent d8ede7a942
commit 05c09182fd
2 changed files with 34 additions and 7 deletions

View File

@@ -4,6 +4,7 @@ import type { CandidateDelta } from "../hooks/useImpactDeltas";
import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
import { RankBadge } from "./CandidateListItem";
import { GearImage, imageContainerBg } from "./GearImage";
import { ImpactDeltaBadge } from "./ImpactDeltaBadge";
import { StatusBadge } from "./StatusBadge";
@@ -17,6 +18,10 @@ interface CandidateCardProps {
imageFilename: string | null;
imageUrl?: string | null;
productUrl?: string | null;
dominantColor?: string | null;
cropZoom?: number | null;
cropX?: number | null;
cropY?: number | null;
threadId: number;
isActive: boolean;
status: "researching" | "ordered" | "arrived";
@@ -37,6 +42,10 @@ export function CandidateCard({
imageFilename: _imageFilename,
imageUrl,
productUrl,
dominantColor,
cropZoom,
cropX,
cropY,
threadId,
isActive,
status,
@@ -149,15 +158,25 @@ export function CandidateCard({
</svg>
</span>
)}
<div className="aspect-[4/3] bg-gray-50">
<div
className="aspect-[4/3] overflow-hidden"
style={{
backgroundColor: imageUrl
? imageContainerBg(dominantColor)
: undefined,
}}
>
{imageUrl ? (
<img
<GearImage
src={imageUrl}
alt={name}
className="w-full h-full object-cover"
dominantColor={dominantColor}
cropZoom={cropZoom}
cropX={cropX}
cropY={cropY}
/>
) : (
<div className="w-full h-full flex flex-col items-center justify-center">
<div className="w-full h-full bg-gray-50 flex flex-col items-center justify-center">
<LucideIcon
name={categoryIcon}
size={36}

View File

@@ -5,6 +5,7 @@ import { useFormatters } from "../hooks/useFormatters";
import type { CandidateDelta } from "../hooks/useImpactDeltas";
import { LucideIcon } from "../lib/iconData";
import { useUIStore } from "../stores/uiStore";
import { GearImage, imageContainerBg } from "./GearImage";
import { ImpactDeltaBadge } from "./ImpactDeltaBadge";
import { StatusBadge } from "./StatusBadge";
@@ -19,6 +20,7 @@ interface CandidateWithCategory {
productUrl: string | null;
imageFilename: string | null;
imageUrl?: string | null;
dominantColor?: string | null;
status: "researching" | "ordered" | "arrived";
pros: string | null;
cons: string | null;
@@ -84,12 +86,18 @@ export function CandidateListItem({
<RankBadge rank={rank} />
{/* Image thumbnail */}
<div className="w-12 h-12 rounded-lg overflow-hidden shrink-0 bg-gray-50 flex items-center justify-center">
<div
className="w-12 h-12 rounded-lg overflow-hidden shrink-0 flex items-center justify-center"
style={{
backgroundColor: candidate.imageUrl
? imageContainerBg(candidate.dominantColor)
: undefined,
}}
>
{candidate.imageUrl ? (
<img
<GearImage
src={candidate.imageUrl}
alt={candidate.name}
className="w-full h-full object-cover"
/>
) : (
<LucideIcon