Skip to content

Instantly share code, notes, and snippets.

@msichterman
Forked from JaumeGelabert/UploadDragDrop.tsx
Created November 12, 2023 15:25
Show Gist options
  • Save msichterman/c7b66961a46c97c1ca0d16743bcd3cdb to your computer and use it in GitHub Desktop.
Save msichterman/c7b66961a46c97c1ca0d16743bcd3cdb to your computer and use it in GitHub Desktop.
"use client";
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog";
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] = useState<boolean>(false);
const [fileDropError, setFileDropError] = useState<string>("");
const onDragOver = (e: React.SyntheticEvent) => {
e.preventDefault();
setDragOver(true);
};
const onDragLeave = () => setDragOver(false);
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>
)}
</>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment