Last active
May 19, 2024 15:24
-
-
Save JaumeGelabert/2ea16a4649f4fdbe1a589cf5c23e0723 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use client"; | |
import { | |
Dialog, | |
DialogContent, | |
DialogTitle, | |
DialogTrigger | |
} from "@/components/ui/dialog"; | |
import useDragDrop from "@/hooks/useDragDrop"; | |
import { cn, formatBytes } from "@/lib/utils"; | |
import { | |
Expand, | |
File, | |
Loader2, | |
RotateCcw, | |
Trash2, | |
UploadCloud, | |
Video | |
} from "lucide-react"; | |
import Image from "next/image"; | |
import { useEffect, useState } from "react"; | |
import { Label } from "@/components/ui/label"; | |
import { Switch } from "@/components/ui/switch"; | |
import { | |
Tooltip, | |
TooltipContent, | |
TooltipProvider, | |
TooltipTrigger | |
} from "@/components/ui/tooltip"; | |
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 [isVideoValid, setIsVideoValid] = useState<boolean>(false); | |
const [acceptedTypes, setAcceptedTypes] = useState<string>("images"); | |
const [totalWeight, setTotalWeight] = useState<number>(0); | |
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); | |
// console.log the types of the files | |
console.log(selectedFiles.map((file) => file.type.split("/")[0])); | |
if ( | |
selectedFiles.some((file) => { | |
const fileType = file.type.split("/")[0]; | |
return isVideoValid | |
? fileType !== "image" && fileType !== "video" | |
: fileType !== "image"; | |
}) | |
) { | |
return setFileDropError("Invalid file type!"); | |
} | |
setFiles((prevFiles) => [...prevFiles, ...selectedFiles]); | |
setFileDropError(""); | |
}; | |
const fileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { | |
const selectedFiles = Array.from(e.target.files as FileList); | |
if ( | |
selectedFiles.some((file) => { | |
const fileType = file.type.split("/")[0]; | |
return isVideoValid | |
? fileType !== "image" && fileType !== "video" | |
: fileType !== "image"; | |
}) | |
) { | |
return setFileDropError("Invalid file type!"); | |
} | |
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 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); | |
} | |
}); | |
setTotalWeight(files.reduce((acc, file) => acc + file.size, 0)); | |
console.log(files); | |
console.log(files.length); | |
}, [files]); | |
useEffect(() => { | |
if (isVideoValid) { | |
setAcceptedTypes("images and videos"); | |
} else { | |
setAcceptedTypes("images"); | |
} | |
}, [isVideoValid]); | |
return ( | |
<> | |
{/* File type selection */} | |
<div className="items-center space-x-2 w-full max-w-lg flex flex-row justify-end mb-4"> | |
<Switch | |
id="allow-video" | |
defaultChecked={isVideoValid} | |
onCheckedChange={() => { | |
console.log(!isVideoValid); | |
setIsVideoValid(!isVideoValid); | |
}} | |
/> | |
<Label | |
htmlFor="allow-video" | |
className={cn( | |
"transition-all hover:cursor-pointer", | |
isVideoValid ? "text-black" : "text-neutral-400" | |
)} | |
> | |
Allow videos | |
</Label> | |
</div> | |
{/* Uploader */} | |
<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 {acceptedTypes}</p> | |
<p className="text-sm text-neutral-500 -mt-1"> | |
Drag and drop your {acceptedTypes}. 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"> | |
Only {acceptedTypes}. 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"> | |
<div className="w-full flex flex-row justify-end items-center"> | |
<p className="bg-neutral-100 px-2 text-sm py-1 rounded-full text-neutral-500"> | |
{files.length} {files.length === 1 ? "file" : "files"},{" "} | |
{formatBytes(totalWeight)} | |
</p> | |
</div> | |
{files.map((file, index) => { | |
const isLoading = loadingState[file.name]; | |
const preview = imagePreviews[file.name]; | |
// Check if file is an image | |
const isImage = (file: string) => { | |
return file.match(/image.*/); | |
}; | |
// Check if file is a video | |
const isVideo = (file: string) => { | |
return file.match(/video.*/); | |
}; | |
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"> | |
{isImage(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> | |
)} | |
{isVideo(preview) && ( | |
<div className="relative h-10 w-10 flex flex-row justify-center items-center border rounded-lg text-neutral-500 hover:text-neutral-700 transition-all"> | |
<Video className="h-5 w-5" /> | |
</div> | |
)} | |
</div> | |
) | |
)} | |
</div> | |
<div className="flex flex-col justify-start items-start gap-1"> | |
<div className="flex flex-row justify-start items-center gap-2"> | |
<div className="max-w-[300px] truncate"> | |
<TooltipProvider> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<p className="truncate hover:cursor-help"> | |
{file.name} | |
</p> | |
</TooltipTrigger> | |
<TooltipContent> | |
<p>{file.name}</p> | |
</TooltipContent> | |
</Tooltip> | |
</TooltipProvider> | |
</div> | |
</div> | |
<div className="flex flex-row justify-start items-center gap-2"> | |
<p className="text-xs text-neutral-500"> | |
{formatBytes(file.size)} | |
</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> | |
</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 ? ( | |
isImage(previewImage) ? ( | |
<Image | |
src={previewImage} | |
alt="Image Preview" | |
fill | |
className="rounded-md h-full w-full border" | |
style={{ objectFit: "cover" }} | |
/> | |
) : isVideo(previewImage) ? ( | |
<video | |
src={previewImage} | |
controls | |
className="rounded-md h-full w-full border" | |
style={{ objectFit: "cover" }} | |
/> | |
) : null | |
) : ( | |
<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 && ( | |
<div className="bg-orange-50 py-1 mx-2 rounded-lg text-center my-2 border border-orange-200 flex flex-row justify-center items-center gap-2"> | |
<File className="h-4 w-4 text-orange-400" /> | |
<p className="text-orange-400 text-sm font-medium"> | |
{fileDropError} | |
</p> | |
</div> | |
)} | |
</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> | |
)} | |
</> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use client"; | |
import { useState } from "react"; | |
export default function useDragDrop() { | |
const [dragOver, setDragOver] = useState<boolean>(false); | |
const [fileDropError, setFileDropError] = useState<string>(""); | |
const onDragOver = (e: React.SyntheticEvent) => { | |
e.preventDefault(); | |
setDragOver(true); | |
}; | |
const onDragLeave = () => setDragOver(false); | |
return { | |
// Drag | |
dragOver, | |
setDragOver, | |
onDragOver, | |
onDragLeave, | |
// Errors | |
fileDropError, | |
setFileDropError | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { type ClassValue, clsx } from "clsx"; | |
import { twMerge } from "tailwind-merge"; | |
export function cn(...inputs: ClassValue[]) { | |
return twMerge(clsx(inputs)); | |
} | |
export function formatBytes(bytes: number, decimals: number = 2): string { | |
if (bytes === 0) return "0 Bytes"; | |
const k = 1024; | |
const dm = decimals < 0 ? 0 : decimals; | |
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; | |
const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment