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>
  );
}