File Storage UI Component
A complete file storage solution for Next.js applications with support for AWS S3 and Cloudflare R2.
File Storage Registry Component
A complete file storage solution for Next.js applications with support for AWS S3 and Cloudflare R2. Built as a shadcn/ui registry component for easy installation and customization.
Features
- Multi-Provider Support: Works with both AWS S3 and Cloudflare R2
- Dropzone Component: Beautiful drag-and-drop file uploads with 5 variants
- Progress Tracking: Real-time upload progress with XHR
- File Management: Track, list, and delete uploaded files
- Presigned URLs: Secure direct-to-storage uploads
- Type Safe: Full TypeScript support
- Database Integration: Prisma models for file metadata tracking
Quick Install
pnpm dlx shadcn@latest add https://file-storage-registry.vercel.app/r/file-storage.jsonWhat Gets Installed
your-project/
├── app/
│ ├── (example)/
│ │ ├── categories/ # /categories - List categories with image upload
│ │ │ └── page.tsx
│ │ └── file-storage/ # /file-storage - Track files & storage stats
│ │ └── page.tsx
│ └── api/
│ ├── s3/
│ │ ├── upload/route.ts # S3 presigned URL generation
│ │ └── delete/route.ts # S3 file deletion
│ ├── r2/
│ │ ├── upload/route.ts # R2 presigned URL generation
│ │ └── delete/route.ts # R2 file deletion
│ └── v1/
│ ├── categories/ # Category CRUD endpoints
│ └── files/ # File listing & stats endpoints
├── components/
│ ├── ui/
│ │ ├── dropzone.tsx # Main dropzone component (5 variants)
│ │ └── error-display.tsx # Error display component
│ └── file-storage/
│ ├── categories/ # Category management components
│ │ ├── Categories.tsx
│ │ ├── CategoryForm.tsx
│ │ └── DeleteCategoryButton.tsx
│ └── files/ # File management components
│ ├── Files.tsx
│ └── DeleteFileButton.tsx
├── lib/
│ ├── s3Client.ts # AWS S3 client configuration
│ ├── r2Client.ts # Cloudflare R2 client configuration
│ ├── prisma.ts # Prisma client singleton
│ ├── fileDataExtractor.ts # URL metadata extraction
│ ├── getNormalDate.ts # Date formatting utility
│ └── api/
│ ├── categories/ # Category API functions
│ └── files/ # File API functions
└── prisma/
└── schema.prisma.example # Prisma models to add
Installation Guide
Step 1: Install the Component
pnpm dlx shadcn@latest add https://file-storage-registry.vercel.app/r/file-storage.jsonStep 2: Install Dependencies
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner uuid
pnpm add -D @types/uuidStep 3: Set Up Prisma (if not already configured)
If you don't have Prisma set up yet:
pnpm add -D prisma @prisma/client
pnpm dlx prisma initStep 4: Add Prisma Models
Open prisma/schema.prisma.example and copy the models to your existing schema.prisma:
// Add these models to your existing schema.prisma file
model File {
id String @id @default(cuid())
name String
size Int
publicUrl String
type String
key String @unique
provider StorageProvider @default(cloudflare)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum StorageProvider {
aws
cloudflare
}
// Optional: Category model for the demo
model Category {
id String @id @default(cuid())
name String
slug String @unique
image String
description String?
isFeatured Boolean @default(false)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Then run:
pnpm dlx prisma generate
pnpm dlx prisma db pushStep 5: Configure Environment Variables
Add these to your .env file:
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
# AWS S3 (if using S3)
AWS_S3_REGION="us-east-1"
AWS_S3_BUCKET_NAME="your-bucket-name"
AWS_S3_ACCESS_KEY_ID="your-access-key"
AWS_S3_SECRET_ACCESS_KEY="your-secret-key"
# Cloudflare R2 (if using R2)
CLOUDFLARE_R2_ACCESS_KEY_ID="your-r2-access-key"
CLOUDFLARE_R2_SECRET_ACCESS_KEY="your-r2-secret-key"
CLOUDFLARE_R2_ENDPOINT="https://your-account-id.r2.cloudflarestorage.com"
CLOUDFLARE_R2_BUCKET_NAME="your-bucket-name"
CLOUDFLARE_R2_PUBLIC_DEV_URL="https://pub-xxx.r2.dev"
# API URL
NEXT_PUBLIC_API_URL="http://localhost:3000"Usage
Basic Dropzone
import { Dropzone } from "@/components/ui/dropzone";
export default function MyComponent() {
const handleUploadComplete = (url: string) => {
console.log("File uploaded:", url);
};
return (
<Dropzone
provider="r2" // or "s3"
onUploadComplete={handleUploadComplete}
maxSize={5 * 1024 * 1024} // 5MB
accept={{
"image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"],
}}
/>
);
}Dropzone Variants
The dropzone comes with 5 built-in variants:
// Default - Full featured with icon and description
<Dropzone variant="default" provider="r2" onUploadComplete={handleUpload} />
// Compact - Smaller with less padding
<Dropzone variant="compact" provider="r2" onUploadComplete={handleUpload} />
// Minimal - Just text, very compact
<Dropzone variant="minimal" provider="r2" onUploadComplete={handleUpload} />
// Avatar - Circular for profile pictures
<Dropzone variant="avatar" provider="r2" onUploadComplete={handleUpload} />
// Inline - Horizontal layout
<Dropzone variant="inline" provider="r2" onUploadComplete={handleUpload} />With Preview and Remove
import { useState } from "react";
import { Dropzone } from "@/components/ui/dropzone";
export default function ImageUploader() {
const [imageUrl, setImageUrl] = useState<string>("");
return (
<Dropzone
provider="r2"
value={imageUrl}
onUploadComplete={setImageUrl}
onRemove={() => setImageUrl("")}
showPreview
/>
);
}With Form Integration (React Hook Form)
import { useForm } from "react-hook-form";
import { Dropzone } from "@/components/ui/dropzone";
export default function ProductForm() {
const form = useForm({
defaultValues: {
name: "",
image: "",
},
});
const imageUrl = form.watch("image");
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Dropzone
provider="r2"
value={imageUrl}
onUploadComplete={(url) => form.setValue("image", url)}
onRemove={() => form.setValue("image", "")}
showPreview
/>
{/* other form fields */}
</form>
);
}API Routes
Upload Presigned URL
S3: POST /api/s3/upload
R2: POST /api/r2/upload
Request:
{
"filename": "image.png",
"contentType": "image/png"
}Response:
{
"presignedUrl": "https://...",
"publicUrl": "https://...",
"key": "unique-file-key"
}Delete File
S3: DELETE /api/s3/delete
R2: DELETE /api/r2/delete
Request:
{
"key": "file-key-to-delete"
}List Files
GET /api/v1/files
Returns all tracked files with metadata.
Storage Stats
GET /api/v1/files/stats
Returns storage statistics:
{
"totalFiles": 42,
"totalSize": 157286400,
"byProvider": {
"cloudflare": { "count": 30, "size": 100000000 },
"aws": { "count": 12, "size": 57286400 }
}
}Storage Provider Setup
AWS S3
- Create an S3 bucket in AWS Console
- Configure bucket for public access (or use CloudFront)
- Create IAM credentials with S3 access
- Add CORS configuration:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedOrigins": ["http://localhost:3000", "https://yourdomain.com"],
"ExposeHeaders": ["ETag"]
}
]Cloudflare R2
- Create an R2 bucket in Cloudflare Dashboard
- Enable public access via R2.dev subdomain or custom domain
- Generate API tokens with read/write permissions
- Add CORS policy in bucket settings
File Metadata Extraction
The component automatically tracks file metadata by encoding it in the URL:
https://storage.example.com/file.png?name=file.png&size=1024&type=image/png&key=abc123
This allows reconstruction of file metadata when saving to the database:
import { extractFileDataFromUrl } from "@/lib/fileDataExtractor";
const fileData = extractFileDataFromUrl(imageUrl);
// { name: "file.png", size: 1024, type: "image/png", key: "abc123", publicUrl: "...", provider: "cloudflare" }Example Pages Included
After installation, you get 2 ready-to-use example pages:
/categories - Categories Page
- List all categories with images in a responsive grid
- Create new categories with image upload using the Dropzone
- Edit existing categories
- Delete categories (automatically removes files from storage)
- Pagination support
/file-storage - File Storage Dashboard
- Track all uploaded files across your application
- View total storage space used (formatted in KB/MB/GB)
- See provider breakdown (how much is stored in S3 vs R2)
- View file details: name, size, type, provider, upload date
- Delete files directly with confirmation modal
Dependencies
@aws-sdk/client-s3- AWS S3 SDK@aws-sdk/s3-request-presigner- Presigned URL generation@tanstack/react-query- Data fetching and cachingreact-dropzone- Drag and drop file handlingreact-hook-form- Form managementzod- Schema validationuuid- Unique ID generationprisma/@prisma/client- Database ORM
TypeScript Support
All components and utilities are fully typed. Key types:
interface DropzoneProps {
provider: "s3" | "r2";
onUploadComplete: (url: string) => void;
onRemove?: () => void;
value?: string;
showPreview?: boolean;
variant?: "default" | "compact" | "minimal" | "avatar" | "inline";
maxSize?: number;
accept?: Accept;
disabled?: boolean;
className?: string;
}
interface FileData {
name: string;
size: number;
publicUrl: string;
type: string;
key: string;
provider: "aws" | "cloudflare";
}License
MIT
Contributing
Contributions are welcome! Please open an issue or submit a pull request.

