@moontra/moonui-pro 2.8.15 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,363 +1,1260 @@
1
1
  "use client"
2
2
 
3
- import React from 'react'
4
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
5
- import { Button } from '../ui/button'
6
- import { MoonUIBadgePro as Badge } from '../ui/badge'
3
+ import React, { useCallback, useState, useRef } from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import { cva } from 'class-variance-authority'
7
6
  import {
8
7
  Upload,
8
+ X,
9
9
  File,
10
- Image,
11
- X,
12
- Download,
10
+ Image as ImageIcon,
11
+ Video,
12
+ Music,
13
13
  FileText,
14
- Video,
15
- Music,
16
- Archive,
17
14
  AlertCircle,
18
- CheckCircle2,
19
15
  Loader2,
16
+ Play,
17
+ Pause,
18
+ RotateCcw,
19
+ Download,
20
+ Eye,
21
+ Trash2,
22
+ MoreHorizontal,
23
+ Copy,
24
+ Share,
25
+ Archive,
20
26
  Lock,
21
- Sparkles
27
+ Sparkles,
28
+ CheckCircle2
22
29
  } from 'lucide-react'
23
30
  import { cn } from '../../lib/utils'
31
+ import { Button } from '../ui/button'
32
+ import { Badge } from '../ui/badge'
33
+ import { Progress } from '../ui/progress'
34
+ import { Card, CardContent } from '../ui/card'
35
+ import {
36
+ DropdownMenu,
37
+ DropdownMenuContent,
38
+ DropdownMenuItem,
39
+ DropdownMenuTrigger
40
+ } from '../ui/dropdown-menu'
41
+ import {
42
+ Dialog,
43
+ DialogContent,
44
+ DialogHeader,
45
+ DialogTitle
46
+ } from '../ui/dialog'
24
47
  import { useSubscription } from '../../hooks/use-subscription'
25
48
 
