mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Remove nested git repository from bugulma/frontend/.git - Add all frontend files to main repository tracking - Convert from separate frontend/backend repos to unified monorepo - Preserve all frontend code and development history as tracked files - Eliminate nested repository complexity for simpler development workflow This creates a proper monorepo structure with frontend and backend coexisting in the same repository for easier development and deployment.
207 lines
6.9 KiB
TypeScript
207 lines
6.9 KiB
TypeScript
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;
|