← Components/Utility

FileUploadZone

fileuploadzone-1779379671764.tsx
'use client'
import { useState, useCallback } from 'react'

interface UploadedFile {
  id: number
  name: string
  size: number
  type: string
  progress: number
  done: boolean
  error?: string
}

const ALLOWED = ['image/png', 'image/jpeg', 'image/webp', 'image/svg+xml', 'application/pdf', 'text/plain', 'application/json']
const MAX_SIZE = 5 * 1024 * 1024

function 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`
}

function fileIcon(type: string): string {
  if (type.startsWith('image/')) return '🖼'
  if (type === 'application/pdf') return '📄'
  if (type.includes('json')) return '{ }'
  return '📝'
}

let nextId = 1

export default function FileUploadZone() {
  const [files, setFiles] = useState<UploadedFile[]>([])
  const [dragging, setDragging] = useState(false)

  const processFiles = useCallback((fileList: FileList) => {
    Array.from(fileList).forEach(f => {
      const id = nextId++
      const error = !ALLOWED.includes(f.type) ? 'File type not allowed' : f.size > MAX_SIZE ? 'File too large (max 5 MB)' : undefined

      const entry: UploadedFile = { id, name: f.name, size: f.size, type: f.type, progress: 0, done: false, error }
      setFiles(fs => [...fs, entry])

      if (!error) {
        let prog = 0
        const interval = setInterval(() => {
          prog += Math.random() * 25 + 10
          const done = prog >= 100
          setFiles(fs => fs.map(x => x.id === id ? { ...x, progress: Math.min(100, prog), done } : x))
          if (done) clearInterval(interval)
        }, 200)
      }
    })
  }, [])

  const onDrop = useCallback((e: React.DragEvent) => {
    e.preventDefault()
    setDragging(false)
    processFiles(e.dataTransfer.files)
  }, [processFiles])

  return (
    <div style={{ padding: 32, background: '#0A0A0A', display: 'flex', flexDirection: 'column', gap: 16 }}>
      {/* Drop zone */}
      <div onDragEnter={() => setDragging(true)} onDragOver={e => { e.preventDefault(); setDragging(true) }}
        onDragLeave={() => setDragging(false)} onDrop={onDrop}
        style={{ border: `2px dashed ${dragging ? '#C9A84C' : 'rgba(255,255,255,0.12)'}`, borderRadius: 16, padding: '40px 24px', textAlign: 'center', background: dragging ? 'rgba(201,168,76,0.04)' : 'rgba(255,255,255,0.02)', transition: 'all 0.2s', cursor: 'pointer' }}
        onClick={() => { const el = document.createElement('input'); el.type = 'file'; el.multiple = true; el.accept = ALLOWED.join(','); el.onchange = e => e.target && processFiles((e.target as HTMLInputElement).files!); el.click() }}>
        <div style={{ fontSize: 40, marginBottom: 12 }}>📁</div>
        <div style={{ color: '#F5F5F0', fontSize: 15, fontWeight: 600, marginBottom: 6 }}>Drop files here or click to browse</div>
        <div style={{ color: 'rgba(255,255,255,0.35)', fontSize: 12 }}>PNG, JPG, PDF, JSON, TXT up to 5 MB</div>
      </div>

      {/* File list */}
      {files.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {files.map(f => (
            <div key={f.id} style={{ background: '#111', border: `1px solid ${f.error ? 'rgba(239,68,68,0.2)' : f.done ? 'rgba(34,197,94,0.2)' : 'rgba(255,255,255,0.06)'}`, borderRadius: 10, padding: '12px 14px' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: f.error || !f.done ? 8 : 0 }}>
                <span style={{ fontSize: 18 }}>{fileIcon(f.type)}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ color: '#F5F5F0', fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</div>
                  <div style={{ color: 'rgba(255,255,255,0.3)', fontSize: 11 }}>{formatSize(f.size)}</div>
                </div>
                <button onClick={() => setFiles(fs => fs.filter(x => x.id !== f.id))} style={{ background: 'none', border: 'none', color: 'rgba(255,255,255,0.25)', cursor: 'pointer', fontSize: 16 }}>×</button>
              </div>
              {f.error && <div style={{ color: '#ef4444', fontSize: 11 }}>⚠ {f.error}</div>}
              {!f.error && !f.done && (
                <div style={{ height: 4, background: 'rgba(255,255,255,0.06)', borderRadius: 2, overflow: 'hidden' }}>
                  <div style={{ height: '100%', width: `${f.progress}%`, background: 'linear-gradient(90deg, #C9A84C, #6366f1)', borderRadius: 2, transition: 'width 0.15s' }} />
                </div>
              )}
              {f.done && <div style={{ color: '#22c55e', fontSize: 11 }}>✓ Upload complete</div>}
            </div>
          ))}
          <button onClick={() => setFiles([])} style={{ alignSelf: 'flex-start', background: 'none', border: '1px solid rgba(255,255,255,0.1)', color: 'rgba(255,255,255,0.4)', borderRadius: 6, padding: '5px 12px', cursor: 'pointer', fontSize: 12 }}>Clear all</button>
        </div>
      )}
    </div>
  )
}

Component info

CategoryUtility
Frameworkreact
TierFREE
Views0
Copies0

About

Drag-and-drop file upload zone with type validation, progress simulation, preview, and multi-file

More from Utility

'use client'
import { useState } from 'react'

const PRESETS = [
  { name: 'Gold', primary: '#C9A84C', bg: '#0A0A0A', accent: '#6366f1' },
  { name: 'Neon', primary: '#22d3ee', bg: '#030712', accent: '#a78bfa' },
  { name: 'Crimson', primary: '#ef444
ThemeCustomizer
Utility
'use client'
import { useState } from 'react'

interface ClipItem {
  id: number
  content: string
  type: 'text' | 'code' | 'url' | 'email'
  pinned: boolean
  time: number
}

const INITIAL: ClipItem[] = [
  { id: 1, content: 'npx empire-ui-mcp --st
ClipboardManager
Utility