# 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([]) return ( <> setFiles(prev => [...prev, f])} onError={msg => console.error(msg)} config={{ maxSizeMB: 5, allowedTypes: ['image/jpeg', 'image/png', 'application/pdf'] }} /> setFiles(prev => prev.filter(f => f.path !== path))} /> ) } ``` ```tsx // Multi-file upload with custom folder 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