├── .eslintrc.json ├── app ├── favicon.ico ├── fonts │ ├── GeistVF.woff │ └── GeistMonoVF.woff ├── lib │ ├── utils.ts │ └── pinata.ts ├── page.tsx ├── actions.ts ├── components │ ├── DeleteButton.tsx │ ├── Toast.tsx │ ├── Button.tsx │ └── Dropzone.tsx ├── hello │ └── page.tsx ├── api │ ├── files │ │ └── route.ts │ └── key │ │ └── route.ts ├── layout.tsx └── globals.css ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── next.config.ts ├── postcss.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── package.json ├── README.md └── tailwind.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ski043/uploadmarshal-15/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ski043/uploadmarshal-15/HEAD/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ski043/uploadmarshal-15/HEAD/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /app/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { ClassValue } from "clsx"; 3 | import { twMerge } from "tailwind-merge"; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | -------------------------------------------------------------------------------- /app/lib/pinata.ts: -------------------------------------------------------------------------------- 1 | "server only"; 2 | 3 | import { PinataSDK } from "pinata"; 4 | 5 | export const pinata = new PinataSDK({ 6 | pinataJwt: `${process.env.PINATA_JWT}`, 7 | pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`, 8 | }); 9 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Dropzone } from "./components/Dropzone"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |

7 | UploadMarshal 8 |

9 | 10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { pinata } from "./lib/pinata"; 4 | 5 | export async function deleteImage(fileId: string) { 6 | try { 7 | await pinata.files.delete([fileId]); 8 | 9 | return { 10 | success: true, 11 | }; 12 | } catch (error) { 13 | console.log(error); 14 | return { 15 | success: false, 16 | message: "Failed to delete file", 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /app/components/DeleteButton.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2, XIcon } from "lucide-react"; 2 | import { Button } from "./Button"; 3 | import { useFormStatus } from "react-dom"; 4 | 5 | export function DeleteButton() { 6 | const { pending } = useFormStatus(); 7 | return ( 8 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/hello/page.tsx: -------------------------------------------------------------------------------- 1 | import { pinata } from "../lib/pinata"; 2 | 3 | async function getData() { 4 | //fetch data from db 5 | 6 | const url = await pinata.gateways 7 | .createSignedURL({ 8 | cid: "xyz", 9 | expires: 500, 10 | }) 11 | .optimizeImage({ 12 | width: 500, 13 | height: 500, 14 | format: "webp", 15 | quality: 70, 16 | }); 17 | 18 | return url; 19 | } 20 | 21 | export default async function HelloRoute() { 22 | const data = await getData(); 23 | 24 | //1.4kb 25 | 26 | return ( 27 |
28 | image 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/api/files/route.ts: -------------------------------------------------------------------------------- 1 | import { pinata } from "@/app/lib/pinata"; 2 | import { NextResponse, NextRequest } from "next/server"; 3 | 4 | export async function POST(request: NextRequest) { 5 | try { 6 | const data = await request.formData(); 7 | const file: File | null = data.get("file") as unknown as File; 8 | const uploadData = await pinata.upload.file(file); 9 | const url = await pinata.gateways.createSignedURL({ 10 | cid: uploadData.cid, 11 | expires: 3600, 12 | }); 13 | return NextResponse.json(url, { status: 200 }); 14 | } catch (e) { 15 | console.log(e); 16 | return NextResponse.json( 17 | { error: "Internal Server Error" }, 18 | { status: 500 } 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/api/key/route.ts: -------------------------------------------------------------------------------- 1 | import { pinata } from "@/app/lib/pinata"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export const dynamic = "force-dynamic"; 5 | 6 | export async function GET() { 7 | try { 8 | const uuid = crypto.randomUUID(); 9 | const keyData = await pinata.keys.create({ 10 | keyName: uuid.toString(), 11 | permissions: { 12 | endpoints: { 13 | pinning: { 14 | pinFileToIPFS: true, 15 | }, 16 | }, 17 | }, 18 | maxUses: 1, 19 | }); 20 | return NextResponse.json(keyData, { status: 200 }); 21 | } catch (error) { 22 | console.log(error); 23 | return NextResponse.json( 24 | { text: "Error creating API Key:" }, 25 | { status: 500 } 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { Toaster } from "./components/Toast"; 5 | 6 | const geistSans = localFont({ 7 | src: "./fonts/GeistVF.woff", 8 | variable: "--font-geist-sans", 9 | weight: "100 900", 10 | }); 11 | const geistMono = localFont({ 12 | src: "./fonts/GeistMonoVF.woff", 13 | variable: "--font-geist-mono", 14 | weight: "100 900", 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Create Next App", 19 | description: "Generated by create next app", 20 | }; 21 | 22 | export default function RootLayout({ 23 | children, 24 | }: Readonly<{ 25 | children: React.ReactNode; 26 | }>) { 27 | return ( 28 | 29 | 32 | {children} 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/Toast.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner } from "sonner"; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme(); 10 | 11 | return ( 12 | 28 | ); 29 | }; 30 | 31 | export { Toaster }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uploadmarshal-15", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbo", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-icons": "^1.3.0", 13 | "@radix-ui/react-slot": "^1.1.0", 14 | "class-variance-authority": "^0.7.0", 15 | "clsx": "^2.1.1", 16 | "lucide-react": "^0.453.0", 17 | "next": "15.0.0", 18 | "next-themes": "^0.3.0", 19 | "pinata": "^1.6.0", 20 | "react": "19.0.0-rc-65a56d0e-20241020", 21 | "react-dom": "19.0.0-rc-65a56d0e-20241020", 22 | "react-dropzone": "^14.2.10", 23 | "sonner": "^1.5.0", 24 | "tailwind-merge": "^2.5.4", 25 | "tailwindcss-animate": "^1.0.7" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^20", 29 | "@types/react": "^18", 30 | "@types/react-dom": "^18", 31 | "eslint": "^8", 32 | "eslint-config-next": "15.0.0", 33 | "postcss": "^8", 34 | "tailwindcss": "^3.4.1", 35 | "typescript": "^5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy File Uploads with Pinata 🎉 2 | 3 | **File Uploads are hard, but after watching this video they will be super easy!** 4 | 5 | This project walks you through a complete file upload solution using Pinata's API, including both server-side and client-side uploads. You'll be building a custom drag-and-drop interface, handling file deletions, and optimizing images with ease. 6 | 7 | ## Features 8 | 9 | - 🎨 **Custom Dropzone with Drag and Drop** 10 | An interactive, user-friendly dropzone for smooth file uploads. 11 | 12 | - 🚀 **File Uploads (Server and Client-side)** 13 | Learn to handle both server-side and client-side uploads with @pinatacloud. 14 | 15 | - 🔒 **Temporary API Key (Presigned URLs)** 16 | Secure your uploads with temporary access using presigned URLs. 17 | 18 | - ❌ **File Deletion** 19 | Easily delete uploaded files when no longer needed. 20 | 21 | - 🖊️ **Signed URLs** 22 | Use signed URLs for secure file access. 23 | 24 | - 🖼️ **Image Optimization** 25 | Optimize images for faster loading and improved performance. 26 | 27 | ## Getting Started 28 | 29 | 1. **Clone the Repository** 30 | ```bash 31 | git clone https://github.com/yourusername/easy-file-uploads.git 32 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | }; 63 | export default config; 64 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 222.2 84% 4.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 222.2 84% 4.9%; 13 | --primary: 221.2 83.2% 53.3%; 14 | --primary-foreground: 210 40% 98%; 15 | --secondary: 210 40% 96.1%; 16 | --secondary-foreground: 222.2 47.4% 11.2%; 17 | --muted: 210 40% 96.1%; 18 | --muted-foreground: 215.4 16.3% 46.9%; 19 | --accent: 210 40% 96.1%; 20 | --accent-foreground: 222.2 47.4% 11.2%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 214.3 31.8% 91.4%; 24 | --input: 214.3 31.8% 91.4%; 25 | --ring: 221.2 83.2% 53.3%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 222.2 84% 4.9%; 36 | --foreground: 210 40% 98%; 37 | --card: 222.2 84% 4.9%; 38 | --card-foreground: 210 40% 98%; 39 | --popover: 222.2 84% 4.9%; 40 | --popover-foreground: 210 40% 98%; 41 | --primary: 217.2 91.2% 59.8%; 42 | --primary-foreground: 222.2 47.4% 11.2%; 43 | --secondary: 217.2 32.6% 17.5%; 44 | --secondary-foreground: 210 40% 98%; 45 | --muted: 217.2 32.6% 17.5%; 46 | --muted-foreground: 215 20.2% 65.1%; 47 | --accent: 217.2 32.6% 17.5%; 48 | --accent-foreground: 210 40% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 210 40% 98%; 51 | --border: 217.2 32.6% 17.5%; 52 | --input: 217.2 32.6% 17.5%; 53 | --ring: 224.3 76.3% 48%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | import { cn } from "../lib/utils"; 5 | 6 | const buttonVariants = cva( 7 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 12 | destructive: 13 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 14 | outline: 15 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 16 | secondary: 17 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 18 | ghost: "hover:bg-accent hover:text-accent-foreground", 19 | link: "text-primary underline-offset-4 hover:underline", 20 | }, 21 | size: { 22 | default: "h-10 px-4 py-2", 23 | sm: "h-9 rounded-md px-3", 24 | lg: "h-11 rounded-md px-8", 25 | icon: "h-10 w-10", 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: "default", 30 | size: "default", 31 | }, 32 | } 33 | ); 34 | 35 | export interface ButtonProps 36 | extends React.ButtonHTMLAttributes, 37 | VariantProps { 38 | asChild?: boolean; 39 | } 40 | 41 | const Button = React.forwardRef( 42 | ({ className, variant, size, asChild = false, ...props }, ref) => { 43 | const Comp = asChild ? Slot : "button"; 44 | return ( 45 | 50 | ); 51 | } 52 | ); 53 | Button.displayName = "Button"; 54 | 55 | export { Button, buttonVariants }; 56 | -------------------------------------------------------------------------------- /app/components/Dropzone.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useCallback, useState } from "react"; 4 | import { FileRejection, useDropzone } from "react-dropzone"; 5 | import { Button } from "./Button"; 6 | import { toast } from "sonner"; 7 | import { pinata } from "../lib/pinata"; 8 | import Image from "next/image"; 9 | import { Loader2, XIcon } from "lucide-react"; 10 | import { cn } from "../lib/utils"; 11 | import { deleteImage } from "../actions"; 12 | import { DeleteButton } from "./DeleteButton"; 13 | 14 | export function Dropzone() { 15 | const [files, setFiles] = useState< 16 | Array<{ file: File; uploading: boolean; id?: string }> 17 | >([]); 18 | 19 | const uploadFile = async (file: File) => { 20 | try { 21 | //we will upload everything right here... 22 | 23 | setFiles((prevFiles) => 24 | prevFiles.map((f) => (f.file === file ? { ...f, uploading: true } : f)) 25 | ); 26 | 27 | const keyRequest = await fetch("/api/key"); 28 | const keyData = await keyRequest.json(); 29 | 30 | const upload = await pinata.upload.file(file).key(keyData.JWT); 31 | 32 | setFiles((prevFiles) => 33 | prevFiles.map((f) => 34 | f.file === file ? { ...f, uploading: false, id: upload.id } : f 35 | ) 36 | ); 37 | 38 | toast.success(`File ${file.name} uploaded successfully`); 39 | } catch (error) { 40 | console.log(error); 41 | 42 | setFiles((prevFiles) => 43 | prevFiles.map((f) => (f.file === file ? { ...f, uploading: false } : f)) 44 | ); 45 | 46 | toast.error("Something went wrong"); 47 | } 48 | }; 49 | 50 | const removeFile = async (fielId: string, fielName: string) => { 51 | if (fielId) { 52 | const result = await deleteImage(fielId); 53 | 54 | if (result.success) { 55 | setFiles((prevFiles) => prevFiles.filter((f) => f.id !== fielId)); 56 | toast.success(`File ${fielName} deleted successfully`); 57 | } else { 58 | toast.error("Error deleting File..."); 59 | } 60 | } 61 | }; 62 | 63 | const onDrop = useCallback((acceptedFiles: File[]) => { 64 | if (acceptedFiles.length) { 65 | setFiles((prevFiles) => [ 66 | ...prevFiles, 67 | ...acceptedFiles.map((file) => ({ file, uploading: false })), 68 | ]); 69 | 70 | acceptedFiles.forEach(uploadFile); 71 | } 72 | }, []); 73 | 74 | const rejectedFiles = useCallback((fileRejection: FileRejection[]) => { 75 | if (fileRejection.length) { 76 | const toomanyFiles = fileRejection.find( 77 | (rejection) => rejection.errors[0].code === "too-many-files" 78 | ); 79 | 80 | const fileSizetoBig = fileRejection.find( 81 | (rejection) => rejection.errors[0].code === "file-too-large" 82 | ); 83 | 84 | if (toomanyFiles) { 85 | toast.error("Too many files selected, max is 5"); 86 | } 87 | 88 | if (fileSizetoBig) { 89 | toast.error("File size exceeds 5mb limit"); 90 | } 91 | } 92 | }, []); 93 | 94 | const { getRootProps, getInputProps, isDragActive } = useDropzone({ 95 | onDrop, 96 | onDropRejected: rejectedFiles, 97 | maxFiles: 5, 98 | maxSize: 1024 * 1024 * 5, // 5mb 99 | accept: { 100 | "image/*": [], 101 | }, 102 | }); 103 | 104 | return ( 105 | <> 106 |
111 | 112 | {isDragActive ? ( 113 |

Drop the files here ...

114 | ) : ( 115 |
116 |

Drag 'n' drop some files here, or click to select files

117 | 118 |
119 | )} 120 |
121 | 122 |
123 | {files.map(({ file, uploading, id }) => ( 124 |
125 |
126 | {file.name} 136 | 137 | {uploading && ( 138 |
139 | 140 |
141 | )} 142 |
143 | 144 |
removeFile(id!, file.name)} 146 | className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity" 147 | > 148 | 149 | 150 | 151 |

{file.name}

152 |
153 | ))} 154 |
155 | 156 | ); 157 | } 158 | --------------------------------------------------------------------------------