File size: 4,872 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
152
153
154
155
import type { ReactNode } from 'react';
import { Car, ImageOff } from 'lucide-react';
import { cn } from '../utils/cn';

export interface ThumbnailItem {
  /** Backend image URL — null/undefined renders 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: ThumbnailItem[];
  className?: string;
  /** Click handler — called with item index */
  onItemClick?: (index: number) => void;
  /** Aspect ratio class — defaults to 4:3 for vehicle photos */
  aspectRatioClass?: string;
}

/**
 * Responsive thumbnail grid for multi-photo inspections.
 * - 3 columns on mobile, 4 on sm, 6 on lg
 * - Fixed aspect ratio reserves space (prevents CLS)
 * - Renders car-icon placeholder when image url is missing
 */
export function MultiImageThumbnailGrid({
  items,
  className,
  onItemClick,
  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 content = (
          <>
            <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 ? (
                // packages/ui is framework-agnostic — plain <img> with
                // explicit width/height reserves space (parent aspect ratio
                // prevents CLS). Consumer apps can wrap with next/image if
                // they need optimised srcSet.
                <img
                  src={item.url!}
                  alt={alt}
                  width={400}
                  height={300}
                  loading="lazy"
                  decoding="async"
                  className="absolute inset-0 h-full w-full object-cover"
                />
              ) : (
                <ThumbnailPlaceholder 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"
              >
                {content}
              </button>
            ) : (
              <div>{content}</div>
            )}
          </li>
        );
      })}
    </ul>
  );
}

function ThumbnailPlaceholder({ 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
    >
      <Car className="h-6 w-6 sm:h-7 sm:w-7" />
      <span className="text-[10px] font-medium uppercase tracking-wider">
        #{index + 1}
      </span>
    </div>
  );
}

/**
 * Compact placeholder for history rows when no thumbnail is available.
 * Use as standalone (square) icon block.
 */
export function ThumbnailFallback({
  className,
  size = 'md',
}: {
  className?: string;
  size?: 'sm' | 'md' | 'lg';
}) {
  const dimensions =
    size === 'sm' ? 'h-10 w-10' : size === 'lg' ? 'h-20 w-20' : 'h-14 w-14';
  const iconSize =
    size === 'sm' ? 'h-5 w-5' : size === 'lg' ? 'h-9 w-9' : 'h-7 w-7';
  return (
    <div
      className={cn(
        'flex flex-none items-center justify-center rounded-lg bg-gradient-to-br from-brand-50 to-brand-100 text-brand-600 ring-1 ring-inset ring-brand-200',
        dimensions,
        className,
      )}
      role="img"
      aria-label="Görsel hazırlanıyor"
    >
      <ImageOff className={iconSize} aria-hidden />
    </div>
  );
}