@moontra/moonui-pro 2.20.2 → 2.20.3

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.
Files changed (153) hide show
  1. package/package.json +8 -3
  2. package/plugin/index.d.ts +86 -0
  3. package/plugin/index.js +308 -0
  4. package/scripts/postinstall.js +176 -23
  5. package/src/components/advanced-chart/index.tsx +0 -1246
  6. package/src/components/advanced-forms/index.tsx +0 -585
  7. package/src/components/animated-button/index.tsx +0 -385
  8. package/src/components/calendar/event-dialog.tsx +0 -377
  9. package/src/components/calendar/index.tsx +0 -1220
  10. package/src/components/calendar-pro/index.tsx +0 -1697
  11. package/src/components/color-picker/index.tsx +0 -432
  12. package/src/components/credit-card-input/index.tsx +0 -406
  13. package/src/components/dashboard/dashboard-grid.tsx +0 -480
  14. package/src/components/dashboard/demo.tsx +0 -425
  15. package/src/components/dashboard/index.tsx +0 -1046
  16. package/src/components/dashboard/time-range-picker.tsx +0 -336
  17. package/src/components/dashboard/types.ts +0 -225
  18. package/src/components/dashboard/widgets/activity-feed.tsx +0 -349
  19. package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
  20. package/src/components/dashboard/widgets/comparison-widget.tsx +0 -177
  21. package/src/components/dashboard/widgets/index.ts +0 -5
  22. package/src/components/dashboard/widgets/metric-card.tsx +0 -363
  23. package/src/components/dashboard/widgets/progress-widget.tsx +0 -113
  24. package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
  25. package/src/components/data-table/data-table-column-toggle.tsx +0 -169
  26. package/src/components/data-table/data-table-export.ts +0 -156
  27. package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
  28. package/src/components/data-table/index.tsx +0 -845
  29. package/src/components/draggable-list/index.tsx +0 -100
  30. package/src/components/error-boundary/index.tsx +0 -232
  31. package/src/components/file-upload/index.tsx +0 -1660
  32. package/src/components/floating-action-button/index.tsx +0 -206
  33. package/src/components/form-wizard/form-wizard-context.tsx +0 -335
  34. package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
  35. package/src/components/form-wizard/form-wizard-progress.tsx +0 -329
  36. package/src/components/form-wizard/form-wizard-step.tsx +0 -111
  37. package/src/components/form-wizard/index.tsx +0 -102
  38. package/src/components/form-wizard/types.ts +0 -77
  39. package/src/components/gesture-drawer/index.tsx +0 -551
  40. package/src/components/github-stars/github-api.ts +0 -426
  41. package/src/components/github-stars/hooks.ts +0 -517
  42. package/src/components/github-stars/index.tsx +0 -375
  43. package/src/components/github-stars/types.ts +0 -148
  44. package/src/components/github-stars/variants.tsx +0 -515
  45. package/src/components/health-check/index.tsx +0 -439
  46. package/src/components/hover-card-3d/index.tsx +0 -529
  47. package/src/components/index.ts +0 -130
  48. package/src/components/internal/index.ts +0 -78
  49. package/src/components/kanban/add-card-modal.tsx +0 -502
  50. package/src/components/kanban/card-detail-modal.tsx +0 -761
  51. package/src/components/kanban/index.ts +0 -13
  52. package/src/components/kanban/kanban.tsx +0 -1689
  53. package/src/components/kanban/types.ts +0 -168
  54. package/src/components/lazy-component/index.tsx +0 -823
  55. package/src/components/license-error/index.tsx +0 -31
  56. package/src/components/magnetic-button/index.tsx +0 -216
  57. package/src/components/memory-efficient-data/index.tsx +0 -1018
  58. package/src/components/moonui-quiz-form/index.tsx +0 -817
  59. package/src/components/navbar/index.tsx +0 -781
  60. package/src/components/optimized-image/index.tsx +0 -425
  61. package/src/components/performance-debugger/index.tsx +0 -613
  62. package/src/components/performance-monitor/index.tsx +0 -808
  63. package/src/components/phone-number-input/index.tsx +0 -343
  64. package/src/components/phone-number-input/phone-number-input-simple.tsx +0 -167
  65. package/src/components/pinch-zoom/index.tsx +0 -566
  66. package/src/components/quiz-form/index.tsx +0 -479
  67. package/src/components/rich-text-editor/index.tsx +0 -2322
  68. package/src/components/rich-text-editor/slash-commands-extension.ts +0 -230
  69. package/src/components/rich-text-editor/slash-commands.css +0 -35
  70. package/src/components/rich-text-editor/table-styles.css +0 -65
  71. package/src/components/sidebar/index.tsx +0 -884
  72. package/src/components/spotlight-card/index.tsx +0 -191
  73. package/src/components/swipeable-card/index.tsx +0 -100
  74. package/src/components/timeline/index.tsx +0 -1183
  75. package/src/components/ui/accordion.tsx +0 -581
  76. package/src/components/ui/alert-dialog.tsx +0 -141
  77. package/src/components/ui/alert.tsx +0 -141
  78. package/src/components/ui/aspect-ratio.tsx +0 -245
  79. package/src/components/ui/avatar.tsx +0 -155
  80. package/src/components/ui/badge.tsx +0 -230
  81. package/src/components/ui/breadcrumb.tsx +0 -216
  82. package/src/components/ui/button.tsx +0 -228
  83. package/src/components/ui/calendar.tsx +0 -387
  84. package/src/components/ui/card.tsx +0 -216
  85. package/src/components/ui/checkbox.tsx +0 -259
  86. package/src/components/ui/collapsible.tsx +0 -631
  87. package/src/components/ui/color-picker.tsx +0 -97
  88. package/src/components/ui/command.tsx +0 -948
  89. package/src/components/ui/dialog.tsx +0 -752
  90. package/src/components/ui/dropdown-menu.tsx +0 -706
  91. package/src/components/ui/gesture-drawer.tsx +0 -11
  92. package/src/components/ui/hover-card.tsx +0 -29
  93. package/src/components/ui/index.ts +0 -222
  94. package/src/components/ui/input.tsx +0 -224
  95. package/src/components/ui/label.tsx +0 -29
  96. package/src/components/ui/lightbox.tsx +0 -606
  97. package/src/components/ui/magnetic-button.tsx +0 -129
  98. package/src/components/ui/media-gallery.tsx +0 -611
  99. package/src/components/ui/navigation-menu.tsx +0 -130
  100. package/src/components/ui/pagination.tsx +0 -125
  101. package/src/components/ui/popover.tsx +0 -185
  102. package/src/components/ui/progress.tsx +0 -30
  103. package/src/components/ui/radio-group.tsx +0 -257
  104. package/src/components/ui/scroll-area.tsx +0 -47
  105. package/src/components/ui/select.tsx +0 -378
  106. package/src/components/ui/separator.tsx +0 -145
  107. package/src/components/ui/sheet.tsx +0 -139
  108. package/src/components/ui/skeleton.tsx +0 -20
  109. package/src/components/ui/slider.tsx +0 -354
  110. package/src/components/ui/spotlight-card.tsx +0 -119
  111. package/src/components/ui/switch.tsx +0 -86
  112. package/src/components/ui/table.tsx +0 -331
  113. package/src/components/ui/tabs-pro.tsx +0 -542
  114. package/src/components/ui/tabs.tsx +0 -54
  115. package/src/components/ui/textarea.tsx +0 -28
  116. package/src/components/ui/toast.tsx +0 -317
  117. package/src/components/ui/toggle.tsx +0 -119
  118. package/src/components/ui/tooltip.tsx +0 -151
  119. package/src/components/virtual-list/index.tsx +0 -668
  120. package/src/hooks/use-chart.ts +0 -205
  121. package/src/hooks/use-data-table.ts +0 -182
  122. package/src/hooks/use-docs-pro-access.ts +0 -13
  123. package/src/hooks/use-license-check.ts +0 -65
  124. package/src/hooks/use-subscription.ts +0 -19
  125. package/src/hooks/use-toast.ts +0 -15
  126. package/src/index.ts +0 -22
  127. package/src/lib/ai-providers.ts +0 -377
  128. package/src/lib/component-metadata.ts +0 -18
  129. package/src/lib/micro-interactions.ts +0 -255
  130. package/src/lib/paddle.ts +0 -17
  131. package/src/lib/utils.ts +0 -6
  132. package/src/patterns/login-form/index.tsx +0 -276
  133. package/src/patterns/login-form/types.ts +0 -67
  134. package/src/setupTests.ts +0 -41
  135. package/src/styles/advanced-chart.css +0 -239
  136. package/src/styles/calendar.css +0 -35
  137. package/src/styles/design-system.css +0 -363
  138. package/src/styles/index.css +0 -681
  139. package/src/styles/tailwind.css +0 -7
  140. package/src/styles/tokens.css +0 -455
  141. package/src/types/next-auth.d.ts +0 -21
  142. package/src/use-intersection-observer.tsx +0 -154
  143. package/src/use-local-storage.tsx +0 -71
  144. package/src/use-paddle.ts +0 -138
  145. package/src/use-performance-optimizer.ts +0 -389
  146. package/src/use-pro-access.ts +0 -141
  147. package/src/use-scroll-animation.ts +0 -219
  148. package/src/use-subscription.ts +0 -37
  149. package/src/use-toast.ts +0 -32
  150. package/src/utils/chart-helpers.ts +0 -357
  151. package/src/utils/cn.ts +0 -6
  152. package/src/utils/data-processing.ts +0 -151
  153. package/src/utils/license-validator.tsx +0 -183
