'use client'; // File List Manager for Vercel Blob // Display uploaded files with delete and download actions import { useState, useEffect } from 'react'; import { list, del, type ListBlobResult } from '@vercel/blob'; interface BlobFile { url: string; pathname: string; size: number; uploadedAt: Date; } export function FileListManager({ prefix = '' }: { prefix?: string }) { const [files, setFiles] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(''); const [deletingFiles, setDeletingFiles] = useState>(new Set()); const [cursor, setCursor] = useState(); const [hasMore, setHasMore] = useState(false); // Load files const loadFiles = async (loadMore = false) => { setIsLoading(true); setError(''); try { const response = await fetch(`/api/files?prefix=${prefix}&cursor=${cursor || ''}`); if (!response.ok) { throw new Error('Failed to load files'); } const data: { blobs: BlobFile[]; cursor?: string; hasMore: boolean } = await response.json(); if (loadMore) { setFiles((prev) => [...prev, ...data.blobs]); } else { setFiles(data.blobs); } setCursor(data.cursor); setHasMore(data.hasMore); } catch (err) { console.error('Failed to load files:', err); setError(err instanceof Error ? err.message : 'Failed to load files'); } finally { setIsLoading(false); } }; // Initial load useEffect(() => { loadFiles(); }, [prefix]); // Delete file const handleDelete = async (url: string, pathname: string) => { if (!confirm(`Delete ${pathname}?`)) return; setDeletingFiles((prev) => new Set(prev).add(url)); try { const response = await fetch('/api/files', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); if (!response.ok) { throw new Error('Failed to delete file'); } // Remove from list setFiles((prev) => prev.filter((f) => f.url !== url)); } catch (err) { console.error('Failed to delete file:', err); alert(err instanceof Error ? err.message : 'Failed to delete file'); } finally { setDeletingFiles((prev) => { const next = new Set(prev); next.delete(url); return next; }); } }; // Download file const handleDownload = (url: string, filename: string) => { const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); }; // Format file size const formatSize = (bytes: number): string => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; // Format date const formatDate = (date: Date): string => { return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(date)); }; // Get file icon const getFileIcon = (pathname: string) => { const ext = pathname.split('.').pop()?.toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext || '')) { return ( ); } if (ext === 'pdf') { return ( ); } return ( ); }; return (

Files

{error && (
{error}
)} {files.length === 0 && !isLoading && (

No files uploaded yet

)} {files.length > 0 && (
{files.map((file) => { const isDeleting = deletingFiles.has(file.url); const filename = file.pathname.split('/').pop() || file.pathname; return (
{/* File icon */}
{getFileIcon(file.pathname)}
{/* File info */}

{filename}

{formatSize(file.size)} ยท {formatDate(file.uploadedAt)}

{/* Actions */}
); })}
)} {/* Load more */} {hasMore && ( )}
); } // ============================================================================ // SERVER: API Route for List & Delete // ============================================================================ // app/api/files/route.ts /* import { list, del } from '@vercel/blob'; import { NextResponse } from 'next/server'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const prefix = searchParams.get('prefix') || ''; const cursor = searchParams.get('cursor') || undefined; try { const { blobs, cursor: nextCursor, hasMore } = await list({ prefix, limit: 20, cursor }); return NextResponse.json({ blobs: blobs.map(blob => ({ url: blob.url, pathname: blob.pathname, size: blob.size, uploadedAt: blob.uploadedAt })), cursor: nextCursor, hasMore }); } catch (error) { return NextResponse.json( { error: 'Failed to list files' }, { status: 500 } ); } } export async function DELETE(request: Request) { try { const { url } = await request.json(); if (!url) { return NextResponse.json( { error: 'URL is required' }, { status: 400 } ); } await del(url); return NextResponse.json({ success: true }); } catch (error) { return NextResponse.json( { error: 'Failed to delete file' }, { status: 500 } ); } } */