hasari-api / packages /ui /src /components /PartCard.tsx
erdoganpeker's picture
v0.3.0 — multimodal vehicle damage MVP
e327f0d
import type { Part } from '@arac-hasar/types';
import { PART_STATUS_TR } from '@arac-hasar/types';
import { DamageBadge } from './DamageBadge';
import { cn } from '../utils/cn';
interface Props {
part: Part;
className?: string;
onDamageClick?: (damageId: number) => void;
onPartClick?: () => void;
}
const STATUS_RING: Record<Part['status'], string> = {
clean: 'ring-emerald-200 bg-emerald-50/40',
minor_damage: 'ring-amber-300 bg-amber-50/30',
moderate_damage: 'ring-orange-300 bg-orange-50/30',
severe_damage: 'ring-red-300 bg-red-50/30',
};
const STATUS_DOT: Record<Part['status'], string> = {
clean: 'bg-emerald-500',
minor_damage: 'bg-amber-500',
moderate_damage: 'bg-orange-500',
severe_damage: 'bg-red-500',
};
export function PartCard({ part, className, onDamageClick, onPartClick }: Props) {
const isClean = part.status === 'clean';
const interactive = !!onPartClick;
return (
<div
role={interactive ? 'button' : undefined}
tabIndex={interactive ? 0 : undefined}
onClick={onPartClick}
onKeyDown={(e) => {
if (!interactive) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onPartClick?.();
}
}}
aria-label={interactive ? `${part.name_tr}, ${PART_STATUS_TR[part.status]}` : undefined}
className={cn(
'rounded-xl ring-1 ring-inset p-4 transition-shadow',
STATUS_RING[part.status],
interactive &&
'cursor-pointer hover:shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-1',
className,
)}
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2">
<span className={cn('h-2 w-2 rounded-full', STATUS_DOT[part.status])} aria-hidden />
<h3 className="font-semibold text-slate-900">{part.name_tr}</h3>
</div>
<span
className={cn(
'rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
isClean ? 'bg-emerald-100 text-emerald-800' : 'bg-slate-900 text-white',
)}
>
{isClean ? 'Hasarsız' : `${part.damage_count} hasar`}
</span>
</div>
<div className="mt-1 text-[11px] uppercase tracking-wider text-slate-400">
{PART_STATUS_TR[part.status]}
</div>
{!isClean && (
<>
<div className="mt-3 space-y-2">
{part.damages.map((d) => (
<DamageBadge
key={d.id}
damage={d}
onClick={onDamageClick ? () => onDamageClick(d.id) : undefined}
/>
))}
</div>
{part.part_cost_max_tl > 0 && (
<div className="mt-3 flex items-baseline justify-between border-t border-slate-200/70 pt-2">
<span className="text-xs text-slate-500">Parça toplam</span>
<span className="text-sm font-semibold text-slate-900 tabular-nums">
{part.part_cost_min_tl.toLocaleString('tr-TR')} –{' '}
{part.part_cost_max_tl.toLocaleString('tr-TR')} ₺
</span>
</div>
)}
{part.cost_note && (
<p className="mt-1 text-[11px] italic text-slate-500">{part.cost_note}</p>
)}
</>
)}
</div>
);
}