turash/bugulma/frontend/components/ui/ImageGallery.tsx
Damir Mukimov 673e8d4361
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m37s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
fix: resolve all frontend lint errors (85 issues fixed)
- Replace all 'any' types with proper TypeScript interfaces
- Fix React hooks setState in useEffect issues with lazy initialization
- Remove unused variables and imports across all files
- Fix React Compiler memoization dependency issues
- Add comprehensive i18n translation keys for admin interfaces
- Apply consistent prettier formatting throughout codebase
- Clean up unused bulk editing functionality
- Improve type safety and code quality across frontend

Files changed: 39
- ImpactMetrics.tsx: Fixed any types and interfaces
- AdminVerificationQueuePage.tsx: Added i18n keys, removed unused vars
- LocalizationUIPage.tsx: Fixed memoization, added translations
- LocalizationDataPage.tsx: Added type safety and translations
- And 35+ other files with various lint fixes
2025-12-25 14:14:58 +01:00

203 lines
6.9 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"
>
{t('common.closeIcon')}
</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;