Last active
July 3, 2025 00:25
Revisions
-
JaumeGelabert revised this gist
Nov 17, 2023 . 2 changed files with 55 additions and 12 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -7,7 +7,7 @@ import { DialogTrigger } from "@/components/ui/dialog"; import useDragDrop from "@/hooks/useDragDrop"; import { cn, formatBytes } from "@/lib/utils"; import { Expand, File, @@ -21,6 +21,13 @@ 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>({}); @@ -30,6 +37,7 @@ export default function UploadComponent() { ); const [isVideoValid, setIsVideoValid] = useState<boolean>(false); const [acceptedTypes, setAcceptedTypes] = useState<string>("images"); const [totalWeight, setTotalWeight] = useState<number>(0); const { dragOver, @@ -98,13 +106,6 @@ export default function UploadComponent() { setFiles(files.filter((file) => file.name !== fileName)); }; const handlePreview = (file: File) => { const reader = new FileReader(); reader.onload = (e: any) => { @@ -131,6 +132,9 @@ export default function UploadComponent() { simulateLoading(file); } }); setTotalWeight(files.reduce((acc, file) => acc + file.size, 0)); console.log(files); console.log(files.length); }, [files]); useEffect(() => { @@ -224,6 +228,12 @@ export default function UploadComponent() { {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]; @@ -273,17 +283,32 @@ export default function UploadComponent() { </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"> This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,18 @@ 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]; } -
JaumeGelabert revised this gist
Nov 17, 2023 . 1 changed file with 9 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -67,8 +67,15 @@ export default function UploadComponent() { 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]); -
JaumeGelabert revised this gist
Nov 16, 2023 . 1 changed file with 26 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,26 @@ "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 }; } -
JaumeGelabert revised this gist
Nov 16, 2023 . 1 changed file with 117 additions and 30 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -6,36 +6,58 @@ import { DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import useDragDrop from "@/hooks/useDragDrop"; import { cn } 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"; 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 { 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]); @@ -104,18 +126,49 @@ export default function UploadComponent() { }); }, [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> @@ -144,7 +197,7 @@ export default function UploadComponent() { 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 @@ -168,6 +221,15 @@ export default function UploadComponent() { 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} @@ -182,13 +244,22 @@ export default function UploadComponent() { ) : ( 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> ) )} @@ -222,13 +293,22 @@ export default function UploadComponent() { <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" /> )} @@ -247,7 +327,14 @@ export default function UploadComponent() { })} </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 -
JaumeGelabert revised this gist
Nov 12, 2023 . 1 changed file with 8 additions and 9 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -6,7 +6,6 @@ import { 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"; @@ -19,15 +18,15 @@ export default function UploadComponent() { 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(); -
JaumeGelabert created this gist
Nov 12, 2023 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -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> )} </> ); }