@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.
- package/dist/index.d.ts +691 -261
- package/dist/index.mjs +7418 -4934
- package/package.json +11 -5
- package/plugin/index.d.ts +86 -0
- package/plugin/index.js +308 -0
- package/scripts/postbuild.js +27 -0
- package/scripts/postinstall.js +176 -23
- package/src/__tests__/use-intersection-observer.test.tsx +0 -216
- package/src/__tests__/use-local-storage.test.tsx +0 -174
- package/src/__tests__/use-pro-access.test.tsx +0 -183
- package/src/components/advanced-chart/advanced-chart.test.tsx +0 -281
- package/src/components/advanced-chart/index.tsx +0 -1242
- package/src/components/advanced-forms/index.tsx +0 -426
- package/src/components/animated-button/index.tsx +0 -385
- package/src/components/calendar/event-dialog.tsx +0 -372
- package/src/components/calendar/index.tsx +0 -1073
- package/src/components/calendar-pro/index.tsx +0 -1697
- package/src/components/color-picker/index.tsx +0 -432
- package/src/components/credit-card-input/index.tsx +0 -406
- package/src/components/dashboard/dashboard-grid.tsx +0 -462
- package/src/components/dashboard/demo.tsx +0 -425
- package/src/components/dashboard/index.tsx +0 -1046
- package/src/components/dashboard/time-range-picker.tsx +0 -336
- package/src/components/dashboard/types.ts +0 -222
- package/src/components/dashboard/widgets/activity-feed.tsx +0 -344
- package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
- package/src/components/dashboard/widgets/metric-card.tsx +0 -343
- package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
- package/src/components/data-table/data-table-column-toggle.tsx +0 -169
- package/src/components/data-table/data-table-export.ts +0 -156
- package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
- package/src/components/data-table/data-table.test.tsx +0 -187
- package/src/components/data-table/index.tsx +0 -845
- package/src/components/draggable-list/index.tsx +0 -100
- package/src/components/enhanced/badge.tsx +0 -191
- package/src/components/enhanced/button.tsx +0 -362
- package/src/components/enhanced/card.tsx +0 -266
- package/src/components/enhanced/dialog.tsx +0 -246
- package/src/components/enhanced/index.ts +0 -4
- package/src/components/error-boundary/index.tsx +0 -109
- package/src/components/file-upload/file-upload.test.tsx +0 -243
- package/src/components/file-upload/index.tsx +0 -1660
- package/src/components/floating-action-button/index.tsx +0 -206
- package/src/components/form-wizard/form-wizard-context.tsx +0 -307
- package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
- package/src/components/form-wizard/form-wizard-progress.tsx +0 -298
- package/src/components/form-wizard/form-wizard-step.tsx +0 -111
- package/src/components/form-wizard/index.tsx +0 -102
- package/src/components/form-wizard/types.ts +0 -76
- package/src/components/gesture-drawer/index.tsx +0 -551
- package/src/components/github-stars/github-api.ts +0 -426
- package/src/components/github-stars/hooks.ts +0 -516
- package/src/components/github-stars/index.tsx +0 -375
- package/src/components/github-stars/types.ts +0 -148
- package/src/components/github-stars/variants.tsx +0 -513
- package/src/components/health-check/index.tsx +0 -439
- package/src/components/hover-card-3d/index.tsx +0 -530
- package/src/components/index.ts +0 -128
- package/src/components/internal/index.ts +0 -78
- package/src/components/kanban/add-card-modal.tsx +0 -502
- package/src/components/kanban/card-detail-modal.tsx +0 -761
- package/src/components/kanban/index.ts +0 -13
- package/src/components/kanban/kanban.tsx +0 -1684
- package/src/components/kanban/types.ts +0 -168
- package/src/components/lazy-component/index.tsx +0 -823
- package/src/components/license-error/index.tsx +0 -29
- package/src/components/magnetic-button/index.tsx +0 -167
- package/src/components/memory-efficient-data/index.tsx +0 -1016
- package/src/components/moonui-quiz-form/index.tsx +0 -817
- package/src/components/optimized-image/index.tsx +0 -425
- package/src/components/performance-debugger/index.tsx +0 -589
- package/src/components/performance-monitor/index.tsx +0 -794
- package/src/components/phone-number-input/index.tsx +0 -338
- package/src/components/pinch-zoom/index.tsx +0 -566
- package/src/components/quiz-form/index.tsx +0 -479
- package/src/components/rich-text-editor/index-old-backup.tsx +0 -437
- package/src/components/rich-text-editor/index.tsx +0 -2324
- package/src/components/rich-text-editor/slash-commands-extension.ts +0 -220
- package/src/components/rich-text-editor/slash-commands.css +0 -35
- package/src/components/rich-text-editor/table-styles.css +0 -65
- package/src/components/sidebar/index.tsx +0 -865
- package/src/components/spotlight-card/index.tsx +0 -191
- package/src/components/swipeable-card/index.tsx +0 -100
- package/src/components/timeline/index.tsx +0 -1148
- package/src/components/ui/accordion.tsx +0 -73
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/alert.tsx +0 -141
- package/src/components/ui/aspect-ratio.tsx +0 -245
- package/src/components/ui/avatar.tsx +0 -153
- package/src/components/ui/badge.tsx +0 -228
- package/src/components/ui/breadcrumb.tsx +0 -214
- package/src/components/ui/button.tsx +0 -222
- package/src/components/ui/calendar.tsx +0 -387
- package/src/components/ui/card.tsx +0 -214
- package/src/components/ui/checkbox.tsx +0 -259
- package/src/components/ui/collapsible.tsx +0 -135
- package/src/components/ui/color-picker.tsx +0 -97
- package/src/components/ui/command.tsx +0 -225
- package/src/components/ui/dialog.tsx +0 -334
- package/src/components/ui/dropdown-menu.tsx +0 -218
- package/src/components/ui/gesture-drawer.tsx +0 -11
- package/src/components/ui/hover-card.tsx +0 -29
- package/src/components/ui/index.ts +0 -190
- package/src/components/ui/input.tsx +0 -222
- package/src/components/ui/label.tsx +0 -29
- package/src/components/ui/lightbox.tsx +0 -606
- package/src/components/ui/magnetic-button.tsx +0 -129
- package/src/components/ui/media-gallery.tsx +0 -612
- package/src/components/ui/pagination.tsx +0 -123
- package/src/components/ui/popover.tsx +0 -185
- package/src/components/ui/progress.tsx +0 -30
- package/src/components/ui/radio-group.tsx +0 -257
- package/src/components/ui/scroll-area.tsx +0 -47
- package/src/components/ui/select.tsx +0 -374
- package/src/components/ui/separator.tsx +0 -145
- package/src/components/ui/sheet.tsx +0 -139
- package/src/components/ui/skeleton.tsx +0 -20
- package/src/components/ui/slider.tsx +0 -354
- package/src/components/ui/spotlight-card.tsx +0 -119
- package/src/components/ui/switch.tsx +0 -86
- package/src/components/ui/table.tsx +0 -329
- package/src/components/ui/tabs.tsx +0 -198
- package/src/components/ui/textarea.tsx +0 -28
- package/src/components/ui/toast.tsx +0 -317
- package/src/components/ui/toggle.tsx +0 -119
- package/src/components/ui/tooltip.tsx +0 -151
- package/src/components/virtual-list/index.tsx +0 -668
- package/src/hooks/use-chart.ts +0 -205
- package/src/hooks/use-data-table.ts +0 -182
- package/src/hooks/use-docs-pro-access.ts +0 -13
- package/src/hooks/use-license-check.ts +0 -65
- package/src/hooks/use-subscription.ts +0 -19
- package/src/hooks/use-toast.ts +0 -15
- package/src/index.ts +0 -14
- package/src/lib/ai-providers.ts +0 -377
- package/src/lib/component-metadata.ts +0 -18
- package/src/lib/micro-interactions.ts +0 -255
- package/src/lib/paddle.ts +0 -17
- package/src/lib/utils.ts +0 -6
- package/src/patterns/login-form/index.tsx +0 -276
- package/src/patterns/login-form/types.ts +0 -67
- package/src/setupTests.ts +0 -41
- package/src/styles/advanced-chart.css +0 -239
- package/src/styles/calendar.css +0 -35
- package/src/styles/design-system.css +0 -363
- package/src/styles/index.css +0 -85
- package/src/styles/tailwind.css +0 -7
- package/src/styles/tokens.css +0 -455
- package/src/types/moonui.d.ts +0 -22
- package/src/types/next-auth.d.ts +0 -21
- package/src/use-intersection-observer.tsx +0 -154
- package/src/use-local-storage.tsx +0 -71
- package/src/use-paddle.ts +0 -138
- package/src/use-performance-optimizer.ts +0 -389
- package/src/use-pro-access.ts +0 -141
- package/src/use-scroll-animation.ts +0 -219
- package/src/use-subscription.ts +0 -37
- package/src/use-toast.ts +0 -32
- package/src/utils/chart-helpers.ts +0 -357
- package/src/utils/cn.ts +0 -6
- package/src/utils/data-processing.ts +0 -151
- 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
|