├── .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 |

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 |
136 |
137 | {uploading && (
138 |
139 |
140 |
141 | )}
142 |
143 |
144 |
150 |
151 |
{file.name}
152 |
153 | ))}
154 |
155 | >
156 | );
157 | }
158 |
--------------------------------------------------------------------------------