File size: 6,929 Bytes
3a2dfa1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
'use client'

import { useCallback, useState } from 'react'
import { useDropzone } from 'react-dropzone'
import { motion, AnimatePresence } from 'framer-motion'
import { Upload, Image, Video, FileText, X, AlertCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from './ui/button'
import toast from 'react-hot-toast'

interface UploadZoneProps {
  onUpload?: (files: File[]) => void
  onDragChange?: (isDragging: boolean) => void
  accept?: Record<string, string[]>
  maxSize?: number
  maxFiles?: number
  className?: string
}

export function UploadZone({
  onUpload,
  onDragChange,
  accept = {
    'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'],
    'video/*': ['.mp4', '.webm', '.mov'],
  },
  maxSize = 50 * 1024 * 1024, // 50MB
  maxFiles = 10,
  className,
}: UploadZoneProps) {
  const [files, setFiles] = useState<File[]>([])
  const [isProcessing, setIsProcessing] = useState(false)

  const onDrop = useCallback(
    async (acceptedFiles: File[], rejectedFiles: any[]) => {
      if (rejectedFiles.length > 0) {
        const errors = rejectedFiles.map((file) => {
          if (file.errors[0]?.code === 'file-too-large') {
            return `${file.file.name} is too large (max ${maxSize / 1024 / 1024}MB)`
          }
          return `${file.file.name} is not supported`
        })
        toast.error(errors.join(', '))
        return
      }

      setFiles(acceptedFiles)
      
      if (onUpload) {
        setIsProcessing(true)
        try {
          await onUpload(acceptedFiles)
        } catch (error) {
          toast.error('Failed to upload files')
        } finally {
          setIsProcessing(false)
        }
      }
    },
    [onUpload, maxSize]
  )

  const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({
    onDrop,
    accept,
    maxSize,
    maxFiles,
    onDragEnter: () => onDragChange?.(true),
    onDragLeave: () => onDragChange?.(false),
    onDropAccepted: () => onDragChange?.(false),
    onDropRejected: () => onDragChange?.(false),
  })

  const getFileIcon = (file: File) => {
    if (file.type.startsWith('image/')) return <Image className="w-4 h-4" />
    if (file.type.startsWith('video/')) return <Video className="w-4 h-4" />
    return <FileText className="w-4 h-4" />
  }

  const removeFile = (index: number) => {
    setFiles((prev) => prev.filter((_, i) => i !== index))
  }

  return (
    <div className={className}>
      <div
        {...getRootProps()}
        className={cn(
          'relative rounded-xl border-2 border-dashed transition-all duration-200 cursor-pointer',
          'bg-gray-800/50 hover:bg-gray-800/70',
          isDragActive && !isDragReject && 'border-purple-500 bg-purple-500/10',
          isDragReject && 'border-red-500 bg-red-500/10',
          !isDragActive && !isDragReject && 'border-gray-700',
          isProcessing && 'pointer-events-none opacity-50'
        )}
      >
        <input {...getInputProps()} />
        
        <div className="p-12 text-center">
          <AnimatePresence mode="wait">
            {isDragReject ? (
              <motion.div
                key="reject"
                initial={{ opacity: 0, scale: 0.9 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={{ opacity: 0, scale: 0.9 }}
                className="text-red-500"
              >
                <AlertCircle className="w-12 h-12 mx-auto mb-4" />
                <p className="text-lg font-medium">File not supported</p>
                <p className="text-sm mt-2 text-red-400">
                  Please upload image or video files only
                </p>
              </motion.div>
            ) : isDragActive ? (
              <motion.div
                key="active"
                initial={{ opacity: 0, scale: 0.9 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={{ opacity: 0, scale: 0.9 }}
                className="text-purple-400"
              >
                <Upload className="w-12 h-12 mx-auto mb-4 animate-bounce" />
                <p className="text-lg font-medium">Drop files here</p>
                <p className="text-sm mt-2 text-purple-300">
                  Release to upload
                </p>
              </motion.div>
            ) : (
              <motion.div
                key="default"
                initial={{ opacity: 0, scale: 0.9 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={{ opacity: 0, scale: 0.9 }}
              >
                <Upload className="w-12 h-12 mx-auto mb-4 text-gray-500" />
                <p className="text-lg font-medium text-white mb-2">
                  Drop files here or click to browse
                </p>
                <p className="text-sm text-gray-400">
                  Support for PNG, JPG, GIF, WebP, MP4, MOV
                </p>
                <p className="text-xs text-gray-500 mt-2">
                  Max {maxSize / 1024 / 1024}MB per file • Up to {maxFiles} files
                </p>
                
                <Button className="mt-6" variant="outline">
                  Select Files
                </Button>
              </motion.div>
            )}
          </AnimatePresence>
        </div>

        {isProcessing && (
          <div className="absolute inset-0 bg-gray-900/80 rounded-xl flex items-center justify-center">
            <div className="text-center">
              <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-purple-500 mb-4"></div>
              <p className="text-white">Processing...</p>
            </div>
          </div>
        )}
      </div>

      {/* File List */}
      {files.length > 0 && (
        <div className="mt-4 space-y-2">
          <p className="text-sm font-medium text-gray-300 mb-2">
            Selected files ({files.length})
          </p>
          {files.map((file, index) => (
            <motion.div
              key={`${file.name}-${index}`}
              initial={{ opacity: 0, x: -20 }}
              animate={{ opacity: 1, x: 0 }}
              transition={{ delay: index * 0.05 }}
              className="flex items-center gap-3 p-3 bg-gray-800 rounded-lg"
            >
              {getFileIcon(file)}
              <div className="flex-1 min-w-0">
                <p className="text-sm font-medium text-white truncate">
                  {file.name}
                </p>
                <p className="text-xs text-gray-400">
                  {(file.size / 1024 / 1024).toFixed(2)} MB
                </p>
              </div>
              <Button
                size="sm"
                variant="ghost"
                onClick={() => removeFile(index)}
                className="text-gray-400 hover:text-red-400"
              >
                <X className="w-4 h-4" />
              </Button>
            </motion.div>
          ))}
        </div>
      )}
    </div>
  )
}