2026-07-17 01:38:48 +02:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
listFiles,
|
|
|
|
|
isImageMime,
|
|
|
|
|
getThumbnailUrl,
|
|
|
|
|
type FileResponse,
|
|
|
|
|
} from '@/lib/files';
|
2026-07-17 21:16:38 +02:00
|
|
|
import Image from 'next/image';
|
2026-07-17 01:38:48 +02:00
|
|
|
import { FilePreview } from './FilePreview';
|
|
|
|
|
|
|
|
|
|
interface FileGalleryProps {
|
|
|
|
|
vehicleId: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function FileGallery({ vehicleId }: FileGalleryProps) {
|
|
|
|
|
const [images, setImages] = useState<FileResponse[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [selectedImage, setSelectedImage] = useState<FileResponse | null>(null);
|
|
|
|
|
|
|
|
|
|
const fetchImages = useCallback(async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
try {
|
|
|
|
|
// Fetch all files and filter for images
|
|
|
|
|
const data = await listFiles(vehicleId, 1, 100);
|
|
|
|
|
const imageFiles = data.items.filter((f) => isImageMime(f.mime_type));
|
|
|
|
|
setImages(imageFiles);
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
const apiErr = err as { error?: { message?: string } };
|
|
|
|
|
setError(apiErr?.error?.message || 'Failed to load images');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [vehicleId]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchImages();
|
|
|
|
|
}, [fetchImages]);
|
|
|
|
|
|
|
|
|
|
if (loading && images.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="text-center py-8 text-text-muted" data-testid="gallery-loading">
|
|
|
|
|
Loading gallery...
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="text-center py-8 text-error" data-testid="gallery-error">
|
|
|
|
|
{error}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (images.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="text-center py-8 text-text-muted" data-testid="gallery-empty">
|
|
|
|
|
No images available for this vehicle.
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div data-testid="file-gallery">
|
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
|
|
|
|
{images.map((image) => {
|
|
|
|
|
const thumbUrl = getThumbnailUrl(image);
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={image.id}
|
|
|
|
|
className="relative aspect-square rounded-lg overflow-hidden cursor-pointer group"
|
|
|
|
|
onClick={() => setSelectedImage(image)}
|
|
|
|
|
data-testid={`gallery-item-${image.id}`}
|
|
|
|
|
>
|
|
|
|
|
{thumbUrl && (
|
2026-07-17 21:16:38 +02:00
|
|
|
<Image
|
2026-07-17 01:38:48 +02:00
|
|
|
src={thumbUrl}
|
|
|
|
|
alt={image.original_filename}
|
2026-07-17 21:16:38 +02:00
|
|
|
fill
|
|
|
|
|
className="object-cover transition-transform group-hover:scale-105"
|
|
|
|
|
unoptimized
|
2026-07-17 01:38:48 +02:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors flex items-end p-2">
|
|
|
|
|
<span className="text-white text-xs opacity-0 group-hover:opacity-100 transition-opacity truncate">
|
|
|
|
|
{image.original_filename}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Full preview modal */}
|
|
|
|
|
<FilePreview
|
|
|
|
|
file={selectedImage}
|
|
|
|
|
open={!!selectedImage}
|
|
|
|
|
onClose={() => setSelectedImage(null)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|