Skip to content

Instantly share code, notes, and snippets.

@JaumeGelabert
Last active July 3, 2025 00:25

Revisions

  1. JaumeGelabert revised this gist Nov 17, 2023. 2 changed files with 55 additions and 12 deletions.
    49 changes: 37 additions & 12 deletions UploadDragDrop.tsx
    Original 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 } from "@/lib/utils";
    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 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) => {
    @@ -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">
    <p>{file.name}</p>
    <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>
    <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">
    18 changes: 18 additions & 0 deletions utils.ts
    Original 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];
    }
  2. JaumeGelabert revised this gist Nov 17, 2023. 1 changed file with 9 additions and 2 deletions.
    11 changes: 9 additions & 2 deletions UploadDragDrop.tsx
    Original 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) => file.type.split("/")[0] !== "image")) {
    return setFileDropError("Please provide only image files to upload!");
    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]);
  3. JaumeGelabert revised this gist Nov 16, 2023. 1 changed file with 26 additions and 0 deletions.
    26 changes: 26 additions & 0 deletions useDragDrop.ts
    Original 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
    };
    }
  4. JaumeGelabert revised this gist Nov 16, 2023. 1 changed file with 117 additions and 30 deletions.
    147 changes: 117 additions & 30 deletions UploadDragDrop.tsx
    Original 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, Loader2, RotateCcw, Trash2, UploadCloud } from "lucide-react";
    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 [dragOver, setDragOver] = useState<boolean>(false);
    const [fileDropError, setFileDropError] = useState<string>("");

    const onDragOver = (e: React.SyntheticEvent) => {
    e.preventDefault();
    setDragOver(true);
    };
    const [isVideoValid, setIsVideoValid] = useState<boolean>(false);
    const [acceptedTypes, setAcceptedTypes] = useState<string>("images");

    const onDragLeave = () => setDragOver(false);
    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!");
    // 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 files</p>
    <p className="font-semibold mb-0">Upload {acceptedTypes}</p>
    <p className="text-sm text-neutral-500 -mt-1">
    Drag and drop your files. Will not be saved.
    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">
    JPEG, PNG formats. Up to 50 MB.
    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">
    <Image
    src={preview}
    alt="Preview"
    fill
    className="rounded-md h-full w-full border"
    style={{ objectFit: "cover" }}
    />
    {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 ? (
    <Image
    src={previewImage}
    alt="Preview"
    fill
    className="rounded-md h-full w-full border"
    style={{ objectFit: "cover" }}
    />
    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 && <p style={{ color: "red" }}>{fileDropError}</p>}
    {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
  5. JaumeGelabert revised this gist Nov 12, 2023. 1 changed file with 8 additions and 9 deletions.
    17 changes: 8 additions & 9 deletions UploadDragDrop.tsx
    Original file line number Diff line number Diff line change
    @@ -6,7 +6,6 @@ import {
    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";
    @@ -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 {
    dragOver,
    setDragOver,
    onDragOver,
    onDragLeave,
    fileDropError,
    setFileDropError
    } = useDragDrop();
    const onDragOver = (e: React.SyntheticEvent) => {
    e.preventDefault();
    setDragOver(true);
    };

    const onDragLeave = () => setDragOver(false);

    const onDrop = (e: React.DragEvent<HTMLLabelElement>) => {
    e.preventDefault();
  6. JaumeGelabert created this gist Nov 12, 2023.
    264 changes: 264 additions & 0 deletions UploadDragDrop.tsx
    Original 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>
    )}
    </>
    );
    }