@moontra/moonui-pro 2.20.1 → 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 (162) hide show
  1. package/dist/index.d.ts +691 -261
  2. package/dist/index.mjs +7418 -4934
  3. package/package.json +11 -5
  4. package/plugin/index.d.ts +86 -0
  5. package/plugin/index.js +308 -0
  6. package/scripts/postbuild.js +27 -0
  7. package/scripts/postinstall.js +176 -23
  8. package/src/__tests__/use-intersection-observer.test.tsx +0 -216
  9. package/src/__tests__/use-local-storage.test.tsx +0 -174
  10. package/src/__tests__/use-pro-access.test.tsx +0 -183
  11. package/src/components/advanced-chart/advanced-chart.test.tsx +0 -281
  12. package/src/components/advanced-chart/index.tsx +0 -1242
  13. package/src/components/advanced-forms/index.tsx +0 -426
  14. package/src/components/animated-button/index.tsx +0 -385
  15. package/src/components/calendar/event-dialog.tsx +0 -372
  16. package/src/components/calendar/index.tsx +0 -1073
  17. package/src/components/calendar-pro/index.tsx +0 -1697
  18. package/src/components/color-picker/index.tsx +0 -432
  19. package/src/components/credit-card-input/index.tsx +0 -406
  20. package/src/components/dashboard/dashboard-grid.tsx +0 -462
  21. package/src/components/dashboard/demo.tsx +0 -425
  22. package/src/components/dashboard/index.tsx +0 -1046
  23. package/src/components/dashboard/time-range-picker.tsx +0 -336
  24. package/src/components/dashboard/types.ts +0 -222
  25. package/src/components/dashboard/widgets/activity-feed.tsx +0 -344
  26. package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
  27. package/src/components/dashboard/widgets/metric-card.tsx +0 -343
  28. package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
  29. package/src/components/data-table/data-table-column-toggle.tsx +0 -169
  30. package/src/components/data-table/data-table-export.ts +0 -156
  31. package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
  32. package/src/components/data-table/data-table.test.tsx +0 -187
  33. package/src/components/data-table/index.tsx +0 -845
  34. package/src/components/draggable-list/index.tsx +0 -100
  35. package/src/components/enhanced/badge.tsx +0 -191
  36. package/src/components/enhanced/button.tsx +0 -362
  37. package/src/components/enhanced/card.tsx +0 -266
  38. package/src/components/enhanced/dialog.tsx +0 -246
  39. package/src/components/enhanced/index.ts +0 -4
  40. package/src/components/error-boundary/index.tsx +0 -109
  41. package/src/components/file-upload/file-upload.test.tsx +0 -243
  42. package/src/components/file-upload/index.tsx +0 -1660
  43. package/src/components/floating-action-button/index.tsx +0 -206
  44. package/src/components/form-wizard/form-wizard-context.tsx +0 -307
  45. package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
  46. package/src/components/form-wizard/form-wizard-progress.tsx +0 -298
  47. package/src/components/form-wizard/form-wizard-step.tsx +0 -111
  48. package/src/components/form-wizard/index.tsx +0 -102
  49. package/src/components/form-wizard/types.ts +0 -76
  50. package/src/components/gesture-drawer/index.tsx +0 -551
  51. package/src/components/github-stars/github-api.ts +0 -426
  52. package/src/components/github-stars/hooks.ts +0 -516
  53. package/src/components/github-stars/index.tsx +0 -375
  54. package/src/components/github-stars/types.ts +0 -148
  55. package/src/components/github-stars/variants.tsx +0 -513
  56. package/src/components/health-check/index.tsx +0 -439
  57. package/src/components/hover-card-3d/index.tsx +0 -530
  58. package/src/components/index.ts +0 -128
  59. package/src/components/internal/index.ts +0 -78
  60. package/src/components/kanban/add-card-modal.tsx +0 -502
  61. package/src/components/kanban/card-detail-modal.tsx +0 -761
  62. package/src/components/kanban/index.ts +0 -13
  63. package/src/components/kanban/kanban.tsx +0 -1684
  64. package/src/components/kanban/types.ts +0 -168
  65. package/src/components/lazy-component/index.tsx +0 -823
  66. package/src/components/license-error/index.tsx +0 -29
  67. package/src/components/magnetic-button/index.tsx +0 -167
  68. package/src/components/memory-efficient-data/index.tsx +0 -1016
  69. package/src/components/moonui-quiz-form/index.tsx +0 -817
  70. package/src/components/optimized-image/index.tsx +0 -425
  71. package/src/components/performance-debugger/index.tsx +0 -589
  72. package/src/components/performance-monitor/index.tsx +0 -794
  73. package/src/components/phone-number-input/index.tsx +0 -338
  74. package/src/components/pinch-zoom/index.tsx +0 -566
  75. package/src/components/quiz-form/index.tsx +0 -479
  76. package/src/components/rich-text-editor/index-old-backup.tsx +0 -437
  77. package/src/components/rich-text-editor/index.tsx +0 -2324
  78. package/src/components/rich-text-editor/slash-commands-extension.ts +0 -220
  79. package/src/components/rich-text-editor/slash-commands.css +0 -35
  80. package/src/components/rich-text-editor/table-styles.css +0 -65
  81. package/src/components/sidebar/index.tsx +0 -865
  82. package/src/components/spotlight-card/index.tsx +0 -191
  83. package/src/components/swipeable-card/index.tsx +0 -100
  84. package/src/components/timeline/index.tsx +0 -1148
  85. package/src/components/ui/accordion.tsx +0 -73
  86. package/src/components/ui/alert-dialog.tsx +0 -141
  87. package/src/components/ui/alert.tsx +0 -141
  88. package/src/components/ui/aspect-ratio.tsx +0 -245
  89. package/src/components/ui/avatar.tsx +0 -153
  90. package/src/components/ui/badge.tsx +0 -228
  91. package/src/components/ui/breadcrumb.tsx +0 -214
  92. package/src/components/ui/button.tsx +0 -222
  93. package/src/components/ui/calendar.tsx +0 -387
  94. package/src/components/ui/card.tsx +0 -214
  95. package/src/components/ui/checkbox.tsx +0 -259
  96. package/src/components/ui/collapsible.tsx +0 -135
  97. package/src/components/ui/color-picker.tsx +0 -97
  98. package/src/components/ui/command.tsx +0 -225
  99. package/src/components/ui/dialog.tsx +0 -334
  100. package/src/components/ui/dropdown-menu.tsx +0 -218
  101. package/src/components/ui/gesture-drawer.tsx +0 -11
  102. package/src/components/ui/hover-card.tsx +0 -29
  103. package/src/components/ui/index.ts +0 -190
  104. package/src/components/ui/input.tsx +0 -222
  105. package/src/components/ui/label.tsx +0 -29
  106. package/src/components/ui/lightbox.tsx +0 -606
  107. package/src/components/ui/magnetic-button.tsx +0 -129
  108. package/src/components/ui/media-gallery.tsx +0 -612
  109. package/src/components/ui/pagination.tsx +0 -123
  110. package/src/components/ui/popover.tsx +0 -185
  111. package/src/components/ui/progress.tsx +0 -30
  112. package/src/components/ui/radio-group.tsx +0 -257
  113. package/src/components/ui/scroll-area.tsx +0 -47
  114. package/src/components/ui/select.tsx +0 -374
  115. package/src/components/ui/separator.tsx +0 -145
  116. package/src/components/ui/sheet.tsx +0 -139
  117. package/src/components/ui/skeleton.tsx +0 -20
  118. package/src/components/ui/slider.tsx +0 -354
  119. package/src/components/ui/spotlight-card.tsx +0 -119
  120. package/src/components/ui/switch.tsx +0 -86
  121. package/src/components/ui/table.tsx +0 -329
  122. package/src/components/ui/tabs.tsx +0 -198
  123. package/src/components/ui/textarea.tsx +0 -28
  124. package/src/components/ui/toast.tsx +0 -317
  125. package/src/components/ui/toggle.tsx +0 -119
  126. package/src/components/ui/tooltip.tsx +0 -151
  127. package/src/components/virtual-list/index.tsx +0 -668
  128. package/src/hooks/use-chart.ts +0 -205
  129. package/src/hooks/use-data-table.ts +0 -182
  130. package/src/hooks/use-docs-pro-access.ts +0 -13
  131. package/src/hooks/use-license-check.ts +0 -65
  132. package/src/hooks/use-subscription.ts +0 -19
  133. package/src/hooks/use-toast.ts +0 -15
  134. package/src/index.ts +0 -14
  135. package/src/lib/ai-providers.ts +0 -377
  136. package/src/lib/component-metadata.ts +0 -18
  137. package/src/lib/micro-interactions.ts +0 -255
  138. package/src/lib/paddle.ts +0 -17
  139. package/src/lib/utils.ts +0 -6
  140. package/src/patterns/login-form/index.tsx +0 -276
  141. package/src/patterns/login-form/types.ts +0 -67
  142. package/src/setupTests.ts +0 -41
  143. package/src/styles/advanced-chart.css +0 -239
  144. package/src/styles/calendar.css +0 -35
  145. package/src/styles/design-system.css +0 -363
  146. package/src/styles/index.css +0 -85
  147. package/src/styles/tailwind.css +0 -7
  148. package/src/styles/tokens.css +0 -455
  149. package/src/types/moonui.d.ts +0 -22
  150. package/src/types/next-auth.d.ts +0 -21
  151. package/src/use-intersection-observer.tsx +0 -154
  152. package/src/use-local-storage.tsx +0 -71
  153. package/src/use-paddle.ts +0 -138
  154. package/src/use-performance-optimizer.ts +0 -389
  155. package/src/use-pro-access.ts +0 -141
  156. package/src/use-scroll-animation.ts +0 -219
  157. package/src/use-subscription.ts +0 -37
  158. package/src/use-toast.ts +0 -32
  159. package/src/utils/chart-helpers.ts +0 -357
  160. package/src/utils/cn.ts +0 -6
  161. package/src/utils/data-processing.ts +0 -151
  162. 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