turash/bugulma/frontend/components/ui/ImageGallery.tsx
2025-12-15 10:06:41 +01:00

203 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import { useTranslation } from '@/hooks/useI18n.tsx';
import { Eye, Trash2, UploadCloud } from 'lucide-react';
import Button from '@/components/ui/Button.tsx';
import { Card, CardContent } from '@/components/ui/Card.tsx';
interface ImageGalleryProps {
images: string[];
onChange?: (images: string[]) => void;
maxImages?: number;
className?: string;
editable?: boolean;
title?: string;
}
const ImageGallery: React.FC<ImageGalleryProps> = ({
images = [],
onChange,
maxImages = 10,
className = '',
editable = false,
title,
}) => {
const { t } = useTranslation();
const [selectedImageIndex, setSelectedImageIndex] = useState<number | null>(null);
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (!files || !onChange) return;
const newImages: string[] = [];
Array.from(files).forEach((file) => {
if (images.length + newImages.length >= maxImages) return;
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result as string;
if (result) {
newImages.push(result);
if (newImages.length === files.length || images.length + newImages.length >= maxImages) {
onChange([...images, ...newImages]);
}
}
};
reader.readAsDataURL(file);
});
};
const handleRemoveImage = (index: number) => {
if (!onChange) return;
const newImages = images.filter((_, i) => i !== index);
onChange(newImages);
};
const openLightbox = (index: number) => {
setSelectedImageIndex(index);
};
const closeLightbox = () => {
setSelectedImageIndex(null);
};
const goToPrevious = () => {
if (selectedImageIndex === null) return;
setSelectedImageIndex(selectedImageIndex > 0 ? selectedImageIndex - 1 : images.length - 1);
};
const goToNext = () => {
if (selectedImageIndex === null) return;
setSelectedImageIndex(selectedImageIndex < images.length - 1 ? selectedImageIndex + 1 : 0);
};
return (
<div className={className}>
{title && <h3 className="text-lg font-semibold mb-4">{title}</h3>}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{images.map((image, index) => (
<Card key={index} className="group relative overflow-hidden">
<CardContent className="p-2">
<div className="aspect-square relative">
<img
src={image}
alt={`${t('imageGallery.imageAlt')} ${index + 1}`}
className="w-full h-full object-cover rounded cursor-pointer hover:scale-105 transition-transform"
onClick={() => openLightbox(index)}
loading="lazy"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
{/* Overlay with actions */}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={() => openLightbox(index)}
className="bg-white/90 hover:bg-white"
>
<Eye className="h-4 text-current w-4" />
</Button>
{editable && onChange && (
<Button
variant="destructive"
size="sm"
onClick={() => handleRemoveImage(index)}
className="bg-red-500/90 hover:bg-red-500"
>
<Trash2 className="h-4 text-current w-4" />
</Button>
)}
</div>
</div>
</CardContent>
</Card>
))}
{/* Upload placeholder */}
{editable && onChange && images.length < maxImages && (
<Card className="border-dashed border-2 hover:border-primary transition-colors">
<CardContent className="p-2">
<div className="aspect-square flex items-center justify-center">
<label className="cursor-pointer w-full h-full flex flex-col items-center justify-center text-muted-foreground hover:text-primary transition-colors">
<UploadCloud className="h-4 h-8 mb-2 text-current w-4 w-8" />
<span className="text-sm text-center">{t('imageGallery.addImage')}</span>
<input
type="file"
multiple
accept="image/png,image/jpeg,image/webp"
onChange={handleImageUpload}
className="hidden"
/>
</label>
</div>
</CardContent>
</Card>
)}
</div>
{images.length === 0 && !editable && (
<div className="text-center py-8 text-muted-foreground">
<p>{t('imageGallery.noImages')}</p>
</div>
)}
{/* Lightbox */}
{selectedImageIndex !== null && (
<div className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4">
<div className="relative max-w-4xl max-h-full">
<img
src={images[selectedImageIndex]}
alt={`${t('imageGallery.imageAlt')} ${selectedImageIndex + 1}`}
className="max-w-full max-h-full object-contain"
/>
{/* Navigation buttons */}
{images.length > 1 && (
<>
<Button
variant="secondary"
size="sm"
onClick={goToPrevious}
className="absolute left-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white"
>
</Button>
<Button
variant="secondary"
size="sm"
onClick={goToNext}
className="absolute right-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white"
>
</Button>
</>
)}
{/* Close button */}
<Button
variant="secondary"
size="sm"
onClick={closeLightbox}
className="absolute top-2 right-2 bg-black/50 hover:bg-black/70 text-white"
>
</Button>
{/* Image counter */}
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 bg-black/50 text-white px-3 py-1 rounded text-sm">
{selectedImageIndex + 1} / {images.length}
</div>
</div>
</div>
)}
</div>
);
};
export default ImageGallery;