|
|
@@ -0,0 +1,264 @@ |
|
|
"use client"; |
|
|
|
|
|
import { |
|
|
Dialog, |
|
|
DialogContent, |
|
|
DialogTitle, |
|
|
DialogTrigger |
|
|
} from "@/components/ui/dialog"; |
|
|
import useDragDrop from "@/hooks/useDragDrop"; |
|
|
import { cn } from "@/lib/utils"; |
|
|
import { Expand, Loader2, RotateCcw, Trash2, UploadCloud } from "lucide-react"; |
|
|
import Image from "next/image"; |
|
|
import { useEffect, useState } from "react"; |
|
|
|
|
|
export default function UploadComponent() { |
|
|
const [files, setFiles] = useState<File[]>([]); |
|
|
const [loadingState, setLoadingState] = useState<any>({}); |
|
|
const [previewImage, setPreviewImage] = useState<any>(null); |
|
|
const [imagePreviews, setImagePreviews] = useState<{ [key: string]: string }>( |
|
|
{} |
|
|
); |
|
|
|
|
|
const { |
|
|
dragOver, |
|
|
setDragOver, |
|
|
onDragOver, |
|
|
onDragLeave, |
|
|
fileDropError, |
|
|
setFileDropError |
|
|
} = useDragDrop(); |
|
|
|
|
|
const onDrop = (e: React.DragEvent<HTMLLabelElement>) => { |
|
|
e.preventDefault(); |
|
|
setDragOver(false); |
|
|
|
|
|
const selectedFiles = Array.from(e.dataTransfer.files); |
|
|
|
|
|
if (selectedFiles.some((file) => file.type.split("/")[0] !== "image")) { |
|
|
return setFileDropError("Please provide only image files to upload!"); |
|
|
} |
|
|
|
|
|
setFiles((prevFiles) => [...prevFiles, ...selectedFiles]); |
|
|
setFileDropError(""); |
|
|
}; |
|
|
|
|
|
const fileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
const selectedFiles = Array.from(e.target.files as FileList); |
|
|
|
|
|
if (selectedFiles.some((file) => file.type.split("/")[0] !== "image")) { |
|
|
return setFileDropError("Please provide only image files to upload!"); |
|
|
} |
|
|
|
|
|
setFiles((prevFiles) => [...prevFiles, ...selectedFiles]); |
|
|
setFileDropError(""); |
|
|
}; |
|
|
|
|
|
const simulateLoading = (file: File) => { |
|
|
// Calcula la duración del temporizador en milisegundos |
|
|
const duration = Math.max(1000, Math.min(file.size / 750, 4000)); |
|
|
|
|
|
setLoadingState((prev: any) => ({ ...prev, [file.name]: true })); |
|
|
|
|
|
setTimeout(() => { |
|
|
setLoadingState((prev: any) => ({ ...prev, [file.name]: false })); |
|
|
}, duration); |
|
|
}; |
|
|
|
|
|
const handleDelete = (fileName: string) => { |
|
|
// Filtrar los archivos para eliminar el seleccionado |
|
|
setFiles(files.filter((file) => file.name !== fileName)); |
|
|
}; |
|
|
|
|
|
const formatNumberWithDots = (number: number): string => { |
|
|
const numStr = number.toString(); |
|
|
const reversedStr = numStr.split("").reverse().join(""); |
|
|
const withDots = reversedStr.replace(/(\d{3})(?=\d)/g, "$1."); |
|
|
return withDots.split("").reverse().join(""); |
|
|
}; |
|
|
|
|
|
const handlePreview = (file: File) => { |
|
|
const reader = new FileReader(); |
|
|
reader.onload = (e: any) => { |
|
|
setPreviewImage(e.target.result); |
|
|
}; |
|
|
reader.readAsDataURL(file); |
|
|
}; |
|
|
|
|
|
const generatePreview = (file: File) => { |
|
|
const reader = new FileReader(); |
|
|
reader.onloadend = () => { |
|
|
setImagePreviews((prev) => ({ |
|
|
...prev, |
|
|
[file.name]: reader.result as string |
|
|
})); |
|
|
}; |
|
|
reader.readAsDataURL(file); |
|
|
}; |
|
|
|
|
|
useEffect(() => { |
|
|
files.forEach((file) => { |
|
|
if (loadingState[file.name] === undefined) { |
|
|
generatePreview(file); |
|
|
simulateLoading(file); |
|
|
} |
|
|
}); |
|
|
}, [files]); |
|
|
|
|
|
return ( |
|
|
<> |
|
|
<div className="dark:bg-neutral-800 bg-white border dark:border-neutral-700 w-full max-w-lg rounded-xl"> |
|
|
<div className="border-b dark:border-neutral-700"> |
|
|
<div className="flex flex-row justify-start items-center px-4 py-2 gap-2"> |
|
|
<div className="rounded-full border p-2 flex flex-row justify-center items-center dark:border-neutral-700"> |
|
|
<UploadCloud className="h-5 w-5 text-neutral-600" /> |
|
|
</div> |
|
|
<div> |
|
|
<p className="font-semibold mb-0">Upload files</p> |
|
|
<p className="text-sm text-neutral-500 -mt-1"> |
|
|
Drag and drop your files. Will not be saved. |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<form> |
|
|
<label |
|
|
htmlFor="file" |
|
|
onDragOver={onDragOver} |
|
|
onDragLeave={onDragLeave} |
|
|
onDrop={onDrop} |
|
|
> |
|
|
<div |
|
|
className={cn( |
|
|
"px-4 py-2 border-[1.5px] border-dashed dark:border-neutral-700 m-2 rounded-xl flex flex-col justify-start items-center hover:cursor-pointer", |
|
|
dragOver && "border-blue-600 bg-blue-50" |
|
|
)} |
|
|
> |
|
|
<div className="flex flex-col justify-start items-center"> |
|
|
<UploadCloud |
|
|
className={cn( |
|
|
"h-5 w-5 text-neutral-600 my-4", |
|
|
dragOver && "text-blue-500" |
|
|
)} |
|
|
/> |
|
|
<p className="font-semibold"> |
|
|
Choose a file or drag & drop it here |
|
|
</p> |
|
|
<p className="text-neutral-500 text-sm"> |
|
|
JPEG, PNG formats. Up to 50 MB. |
|
|
</p> |
|
|
<div className="px-3 py-1 border dark:border-neutral-700 rounded-xl mt-4 mb-2 drop-shadow-sm hover:drop-shadow transition-all hover:cursor-pointer bg-white dark:bg-neutral-700"> |
|
|
Select files |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</label> |
|
|
<input |
|
|
type="file" |
|
|
name="file" |
|
|
id="file" |
|
|
className="hidden" |
|
|
onChange={fileSelect} |
|
|
multiple |
|
|
/> |
|
|
</form> |
|
|
|
|
|
{files.length > 0 && ( |
|
|
<div className="w-full px-4 py-2 gap-2 flex flex-col justify-start items-center border-t dark:border-neutral-700 max-h-52 overflow-auto"> |
|
|
{files.map((file, index) => { |
|
|
const isLoading = loadingState[file.name]; |
|
|
const preview = imagePreviews[file.name]; |
|
|
|
|
|
return ( |
|
|
<div |
|
|
key={index} |
|
|
className="flex flex-row justify-between items-center border dark:border-neutral-700 rounded-lg px-2 py-1 w-full group" |
|
|
> |
|
|
<div className="flex flex-row justify-start items-center gap-2"> |
|
|
<div> |
|
|
{isLoading ? ( |
|
|
<div className="flex flex-row justify-center items-center gap-2 h-10 w-10 border rounded-md"> |
|
|
<Loader2 className="h-4 w-4 animate-spin text-neutral-500" /> |
|
|
</div> |
|
|
) : ( |
|
|
preview && ( |
|
|
<div className="relative h-10 w-10"> |
|
|
<Image |
|
|
src={preview} |
|
|
alt="Preview" |
|
|
fill |
|
|
className="rounded-md h-full w-full border" |
|
|
style={{ objectFit: "cover" }} |
|
|
/> |
|
|
</div> |
|
|
) |
|
|
)} |
|
|
</div> |
|
|
<div className="flex flex-col justify-start items-start gap-1"> |
|
|
<div className="flex flex-row justify-start items-center gap-2"> |
|
|
<p>{file.name}</p> |
|
|
{!isLoading && ( |
|
|
<div className="flex flex-row justify-start items-center text-xs rounded-full px-2 py-[0.5px] gap-1"> |
|
|
<div className="h-2 w-2 bg-green-400 rounded-full" /> |
|
|
<p className="text-neutral-500">Uploaded</p> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
<p className="text-xs text-neutral-500"> |
|
|
{formatNumberWithDots(file.size)} Bytes |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
<div className="flex flex-row justify-end items-center gap-2"> |
|
|
<Dialog> |
|
|
<DialogTrigger asChild> |
|
|
<button |
|
|
onClick={() => handlePreview(file)} |
|
|
className="text-neutral-400 hidden group-hover:flex flex-row justify-end bg-neutral-100 p-1 rounded-lg hover:text-black transition-all hover:cursor-pointer" |
|
|
> |
|
|
<Expand className="h-4 w-4" /> |
|
|
</button> |
|
|
</DialogTrigger> |
|
|
<DialogContent> |
|
|
<DialogTitle>{file.name}</DialogTitle> |
|
|
<div className="bg-neutral-100 rounded-xl relative w-full min-h-[300px] h-full flex flex-col justify-center items-center "> |
|
|
{previewImage ? ( |
|
|
<Image |
|
|
src={previewImage} |
|
|
alt="Preview" |
|
|
fill |
|
|
className="rounded-md h-full w-full border" |
|
|
style={{ objectFit: "cover" }} |
|
|
/> |
|
|
) : ( |
|
|
<Loader2 className="h-4 w-4 animate-spin text-neutral-500" /> |
|
|
)} |
|
|
</div> |
|
|
</DialogContent> |
|
|
</Dialog> |
|
|
<button |
|
|
className="text-neutral-400 hidden group-hover:flex flex-row justify-end bg-neutral-100 p-1 rounded-lg hover:text-black transition-all hover:cursor-pointer" |
|
|
onClick={() => handleDelete(file.name)} |
|
|
> |
|
|
<Trash2 className="h-4 w-4" /> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
})} |
|
|
</div> |
|
|
)} |
|
|
{fileDropError && <p style={{ color: "red" }}>{fileDropError}</p>} |
|
|
</div> |
|
|
{files.length > 0 && ( |
|
|
<p |
|
|
className="text-sm mt-4 rounded-full px-4 py-1 hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:cursor-pointer transition-all border text-neutral-500 hover:text-black" |
|
|
onClick={() => setFiles([])} |
|
|
> |
|
|
<RotateCcw className="inline-block h-4 w-4 mr-1" /> |
|
|
Reset |
|
|
</p> |
|
|
)} |
|
|
</> |
|
|
); |
|
|
} |