Spaces:
Sleeping
Sleeping
File size: 4,739 Bytes
e327f0d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | import type { ReactNode } from 'react';
import { cn } from '../utils/cn';
export interface MultiImageGridItem {
/** Backend image URL — null/undefined renders default car placeholder */
url?: string | null;
/** Alt text for accessibility */
alt?: string;
/** Optional badge content rendered top-right (e.g. damage count) */
badge?: ReactNode;
/** Optional label rendered bottom (e.g. "Ön sol") */
label?: string;
}
interface Props {
items: MultiImageGridItem[];
className?: string;
/** Click handler — called with item index */
onItemClick?: (index: number) => void;
/**
* Intrinsic image dimensions hinted to the browser to reserve space
* and prevent CLS — actual rendered size still follows the aspect ratio.
* Defaults to 400x300 (4:3).
*/
intrinsicWidth?: number;
intrinsicHeight?: number;
/** Aspect ratio class — defaults to 4:3 (matches default intrinsic dims) */
aspectRatioClass?: string;
}
/**
* Responsive multi-image thumbnail grid (spec-aligned).
*
* - 3 columns on mobile, 4 on sm (≥640px), 6 on lg (≥1024px)
* - Fixed aspect ratio per cell (no layout shift)
* - Every <img> receives explicit width/height attributes so the browser can
* reserve space even without next/image — usable in Tauri/RN-web shells.
* - Falls back to inline car SVG when a URL is missing.
*/
export function MultiImageGrid({
items,
className,
onItemClick,
intrinsicWidth = 400,
intrinsicHeight = 300,
aspectRatioClass = 'aspect-[4/3]',
}: Props) {
if (!items || items.length === 0) return null;
return (
<ul
role="list"
className={cn(
'grid grid-cols-3 gap-2 sm:grid-cols-4 sm:gap-3 lg:grid-cols-6',
className,
)}
>
{items.map((item, i) => {
const interactive = !!onItemClick;
const hasUrl = !!item.url;
const alt = item.alt ?? `Fotoğraf ${i + 1}`;
const cell = (
<>
<div
className={cn(
'relative w-full overflow-hidden rounded-lg bg-slate-100 ring-1 ring-inset ring-slate-200',
aspectRatioClass,
interactive &&
'transition-shadow group-hover:shadow-md group-focus-visible:shadow-md',
)}
>
{hasUrl ? (
<img
src={item.url!}
alt={alt}
width={intrinsicWidth}
height={intrinsicHeight}
loading="lazy"
decoding="async"
className="absolute inset-0 h-full w-full object-cover"
/>
) : (
<DefaultCarPlaceholder index={i} />
)}
{item.badge && (
<span className="absolute right-1.5 top-1.5 inline-flex items-center rounded-full bg-slate-900/85 px-1.5 py-0.5 text-[10px] font-semibold text-white shadow-sm">
{item.badge}
</span>
)}
</div>
{item.label && (
<div className="mt-1.5 truncate text-center text-[11px] font-medium text-slate-600">
{item.label}
</div>
)}
</>
);
return (
<li key={i}>
{interactive ? (
<button
type="button"
onClick={() => onItemClick?.(i)}
aria-label={item.label ? `${item.label} — ${alt}` : alt}
className="group block w-full rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2"
>
{cell}
</button>
) : (
<div>{cell}</div>
)}
</li>
);
})}
</ul>
);
}
function DefaultCarPlaceholder({ index }: { index: number }) {
return (
<div
className="absolute inset-0 flex flex-col items-center justify-center gap-1 bg-gradient-to-br from-slate-50 to-slate-100 text-slate-400"
aria-hidden
>
<svg
viewBox="0 0 48 30"
xmlns="http://www.w3.org/2000/svg"
className="h-7 w-7 sm:h-8 sm:w-8"
aria-hidden
>
<path
d="M6 22 L9 14 Q10.5 11 14 11 H34 Q37.5 11 39 14 L42 22 Z"
fill="#94a3b8"
/>
<path
d="M13 16 Q14.5 13 17.5 13 H30.5 Q33.5 13 35 16 H13 Z"
fill="#cbd5e1"
opacity="0.85"
/>
<circle cx="14" cy="23" r="3" fill="#0f172a" />
<circle cx="34" cy="23" r="3" fill="#0f172a" />
</svg>
<span className="text-[10px] font-medium uppercase tracking-wider">
#{index + 1}
</span>
</div>
);
}
|