feat(T05): Dateiablage pro Fahrzeug + File UI with thumbnails and gallery
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
listFiles,
|
||||
isImageMime,
|
||||
getThumbnailUrl,
|
||||
type FileResponse,
|
||||
} from '@/lib/files';
|
||||
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 && (
|
||||
<img
|
||||
src={thumbUrl}
|
||||
alt={image.original_filename}
|
||||
className="w-full h-full object-cover transition-transform group-hover:scale-105"
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user