@@ -1,1660 +0,0 @@
1
- "use client"
2
-
3
- import React, { useCallback, useState, useRef } from 'react'
4
- import { motion, AnimatePresence } from 'framer-motion'
5
- import { cva } from 'class-variance-authority'
6
- import {
7
- Upload,
8
- X,
9
- File,
10
- Image as ImageIcon,
11
- Video,
12
- Music,
13
- FileText,
14
- AlertCircle,
15
- Loader2,
16
- Play,
17
- Pause,
18
- RotateCcw,
19
- Download,
20
- Eye,
21
- Trash2,
22
- MoreHorizontal,
23
- Copy,
24
- Share,
25
- Archive,
26
- Lock,
27
- Sparkles,
28
- CheckCircle2
29
- } from 'lucide-react'
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'
47
- import { useSubscription } from '../../hooks/use-subscription'
48
- import { useToast } from '../../use-toast'
49
-
50
- // Ana interface'ler ve type'lar
51
- export interface FileUploadProProps {
52
- accept?: string
53
- multiple?: boolean
54
- maxSize?: number // bytes cinsinden
55
- maxFiles?: number
56
- disabled?: boolean
57
- className?: string
58
- variant?: 'default' | 'compact' | 'grid'
59
- theme?: 'light' | 'dark' | 'auto'
60
-
61
- // Gelişmiş özellikler
62
- chunkSize?: number // chunk upload için
63
- resumable?: boolean
64
- compression?: boolean
65
- imageResize?: {
66
- maxWidth: number
67
- maxHeight: number
68
- quality: number
69
- }
70
-
71
- // Validasyon
72
- allowedMimeTypes?: string[]
73
- maxTotalSize?: number
74
- customValidation?: (file: File) => Promise<string | null>
75
- duplicateCheck?: boolean
76
-
77
- // Upload ayarları
78
- uploadStrategy?: 'direct' | 'presigned' | 'multipart'
79
- endpoint?: string
80
- headers?: Record<string, string>
81
-
82
- // Service integrations
83
- serviceConfig?: {
84
- type: 'aws-s3' | 'cloudinary' | 'firebase' | 'custom'
85
- config?: any
86
- }
87
-
88
- // Callbacks
89
- onUpload?: (files: FileUploadItem[]) => Promise<void>
90
- onProgress?: (fileId: string, progress: number) => void
91
- onComplete?: (fileId: string, result: any) => void
92
- onError?: (fileId: string, error: string) => void
93
- onRemove?: (fileId: string) => void
94
- onPreview?: (file: FileUploadItem) => void
95
- onBulkSelect?: (selectedIds: string[]) => void
96
-
97
- // UI özelleştirme
98
- showPreview?: boolean
99
- showProgress?: boolean
100
- showMetadata?: boolean
101
- allowBulkOperations?: boolean
102
- previewTypes?: ('image' | 'video' | 'audio' | 'pdf' | 'document')[]
103
- }
104
-
105
- export interface FileUploadItem {
106
- id: string
107
- file: File
108
- status: 'pending' | 'uploading' | 'paused' | 'success' | 'error' | 'cancelled'
109
- progress: number
110
- uploadedBytes?: number
111
- totalBytes: number
112
- speed?: number // bytes/second
113
- estimatedTime?: number // seconds
114
- error?: string
115
- chunks?: FileChunk[]
116
- preview?: FilePreview
117
- metadata?: FileMetadata
118
- result?: any // upload sonucu
119
- }
120
-
121
- interface FileChunk {
122
- index: number
123
- start: number
124
- end: number
125
- status: 'pending' | 'uploading' | 'success' | 'error'
126
- attempts: number
127
- }
128
-
129
- interface FilePreview {
130
- type: 'image' | 'video' | 'audio' | 'pdf' | 'document' | 'unknown'
131
- url?: string
132
- thumbnail?: string
133
- duration?: number // video/audio için
134
- dimensions?: { width: number; height: number }
135
- pages?: number // PDF için
136
- }
137
-
138
- interface FileMetadata {
139
- name: string
140
- size: number
141
- type: string
142
- lastModified: number
143
- hash?: string
144
- dimensions?: { width: number; height: number }
145
- duration?: number
146
- bitrate?: number
147
- }
148
-
149
- // CVA variants
150
- const fileUploadVariants = cva(
151
- "relative overflow-hidden transition-all duration-200",
152
- {
153
- variants: {
154
- variant: {
155
- default: "border-2 border-dashed rounded-lg p-8",
156
- compact: "border border-dashed rounded-md p-4",
157
- grid: "border-2 border-dashed rounded-lg p-6"
158
- },
159
- state: {
160
- idle: "border-muted-foreground/25 hover:border-primary/50",
161
- dragover: "border-primary bg-primary/5 scale-102",
162
- disabled: "opacity-50 cursor-not-allowed"
163
- }
164
- },
165
- defaultVariants: {
166
- variant: "default",
167
- state: "idle"
168
- }
169
- }
170
- )
171
-
172
- // Utility fonksiyonlar
173
- const formatFileSize = (bytes: number): string => {
174
- if (bytes === 0) return '0 B'
175
- const k = 1024
176
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
177
- const i = Math.floor(Math.log(bytes) / Math.log(k))
178
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
179
- }
180
-
181
- const formatTime = (seconds: number): string => {
182
- if (seconds < 60) return `${Math.round(seconds)}s`
183
- const minutes = Math.floor(seconds / 60)
184
- const remainingSeconds = Math.round(seconds % 60)
185
- return `${minutes}m ${remainingSeconds}s`
186
- }
187
-
188
- const getFileIcon = (type: string, size: 'sm' | 'md' | 'lg' = 'md') => {
189
- const iconSize = size === 'sm' ? 'h-4 w-4' : size === 'md' ? 'h-5 w-5' : 'h-6 w-6'
190
-
191
- if (type.startsWith('image/')) return <ImageIcon className={iconSize} />
192
- if (type.startsWith('video/')) return <Video className={iconSize} />
193
- if (type.startsWith('audio/')) return <Music className={iconSize} />
194
- if (type.includes('pdf')) return <FileText className={iconSize} />
195
- if (type.includes('zip') || type.includes('rar') || type.includes('7z')) {
196
- return <Archive className={iconSize} />
197
- }
198
- return <File className={iconSize} />
199
- }
200
-
201
- const generateFileHash = async (file: File): Promise<string> => {
202
- const buffer = await file.arrayBuffer()
203
- const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
204
- const hashArray = Array.from(new Uint8Array(hashBuffer))
205
- return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
206
- }
207
-
208
- const createImagePreview = (file: File): Promise<FilePreview> => {
209
- return new Promise((resolve) => {
210
- const reader = new FileReader()
211
-
212
- reader.onload = (e) => {
213
- const result = e.target?.result as string
214
- const img = new Image()
215
-
216
- img.onload = () => {
217
- // Create high-quality thumbnail
218
- const canvas = document.createElement('canvas')
219
- const ctx = canvas.getContext('2d')!
220
-
221
- // Calculate thumbnail dimensions (max 400px)
222
- const maxThumbSize = 400
223
- let thumbWidth = img.width
224
- let thumbHeight = img.height
225
-
226
- if (thumbWidth > maxThumbSize || thumbHeight > maxThumbSize) {
227
- const aspectRatio = img.width / img.height
228
- if (aspectRatio > 1) {
229
- thumbWidth = maxThumbSize
230
- thumbHeight = maxThumbSize / aspectRatio
231
- } else {
232
- thumbHeight = maxThumbSize
233
- thumbWidth = maxThumbSize * aspectRatio
234
- }
235
- }
236
-
237
- canvas.width = thumbWidth
238
- canvas.height = thumbHeight
239
- ctx.drawImage(img, 0, 0, thumbWidth, thumbHeight)
240
-
241
- const thumbnail = canvas.toDataURL('image/jpeg', 0.9)
242
-
243
- resolve({
244
- type: 'image',
245
- url: result,
246
- thumbnail,
247
- dimensions: { width: img.width, height: img.height }
248
- })
249
- }
250
-
251
- img.onerror = () => {
252
- resolve({
253
- type: 'image',
254
- url: result,
255
- thumbnail: result
256
- })
257
- }
258
-
259
- img.src = result
260
- }
261
-
262
- reader.onerror = () => {
263
- const url = URL.createObjectURL(file)
264
- resolve({
265
- type: 'image',
266
- url
267
- })
268
- }
269
-
270
- reader.readAsDataURL(file)
271
- })
272
- }
273
-
274
- const createVideoPreview = (file: File): Promise<FilePreview> => {
275
- return new Promise((resolve) => {
276
- const video = document.createElement('video')
277
- const url = URL.createObjectURL(file)
278
-
279
- video.onloadedmetadata = () => {
280
- // Video thumbnail oluşturma
281
- const canvas = document.createElement('canvas')
282
- const ctx = canvas.getContext('2d')!
283
-
284
- canvas.width = 320
285
- canvas.height = (video.videoHeight / video.videoWidth) * 320
286
-
287
- video.currentTime = Math.min(1, video.duration / 4) // İlk saniye veya %25
288
-
289
- video.onseeked = () => {
290
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
291
- const thumbnail = canvas.toDataURL('image/jpeg', 0.8)
292
-
293
- resolve({
294
- type: 'video',
295
- url,
296
- thumbnail,
297
- duration: video.duration,
298
- dimensions: { width: video.videoWidth, height: video.videoHeight }
299
- })
300
- }
301
- }
302
-
303
- video.onerror = () => {
304
- resolve({
305
- type: 'video',
306
- url
307
- })
308
- }
309
-
310
- video.src = url
311
- })
312
- }
313
-
314
- // Bulk operation bileşeni
315
- const BulkActions = ({
316
- selectedIds,
317
- onClearSelection,
318
- onBulkRemove,
319
- onBulkDownload
320
- }: {
321
- selectedIds: string[]
322
- onClearSelection: () => void
323
- onBulkRemove: () => void
324
- onBulkDownload: () => void
325
- }) => {
326
- if (selectedIds.length === 0) return null
327
-
328
- return (
329
- <motion.div
330
- initial={{ opacity: 0, y: -10 }}
331
- animate={{ opacity: 1, y: 0 }}
332
- exit={{ opacity: 0, y: -10 }}
333
- className="flex items-center gap-2 p-3 bg-primary/5 rounded-lg border"
334
- >
335
- <span className="text-sm font-medium">{selectedIds.length} files selected</span>
336
- <div className="flex gap-1 ml-auto">
337
- <Button variant="ghost" size="sm" onClick={onBulkDownload}>
338
- <Download className="h-4 w-4 mr-1" />
339
- Download
340
- </Button>
341
- <Button variant="ghost" size="sm" onClick={onBulkRemove}>
342
- <Trash2 className="h-4 w-4 mr-1" />
343
- Delete
344
- </Button>
345
- <Button variant="ghost" size="sm" onClick={onClearSelection}>
346
- <X className="h-4 w-4" />
347
- </Button>
348
- </div>
349
- </motion.div>
350
- )
351
- }
352
-
353
- // Dosya önizleme modal
354
- const FilePreviewModal = ({
355
- file,
356
- isOpen,
357
- onClose
358
- }: {
359
- file: FileUploadItem | null
360
- isOpen: boolean
361
- onClose: () => void
362
- }) => {
363
- if (!file || !file.preview) return null
364
-
365
- return (
366
- <Dialog open={isOpen} onOpenChange={onClose}>
367
- <DialogContent className="max-w-4xl max-h-[90vh]">
368
- <DialogHeader>
369
- <DialogTitle className="flex items-center gap-2">
370
- {getFileIcon(file.file.type)}
371
- {file.file.name}
372
- <Badge variant="secondary">
373
- {formatFileSize(file.file.size)}
374
- </Badge>
375
- </DialogTitle>
376
- </DialogHeader>
377
-
378
- <div className="flex items-center justify-center p-4 bg-muted/20 rounded-lg">
379
- {file.preview.type === 'image' && (
380
- <div className="relative">
381
- <img
382
- src={file.preview.url}
383
- alt={file.file.name}
384
- className="max-w-full max-h-[60vh] object-contain rounded shadow-lg"
385
- loading="eager"
386
- />
387
- {/* Image loading indicator */}
388
- <div className="absolute inset-0 flex items-center justify-center bg-background/80 rounded opacity-0 pointer-events-none transition-opacity duration-300">
389
- <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
390
- </div>
391
- </div>
392
- )}
393
-
394
- {file.preview.type === 'video' && (
395
- <div className="relative w-full max-w-4xl">
396
- <video
397
- src={file.preview.url}
398
- controls
399
- className="w-full max-h-[60vh] rounded shadow-lg"
400
- poster={file.preview.thumbnail}
401
- />
402
- </div>
403
- )}
404
-
405
- {file.preview.type === 'audio' && (
406
- <div className="w-full max-w-md space-y-4">
407
- <div className="p-8 bg-gradient-to-br from-primary/10 to-primary/5 rounded-lg">
408
- <Music className="h-16 w-16 mx-auto text-primary mb-4" />
409
- <audio src={file.preview.url} controls className="w-full" />
410
- </div>
411
- </div>
412
- )}
413
-
414
- {!['image', 'video', 'audio'].includes(file.preview.type) && (
415
- <div className="text-center py-8">
416
- {getFileIcon(file.file.type, 'lg')}
417
- <p className="mt-2 text-muted-foreground">Preview not available</p>
418
- </div>
419
- )}
420
- </div>
421
-
422
- {file.preview.dimensions && (
423
- <div className="flex gap-4 text-sm text-muted-foreground">
424
- <span>Dimensions: {file.preview.dimensions.width} × {file.preview.dimensions.height}</span>
425
- {file.preview.duration && (
426
- <span>Duration: {formatTime(file.preview.duration)}</span>
427
- )}
428
- </div>
429
- )}
430
- </DialogContent>
431
- </Dialog>
432
- )
433
- }
434
-
435
- // Ana bileşen
436
- export const MoonUIFileUploadPro = React.forwardRef<HTMLDivElement, FileUploadProProps>(
437
- ({
438
- accept = "*",
439
- multiple = true,
440
- maxSize = 100 * 1024 * 1024, // 100MB
441
- maxFiles = 10,
442
- disabled = false,
443
- className,
444
- variant = "default",
445
- chunkSize = 1024 * 1024, // 1MB chunks
446
- resumable = true,
447
- compression = false,
448
- allowedMimeTypes = [],
449
- maxTotalSize,
450
- duplicateCheck = true,
451
- uploadStrategy = 'direct',
452
- showPreview = true,
453
- showProgress = true,
454
- showMetadata = true,
455
- allowBulkOperations = true,
456
- previewTypes = ['image', 'video', 'audio', 'pdf'],
457
- onUpload,
458
- onProgress,
459
- onComplete,
460
- onError,
461
- onRemove,
462
- onPreview,
463
- onBulkSelect,
464
- customValidation,
465
- serviceConfig,
466
- imageResize,
467
- endpoint,
468
- headers,
469
- ...props
470
- }, ref) => {
471
-
472
- // Subscription kontrolü - Pro bileşen
473
- const { hasProAccess, isLoading } = useSubscription()
474
-
475
- // State'ler
476
- const [files, setFiles] = useState<FileUploadItem[]>([])
477
- const [isDragOver, setIsDragOver] = useState(false)
478
- const [selectedIds, setSelectedIds] = useState<string[]>([])
479
- const [previewFile, setPreviewFile] = useState<FileUploadItem | null>(null)
480
- const [isPreviewOpen, setIsPreviewOpen] = useState(false)
481
- const [error, setError] = useState<string | null>(null)
482
-
483
- const fileInputRef = useRef<HTMLInputElement>(null)
484
- const uploadQueue = useRef<Map<string, AbortController>>(new Map())
485
-
486
- // Pro lisans kontrolü
487
- if (!isLoading && !hasProAccess) {
488
- return (
489
- <Card className={cn("w-full", className)}>
490
- <CardContent className="py-12 text-center">
491
- <div className="max-w-md mx-auto space-y-4">
492
- <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
493
- <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
494
- </div>
495
- <div>
496
- <h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
497
- <p className="text-muted-foreground text-sm mb-4">
498
- Advanced File Upload is exclusive to MoonUI Pro subscribers.
499
- </p>
500
- <div className="flex gap-3 justify-center">
501
- <a href="/pricing">
502
- <Button size="sm">
503
- <Sparkles className="mr-2 h-4 w-4" />
504
- Upgrade to Pro
505
- </Button>
506
- </a>
507
- </div>
508
- </div>
509
- </div>
510
- </CardContent>
511
- </Card>
512
- )
513
- }
514
-
515
- // Dosya validasyonu
516
- const validateFile = useCallback(async (file: File): Promise<string | null> => {
517
- // Boyut kontrolü
518
- if (file.size > maxSize) {
519
- return `File size exceeds ${formatFileSize(maxSize)} limit`
520
- }
521
-
522
- // MIME type kontrolü
523
- if (allowedMimeTypes.length > 0 && !allowedMimeTypes.includes(file.type)) {
524
- return `File type ${file.type} is not supported`
525
- }
526
-
527
- // Accept kontrolü
528
- if (accept !== "*") {
529
- const acceptTypes = accept.split(',').map(t => t.trim())
530
- const isAccepted = acceptTypes.some(acceptType => {
531
- if (acceptType.includes('*')) {
532
- return file.type.startsWith(acceptType.replace('*', ''))
533
- }
534
- return file.type === acceptType
535
- })
536
-
537
- if (!isAccepted) {
538
- return `File type not accepted`
539
- }
540
- }
541
-
542
- // Özel validasyon
543
- if (customValidation) {
544
- const customError = await customValidation(file)
545
- if (customError) return customError
546
- }
547
-
548
- return null
549
- }, [maxSize, allowedMimeTypes, accept, customValidation])
550
-
551
- // Duplicate kontrolü
552
- const checkDuplicate = useCallback(async (file: File): Promise<boolean> => {
553
- if (!duplicateCheck) return false
554
-
555
- const hash = await generateFileHash(file)
556
- return files.some(f => f.metadata?.hash === hash)
557
- }, [files, duplicateCheck])
558
-
559
- // Dosya önizleme oluşturma
560
- const createPreview = useCallback(async (file: File): Promise<FilePreview | undefined> => {
561
- if (!showPreview) return undefined
562
-
563
- const fileType = file.type.split('/')[0] as 'image' | 'video' | 'audio'
564
-
565
- if (!previewTypes.includes(fileType)) return undefined
566
-
567
- try {
568
- if (fileType === 'image') {
569
- return await createImagePreview(file)
570
- } else if (fileType === 'video') {
571
- return await createVideoPreview(file)
572
- } else if (fileType === 'audio') {
573
- const url = URL.createObjectURL(file)
574
- return {
575
- type: 'audio',
576
- url
577
- }
578
- }
579
- } catch (error) {
580
- console.warn('Failed to create preview:', error)
581
- }
582
-
583
- return undefined
584
- }, [showPreview, previewTypes])
585
-
586
- // Dosya metadata oluşturma
587
- const createMetadata = useCallback(async (file: File): Promise<FileMetadata> => {
588
- const metadata: FileMetadata = {
589
- name: file.name,
590
- size: file.size,
591
- type: file.type,
592
- lastModified: file.lastModified
593
- }
594
-
595
- if (duplicateCheck) {
596
- metadata.hash = await generateFileHash(file)
597
- }
598
-
599
- return metadata
600
- }, [duplicateCheck])
601
-
602
- // Image resize handler
603
- const resizeImage = useCallback(async (file: File): Promise<File> => {
604
- if (!imageResize || !file.type.startsWith('image/')) return file
605
-
606
- const { maxWidth, maxHeight, quality } = imageResize
607
-
608
- return new Promise((resolve) => {
609
- const img = new Image()
610
- const reader = new FileReader()
611
-
612
- reader.onload = (e) => {
613
- img.src = e.target?.result as string
614
-
615
- img.onload = () => {
616
- const canvas = document.createElement('canvas')
617
- const ctx = canvas.getContext('2d')!
618
-
619
- let width = img.width
620
- let height = img.height
621
-
622
- // Calculate new dimensions
623
- if (width > maxWidth || height > maxHeight) {
624
- const aspectRatio = width / height
625
- if (width > height) {
626
- width = maxWidth
627
- height = maxWidth / aspectRatio
628
- } else {
629
- height = maxHeight
630
- width = maxHeight * aspectRatio
631
- }
632
- }
633
-
634
- canvas.width = width
635
- canvas.height = height
636
- ctx.drawImage(img, 0, 0, width, height)
637
-
638
- canvas.toBlob(
639
- (blob) => {
640
- if (blob) {
641
- // Create a resized file
642
- const resizedFile = new window.File([blob], file.name, {
643
- type: file.type,
644
- lastModified: Date.now()
645
- })
646
- resolve(resizedFile)
647
- } else {
648
- resolve(file)
649
- }
650
- },
651
- file.type,
652
- quality
653
- )
654
- }
655
- }
656
-
657
- reader.readAsDataURL(file)
658
- })
659
- }, [imageResize])
660
-
661
- // Service specific upload handler
662
- const uploadToService = useCallback(async (fileItem: FileUploadItem): Promise<any> => {
663
- if (!serviceConfig) return null
664
-
665
- const { file } = fileItem
666
- let processedFile = file
667
-
668
- // Resize image if needed
669
- if (imageResize && file.type.startsWith('image/')) {
670
- processedFile = await resizeImage(file)
671
- }
672
-
673
- try {
674
- switch (serviceConfig.type) {
675
- case 'aws-s3': {
676
- // AWS S3 Direct Upload
677
- const { config } = serviceConfig as any
678
- if (!config) throw new Error('AWS S3 config is required')
679
-
680
- const { bucketUrl, bucketName, region, accessKeyId, policy, signature, algorithm, credential, date, fields } = config
681
-
682
- if (!bucketUrl && !bucketName) {
683
- throw new Error('AWS S3 requires either bucketUrl or bucketName')
684
- }
685
-
686
- // Option 1: Direct upload to custom S3-compatible endpoint
687
- if (bucketUrl && !bucketName) {
688
- const formData = new FormData()
689
- formData.append('file', processedFile || file)
690
-
691
- const response = await fetch(bucketUrl, {
692
- method: 'POST',
693
- body: formData,
694
- headers: {
695
- ...headers,
696
- ...(config.headers || {})
697
- }
698
- })
699
-
700
- if (!response.ok) {
701
- throw new Error(`Upload failed: ${response.statusText}`)
702
- }
703
-
704
- return await response.json()
705
- }
706
-
707
- // Option 2: AWS S3 POST upload with policy-based authentication
708
- // This requires server-side generation of policy and signature
709
- const s3Endpoint = bucketUrl || `https://${bucketName}.s3.${region || 'us-east-1'}.amazonaws.com/`
710
- const key = fields?.key || `uploads/${Date.now()}_${file.name}`
711
-
712
- const formData = new FormData()
713
-
714
- // AWS S3 POST policy fields must be added in specific order
715
- if (fields) {
716
- // Add all policy fields first
717
- Object.entries(fields).forEach(([k, v]) => {
718
- if (k !== 'file') {
719
- formData.append(k, v as string)
720
- }
721
- })
722
- } else {
723
- // Basic fields for public bucket upload
724
- formData.append('key', key)
725
- if (accessKeyId) formData.append('AWSAccessKeyId', accessKeyId)
726
- formData.append('Content-Type', file.type)
727
- if (policy) formData.append('policy', policy)
728
- if (signature) formData.append('signature', signature)
729
- if (algorithm) formData.append('x-amz-algorithm', algorithm)
730
- if (credential) formData.append('x-amz-credential', credential)
731
- if (date) formData.append('x-amz-date', date)
732
- }
733
-
734
- // File must be the last field
735
- formData.append('file', processedFile || file)
736
-
737
- const response = await fetch(s3Endpoint, {
738
- method: 'POST',
739
- body: formData
740
- })
741
-
742
- // S3 returns 204 No Content on success for POST uploads
743
- if (!response.ok && response.status !== 204) {
744
- const errorText = await response.text()
745
- throw new Error(`S3 upload failed: ${response.status} - ${errorText}`)
746
- }
747
-
748
- return {
749
- url: `${s3Endpoint}${key}`,
750
- key,
751
- bucket: bucketName,
752
- etag: response.headers.get('ETag'),
753
- location: response.headers.get('Location') || `${s3Endpoint}${key}`
754
- }
755
- }
756
-
757
- case 'cloudinary': {
758
- // Cloudinary upload logic
759
- const { config } = serviceConfig as any
760
- if (!config) throw new Error('Cloudinary config is required')
761
-
762
- const { cloudName, uploadPreset, apiKey } = config
763
- const formData = new FormData()
764
- formData.append('file', processedFile || file)
765
- formData.append('upload_preset', uploadPreset)
766
-
767
- if (apiKey) {
768
- formData.append('api_key', apiKey)
769
- }
770
-
771
- // Add optional transformations
772
- if (imageResize) {
773
- formData.append('eager', `w_${imageResize.maxWidth},h_${imageResize.maxHeight},c_limit,q_${Math.round(imageResize.quality * 100)}`)
774
- }
775
-
776
- const response = await fetch(
777
- `https://api.cloudinary.com/v1_1/${cloudName}/auto/upload`,
778
- {
779
- method: 'POST',
780
- body: formData
781
- }
782
- )
783
-
784
- if (!response.ok) throw new Error('Cloudinary upload failed')
785
- const result = await response.json()
786
-
787
- return {
788
- url: result.secure_url,
789
- publicId: result.public_id,
790
- format: result.format,
791
- width: result.width,
792
- height: result.height,
793
- bytes: result.bytes,
794
- cloudName
795
- }
796
- }
797
-
798
- case 'firebase': {
799
- // Firebase Storage REST API upload (no SDK required)
800
- const { config } = serviceConfig as any
801
- if (!config) throw new Error('Firebase config is required')
802
-
803
- const { storageBucket, folder = 'uploads' } = config
804
- const fileName = `${folder}/${Date.now()}-${file.name}`
805
-
806
- // Firebase Storage uses Google Cloud Storage REST API
807
- const uploadUrl = `https://storage.googleapis.com/upload/storage/v1/b/${storageBucket}/o?uploadType=media&name=${encodeURIComponent(fileName)}`
808
-
809
- const response = await fetch(uploadUrl, {
810
- method: 'POST',
811
- body: processedFile || file,
812
- headers: {
813
- 'Content-Type': file.type,
814
- ...headers // Should include Authorization: Bearer [token]
815
- }
816
- })
817
-
818
- if (!response.ok) throw new Error('Firebase upload failed')
819
- const result = await response.json()
820
-
821
- return {
822
- url: `https://storage.googleapis.com/${storageBucket}/${fileName}`,
823
- name: result.name,
824
- bucket: result.bucket,
825
- generation: result.generation,
826
- contentType: result.contentType,
827
- size: result.size
828
- }
829
- }
830
-
831
- case 'custom': {
832
- // Custom endpoint upload
833
- if (!endpoint) throw new Error('Custom endpoint is required')
834
-
835
- const formData = new FormData()
836
- formData.append('file', file)
837
-
838
- const response = await fetch(endpoint, {
839
- method: 'POST',
840
- body: formData,
841
- headers: headers
842
- })
843
-
844
- if (!response.ok) throw new Error('Custom upload failed')
845
- return response.json()
846
- }
847
-
848
- default:
849
- throw new Error(`Unsupported service type: ${serviceConfig.type}`)
850
- }
851
- } catch (error) {
852
- console.error('Upload service error:', error)
853
- throw error
854
- }
855
- }, [serviceConfig, endpoint, headers, imageResize, resizeImage])
856
-
857
- // Chunked upload simülasyonu
858
- const uploadFileChunked = useCallback(async (fileItem: FileUploadItem) => {
859
- const { file } = fileItem
860
- const chunks: FileChunk[] = []
861
- const chunkCount = Math.ceil(file.size / chunkSize)
862
-
863
- // Chunk'ları oluştur
864
- for (let i = 0; i < chunkCount; i++) {
865
- chunks.push({
866
- index: i,
867
- start: i * chunkSize,
868
- end: Math.min((i + 1) * chunkSize, file.size),
869
- status: 'pending',
870
- attempts: 0
871
- })
872
- }
873
-
874
- // FileItem'i güncelle
875
- setFiles(prev => prev.map(f =>
876
- f.id === fileItem.id
877
- ? { ...f, chunks, status: 'uploading' }
878
- : f
879
- ))
880
-
881
- const abortController = new AbortController()
882
- uploadQueue.current.set(fileItem.id, abortController)
883
-
884
- let uploadedBytes = 0
885
- const startTime = Date.now()
886
-
887
- try {
888
- // Image resize if needed
889
- let processedFile = file
890
- if (imageResize && file.type.startsWith('image/')) {
891
- processedFile = await resizeImage(file)
892
- }
893
-
894
- // If service config exists, do the actual upload
895
- if (serviceConfig) {
896
- const uploadResult = await uploadToService({ ...fileItem, file: processedFile })
897
-
898
- // Update progress to 100% immediately for service uploads
899
- setFiles(prev => prev.map(f =>
900
- f.id === fileItem.id
901
- ? { ...f, status: 'success', progress: 100, result: uploadResult }
902
- : f
903
- ))
904
-
905
- onComplete?.(fileItem.id, uploadResult)
906
- return
907
- }
908
-
909
- // Regular chunk upload simulation for non-service uploads
910
- for (const chunk of chunks) {
911
- if (abortController.signal.aborted) {
912
- throw new Error('Upload cancelled')
913
- }
914
-
915
- await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 200))
916
-
917
- uploadedBytes += (chunk.end - chunk.start)
918
- const progress = Math.round((uploadedBytes / file.size) * 100)
919
- const elapsedTime = (Date.now() - startTime) / 1000
920
- const speed = uploadedBytes / elapsedTime
921
- const estimatedTime = (file.size - uploadedBytes) / speed
922
-
923
- // Progress güncelle
924
- setFiles(prev => prev.map(f =>
925
- f.id === fileItem.id
926
- ? {
927
- ...f,
928
- progress,
929
- uploadedBytes,
930
- speed,
931
- estimatedTime: estimatedTime || 0,
932
- chunks: f.chunks?.map(c =>
933
- c.index === chunk.index
934
- ? { ...c, status: 'success' }
935
- : c
936
- )
937
- }
938
- : f
939
- ))
940
-
941
- onProgress?.(fileItem.id, progress)
942
- }
943
-
944
- // Upload tamamlandı
945
- setFiles(prev => prev.map(f =>
946
- f.id === fileItem.id
947
- ? { ...f, status: 'success', progress: 100 }
948
- : f
949
- ))
950
-
951
- onComplete?.(fileItem.id, { success: true })
952
-
953
- } catch (error) {
954
- setFiles(prev => prev.map(f =>
955
- f.id === fileItem.id
956
- ? { ...f, status: 'error', error: error instanceof Error ? error.message : 'Upload failed' }
957
- : f
958
- ))
959
-
960
- onError?.(fileItem.id, error instanceof Error ? error.message : 'Upload failed')
961
- } finally {
962
- uploadQueue.current.delete(fileItem.id)
963
- }
964
- }, [chunkSize, onProgress, onComplete, onError, serviceConfig, uploadToService, resizeImage, imageResize])
965
-
966
- // Dosya işleme
967
- const processFiles = useCallback(async (fileList: FileList | File[]) => {
968
- const fileArray = Array.from(fileList)
969
- setError(null)
970
-
971
- // Toplam dosya sayısı kontrolü
972
- if (files.length + fileArray.length > maxFiles) {
973
- setError(`Maximum ${maxFiles} files allowed`)
974
- return
975
- }
976
-
977
- // Toplam boyut kontrolü
978
- if (maxTotalSize) {
979
- const currentSize = files.reduce((sum, f) => sum + f.file.size, 0)
980
- const newSize = fileArray.reduce((sum, f) => sum + f.size, 0)
981
-
982
- if (currentSize + newSize > maxTotalSize) {
983
- setError(`Total file size exceeds ${formatFileSize(maxTotalSize)} limit`)
984
- return
985
- }
986
- }
987
-
988
- const validFiles: File[] = []
989
- const errors: string[] = []
990
-
991
- // Her dosyayı validate et
992
- for (const file of fileArray) {
993
- const validationError = await validateFile(file)
994
- if (validationError) {
995
- errors.push(`${file.name}: ${validationError}`)
996
- continue
997
- }
998
-
999
- const isDuplicate = await checkDuplicate(file)
1000
- if (isDuplicate) {
1001
- errors.push(`${file.name}: File already exists`)
1002
- continue
1003
- }
1004
-
1005
- validFiles.push(file)
1006
- }
1007
-
1008
- if (errors.length > 0) {
1009
- setError(errors.join(', '))
1010
- }
1011
-
1012
- if (validFiles.length === 0) return
1013
-
1014
- // FileUploadItem'ları oluştur
1015
- const newFileItems: FileUploadItem[] = []
1016
-
1017
- for (const file of validFiles) {
1018
- const id = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
1019
-
1020
- // Resize image if needed
1021
- const processedFile = await resizeImage(file)
1022
-
1023
- const preview = await createPreview(processedFile)
1024
- const metadata = await createMetadata(processedFile)
1025
-
1026
- newFileItems.push({
1027
- id,
1028
- file: processedFile,
1029
- status: 'pending',
1030
- progress: 0,
1031
- totalBytes: processedFile.size,
1032
- preview,
1033
- metadata
1034
- })
1035
- }
1036
-
1037
- setFiles(prev => [...prev, ...newFileItems])
1038
-
1039
- // Upload işlemini başlat
1040
- if (onUpload) {
1041
- try {
1042
- await onUpload(newFileItems)
1043
- } catch (error) {
1044
- console.error('Upload failed:', error)
1045
- }
1046
- } else {
1047
- // Otomatik upload başlat
1048
- newFileItems.forEach(fileItem => {
1049
- uploadFileChunked(fileItem)
1050
- })
1051
- }
1052
- }, [
1053
- files,
1054
- maxFiles,
1055
- maxTotalSize,
1056
- validateFile,
1057
- checkDuplicate,
1058
- createPreview,
1059
- createMetadata,
1060
- onUpload,
1061
- uploadFileChunked,
1062
- resizeImage
1063
- ])
1064
-
1065
- // Drag & Drop handlers
1066
- const handleDrop = useCallback((e: React.DragEvent) => {
1067
- e.preventDefault()
1068
- setIsDragOver(false)
1069
-
1070
- if (disabled) return
1071
-
1072
- const droppedFiles = Array.from(e.dataTransfer.files)
1073
- if (droppedFiles.length > 0) {
1074
- processFiles(droppedFiles)
1075
- }
1076
- }, [processFiles, disabled])
1077
-
1078
- const handleDragOver = useCallback((e: React.DragEvent) => {
1079
- e.preventDefault()
1080
- if (!disabled) {
1081
- setIsDragOver(true)
1082
- }
1083
- }, [disabled])
1084
-
1085
- const handleDragLeave = useCallback((e: React.DragEvent) => {
1086
- e.preventDefault()
1087
- setIsDragOver(false)
1088
- }, [])
1089
-
1090
- // Dosya seçimi
1091
- const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
1092
- const selectedFiles = e.target.files
1093
- if (selectedFiles && selectedFiles.length > 0) {
1094
- processFiles(selectedFiles)
1095
- }
1096
- // Input'u temizle
1097
- e.target.value = ''
1098
- }, [processFiles])
1099
-
1100
- // Dosya kaldırma
1101
- const removeFile = useCallback((fileId: string) => {
1102
- const fileToRemove = files.find(f => f.id === fileId)
1103
- if (!fileToRemove) return
1104
-
1105
- // Upload'ı iptal et
1106
- const controller = uploadQueue.current.get(fileId)
1107
- if (controller) {
1108
- controller.abort()
1109
- uploadQueue.current.delete(fileId)
1110
- }
1111
-
1112
- // Preview URL'yi temizle
1113
- if (fileToRemove.preview?.url) {
1114
- URL.revokeObjectURL(fileToRemove.preview.url)
1115
- }
1116
-
1117
- setFiles(prev => prev.filter(f => f.id !== fileId))
1118
- setSelectedIds(prev => prev.filter(id => id !== fileId))
1119
-
1120
- onRemove?.(fileId)
1121
- }, [files, onRemove])
1122
-
1123
- // Upload'ı duraklat/devam ettir
1124
- const pauseResumeUpload = useCallback((fileId: string) => {
1125
- const file = files.find(f => f.id === fileId)
1126
- if (!file) return
1127
-
1128
- if (file.status === 'uploading') {
1129
- const controller = uploadQueue.current.get(fileId)
1130
- if (controller) {
1131
- controller.abort()
1132
- uploadQueue.current.delete(fileId)
1133
- }
1134
-
1135
- setFiles(prev => prev.map(f =>
1136
- f.id === fileId ? { ...f, status: 'paused' } : f
1137
- ))
1138
- } else if (file.status === 'paused' && resumable) {
1139
- uploadFileChunked(file)
1140
- }
1141
- }, [files, resumable, uploadFileChunked])
1142
-
1143
- // Bulk operations
1144
- const handleBulkSelect = useCallback((fileId: string, selected: boolean) => {
1145
- setSelectedIds(prev =>
1146
- selected
1147
- ? [...prev, fileId]
1148
- : prev.filter(id => id !== fileId)
1149
- )
1150
- }, [])
1151
-
1152
- const handleSelectAll = useCallback(() => {
1153
- const allIds = files.map(f => f.id)
1154
- setSelectedIds(allIds)
1155
- onBulkSelect?.(allIds)
1156
- }, [files, onBulkSelect])
1157
-
1158
- const handleClearSelection = useCallback(() => {
1159
- setSelectedIds([])
1160
- onBulkSelect?.([])
1161
- }, [onBulkSelect])
1162
-
1163
- const handleBulkRemove = useCallback(() => {
1164
- selectedIds.forEach(id => removeFile(id))
1165
- setSelectedIds([])
1166
- }, [selectedIds, removeFile])
1167
-
1168
- const handleBulkDownload = useCallback(() => {
1169
- // Bulk download implementation
1170
- console.log('Bulk download:', selectedIds)
1171
- }, [selectedIds])
1172
-
1173
- // Preview
1174
- const handlePreview = useCallback((file: FileUploadItem) => {
1175
- setPreviewFile(file)
1176
- setIsPreviewOpen(true)
1177
- onPreview?.(file)
1178
- }, [onPreview])
1179
-
1180
- return (
1181
- <div ref={ref} className={cn("w-full space-y-4", className)} {...props}>
1182
- {/* Bulk Actions */}
1183
- <AnimatePresence>
1184
- {allowBulkOperations && selectedIds.length > 0 && (
1185
- <BulkActions
1186
- selectedIds={selectedIds}
1187
- onClearSelection={handleClearSelection}
1188
- onBulkRemove={handleBulkRemove}
1189
- onBulkDownload={handleBulkDownload}
1190
- />
1191
- )}
1192
- </AnimatePresence>
1193
-
1194
- {/* Upload Area */}
1195
- <motion.div
1196
- className={cn(
1197
- fileUploadVariants({
1198
- variant,
1199
- state: disabled ? "disabled" : isDragOver ? "dragover" : "idle"
1200
- })
1201
- )}
1202
- onDrop={handleDrop}
1203
- onDragOver={handleDragOver}
1204
- onDragLeave={handleDragLeave}
1205
- onClick={() => !disabled && fileInputRef.current?.click()}
1206
- animate={{
1207
- scale: isDragOver ? 1.02 : 1
1208
- }}
1209
- transition={{ duration: 0.2 }}
1210
- >
1211
- <input
1212
- ref={fileInputRef}
1213
- type="file"
1214
- accept={accept}
1215
- multiple={multiple}
1216
- disabled={disabled}
1217
- onChange={handleFileSelect}
1218
- className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
1219
- />
1220
-
1221
- <div className="text-center space-y-4">
1222
- <motion.div
1223
- animate={{
1224
- scale: isDragOver ? 1.1 : 1,
1225
- rotate: isDragOver ? 5 : 0
1226
- }}
1227
- className="mx-auto h-12 w-12 text-muted-foreground"
1228
- >
1229
- <Upload className="h-full w-full" />
1230
- </motion.div>
1231
-
1232
- <div>
1233
- <h3 className="text-lg font-semibold">
1234
- {isDragOver ? 'Drop files here' : 'Upload Files'}
1235
- </h3>
1236
- <p className="text-sm text-muted-foreground mt-1">
1237
- Drag and drop files or click to select
1238
- </p>
1239
- <div className="flex items-center justify-center gap-4 mt-3 text-xs text-muted-foreground">
1240
- <span>Max {maxFiles} files</span>
1241
- <span>•</span>
1242
- <span>{formatFileSize(maxSize)} per file</span>
1243
- {resumable && (
1244
- <>
1245
- <span>•</span>
1246
- <span>Resumable</span>
1247
- </>
1248
- )}
1249
- </div>
1250
- </div>
1251
-
1252
- <Button variant="outline" disabled={disabled} type="button">
1253
- <Upload className="mr-2 h-4 w-4" />
1254
- Select Files
1255
- </Button>
1256
- </div>
1257
- </motion.div>
1258
-
1259
- {/* Error Message */}
1260
- <AnimatePresence>
1261
- {error && (
1262
- <motion.div
1263
- initial={{ opacity: 0, y: -10 }}
1264
- animate={{ opacity: 1, y: 0 }}
1265
- exit={{ opacity: 0, y: -10 }}
1266
- className="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-lg"
1267
- >
1268
- <AlertCircle className="h-4 w-4 text-destructive flex-shrink-0" />
1269
- <span className="text-sm text-destructive">{error}</span>
1270
- <Button
1271
- variant="ghost"
1272
- size="sm"
1273
- className="ml-auto h-6 w-6 p-0"
1274
- onClick={() => setError(null)}
1275
- >
1276
- <X className="h-3 w-3" />
1277
- </Button>
1278
- </motion.div>
1279
- )}
1280
- </AnimatePresence>
1281
-
1282
- {/* File List */}
1283
- <AnimatePresence>
1284
- {files.length > 0 && (
1285
- <motion.div
1286
- initial={{ opacity: 0, height: 0 }}
1287
- animate={{ opacity: 1, height: 'auto' }}
1288
- exit={{ opacity: 0, height: 0 }}
1289
- className="space-y-3"
1290
- >
1291
- {/* List Header */}
1292
- <div className="flex items-center justify-between">
1293
- <h4 className="text-sm font-medium">
1294
- Uploaded Files ({files.length})
1295
- </h4>
1296
- {allowBulkOperations && files.length > 1 && (
1297
- <Button
1298
- variant="ghost"
1299
- size="sm"
1300
- onClick={selectedIds.length === files.length ? handleClearSelection : handleSelectAll}
1301
- >
1302
- {selectedIds.length === files.length ? 'Clear Selection' : 'Select All'}
1303
- </Button>
1304
- )}
1305
- </div>
1306
-
1307
- {/* File Items */}
1308
- <div className={cn(
1309
- "space-y-2",
1310
- variant === 'grid' && "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 space-y-0"
1311
- )}>
1312
- <AnimatePresence>
1313
- {files.map(file => (
1314
- <FileUploadItem
1315
- key={file.id}
1316
- file={file}
1317
- variant={variant}
1318
- showPreview={showPreview}
1319
- showProgress={showProgress}
1320
- showMetadata={showMetadata}
1321
- allowBulkOperations={allowBulkOperations}
1322
- resumable={resumable}
1323
- selected={selectedIds.includes(file.id)}
1324
- onSelect={(selected) => handleBulkSelect(file.id, selected)}
1325
- onRemove={() => removeFile(file.id)}
1326
- onPauseResume={() => pauseResumeUpload(file.id)}
1327
- onPreview={() => handlePreview(file)}
1328
- />
1329
- ))}
1330
- </AnimatePresence>
1331
- </div>
1332
- </motion.div>
1333
- )}
1334
- </AnimatePresence>
1335
-
1336
- {/* Preview Modal */}
1337
- <FilePreviewModal
1338
- file={previewFile}
1339
- isOpen={isPreviewOpen}
1340
- onClose={() => setIsPreviewOpen(false)}
1341
- />
1342
- </div>
1343
- )
1344
- }
1345
- )
1346
-
1347
- // Tekil dosya bileşeni
1348
- const FileUploadItem = ({
1349
- file,
1350
- variant = 'default',
1351
- showPreview = true,
1352
- showProgress = true,
1353
- showMetadata = true,
1354
- allowBulkOperations = true,
1355
- resumable = true,
1356
- selected = false,
1357
- onSelect,
1358
- onRemove,
1359
- onPauseResume,
1360
- onPreview
1361
- }: {
1362
- file: FileUploadItem
1363
- variant?: 'default' | 'compact' | 'grid'
1364
- showPreview?: boolean
1365
- showProgress?: boolean
1366
- showMetadata?: boolean
1367
- allowBulkOperations?: boolean
1368
- resumable?: boolean
1369
- selected?: boolean
1370
- onSelect?: (selected: boolean) => void
1371
- onRemove?: () => void
1372
- onPauseResume?: () => void
1373
- onPreview?: () => void
1374
- }) => {
1375
- const { toast } = useToast()
1376
- const canPauseResume = resumable && ['uploading', 'paused'].includes(file.status)
1377
- const canPreview = showPreview && file.preview && ['image', 'video', 'audio'].includes(file.preview.type)
1378
-
1379
- return (
1380
- <motion.div
1381
- layout
1382
- initial={{ opacity: 0, y: 10 }}
1383
- animate={{ opacity: 1, y: 0 }}
1384
- exit={{ opacity: 0, y: -10, scale: 0.95 }}
1385
- className={cn(
1386
- "group relative overflow-hidden rounded-lg border bg-card transition-all",
1387
- selected && "ring-2 ring-primary bg-primary/5",
1388
- variant === 'compact' && "p-3",
1389
- variant === 'default' && "p-4",
1390
- variant === 'grid' && "p-4"
1391
- )}
1392
- >
1393
- {/* Selection Checkbox for grid variant only */}
1394
- {allowBulkOperations && variant === 'grid' && (
1395
- <div className="absolute top-3 left-3 z-10">
1396
- <input
1397
- type="checkbox"
1398
- checked={selected}
1399
- onChange={(e) => onSelect?.(e.target.checked)}
1400
- className="rounded border-muted-foreground/25 h-4 w-4"
1401
- />
1402
- </div>
1403
- )}
1404
-
1405
- {/* Checkbox for non-grid variants */}
1406
- {allowBulkOperations && variant !== 'grid' && (
1407
- <input
1408
- type="checkbox"
1409
- checked={selected}
1410
- onChange={(e) => onSelect?.(e.target.checked)}
1411
- className="rounded border-muted-foreground/25 h-4 w-4 mt-1 mr-3 float-left"
1412
- />
1413
- )}
1414
-
1415
- {/* Preview/Thumbnail */}
1416
- {showPreview && file.preview && (
1417
- <div className={cn(
1418
- "relative overflow-hidden rounded bg-muted/20",
1419
- variant === 'grid' ? "aspect-video mb-3" :
1420
- variant === 'compact' ? "w-10 h-10 float-left mr-2" :
1421
- "w-12 h-12 float-left mr-3"
1422
- )}>
1423
- {file.preview.type === 'image' && file.preview.thumbnail && (
1424
- <img
1425
- src={file.preview.thumbnail}
1426
- alt={file.file.name}
1427
- className="w-full h-full object-cover"
1428
- />
1429
- )}
1430
-
1431
- {file.preview.type === 'video' && file.preview.thumbnail && (
1432
- <div className="relative w-full h-full">
1433
- <img
1434
- src={file.preview.thumbnail}
1435
- alt={file.file.name}
1436
- className="w-full h-full object-cover"
1437
- />
1438
- <div className="absolute inset-0 flex items-center justify-center bg-black/20">
1439
- <Play className="h-6 w-6 text-white" />
1440
- </div>
1441
- </div>
1442
- )}
1443
-
1444
- {!file.preview.thumbnail && (
1445
- <div className="w-full h-full flex items-center justify-center">
1446
- {getFileIcon(file.file.type, variant === 'grid' ? 'lg' : 'md')}
1447
- </div>
1448
- )}
1449
-
1450
- {canPreview && (
1451
- <Button
1452
- variant="secondary"
1453
- size="sm"
1454
- className="absolute top-1 right-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
1455
- onClick={onPreview}
1456
- >
1457
- <Eye className="h-3 w-3" />
1458
- </Button>
1459
- )}
1460
- </div>
1461
- )}
1462
-
1463
- {/* File Info */}
1464
- <div className={cn(
1465
- "flex-1 min-w-0",
1466
- variant === 'grid' ? "text-center w-full" : "overflow-hidden"
1467
- )}>
1468
- <div className="flex items-start justify-between gap-3">
1469
- <div className="min-w-0 flex-1">
1470
- <h5 className={cn(
1471
- "font-medium truncate",
1472
- variant === 'compact' ? "text-sm" : "text-base"
1473
- )}>
1474
- {file.file.name}
1475
- </h5>
1476
-
1477
- {showMetadata && (
1478
- <div className={cn(
1479
- "flex items-center gap-2 mt-1 text-muted-foreground",
1480
- variant === 'compact' ? "text-xs" : "text-sm",
1481
- variant === 'grid' && "justify-center"
1482
- )}>
1483
- <span>{formatFileSize(file.file.size)}</span>
1484
- {file.preview?.dimensions && (
1485
- <>
1486
- <span>•</span>
1487
- <span>{file.preview.dimensions.width} × {file.preview.dimensions.height}</span>
1488
- </>
1489
- )}
1490
- {file.preview?.duration && (
1491
- <>
1492
- <span>•</span>
1493
- <span>{formatTime(file.preview.duration)}</span>
1494
- </>
1495
- )}
1496
- </div>
1497
- )}
1498
- </div>
1499
-
1500
- {/* Status Badge */}
1501
- <Badge
1502
- variant={
1503
- file.status === 'success' ? 'success' :
1504
- file.status === 'error' ? 'destructive' :
1505
- file.status === 'paused' ? 'secondary' : 'secondary'
1506
- }
1507
- size={variant === 'compact' ? 'sm' : 'md'}
1508
- className={cn(
1509
- "flex-shrink-0 whitespace-nowrap min-w-fit",
1510
- file.status === 'success' && "bg-green-500 hover:bg-green-600 text-white border-transparent"
1511
- )}
1512
- >
1513
- {file.status === 'uploading' && <Loader2 className={cn("h-3 w-3 animate-spin flex-shrink-0", variant !== 'compact' && "mr-1")} />}
1514
- {file.status === 'success' && <CheckCircle2 className={cn("h-3 w-3 flex-shrink-0", variant !== 'compact' && "mr-1")} />}
1515
- {file.status === 'error' && <AlertCircle className={cn("h-3 w-3 flex-shrink-0", variant !== 'compact' && "mr-1")} />}
1516
- {file.status === 'paused' && <Pause className={cn("h-3 w-3 flex-shrink-0", variant !== 'compact' && "mr-1")} />}
1517
- {variant !== 'compact' && (
1518
- file.status === 'pending' ? 'Pending' :
1519
- file.status === 'uploading' ? 'Uploading' :
1520
- file.status === 'paused' ? 'Paused' :
1521
- file.status === 'success' ? 'Done' :
1522
- file.status === 'error' ? 'Error' : 'Cancelled'
1523
- )}
1524
- </Badge>
1525
- </div>
1526
-
1527
- {/* Progress */}
1528
- {showProgress && file.status === 'uploading' && (
1529
- <div className="mt-3 space-y-1">
1530
- <div className="flex items-center justify-between text-xs text-muted-foreground">
1531
- <span>{file.progress}%</span>
1532
- {file.speed && (
1533
- <span>{formatFileSize(file.speed)}/s</span>
1534
- )}
1535
- {file.estimatedTime && file.estimatedTime > 0 && (
1536
- <span>{formatTime(file.estimatedTime)} remaining</span>
1537
- )}
1538
- </div>
1539
- <Progress value={file.progress} className="h-1" />
1540
- </div>
1541
- )}
1542
-
1543
- {/* Error Message */}
1544
- {file.status === 'error' && file.error && (
1545
- <p className="mt-2 text-xs text-destructive">{file.error}</p>
1546
- )}
1547
-
1548
- {/* Actions */}
1549
- <div className={cn(
1550
- "flex items-center gap-1 mt-3 clear-both",
1551
- variant === 'grid' && "justify-center"
1552
- )}>
1553
- {canPauseResume && (
1554
- <Button
1555
- variant="ghost"
1556
- size="sm"
1557
- onClick={onPauseResume}
1558
- className="h-7 px-2"
1559
- >
1560
- {file.status === 'uploading' ? (
1561
- <Pause className="h-3 w-3" />
1562
- ) : (
1563
- <Play className="h-3 w-3" />
1564
- )}
1565
- </Button>
1566
- )}
1567
-
1568
- {file.status === 'error' && resumable && (
1569
- <Button
1570
- variant="ghost"
1571
- size="sm"
1572
- onClick={onPauseResume}
1573
- className="h-7 px-2"
1574
- >
1575
- <RotateCcw className="h-3 w-3" />
1576
- </Button>
1577
- )}
1578
-
1579
- {file.status === 'success' && (
1580
- <Button
1581
- variant="ghost"
1582
- size="sm"
1583
- className="h-7 px-2"
1584
- onClick={() => {
1585
- // Download file
1586
- const link = document.createElement('a')
1587
- link.href = file.preview?.url || URL.createObjectURL(file.file)
1588
- link.download = file.file.name
1589
- link.click()
1590
- }}
1591
- >
1592
- <Download className="h-3 w-3" />
1593
- </Button>
1594
- )}
1595
-
1596
- <DropdownMenu>
1597
- <DropdownMenuTrigger asChild>
1598
- <Button variant="ghost" size="sm" className="h-7 w-7 p-0">
1599
- <MoreHorizontal className="h-3 w-3" />
1600
- </Button>
1601
- </DropdownMenuTrigger>
1602
- <DropdownMenuContent align="end">
1603
- {canPreview && (
1604
- <DropdownMenuItem onClick={onPreview}>
1605
- <Eye className="mr-2 h-4 w-4" />
1606
- Preview
1607
- </DropdownMenuItem>
1608
- )}
1609
- <DropdownMenuItem onClick={async () => {
1610
- try {
1611
- const url = file.result?.url || file.preview?.url || URL.createObjectURL(file.file)
1612
- await navigator.clipboard.writeText(url)
1613
- toast({
1614
- title: "Link copied",
1615
- description: "File link has been copied to clipboard",
1616
- })
1617
- } catch (err) {
1618
- console.error('Failed to copy link:', err)
1619
- toast({
1620
- title: "Failed to copy",
1621
- description: "Could not copy link to clipboard",
1622
- variant: "destructive"
1623
- })
1624
- }
1625
- }}>
1626
- <Copy className="mr-2 h-4 w-4" />
1627
- Copy Link
1628
- </DropdownMenuItem>
1629
- {navigator.share && (
1630
- <DropdownMenuItem onClick={async () => {
1631
- try {
1632
- await navigator.share({
1633
- title: file.file.name,
1634
- text: `Check out this file: ${file.file.name}`,
1635
- url: file.result?.url || file.preview?.url || window.location.href,
1636
- files: [file.file]
1637
- })
1638
- } catch (err) {
1639
- console.log('Share cancelled or failed:', err)
1640
- }
1641
- }}>
1642
- <Share className="mr-2 h-4 w-4" />
1643
- Share
1644
- </DropdownMenuItem>
1645
- )}
1646
- <DropdownMenuItem onClick={onRemove} className="text-destructive">
1647
- <Trash2 className="mr-2 h-4 w-4" />
1648
- Delete
1649
- </DropdownMenuItem>
1650
- </DropdownMenuContent>
1651
- </DropdownMenu>
1652
- </div>
1653
- </div>
1654
- </motion.div>
1655
- )
1656
- }
1657
-
1658
- MoonUIFileUploadPro.displayName = "MoonUIFileUploadPro"
1659
-
1660
- export default MoonUIFileUploadPro