# File Upload
Drag-and-drop file uploader for Next.js 14 — Supabase Storage with signed URLs, type/size validation, simulated progress bar, image previews, multi-file support, and a file list component with download and delete.
## What's included
**Core functions**
- `uploadFile(userId, file, config?)` — validates type and size, uploads to `{userId}/{folder}/{timestamp}-{random}.{ext}`, generates a signed URL, returns a typed `UploadedFile`
- `uploadAvatar(userId, file)` — public bucket variant; uploads to `avatars/{userId}/avatar.{ext}` with upsert; returns the public URL
- `deleteFile(path, bucket?)` — removes a file from Storage by path
- `refreshSignedUrl(path, expiresIn?, bucket?)` — generates a fresh signed URL for an existing path
- `listUserFiles(userId, bucket?)` — lists files under the user's folder; returns `{ name, path, size }[]`
- `formatFileSize(bytes)` — formats to B / KB / MB
**UI components**
- `FileDropzone` — drag-and-drop zone with click-to-browse fallback; shows simulated progress bar; fires image `Object URL` previews locally before upload completes; accepts `userId`, `onUpload`, `onError`, `config`, `multiple`, `disabled`
- `FileList` — renders uploaded files as a list; shows image thumbnails for image types, `📄` for others; Download link + optional Delete button per row
**Config & types**
- `UploadConfig` — `bucket`, `maxSizeMB`, `allowedTypes`, `signedUrlTTL`, `folder`
- `UploadedFile` — `name`, `path`, `size`, `type`, `url`, `isImage`, `preview?`
- `DEFAULT_CONFIG` — bucket `uploads`, 10MB max, images + PDF, 7-day signed URLs
## Setup
### 1. Install dependencies
```bash
npm install @supabase/supabase-js
```
### 2. Environment variables
```
NEXT_PUBLIC_SUPABASE_URL=your Supabase project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=anon public key
```
### 3. Storage & database
In the Supabase dashboard: **Storage → New Bucket → name `uploads` → Private**.
```sql
CREATE POLICY "upload_own_folder" ON storage.objects
FOR INSERT WITH CHECK (
bucket_id = 'uploads' AND
auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "read_own_files" ON storage.objects
FOR SELECT USING (
bucket_id = 'uploads' AND
auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "delete_own_files" ON storage.objects
FOR DELETE USING (
bucket_id = 'uploads' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- Optional: track uploads in DB
CREATE TABLE IF NOT EXISTS user_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
bucket TEXT NOT NULL DEFAULT 'uploads',
path TEXT NOT NULL,
name TEXT NOT NULL,
size BIGINT NOT NULL,
mime_type TEXT NOT NULL,
url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, path)
);
ALTER TABLE user_files ENABLE ROW LEVEL SECURITY;
CREATE POLICY "files_own" ON user_files FOR ALL USING (user_id::text = auth.uid()::text);
```
## Usage examples
```tsx
// Basic dropzone — single file
'use client'
import { useState } from 'react'
import { FileDropzone, FileList, UploadedFile } from '@/blocks/fileupload'
export function AttachmentSection({ userId }: { userId: string }) {
const [files, setFiles] = useState<UploadedFile[]>([])
return (
<>
<FileDropzone
userId={userId}
onUpload={f => setFiles(prev => [...prev, f])}
onError={msg => console.error(msg)}
config={{ maxSizeMB: 5, allowedTypes: ['image/jpeg', 'image/png', 'application/pdf'] }}
/>
<FileList files={files} onDelete={path => setFiles(prev => prev.filter(f => f.path !== path))} />
</>
)
}
```
```tsx
// Multi-file upload with custom folder
<FileDropzone
userId={userId}
multiple
onUpload={f => setFiles(prev => [...prev, f])}
config={{ folder: 'documents', maxSizeMB: 20, signedUrlTTL: 3600 }}
/>
```
```ts
// Upload programmatically (e.g. from a form submission)
import { uploadFile, deleteFile } from '@/blocks/fileupload'
const uploaded = await uploadFile(userId, formData.get('file') as File, { folder: 'invoices' })
// uploaded.url → signed URL valid for 7 days
// uploaded.path → 'userId/invoices/1712345678-abc123.pdf'
// Later, to delete:
await deleteFile(uploaded.path)
```
## Notes
- The progress bar is simulated — Supabase Storage v2 does not expose upload progress events; the ticker increments to ~85% and jumps to 100% on completion; replace with a real XHR if accurate progress matters
- Signed URLs expire — `signedUrlTTL` defaults to 7 days; if you store `url` in your database you'll need to call `refreshSignedUrl(path)` before serving it to users after expiry; consider storing `path` instead of `url` and generating URLs on demand
- `FileDropzone` creates `Object URL` previews for images via `URL.createObjectURL` and revokes them on unmount — if you unmount the component before the user navigates away the previews will break; pass `preview` from `UploadedFile` (the signed URL) to `FileList` instead for persistent previews
- `uploadAvatar` writes to a separate `avatars` bucket, not `uploads` — make sure that bucket exists and has its own RLS policies; it's the same bucket used by the User Profile block