26
- interface FileUploadProps {
49
+ // Ana interface'ler ve type'lar
50
+ export interface FileUploadProProps {
27
51
  accept?: string
28
52
  multiple?: boolean
29
- maxSize?: number // in MB
53
+ maxSize?: number // bytes cinsinden
30
54
  maxFiles?: number
31
- onUpload?: (files: File[]) => Promise<void>
32
- onRemove?: (file: File) => void
33
- className?: string
34
55
  disabled?: boolean
56
+ className?: string
57
+ variant?: 'default' | 'compact' | 'grid'
58
+ theme?: 'light' | 'dark' | 'auto'
59
+
60
+ // Gelişmiş özellikler
61
+ chunkSize?: number // chunk upload için
62
+ resumable?: boolean
63
+ compression?: boolean
64
+ imageResize?: {
65
+ maxWidth: number
66
+ maxHeight: number
67
+ quality: number
68
+ }
69
+
70
+ // Validasyon
71
+ allowedMimeTypes?: string[]
72
+ maxTotalSize?: number
73
+ customValidation?: (file: File) => Promise<string | null>
74
+ duplicateCheck?: boolean
75
+
76
+ // Upload ayarları
77
+ uploadStrategy?: 'direct' | 'presigned' | 'multipart'
78
+ endpoint?: string
79
+ headers?: Record<string, string>
80
+
81
+ // Callbacks
82
+ onUpload?: (files: FileUploadItem[]) => Promise<void>
83
+ onProgress?: (fileId: string, progress: number) => void
84
+ onComplete?: (fileId: string, result: any) => void
85
+ onError?: (fileId: string, error: string) => void
86
+ onRemove?: (fileId: string) => void
87
+ onPreview?: (file: FileUploadItem) => void
88
+ onBulkSelect?: (selectedIds: string[]) => void
89
+
90
+ // UI özelleştirme
35
91
  showPreview?: boolean
36
- allowedTypes?: string[]
92
+ showProgress?: boolean
93
+ showMetadata?: boolean
94
+ allowBulkOperations?: boolean
95
+ previewTypes?: ('image' | 'video' | 'audio' | 'pdf' | 'document')[]
37
96
  }
38
97
 
39
- interface UploadedFile {
40
- file: File
98
+ export interface FileUploadItem {
41
99
  id: string
42
- status: 'uploading' | 'success' | 'error'
100
+ file: File
101
+ status: 'pending' | 'uploading' | 'paused' | 'success' | 'error' | 'cancelled'
43
102
  progress: number
103
+ uploadedBytes?: number
104
+ totalBytes: number
105
+ speed?: number // bytes/second
106
+ estimatedTime?: number // seconds
44
107
  error?: string
108
+ chunks?: FileChunk[]
109
+ preview?: FilePreview
110
+ metadata?: FileMetadata
111
+ result?: any // upload sonucu
112
+ }
113
+
114
+ interface FileChunk {
115
+ index: number
116
+ start: number
117
+ end: number
118
+ status: 'pending' | 'uploading' | 'success' | 'error'
119
+ attempts: number
120
+ }
121
+
122
+ interface FilePreview {
123
+ type: 'image' | 'video' | 'audio' | 'pdf' | 'document' | 'unknown'
45
124
  url?: string
125
+ thumbnail?: string
126
+ duration?: number // video/audio için
127
+ dimensions?: { width: number; height: number }
128
+ pages?: number // PDF için
46
129
  }
47
130
 
48
- const getFileIcon = (type: string) => {
49
- if (type.startsWith('image/')) return <Image className="h-4 w-4" />
50
- if (type.startsWith('video/')) return <Video className="h-4 w-4" />
51
- if (type.startsWith('audio/')) return <Music className="h-4 w-4" />
52
- if (type.includes('pdf')) return <FileText className="h-4 w-4" />
53
- if (type.includes('zip') || type.includes('rar')) return <Archive className="h-4 w-4" />
54
- return <File className="h-4 w-4" />
131
+ interface FileMetadata {
132
+ name: string
133
+ size: number
134
+ type: string
135
+ lastModified: number
136
+ hash?: string
137
+ dimensions?: { width: number; height: number }
138
+ duration?: number
139
+ bitrate?: number
55
140
  }
56
141
 
142
+ // CVA variants
143
+ const fileUploadVariants = cva(
144
+ "relative overflow-hidden transition-all duration-200",
145
+ {
146
+ variants: {
147
+ variant: {
148
+ default: "border-2 border-dashed rounded-lg p-8",
149
+ compact: "border border-dashed rounded-md p-4",
150
+ grid: "border-2 border-dashed rounded-lg p-6"
151
+ },
152
+ state: {
153
+ idle: "border-muted-foreground/25 hover:border-primary/50",
154
+ dragover: "border-primary bg-primary/5 scale-102",
155
+ disabled: "opacity-50 cursor-not-allowed"
156
+ }
157
+ },
158
+ defaultVariants: {
159
+ variant: "default",
160
+ state: "idle"
161
+ }
162
+ }
163
+ )
164
+
165
+ // Utility fonksiyonlar
57
166
  const formatFileSize = (bytes: number): string => {
58
- if (bytes === 0) return '0 Bytes'
167
+ if (bytes === 0) return '0 B'
59
168
  const k = 1024
60
- const sizes = ['Bytes', 'KB', 'MB', 'GB']
169
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
61
170
  const i = Math.floor(Math.log(bytes) / Math.log(k))
62
171
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
63
172
  }
64
173
 
65
- export function FileUpload({
66
- accept = '*',
67
- multiple = false,
68
- maxSize = 10, // 10MB default
69
- maxFiles = 5,
70
- onUpload,
71
- onRemove,
72
- className,
73
- disabled = false,
74
- showPreview = true,
75
- allowedTypes = []
76
- }: FileUploadProps) {
77
- // Check if we're in docs mode or have pro access
78
- const { hasProAccess, isLoading } = useSubscription()
79
-
80
- // In docs mode, always show the component
174
+ const formatTime = (seconds: number): string => {
175
+ if (seconds < 60) return `${Math.round(seconds)}s`
176
+ const minutes = Math.floor(seconds / 60)
177
+ const remainingSeconds = Math.round(seconds % 60)
178
+ return `${minutes}m ${remainingSeconds}s`
179
+ }
180
+
181
+ const getFileIcon = (type: string, size: 'sm' | 'md' | 'lg' = 'md') => {
182
+ const iconSize = size === 'sm' ? 'h-4 w-4' : size === 'md' ? 'h-5 w-5' : 'h-6 w-6'
81
183
 
82
- // If not in docs mode and no pro access, show upgrade prompt
83
- if (!isLoading && !hasProAccess) {
84
- return (
85
- <Card className={cn("w-full", className)}>
86
- <CardContent className="py-12 text-center">
87
- <div className="max-w-md mx-auto space-y-4">
88
- <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
89
- <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
90
- </div>
91
- <div>
92
- <h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
93
- <p className="text-muted-foreground text-sm mb-4">
94
- File Upload is available exclusively to MoonUI Pro subscribers.
95
- </p>
96
- <div className="flex gap-3 justify-center">
97
- <a href="/pricing">
98
- <Button size="sm">
99
- <Sparkles className="mr-2 h-4 w-4" />
100
- Upgrade to Pro
101
- </Button>
102
- </a>
103
- </div>
104
- </div>
105
- </div>
106
- </CardContent>
107
- </Card>
108
- )
184
+ if (type.startsWith('image/')) return <ImageIcon className={iconSize} />
185
+ if (type.startsWith('video/')) return <Video className={iconSize} />
186
+ if (type.startsWith('audio/')) return <Music className={iconSize} />
187
+ if (type.includes('pdf')) return <FileText className={iconSize} />
188
+ if (type.includes('zip') || type.includes('rar') || type.includes('7z')) {
189
+ return <Archive className={iconSize} />
109
190
  }
110
-
111
- const [files, setFiles] = React.useState<UploadedFile[]>([])
112
- const [isDragOver, setIsDragOver] = React.useState(false)
113
- const [isUploading, setIsUploading] = React.useState(false)
114
- const fileInputRef = React.useRef<HTMLInputElement>(null)
191
+ return <File className={iconSize} />
192
+ }
115
193
 
116
- const validateFile = (file: File): string | null => {
117
- // Check file size
118
- if (file.size > maxSize * 1024 * 1024) {
119
- return `File size must be less than ${maxSize}MB`
120
- }
194
+ const generateFileHash = async (file: File): Promise<string> => {
195
+ const buffer = await file.arrayBuffer()
196
+ const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
197
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
198
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
199
+ }
121
200
 
122
- // Check file type
123
- if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
124
- return `File type ${file.type} is not allowed`
201
+ const createImagePreview = (file: File): Promise<FilePreview> => {
202
+ return new Promise((resolve) => {
203
+ const img = new Image()
204
+ const url = URL.createObjectURL(file)
205
+
206
+ img.onload = () => {
207
+ resolve({
208
+ type: 'image',
209
+ url,
210
+ thumbnail: url,
211
+ dimensions: { width: img.width, height: img.height }
212
+ })
125
213
  }
126
-
127
- return null
128
- }
129
-
130
- const handleFileSelect = async (selectedFiles: FileList | null) => {
131
- if (!selectedFiles || disabled) return
132
-
133
- const fileArray = Array.from(selectedFiles)
134
214
 
135
- // Check max files limit
136
- if (files.length + fileArray.length > maxFiles) {
137
- alert(`Maximum ${maxFiles} files allowed`)
138
- return
215
+ img.onerror = () => {
216
+ resolve({
217
+ type: 'image',
218
+ url
219
+ })
139
220
  }
221
+
222
+ img.src = url
223
+ })
224
+ }
140
225
 
141
- const validFiles: UploadedFile[] = []
142
- const invalidFiles: string[] = []
143
-
144
- fileArray.forEach(file => {
145
- const error = validateFile(file)
146
- if (error) {
147
- invalidFiles.push(`${file.name}: ${error}`)
148
- } else {
149
- validFiles.push({
150
- file,
151
- id: Math.random().toString(36).substr(2, 9),
152
- status: 'uploading',
153
- progress: 0
226
+ const createVideoPreview = (file: File): Promise<FilePreview> => {
227
+ return new Promise((resolve) => {
228
+ const video = document.createElement('video')
229
+ const url = URL.createObjectURL(file)
230
+
231
+ video.onloadedmetadata = () => {
232
+ // Video thumbnail oluşturma
233
+ const canvas = document.createElement('canvas')
234
+ const ctx = canvas.getContext('2d')!
235
+
236
+ canvas.width = 320
237
+ canvas.height = (video.videoHeight / video.videoWidth) * 320
238
+
239
+ video.currentTime = Math.min(1, video.duration / 4) // İlk saniye veya %25
240
+
241
+ video.onseeked = () => {
242
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
243
+ const thumbnail = canvas.toDataURL('image/jpeg', 0.8)
244
+
245
+ resolve({
246
+ type: 'video',
247
+ url,
248
+ thumbnail,
249
+ duration: video.duration,
250
+ dimensions: { width: video.videoWidth, height: video.videoHeight }
154
251
  })
155
252
  }
156
- })
157
-
158
- if (invalidFiles.length > 0) {
159
- alert(invalidFiles.join('\n'))
160
253
  }
254
+
255
+ video.onerror = () => {
256
+ resolve({
257
+ type: 'video',
258
+ url
259
+ })
260
+ }
261
+
262
+ video.src = url
263
+ })
264
+ }
161
265
 
162
- if (validFiles.length === 0) return
266
+ // Bulk operation bileşeni
267
+ const BulkActions = ({
268
+ selectedIds,
269
+ onClearSelection,
270
+ onBulkRemove,
271
+ onBulkDownload
272
+ }: {
273
+ selectedIds: string[]
274
+ onClearSelection: () => void
275
+ onBulkRemove: () => void
276
+ onBulkDownload: () => void
277
+ }) => {
278
+ if (selectedIds.length === 0) return null
279
+
280
+ return (
281
+ <motion.div
282
+ initial={{ opacity: 0, y: -10 }}
283
+ animate={{ opacity: 1, y: 0 }}
284
+ exit={{ opacity: 0, y: -10 }}
285
+ className="flex items-center gap-2 p-3 bg-primary/5 rounded-lg border"
286
+ >
287
+ <span className="text-sm font-medium">{selectedIds.length} dosya seçildi</span>
288
+ <div className="flex gap-1 ml-auto">
289
+ <Button variant="ghost" size="sm" onClick={onBulkDownload}>
290
+ <Download className="h-4 w-4 mr-1" />
291
+ İndir
292
+ </Button>
293
+ <Button variant="ghost" size="sm" onClick={onBulkRemove}>
294
+ <Trash2 className="h-4 w-4 mr-1" />
295
+ Sil
296
+ </Button>
297
+ <Button variant="ghost" size="sm" onClick={onClearSelection}>
298
+ <X className="h-4 w-4" />
299
+ </Button>
300
+ </div>
301
+ </motion.div>
302
+ )
303
+ }
163
304
 
164
- setFiles(prev => [...prev, ...validFiles])
165
- setIsUploading(true)
305
+ // Dosya önizleme modal
306
+ const FilePreviewModal = ({
307
+ file,
308
+ isOpen,
309
+ onClose
310
+ }: {
311
+ file: FileUploadItem | null
312
+ isOpen: boolean
313
+ onClose: () => void
314
+ }) => {
315
+ if (!file || !file.preview) return null
316
+
317
+ return (
318
+ <Dialog open={isOpen} onOpenChange={onClose}>
319
+ <DialogContent className="max-w-4xl max-h-[90vh]">
320
+ <DialogHeader>
321
+ <DialogTitle className="flex items-center gap-2">
322
+ {getFileIcon(file.file.type)}
323
+ {file.file.name}
324
+ <Badge variant="secondary">
325
+ {formatFileSize(file.file.size)}
326
+ </Badge>
327
+ </DialogTitle>
328
+ </DialogHeader>
329
+
330
+ <div className="flex items-center justify-center p-4 bg-muted/20 rounded-lg">
331
+ {file.preview.type === 'image' && (
332
+ <img
333
+ src={file.preview.url}
334
+ alt={file.file.name}
335
+ className="max-w-full max-h-[60vh] object-contain rounded"
336
+ />
337
+ )}
338
+
339
+ {file.preview.type === 'video' && (
340
+ <video
341
+ src={file.preview.url}
342
+ controls
343
+ className="max-w-full max-h-[60vh] rounded"
344
+ />
345
+ )}
346
+
347
+ {file.preview.type === 'audio' && (
348
+ <div className="w-full max-w-md">
349
+ <audio src={file.preview.url} controls className="w-full" />
350
+ </div>
351
+ )}
352
+
353
+ {!['image', 'video', 'audio'].includes(file.preview.type) && (
354
+ <div className="text-center py-8">
355
+ {getFileIcon(file.file.type, 'lg')}
356
+ <p className="mt-2 text-muted-foreground">Önizleme mevcut değil</p>
357
+ </div>
358
+ )}
359
+ </div>
360
+
361
+ {file.preview.dimensions && (
362
+ <div className="flex gap-4 text-sm text-muted-foreground">
363
+ <span>Boyutlar: {file.preview.dimensions.width} × {file.preview.dimensions.height}</span>
364
+ {file.preview.duration && (
365
+ <span>Süre: {formatTime(file.preview.duration)}</span>
366
+ )}
367
+ </div>
368
+ )}
369
+ </DialogContent>
370
+ </Dialog>
371
+ )
372
+ }
166
373
 
167
- // Simulate upload process
168
- if (onUpload) {
374
+ // Ana bileşen
375
+ export const MoonUIFileUploadPro = React.forwardRef<HTMLDivElement, FileUploadProProps>(
376
+ ({
377
+ accept = "*",
378
+ multiple = true,
379
+ maxSize = 100 * 1024 * 1024, // 100MB
380
+ maxFiles = 10,
381
+ disabled = false,
382
+ className,
383
+ variant = "default",
384
+ chunkSize = 1024 * 1024, // 1MB chunks
385
+ resumable = true,
386
+ compression = false,
387
+ allowedMimeTypes = [],
388
+ maxTotalSize,
389
+ duplicateCheck = true,
390
+ uploadStrategy = 'direct',
391
+ showPreview = true,
392
+ showProgress = true,
393
+ showMetadata = true,
394
+ allowBulkOperations = true,
395
+ previewTypes = ['image', 'video', 'audio', 'pdf'],
396
+ onUpload,
397
+ onProgress,
398
+ onComplete,
399
+ onError,
400
+ onRemove,
401
+ onPreview,
402
+ onBulkSelect,
403
+ customValidation,
404
+ ...props
405
+ }, ref) => {
406
+
407
+ // Subscription kontrolü - Pro bileşen
408
+ const { hasProAccess, isLoading } = useSubscription()
409
+
410
+ // State'ler
411
+ const [files, setFiles] = useState<FileUploadItem[]>([])
412
+ const [isDragOver, setIsDragOver] = useState(false)
413
+ const [selectedIds, setSelectedIds] = useState<string[]>([])
414
+ const [previewFile, setPreviewFile] = useState<FileUploadItem | null>(null)
415
+ const [isPreviewOpen, setIsPreviewOpen] = useState(false)
416
+ const [error, setError] = useState<string | null>(null)
417
+
418
+ const fileInputRef = useRef<HTMLInputElement>(null)
419
+ const uploadQueue = useRef<Map<string, AbortController>>(new Map())
420
+
421
+ // Pro lisans kontrolü
422
+ if (!isLoading && !hasProAccess) {
423
+ return (
424
+ <Card className={cn("w-full", className)}>
425
+ <CardContent className="py-12 text-center">
426
+ <div className="max-w-md mx-auto space-y-4">
427
+ <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
428
+ <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
429
+ </div>
430
+ <div>
431
+ <h3 className="font-semibold text-lg mb-2">Pro Özellik</h3>
432
+ <p className="text-muted-foreground text-sm mb-4">
433
+ Gelişmiş Dosya Yükleme sadece MoonUI Pro abonelerine özeldir.
434
+ </p>
435
+ <div className="flex gap-3 justify-center">
436
+ <a href="/pricing">
437
+ <Button size="sm">
438
+ <Sparkles className="mr-2 h-4 w-4" />
439
+ Pro'ya Yükseltin
440
+ </Button>
441
+ </a>
442
+ </div>
443
+ </div>
444
+ </div>
445
+ </CardContent>
446
+ </Card>
447
+ )
448
+ }
449
+
450
+ // Dosya validasyonu
451
+ const validateFile = useCallback(async (file: File): Promise<string | null> => {
452
+ // Boyut kontrolü
453
+ if (file.size > maxSize) {
454
+ return `Dosya boyutu ${formatFileSize(maxSize)} limitini aşıyor`
455
+ }
456
+
457
+ // MIME type kontrolü
458
+ if (allowedMimeTypes.length > 0 && !allowedMimeTypes.includes(file.type)) {
459
+ return `Dosya türü ${file.type} desteklenmiyor`
460
+ }
461
+
462
+ // Accept kontrolü
463
+ if (accept !== "*") {
464
+ const acceptTypes = accept.split(',').map(t => t.trim())
465
+ const isAccepted = acceptTypes.some(acceptType => {
466
+ if (acceptType.includes('*')) {
467
+ return file.type.startsWith(acceptType.replace('*', ''))
468
+ }
469
+ return file.type === acceptType
470
+ })
471
+
472
+ if (!isAccepted) {
473
+ return `Dosya türü kabul edilmiyor`
474
+ }
475
+ }
476
+
477
+ // Özel validasyon
478
+ if (customValidation) {
479
+ const customError = await customValidation(file)
480
+ if (customError) return customError
481
+ }
482
+
483
+ return null
484
+ }, [maxSize, allowedMimeTypes, accept, customValidation])
485
+
486
+ // Duplicate kontrolü
487
+ const checkDuplicate = useCallback(async (file: File): Promise<boolean> => {
488
+ if (!duplicateCheck) return false
489
+
490
+ const hash = await generateFileHash(file)
491
+ return files.some(f => f.metadata?.hash === hash)
492
+ }, [files, duplicateCheck])
493
+
494
+ // Dosya önizleme oluşturma
495
+ const createPreview = useCallback(async (file: File): Promise<FilePreview | undefined> => {
496
+ if (!showPreview) return undefined
497
+
498
+ const fileType = file.type.split('/')[0] as 'image' | 'video' | 'audio'
499
+
500
+ if (!previewTypes.includes(fileType)) return undefined
501
+
502
+ try {
503
+ if (fileType === 'image') {
504
+ return await createImagePreview(file)
505
+ } else if (fileType === 'video') {
506
+ return await createVideoPreview(file)
507
+ } else if (fileType === 'audio') {
508
+ const url = URL.createObjectURL(file)
509
+ return {
510
+ type: 'audio',
511
+ url
512
+ }
513
+ }
514
+ } catch (error) {
515
+ console.warn('Önizleme oluşturulamadı:', error)
516
+ }
517
+
518
+ return undefined
519
+ }, [showPreview, previewTypes])
520
+
521
+ // Dosya metadata oluşturma
522
+ const createMetadata = useCallback(async (file: File): Promise<FileMetadata> => {
523
+ const metadata: FileMetadata = {
524
+ name: file.name,
525
+ size: file.size,
526
+ type: file.type,
527
+ lastModified: file.lastModified
528
+ }
529
+
530
+ if (duplicateCheck) {
531
+ metadata.hash = await generateFileHash(file)
532
+ }
533
+
534
+ return metadata
535
+ }, [duplicateCheck])
536
+
537
+ // Chunked upload simülasyonu
538
+ const uploadFileChunked = useCallback(async (fileItem: FileUploadItem) => {
539
+ const { file } = fileItem
540
+ const chunks: FileChunk[] = []
541
+ const chunkCount = Math.ceil(file.size / chunkSize)
542
+
543
+ // Chunk'ları oluştur
544
+ for (let i = 0; i < chunkCount; i++) {
545
+ chunks.push({
546
+ index: i,
547
+ start: i * chunkSize,
548
+ end: Math.min((i + 1) * chunkSize, file.size),
549
+ status: 'pending',
550
+ attempts: 0
551
+ })
552
+ }
553
+
554
+ // FileItem'i güncelle
555
+ setFiles(prev => prev.map(f =>
556
+ f.id === fileItem.id
557
+ ? { ...f, chunks, status: 'uploading' }
558
+ : f
559
+ ))
560
+
561
+ const abortController = new AbortController()
562
+ uploadQueue.current.set(fileItem.id, abortController)
563
+
564
+ let uploadedBytes = 0
565
+ const startTime = Date.now()
566
+
169
567
  try {
170
- await onUpload(validFiles.map(f => f.file))
568
+ // Chunk'ları sırayla yükle
569
+ for (const chunk of chunks) {
570
+ if (abortController.signal.aborted) {
571
+ throw new Error('Upload cancelled')
572
+ }
573
+
574
+ // Chunk upload simülasyonu
575
+ await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 200))
576
+
577
+ uploadedBytes += (chunk.end - chunk.start)
578
+ const progress = Math.round((uploadedBytes / file.size) * 100)
579
+ const elapsedTime = (Date.now() - startTime) / 1000
580
+ const speed = uploadedBytes / elapsedTime
581
+ const estimatedTime = (file.size - uploadedBytes) / speed
582
+
583
+ // Progress güncelle
584
+ setFiles(prev => prev.map(f =>
585
+ f.id === fileItem.id
586
+ ? {
587
+ ...f,
588
+ progress,
589
+ uploadedBytes,
590
+ speed,
591
+ estimatedTime: estimatedTime || 0,
592
+ chunks: f.chunks?.map(c =>
593
+ c.index === chunk.index
594
+ ? { ...c, status: 'success' }
595
+ : c
596
+ )
597
+ }
598
+ : f
599
+ ))
600
+
601
+ onProgress?.(fileItem.id, progress)
602
+ }
171
603
 
172
- // Update all files to success
604
+ // Upload tamamlandı
173
605
  setFiles(prev => prev.map(f =>
174
- validFiles.find(vf => vf.id === f.id)
175
- ? { ...f, status: 'success' as const, progress: 100 }
606
+ f.id === fileItem.id
607
+ ? { ...f, status: 'success', progress: 100 }
176
608
  : f
177
609
  ))
610
+
611
+ onComplete?.(fileItem.id, { success: true })
612
+
178
613
  } catch (error) {
179
- // Update all files to error
180
614
  setFiles(prev => prev.map(f =>
181
- validFiles.find(vf => vf.id === f.id)
182
- ? { ...f, status: 'error' as const, error: 'Upload failed' }
615
+ f.id === fileItem.id
616
+ ? { ...f, status: 'error', error: error instanceof Error ? error.message : 'Upload failed' }
183
617
  : f
184
618
  ))
619
+
620
+ onError?.(fileItem.id, error instanceof Error ? error.message : 'Upload failed')
621
+ } finally {
622
+ uploadQueue.current.delete(fileItem.id)
185
623
  }
186
- } else {
187
- // Simulate progress
188
- for (const validFile of validFiles) {
189
- const interval = setInterval(() => {
190
- setFiles(prev => prev.map(f =>
191
- f.id === validFile.id && f.progress < 100
192
- ? { ...f, progress: f.progress + 10 }
193
- : f
194
- ))
195
- }, 100)
196
-
197
- setTimeout(() => {
198
- clearInterval(interval)
199
- setFiles(prev => prev.map(f =>
200
- f.id === validFile.id
201
- ? { ...f, status: 'success' as const, progress: 100 }
202
- : f
203
- ))
204
- }, 1000)
624
+ }, [chunkSize, onProgress, onComplete, onError])
625
+
626
+ // Dosya işleme
627
+ const processFiles = useCallback(async (fileList: FileList | File[]) => {
628
+ const fileArray = Array.from(fileList)
629
+ setError(null)
630
+
631
+ // Toplam dosya sayısı kontrolü
632
+ if (files.length + fileArray.length > maxFiles) {
633
+ setError(`Maksimum ${maxFiles} dosya yükleyebilirsiniz`)
634
+ return
205
635
  }
206
- }
207
-
208
- setIsUploading(false)
209
- }
210
-
211
- const handleRemove = (fileToRemove: UploadedFile) => {
212
- setFiles(prev => prev.filter(f => f.id !== fileToRemove.id))
213
- if (onRemove) {
214
- onRemove(fileToRemove.file)
215
- }
216
- }
217
-
218
- const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
219
- e.preventDefault()
220
- setIsDragOver(false)
221
- handleFileSelect(e.dataTransfer.files)
222
- }
223
-
224
- const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
225
- e.preventDefault()
226
- setIsDragOver(true)
227
- }
228
-
229
- const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
230
- e.preventDefault()
231
- setIsDragOver(false)
232
- }
233
-
234
- const handleClick = () => {
235
- if (!disabled) {
236
- fileInputRef.current?.click()
237
- }
238
- }
239
-
240
- return (
241
- <Card className={cn("w-full", className)}>
242
- <CardHeader>
243
- <CardTitle className="flex items-center gap-2">
244
- <Upload className="h-5 w-5" />
245
- File Upload
246
- </CardTitle>
247
- <CardDescription>
248
- {multiple ? `Upload up to ${maxFiles} files` : 'Upload a file'} (max {maxSize}MB each)
249
- </CardDescription>
250
- </CardHeader>
251
- <CardContent className="space-y-4">
252
- {/* Drop Zone */}
253
- <div
636
+
637
+ // Toplam boyut kontrolü
638
+ if (maxTotalSize) {
639
+ const currentSize = files.reduce((sum, f) => sum + f.file.size, 0)
640
+ const newSize = fileArray.reduce((sum, f) => sum + f.size, 0)
641
+
642
+ if (currentSize + newSize > maxTotalSize) {
643
+ setError(`Toplam dosya boyutu ${formatFileSize(maxTotalSize)} limitini aşıyor`)
644
+ return
645
+ }
646
+ }
647
+
648
+ const validFiles: File[] = []
649
+ const errors: string[] = []
650
+
651
+ // Her dosyayı validate et
652
+ for (const file of fileArray) {
653
+ const validationError = await validateFile(file)
654
+ if (validationError) {
655
+ errors.push(`${file.name}: ${validationError}`)
656
+ continue
657
+ }
658
+
659
+ const isDuplicate = await checkDuplicate(file)
660
+ if (isDuplicate) {
661
+ errors.push(`${file.name}: Dosya zaten mevcut`)
662
+ continue
663
+ }
664
+
665
+ validFiles.push(file)
666
+ }
667
+
668
+ if (errors.length > 0) {
669
+ setError(errors.join(', '))
670
+ }
671
+
672
+ if (validFiles.length === 0) return
673
+
674
+ // FileUploadItem'ları oluştur
675
+ const newFileItems: FileUploadItem[] = []
676
+
677
+ for (const file of validFiles) {
678
+ const id = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
679
+ const preview = await createPreview(file)
680
+ const metadata = await createMetadata(file)
681
+
682
+ newFileItems.push({
683
+ id,
684
+ file,
685
+ status: 'pending',
686
+ progress: 0,
687
+ totalBytes: file.size,
688
+ preview,
689
+ metadata
690
+ })
691
+ }
692
+
693
+ setFiles(prev => [...prev, ...newFileItems])
694
+
695
+ // Upload işlemini başlat
696
+ if (onUpload) {
697
+ try {
698
+ await onUpload(newFileItems)
699
+ } catch (error) {
700
+ console.error('Upload failed:', error)
701
+ }
702
+ } else {
703
+ // Otomatik upload başlat
704
+ newFileItems.forEach(fileItem => {
705
+ uploadFileChunked(fileItem)
706
+ })
707
+ }
708
+ }, [
709
+ files,
710
+ maxFiles,
711
+ maxTotalSize,
712
+ validateFile,
713
+ checkDuplicate,
714
+ createPreview,
715
+ createMetadata,
716
+ onUpload,
717
+ uploadFileChunked
718
+ ])
719
+
720
+ // Drag & Drop handlers
721
+ const handleDrop = useCallback((e: React.DragEvent) => {
722
+ e.preventDefault()
723
+ setIsDragOver(false)
724
+
725
+ if (disabled) return
726
+
727
+ const droppedFiles = Array.from(e.dataTransfer.files)
728
+ if (droppedFiles.length > 0) {
729
+ processFiles(droppedFiles)
730
+ }
731
+ }, [processFiles, disabled])
732
+
733
+ const handleDragOver = useCallback((e: React.DragEvent) => {
734
+ e.preventDefault()
735
+ if (!disabled) {
736
+ setIsDragOver(true)
737
+ }
738
+ }, [disabled])
739
+
740
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
741
+ e.preventDefault()
742
+ setIsDragOver(false)
743
+ }, [])
744
+
745
+ // Dosya seçimi
746
+ const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
747
+ const selectedFiles = e.target.files
748
+ if (selectedFiles && selectedFiles.length > 0) {
749
+ processFiles(selectedFiles)
750
+ }
751
+ // Input'u temizle
752
+ e.target.value = ''
753
+ }, [processFiles])
754
+
755
+ // Dosya kaldırma
756
+ const removeFile = useCallback((fileId: string) => {
757
+ const fileToRemove = files.find(f => f.id === fileId)
758
+ if (!fileToRemove) return
759
+
760
+ // Upload'ı iptal et
761
+ const controller = uploadQueue.current.get(fileId)
762
+ if (controller) {
763
+ controller.abort()
764
+ uploadQueue.current.delete(fileId)
765
+ }
766
+
767
+ // Preview URL'yi temizle
768
+ if (fileToRemove.preview?.url) {
769
+ URL.revokeObjectURL(fileToRemove.preview.url)
770
+ }
771
+
772
+ setFiles(prev => prev.filter(f => f.id !== fileId))
773
+ setSelectedIds(prev => prev.filter(id => id !== fileId))
774
+
775
+ onRemove?.(fileId)
776
+ }, [files, onRemove])
777
+
778
+ // Upload'ı duraklat/devam ettir
779
+ const pauseResumeUpload = useCallback((fileId: string) => {
780
+ const file = files.find(f => f.id === fileId)
781
+ if (!file) return
782
+
783
+ if (file.status === 'uploading') {
784
+ const controller = uploadQueue.current.get(fileId)
785
+ if (controller) {
786
+ controller.abort()
787
+ uploadQueue.current.delete(fileId)
788
+ }
789
+
790
+ setFiles(prev => prev.map(f =>
791
+ f.id === fileId ? { ...f, status: 'paused' } : f
792
+ ))
793
+ } else if (file.status === 'paused' && resumable) {
794
+ uploadFileChunked(file)
795
+ }
796
+ }, [files, resumable, uploadFileChunked])
797
+
798
+ // Bulk operations
799
+ const handleBulkSelect = useCallback((fileId: string, selected: boolean) => {
800
+ setSelectedIds(prev =>
801
+ selected
802
+ ? [...prev, fileId]
803
+ : prev.filter(id => id !== fileId)
804
+ )
805
+ }, [])
806
+
807
+ const handleSelectAll = useCallback(() => {
808
+ const allIds = files.map(f => f.id)
809
+ setSelectedIds(allIds)
810
+ onBulkSelect?.(allIds)
811
+ }, [files, onBulkSelect])
812
+
813
+ const handleClearSelection = useCallback(() => {
814
+ setSelectedIds([])
815
+ onBulkSelect?.([])
816
+ }, [onBulkSelect])
817
+
818
+ const handleBulkRemove = useCallback(() => {
819
+ selectedIds.forEach(id => removeFile(id))
820
+ setSelectedIds([])
821
+ }, [selectedIds, removeFile])
822
+
823
+ const handleBulkDownload = useCallback(() => {
824
+ // Bulk download implementation
825
+ console.log('Bulk download:', selectedIds)
826
+ }, [selectedIds])
827
+
828
+ // Preview
829
+ const handlePreview = useCallback((file: FileUploadItem) => {
830
+ setPreviewFile(file)
831
+ setIsPreviewOpen(true)
832
+ onPreview?.(file)
833
+ }, [onPreview])
834
+
835
+ return (
836
+ <div ref={ref} className={cn("w-full space-y-4", className)} {...props}>
837
+ {/* Bulk Actions */}
838
+ <AnimatePresence>
839
+ {allowBulkOperations && selectedIds.length > 0 && (
840
+ <BulkActions
841
+ selectedIds={selectedIds}
842
+ onClearSelection={handleClearSelection}
843
+ onBulkRemove={handleBulkRemove}
844
+ onBulkDownload={handleBulkDownload}
845
+ />
846
+ )}
847
+ </AnimatePresence>
848
+
849
+ {/* Upload Area */}
850
+ <motion.div
254
851
  className={cn(
255
- "border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors",
256
- isDragOver ? "border-primary bg-primary/5" : "border-muted-foreground/25",
257
- disabled && "cursor-not-allowed opacity-50"
852
+ fileUploadVariants({
853
+ variant,
854
+ state: disabled ? "disabled" : isDragOver ? "dragover" : "idle"
855
+ })
258
856
  )}
259
857
  onDrop={handleDrop}
260
858
  onDragOver={handleDragOver}
261
859
  onDragLeave={handleDragLeave}
262
- onClick={handleClick}
860
+ onClick={() => !disabled && fileInputRef.current?.click()}
861
+ animate={{
862
+ scale: isDragOver ? 1.02 : 1
863
+ }}
864
+ transition={{ duration: 0.2 }}
263
865
  >
264
- <Upload className="h-10 w-10 mx-auto mb-4 text-muted-foreground" />
265
- <p className="text-sm text-muted-foreground mb-2">
266
- Drag and drop files here, or click to select
267
- </p>
268
- <p className="text-xs text-muted-foreground">
269
- {accept === '*' ? 'Any file type' : `Accepted types: ${accept}`}
270
- </p>
271
- </div>
272
-
273
- {/* File Input */}
274
- <input
275
- ref={fileInputRef}
276
- type="file"
277
- accept={accept}
278
- multiple={multiple}
279
- onChange={(e) => handleFileSelect(e.target.files)}
280
- className="hidden"
281
- disabled={disabled}
282
- />
283
-
866
+ <input
867
+ ref={fileInputRef}
868
+ type="file"
869
+ accept={accept}
870
+ multiple={multiple}
871
+ disabled={disabled}
872
+ onChange={handleFileSelect}
873
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
874
+ />
875
+
876
+ <div className="text-center space-y-4">
877
+ <motion.div
878
+ animate={{
879
+ scale: isDragOver ? 1.1 : 1,
880
+ rotate: isDragOver ? 5 : 0
881
+ }}
882
+ className="mx-auto h-12 w-12 text-muted-foreground"
883
+ >
884
+ <Upload className="h-full w-full" />
885
+ </motion.div>
886
+
887
+ <div>
888
+ <h3 className="text-lg font-semibold">
889
+ {isDragOver ? 'Dosyaları buraya bırakın' : 'Dosya Yükleyin'}
890
+ </h3>
891
+ <p className="text-sm text-muted-foreground mt-1">
892
+ Dosyaları sürükleyip bırakın veya tıklayarak seçin
893
+ </p>
894
+ <div className="flex items-center justify-center gap-4 mt-3 text-xs text-muted-foreground">
895
+ <span>Maks {maxFiles} dosya</span>
896
+ <span>•</span>
897
+ <span>{formatFileSize(maxSize)} her dosya</span>
898
+ {resumable && (
899
+ <>
900
+ <span>•</span>
901
+ <span>Devam ettirilebilir</span>
902
+ </>
903
+ )}
904
+ </div>
905
+ </div>
906
+
907
+ <Button variant="outline" disabled={disabled} type="button">
908
+ <Upload className="mr-2 h-4 w-4" />
909
+ Dosya Seç
910
+ </Button>
911
+ </div>
912
+ </motion.div>
913
+
914
+ {/* Error Message */}
915
+ <AnimatePresence>
916
+ {error && (
917
+ <motion.div
918
+ initial={{ opacity: 0, y: -10 }}
919
+ animate={{ opacity: 1, y: 0 }}
920
+ exit={{ opacity: 0, y: -10 }}
921
+ className="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-lg"
922
+ >
923
+ <AlertCircle className="h-4 w-4 text-destructive flex-shrink-0" />
924
+ <span className="text-sm text-destructive">{error}</span>
925
+ <Button
926
+ variant="ghost"
927
+ size="sm"
928
+ className="ml-auto h-6 w-6 p-0"
929
+ onClick={() => setError(null)}
930
+ >
931
+ <X className="h-3 w-3" />
932
+ </Button>
933
+ </motion.div>
934
+ )}
935
+ </AnimatePresence>
936
+
284
937
  {/* File List */}
285
- {files.length > 0 && (
286
- <div className="space-y-2">
287
- <h4 className="text-sm font-medium">Uploaded Files ({files.length})</h4>
288
- {files.map((uploadedFile) => (
289
- <div key={uploadedFile.id} className="flex items-center gap-3 p-3 border rounded-lg">
290
- <div className="flex-shrink-0">
291
- {getFileIcon(uploadedFile.file.type)}
292
- </div>
293
-
294
- <div className="flex-1 min-w-0">
295
- <div className="flex items-center justify-between">
296
- <p className="text-sm font-medium truncate">{uploadedFile.file.name}</p>
297
- <Badge variant={uploadedFile.status === 'success' ? 'success' : uploadedFile.status === 'error' ? 'destructive' : 'secondary'}>
298
- {uploadedFile.status === 'uploading' && <Loader2 className="h-3 w-3 mr-1 animate-spin" />}
299
- {uploadedFile.status === 'success' && <CheckCircle2 className="h-3 w-3 mr-1" />}
300
- {uploadedFile.status === 'error' && <AlertCircle className="h-3 w-3 mr-1" />}
301
- {uploadedFile.status}
302
- </Badge>
303
- </div>
304
-
305
- <div className="flex items-center justify-between mt-1">
306
- <p className="text-xs text-muted-foreground">
307
- {formatFileSize(uploadedFile.file.size)}
308
- </p>
309
- {uploadedFile.status === 'uploading' && (
310
- <span className="text-xs text-muted-foreground">
311
- {uploadedFile.progress}%
312
- </span>
313
- )}
314
- </div>
315
-
316
- {uploadedFile.status === 'uploading' && (
317
- <div className="mt-2 h-2 bg-secondary rounded-full overflow-hidden">
318
- <div
319
- className="h-full bg-primary transition-all duration-300 ease-out"
320
- style={{ width: `${uploadedFile.progress}%` }}
321
- />
322
- </div>
323
- )}
324
-
325
- {uploadedFile.error && (
326
- <p className="text-xs text-destructive mt-1">{uploadedFile.error}</p>
327
- )}
328
- </div>
329
-
330
- <div className="flex items-center gap-1">
331
- {uploadedFile.status === 'success' && showPreview && uploadedFile.file.type.startsWith('image/') && (
332
- <Button variant="ghost" size="sm">
333
- <Download className="h-4 w-4" />
334
- </Button>
335
- )}
336
-
337
- <Button
338
- variant="ghost"
938
+ <AnimatePresence>
939
+ {files.length > 0 && (
940
+ <motion.div
941
+ initial={{ opacity: 0, height: 0 }}
942
+ animate={{ opacity: 1, height: 'auto' }}
943
+ exit={{ opacity: 0, height: 0 }}
944
+ className="space-y-3"
945
+ >
946
+ {/* List Header */}
947
+ <div className="flex items-center justify-between">
948
+ <h4 className="text-sm font-medium">
949
+ Yüklenen Dosyalar ({files.length})
950
+ </h4>
951
+ {allowBulkOperations && files.length > 1 && (
952
+ <Button
953
+ variant="ghost"
339
954
  size="sm"
340
- onClick={() => handleRemove(uploadedFile)}
341
- disabled={uploadedFile.status === 'uploading'}
955
+ onClick={selectedIds.length === files.length ? handleClearSelection : handleSelectAll}
342
956
  >
343
- <X className="h-4 w-4" />
957
+ {selectedIds.length === files.length ? 'Seçimi Temizle' : 'Tümünü Seç'}
344
958
  </Button>
345
- </div>
959
+ )}
346
960
  </div>
347
- ))}
348
- </div>
349
- )}
961
+
962
+ {/* File Items */}
963
+ <div className={cn(
964
+ "space-y-2",
965
+ variant === 'grid' && "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 space-y-0"
966
+ )}>
967
+ <AnimatePresence>
968
+ {files.map(file => (
969
+ <FileUploadItem
970
+ key={file.id}
971
+ file={file}
972
+ variant={variant}
973
+ showPreview={showPreview}
974
+ showProgress={showProgress}
975
+ showMetadata={showMetadata}
976
+ allowBulkOperations={allowBulkOperations}
977
+ resumable={resumable}
978
+ selected={selectedIds.includes(file.id)}
979
+ onSelect={(selected) => handleBulkSelect(file.id, selected)}
980
+ onRemove={() => removeFile(file.id)}
981
+ onPauseResume={() => pauseResumeUpload(file.id)}
982
+ onPreview={() => handlePreview(file)}
983
+ />
984
+ ))}
985
+ </AnimatePresence>
986
+ </div>
987
+ </motion.div>
988
+ )}
989
+ </AnimatePresence>
990
+
991
+ {/* Preview Modal */}
992
+ <FilePreviewModal
993
+ file={previewFile}
994
+ isOpen={isPreviewOpen}
995
+ onClose={() => setIsPreviewOpen(false)}
996
+ />
997
+ </div>
998
+ )
999
+ }
1000
+ )
350
1001
 
351
- {/* Upload Status */}
352
- {isUploading && (
353
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
354
- <Loader2 className="h-4 w-4 animate-spin" />
355
- Uploading files...
1002
+ // Tekil dosya bileşeni
1003
+ const FileUploadItem = ({
1004
+ file,
1005
+ variant = 'default',
1006
+ showPreview = true,
1007
+ showProgress = true,
1008
+ showMetadata = true,
1009
+ allowBulkOperations = true,
1010
+ resumable = true,
1011
+ selected = false,
1012
+ onSelect,
1013
+ onRemove,
1014
+ onPauseResume,
1015
+ onPreview
1016
+ }: {
1017
+ file: FileUploadItem
1018
+ variant?: 'default' | 'compact' | 'grid'
1019
+ showPreview?: boolean
1020
+ showProgress?: boolean
1021
+ showMetadata?: boolean
1022
+ allowBulkOperations?: boolean
1023
+ resumable?: boolean
1024
+ selected?: boolean
1025
+ onSelect?: (selected: boolean) => void
1026
+ onRemove?: () => void
1027
+ onPauseResume?: () => void
1028
+ onPreview?: () => void
1029
+ }) => {
1030
+ const canPauseResume = resumable && ['uploading', 'paused'].includes(file.status)
1031
+ const canPreview = showPreview && file.preview && ['image', 'video', 'audio'].includes(file.preview.type)
1032
+
1033
+ return (
1034
+ <motion.div
1035
+ layout
1036
+ initial={{ opacity: 0, y: 10 }}
1037
+ animate={{ opacity: 1, y: 0 }}
1038
+ exit={{ opacity: 0, y: -10, scale: 0.95 }}
1039
+ className={cn(
1040
+ "group relative overflow-hidden rounded-lg border bg-card transition-all",
1041
+ selected && "ring-2 ring-primary bg-primary/5",
1042
+ variant === 'compact' && "p-3",
1043
+ variant === 'default' && "p-4",
1044
+ variant === 'grid' && "p-4"
1045
+ )}
1046
+ >
1047
+ {/* Selection Checkbox */}
1048
+ {allowBulkOperations && (
1049
+ <div className="absolute top-3 left-3 z-10">
1050
+ <input
1051
+ type="checkbox"
1052
+ checked={selected}
1053
+ onChange={(e) => onSelect?.(e.target.checked)}
1054
+ className="rounded border-muted-foreground/25"
1055
+ />
1056
+ </div>
1057
+ )}
1058
+
1059
+ {/* Preview/Thumbnail */}
1060
+ {showPreview && file.preview && (
1061
+ <div className={cn(
1062
+ "relative overflow-hidden rounded bg-muted/20",
1063
+ variant === 'grid' ? "aspect-video mb-3" : "w-12 h-12 float-left mr-3"
1064
+ )}>
1065
+ {file.preview.type === 'image' && file.preview.thumbnail && (
1066
+ <img
1067
+ src={file.preview.thumbnail}
1068
+ alt={file.file.name}
1069
+ className="w-full h-full object-cover"
1070
+ />
1071
+ )}
1072
+
1073
+ {file.preview.type === 'video' && file.preview.thumbnail && (
1074
+ <div className="relative w-full h-full">
1075
+ <img
1076
+ src={file.preview.thumbnail}
1077
+ alt={file.file.name}
1078
+ className="w-full h-full object-cover"
1079
+ />
1080
+ <div className="absolute inset-0 flex items-center justify-center bg-black/20">
1081
+ <Play className="h-6 w-6 text-white" />
1082
+ </div>
1083
+ </div>
1084
+ )}
1085
+
1086
+ {!file.preview.thumbnail && (
1087
+ <div className="w-full h-full flex items-center justify-center">
1088
+ {getFileIcon(file.file.type, variant === 'grid' ? 'lg' : 'md')}
1089
+ </div>
1090
+ )}
1091
+
1092
+ {canPreview && (
1093
+ <Button
1094
+ variant="secondary"
1095
+ size="sm"
1096
+ className="absolute top-1 right-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
1097
+ onClick={onPreview}
1098
+ >
1099
+ <Eye className="h-3 w-3" />
1100
+ </Button>
1101
+ )}
1102
+ </div>
1103
+ )}
1104
+
1105
+ {/* File Info */}
1106
+ <div className={cn(
1107
+ "flex-1 min-w-0",
1108
+ variant === 'grid' && "text-center"
1109
+ )}>
1110
+ <div className="flex items-start justify-between gap-2">
1111
+ <div className="flex-1 min-w-0">
1112
+ <h5 className={cn(
1113
+ "font-medium truncate",
1114
+ variant === 'compact' ? "text-sm" : "text-base"
1115
+ )}>
1116
+ {file.file.name}
1117
+ </h5>
1118
+
1119
+ {showMetadata && (
1120
+ <div className={cn(
1121
+ "flex items-center gap-2 mt-1 text-muted-foreground",
1122
+ variant === 'compact' ? "text-xs" : "text-sm",
1123
+ variant === 'grid' && "justify-center"
1124
+ )}>
1125
+ <span>{formatFileSize(file.file.size)}</span>
1126
+ {file.preview?.dimensions && (
1127
+ <>
1128
+ <span>•</span>
1129
+ <span>{file.preview.dimensions.width} × {file.preview.dimensions.height}</span>
1130
+ </>
1131
+ )}
1132
+ {file.preview?.duration && (
1133
+ <>
1134
+ <span>•</span>
1135
+ <span>{formatTime(file.preview.duration)}</span>
1136
+ </>
1137
+ )}
1138
+ </div>
1139
+ )}
356
1140
  </div>
1141
+
1142
+ {/* Status Badge */}
1143
+ <Badge
1144
+ variant={
1145
+ file.status === 'success' ? 'success' :
1146
+ file.status === 'error' ? 'destructive' :
1147
+ file.status === 'paused' ? 'secondary' : 'secondary'
1148
+ }
1149
+ className="flex-shrink-0"
1150
+ >
1151
+ {file.status === 'uploading' && <Loader2 className="h-3 w-3 mr-1 animate-spin" />}
1152
+ {file.status === 'success' && <CheckCircle2 className="h-3 w-3 mr-1" />}
1153
+ {file.status === 'error' && <AlertCircle className="h-3 w-3 mr-1" />}
1154
+ {file.status === 'paused' && <Pause className="h-3 w-3 mr-1" />}
1155
+ {file.status === 'pending' ? 'Bekliyor' :
1156
+ file.status === 'uploading' ? 'Yükleniyor' :
1157
+ file.status === 'paused' ? 'Duraklatıldı' :
1158
+ file.status === 'success' ? 'Tamamlandı' :
1159
+ file.status === 'error' ? 'Hata' : 'İptal'}
1160
+ </Badge>
1161
+ </div>
1162
+
1163
+ {/* Progress */}
1164
+ {showProgress && file.status === 'uploading' && (
1165
+ <div className="mt-3 space-y-1">
1166
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
1167
+ <span>{file.progress}%</span>
1168
+ {file.speed && (
1169
+ <span>{formatFileSize(file.speed)}/s</span>
1170
+ )}
1171
+ {file.estimatedTime && file.estimatedTime > 0 && (
1172
+ <span>{formatTime(file.estimatedTime)} kaldı</span>
1173
+ )}
1174
+ </div>
1175
+ <Progress value={file.progress} className="h-1" />
1176
+ </div>
1177
+ )}
1178
+
1179
+ {/* Error Message */}
1180
+ {file.status === 'error' && file.error && (
1181
+ <p className="mt-2 text-xs text-destructive">{file.error}</p>
357
1182
  )}
358
- </CardContent>
359
- </Card>
1183
+
1184
+ {/* Actions */}
1185
+ <div className={cn(
1186
+ "flex items-center gap-1 mt-3",
1187
+ variant === 'grid' && "justify-center"
1188
+ )}>
1189
+ {canPauseResume && (
1190
+ <Button
1191
+ variant="ghost"
1192
+ size="sm"
1193
+ onClick={onPauseResume}
1194
+ className="h-7 px-2"
1195
+ >
1196
+ {file.status === 'uploading' ? (
1197
+ <Pause className="h-3 w-3" />
1198
+ ) : (
1199
+ <Play className="h-3 w-3" />
1200
+ )}
1201
+ </Button>
1202
+ )}
1203
+
1204
+ {file.status === 'error' && resumable && (
1205
+ <Button
1206
+ variant="ghost"
1207
+ size="sm"
1208
+ onClick={onPauseResume}
1209
+ className="h-7 px-2"
1210
+ >
1211
+ <RotateCcw className="h-3 w-3" />
1212
+ </Button>
1213
+ )}
1214
+
1215
+ {file.status === 'success' && (
1216
+ <Button
1217
+ variant="ghost"
1218
+ size="sm"
1219
+ className="h-7 px-2"
1220
+ >
1221
+ <Download className="h-3 w-3" />
1222
+ </Button>
1223
+ )}
1224
+
1225
+ <DropdownMenu>
1226
+ <DropdownMenuTrigger asChild>
1227
+ <Button variant="ghost" size="sm" className="h-7 w-7 p-0">
1228
+ <MoreHorizontal className="h-3 w-3" />
1229
+ </Button>
1230
+ </DropdownMenuTrigger>
1231
+ <DropdownMenuContent align="end">
1232
+ {canPreview && (
1233
+ <DropdownMenuItem onClick={onPreview}>
1234
+ <Eye className="mr-2 h-4 w-4" />
1235
+ Önizle
1236
+ </DropdownMenuItem>
1237
+ )}
1238
+ <DropdownMenuItem>
1239
+ <Copy className="mr-2 h-4 w-4" />
1240
+ Linki Kopyala
1241
+ </DropdownMenuItem>
1242
+ <DropdownMenuItem>
1243
+ <Share className="mr-2 h-4 w-4" />
1244
+ Paylaş
1245
+ </DropdownMenuItem>
1246
+ <DropdownMenuItem onClick={onRemove} className="text-destructive">
1247
+ <Trash2 className="mr-2 h-4 w-4" />
1248
+ Sil
1249
+ </DropdownMenuItem>
1250
+ </DropdownMenuContent>
1251
+ </DropdownMenu>
1252
+ </div>
1253
+ </div>
1254
+ </motion.div>
360
1255
  )
361
1256
  }
362
1257
 
363
- export default FileUpload
1258
+ MoonUIFileUploadPro.displayName = "MoonUIFileUploadPro"
1259
+
1260
+ export default MoonUIFileUploadPro