Files
sitarapart/components/admin/ImageUploadModal.tsx
T

250 lines
8.1 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.
'use client'
import { useState, useRef } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { uploadImageAction } from '@/lib/actions/upload'
import { ImagePlus, X, UploadCloud, Loader2, CheckCircle2, AlertCircle } from 'lucide-react'
interface ImageUploadModalProps {
onUploadComplete: (urls: string[]) => void
multiple?: boolean
triggerText?: string
folderName?: string
}
export function ImageUploadModal({
onUploadComplete,
multiple = false,
triggerText = "Resim Yükle",
folderName = "starapart"
}: ImageUploadModalProps) {
const [open, setOpen] = useState(false)
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
const [previews, setPreviews] = useState<string[]>([])
const [isUploading, setIsUploading] = useState(false)
const [progress, setProgress] = useState(0) // 0 to total files
const [error, setError] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const resetState = () => {
setSelectedFiles([])
previews.forEach(p => URL.revokeObjectURL(p))
setPreviews([])
setIsUploading(false)
setProgress(0)
setError(null)
}
const handleOpenChange = (newOpen: boolean) => {
if (isUploading) return // Prevent closing while uploading
setOpen(newOpen)
if (!newOpen) {
resetState()
}
}
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files?.length) return
const files = Array.from(e.target.files)
const newFiles = multiple ? [...selectedFiles, ...files] : [files[0]]
setSelectedFiles(newFiles)
// Create preview URLs
const newPreviews = newFiles.map(file => URL.createObjectURL(file))
if (!multiple) {
previews.forEach(p => URL.revokeObjectURL(p)) // cleanup old previews
setPreviews(newPreviews)
} else {
setPreviews([...previews, ...newPreviews])
}
setError(null)
// Reset file input so same file can be selected again if removed
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
const removeFile = (index: number) => {
const newFiles = [...selectedFiles]
newFiles.splice(index, 1)
const newPreviews = [...previews]
URL.revokeObjectURL(newPreviews[index])
newPreviews.splice(index, 1)
setSelectedFiles(newFiles)
setPreviews(newPreviews)
}
const handleUpload = async () => {
if (!selectedFiles.length) return
setIsUploading(true)
setError(null)
setProgress(0)
const uploadedUrls: string[] = []
try {
// Upload one by one to prevent timeout and body size limits
for (let i = 0; i < selectedFiles.length; i++) {
const file = selectedFiles[i]
const fd = new FormData()
fd.append('file', file)
fd.append('folder', folderName)
const res = await uploadImageAction(fd)
if (res.success && res.url) {
uploadedUrls.push(res.url)
} else {
throw new Error(res.error || `${file.name} yüklenemedi.`)
}
setProgress(i + 1)
}
onUploadComplete(uploadedUrls)
setOpen(false)
resetState()
} catch (err: any) {
setError(err.message || "Yükleme sırasında bir hata oluştu.")
setIsUploading(false)
}
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger render={
<Button type="button" variant="outline" className="w-full h-24 border-dashed flex flex-col items-center justify-center gap-2">
<ImagePlus className="h-6 w-6 text-gray-500" />
<span className="text-gray-500">{triggerText}</span>
</Button>
} />
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Resim Yükle</DialogTitle>
<DialogDescription>
{multiple ? "Birden fazla resim seçip yükleyebilirsiniz." : "Odanın ana görselini seçin."}
</DialogDescription>
</DialogHeader>
<div className="py-4 space-y-4">
{!isUploading && (
<div className="flex justify-center">
<input
type="file"
accept="image/*"
multiple={multiple}
className="hidden"
ref={fileInputRef}
onChange={handleFileChange}
/>
<Button type="button" variant="secondary" onClick={() => fileInputRef.current?.click()}>
Bilgisayardan Seç
</Button>
</div>
)}
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-md text-sm flex items-start gap-2">
<AlertCircle className="h-5 w-5 shrink-0" />
<span>{error}</span>
</div>
)}
{previews.length > 0 && (
<div className="bg-gray-50 dark:bg-gray-900 p-4 rounded-lg border">
<h4 className="text-sm font-medium mb-3">Seçilen Resimler ({selectedFiles.length})</h4>
<div className="grid grid-cols-3 sm:grid-cols-4 gap-4 max-h-[300px] overflow-y-auto p-1">
{previews.map((preview, idx) => (
<div key={idx} className="relative group aspect-square rounded-md border bg-white overflow-hidden">
<img src={preview} alt="preview" className="w-full h-full object-cover" />
{!isUploading && (
<button
type="button"
onClick={() => removeFile(idx)}
className="absolute top-1 right-1 bg-red-500 text-white rounded-full p-1 shadow-sm opacity-0 group-hover:opacity-100 transition-opacity"
>
<X className="h-4 w-4" />
</button>
)}
{isUploading && idx < progress && (
<div className="absolute inset-0 bg-white/70 flex items-center justify-center">
<CheckCircle2 className="h-8 w-8 text-green-500" />
</div>
)}
{isUploading && idx === progress && (
<div className="absolute inset-0 bg-white/50 flex items-center justify-center">
<Loader2 className="h-8 w-8 text-blue-500 animate-spin" />
</div>
)}
</div>
))}
</div>
</div>
)}
{isUploading && (
<div className="space-y-2">
<div className="flex justify-between text-sm font-medium text-gray-500">
<span>Yükleniyor...</span>
<span>{progress} / {selectedFiles.length}</span>
</div>
<div className="h-2 w-full bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-blue-600 transition-all duration-300 ease-in-out"
style={{ width: `${(progress / selectedFiles.length) * 100}%` }}
/>
</div>
</div>
)}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => handleOpenChange(false)}
disabled={isUploading}
>
İptal
</Button>
<Button
type="button"
onClick={handleUpload}
disabled={isUploading || selectedFiles.length === 0}
className="min-w-[120px]"
>
{isUploading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Yükleniyor...
</>
) : (
<>
<UploadCloud className="mr-2 h-4 w-4" />
Yüklemeyi Başlat
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}