'use client' import { useEffect, useState } from 'react' import { HardDrive, ChevronRight, AlertCircle, Folder, File, RefreshCw } from 'lucide-react' interface Bucket { name: string; creation_date: string; region?: string } interface R2Object { key: string; size: number; uploaded: string; etag: string } function fmtSize(bytes: number) { if (bytes >= 1e12) return (bytes / 1e12).toFixed(1) + ' TB' if (bytes >= 1e9) return (bytes / 1e9).toFixed(1) + ' GB' if (bytes >= 1e6) return (bytes / 1e6).toFixed(1) + ' MB' if (bytes >= 1e3) return (bytes / 1e3).toFixed(1) + ' KB' return bytes + ' B' } function timeAgo(iso?: string) { if (!iso) return '—' try { const diff = Date.now() - new Date(iso).getTime() const d = Math.floor(diff / 86400000) if (d > 0) return `${d}d ago`; return 'today' } catch { return '—' } } export default function R2Page() { const [buckets, setBuckets] = useState([]) const [selectedBucket, setSelectedBucket] = useState(null) const [objects, setObjects] = useState([]) const [live, setLive] = useState(false) const [loading, setLoading] = useState(true) const [loadingObjs, setLoadingObjs] = useState(false) const [prefix, setPrefix] = useState('') useEffect(() => { fetch('/api/r2').then(r => r.json()).then(d => { setBuckets(d.buckets || []); setLive(d.live); setLoading(false) }).catch(() => setLoading(false)) }, []) const selectBucket = async (b: Bucket) => { setSelectedBucket(b); setPrefix('') setLoadingObjs(true) try { const r = await fetch(`/api/r2?bucket=${b.name}`) const d = await r.json() setObjects(d.objects || []) } finally { setLoadingObjs(false) } } const filterObjs = prefix ? objects.filter(o => o.key.startsWith(prefix)) : objects // Group objects by folder prefix const folders = [...new Set(filterObjs.map(o => o.key.includes('/') ? o.key.split('/')[0] : '').filter(Boolean))] const rootFiles = filterObjs.filter(o => !o.key.includes('/')) const totalSize = objects.reduce((s, o) => s + o.size, 0) return (

R2 Storage

{buckets.length} buckets · Cloudflare R2 {live ? '● live' : '○ mock data'}

{!live && (
Set CLOUDFLARE_API_TOKEN for live data
)}
{/* Bucket list */}
Buckets
{loading ?
Loading…
: buckets.map(b => (
selectBucket(b)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px', cursor: 'pointer', background: selectedBucket?.name === b.name ? 'rgba(41,121,255,0.1)' : 'transparent', borderLeft: `3px solid ${selectedBucket?.name === b.name ? '#2979FF' : 'transparent'}`, }}> {b.name}
))}
{/* Object browser */}
{!selectedBucket ? (
← Select a bucket
) : (
{selectedBucket.name} · {objects.length} objects · {fmtSize(totalSize)} {loadingObjs && }
{prefix && (
)}
{/* Folders */} {!prefix && folders.map(folder => (
setPrefix(folder + '/')} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '9px 16px', cursor: 'pointer', borderBottom: '1px solid rgba(255,255,255,0.04)' }}> {folder}/ {objects.filter(o => o.key.startsWith(folder + '/')).length} files
))} {/* Files */} {(prefix ? filterObjs : rootFiles).map(obj => { const displayKey = prefix ? obj.key.slice(prefix.length) : obj.key return (
{displayKey} {fmtSize(obj.size)} {timeAgo(obj.uploaded)}
) })}
)}
) }