Files
Leopoldadmin 341d0c6f38 fix: resolve all ruff and ESLint lint errors
Backend (ruff):
- F821: Add TYPE_CHECKING imports for Vehicle, Contact, File in models
  (file.py, retouch.py, sale.py, vehicle.py)
- E741: Rename ambiguous variable  to /
  (copilot_service.py, price_compare_service.py, openrouter.py)
- F841: Remove unused variable  in sale_service.py

Frontend (ESLint):
- react/no-unescaped-entities: Escape quotes in ContractPreview.tsx
- @next/next/no-img-element: Replace <img> with <Image> from next/image
  (FileGallery.tsx, FileList.tsx, FilePreview.tsx, BeforeAfterSlider.tsx)

Tests: 392 backend passed, 112 frontend passed, next build successful
2026-07-17 21:16:38 +02:00

107 lines
3.1 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback } from 'react';
import {
listFiles,
isImageMime,
getThumbnailUrl,
type FileResponse,
} from '@/lib/files';
import Image from 'next/image';
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 && (
<Image
src={thumbUrl}
alt={image.original_filename}
fill
className="object-cover transition-transform group-hover:scale-105"
unoptimized
/>
)}
<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>
);
}