@moontra/moonui 2.0.7 → 2.0.9
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/package.json +26 -19
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/ui/__tests__/accordion.test.tsx +571 -0
- package/src/components/ui/__tests__/alert.test.tsx +484 -0
- package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
- package/src/components/ui/__tests__/avatar.test.tsx +382 -0
- package/src/components/ui/__tests__/badge.test.tsx +364 -0
- package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
- package/src/components/ui/__tests__/button.test.tsx +91 -0
- package/src/components/ui/__tests__/card.test.tsx +104 -0
- package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
- package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
- package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
- package/src/components/ui/__tests__/command.test.tsx +677 -0
- package/src/components/ui/__tests__/data-table.test.tsx +256 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
- package/src/components/ui/__tests__/dialog.test.tsx +764 -0
- package/src/components/ui/__tests__/input.test.tsx +318 -0
- package/src/components/ui/__tests__/label.test.tsx +103 -0
- package/src/components/ui/__tests__/popover.test.tsx +637 -0
- package/src/components/ui/__tests__/progress.test.tsx +382 -0
- package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
- package/src/components/ui/__tests__/select.test.tsx +705 -0
- package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
- package/src/components/ui/__tests__/slider.test.tsx +493 -0
- package/src/components/ui/__tests__/switch.test.tsx +372 -0
- package/src/components/ui/__tests__/table.test.tsx +824 -0
- package/src/components/ui/__tests__/tabs.test.tsx +887 -0
- package/src/components/ui/__tests__/toast.test.tsx +458 -0
- package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
- package/src/components/ui/accordion.tsx +63 -0
- package/src/components/ui/alert.tsx +130 -0
- package/src/components/ui/aspect-ratio.tsx +72 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/breadcrumb.tsx +207 -0
- package/src/components/ui/button.stories.tsx +162 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/calendar.tsx +262 -0
- package/src/components/ui/card.stories.tsx +208 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/collapsible.tsx +129 -0
- package/src/components/ui/color-picker.tsx +664 -0
- package/src/components/ui/command.tsx +218 -0
- package/src/components/ui/data-table.stories.tsx +509 -0
- package/src/components/ui/data-table.tsx +623 -0
- package/src/components/ui/date-picker.stories.tsx +321 -0
- package/src/components/ui/date-picker.tsx +603 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/file-upload.tsx +400 -0
- package/src/components/ui/github-stars.tsx +201 -0
- package/src/components/ui/index.ts +12 -0
- package/src/components/ui/input.stories.tsx +255 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/locked-component.tsx +215 -0
- package/src/components/ui/moon-logo.tsx +97 -0
- package/src/components/ui/pagination.tsx +116 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/progress.tsx +182 -0
- package/src/components/ui/radio-group.tsx +243 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/simple-editor.tsx +652 -0
- package/src/components/ui/skeleton.tsx +351 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/table.tsx +322 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +46 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/docs/theme-system.md +1194 -0
- package/src/index.tsx +39 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/theme.tsx +398 -0
- package/src/lib/utils.ts +69 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useState, useId } from 'react'
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion'
|
|
5
|
+
import { Button } from './button'
|
|
6
|
+
import { Progress } from './progress'
|
|
7
|
+
import { Badge } from './badge'
|
|
8
|
+
import { cn } from '../../lib/utils'
|
|
9
|
+
import {
|
|
10
|
+
Upload,
|
|
11
|
+
X,
|
|
12
|
+
File,
|
|
13
|
+
Image as ImageIcon,
|
|
14
|
+
Video,
|
|
15
|
+
Music,
|
|
16
|
+
FileText,
|
|
17
|
+
CheckCircle,
|
|
18
|
+
AlertCircle,
|
|
19
|
+
Loader2
|
|
20
|
+
} from 'lucide-react'
|
|
21
|
+
|
|
22
|
+
export interface FileUploadProps {
|
|
23
|
+
accept?: string
|
|
24
|
+
multiple?: boolean
|
|
25
|
+
maxSize?: number // in bytes
|
|
26
|
+
maxFiles?: number
|
|
27
|
+
disabled?: boolean
|
|
28
|
+
className?: string
|
|
29
|
+
onUpload?: (files: File[]) => Promise<void>
|
|
30
|
+
onRemove?: (fileId: string) => void
|
|
31
|
+
children?: React.ReactNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface UploadedFile {
|
|
35
|
+
id: string
|
|
36
|
+
file: File
|
|
37
|
+
progress: number
|
|
38
|
+
status: 'uploading' | 'success' | 'error'
|
|
39
|
+
preview?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const getFileIcon = (type: string) => {
|
|
43
|
+
if (type.startsWith('image/')) return <ImageIcon className="h-4 w-4" />
|
|
44
|
+
if (type.startsWith('video/')) return <Video className="h-4 w-4" />
|
|
45
|
+
if (type.startsWith('audio/')) return <Music className="h-4 w-4" />
|
|
46
|
+
if (type.includes('text') || type.includes('document')) return <FileText className="h-4 w-4" />
|
|
47
|
+
return <File className="h-4 w-4" />
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const formatFileSize = (bytes: number): string => {
|
|
51
|
+
if (bytes === 0) return '0 Bytes'
|
|
52
|
+
const k = 1024
|
|
53
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
54
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
55
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const FileItem = ({
|
|
59
|
+
uploadedFile,
|
|
60
|
+
onRemove
|
|
61
|
+
}: {
|
|
62
|
+
uploadedFile: UploadedFile
|
|
63
|
+
onRemove: (id: string) => void
|
|
64
|
+
}) => {
|
|
65
|
+
const { id, file, progress, status, preview } = uploadedFile
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<motion.div
|
|
69
|
+
initial={{ opacity: 0, y: 10 }}
|
|
70
|
+
animate={{ opacity: 1, y: 0 }}
|
|
71
|
+
exit={{ opacity: 0, y: -10 }}
|
|
72
|
+
className="flex items-center space-x-3 p-3 border rounded-lg bg-background"
|
|
73
|
+
>
|
|
74
|
+
{/* File Preview/Icon */}
|
|
75
|
+
<div className="flex-shrink-0">
|
|
76
|
+
{preview ? (
|
|
77
|
+
<img
|
|
78
|
+
src={preview}
|
|
79
|
+
alt={file.name}
|
|
80
|
+
className="h-10 w-10 object-cover rounded"
|
|
81
|
+
/>
|
|
82
|
+
) : (
|
|
83
|
+
<div className="h-10 w-10 rounded bg-muted flex items-center justify-center">
|
|
84
|
+
{getFileIcon(file.type)}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* File Info */}
|
|
90
|
+
<div className="flex-1 min-w-0">
|
|
91
|
+
<div className="flex items-center justify-between">
|
|
92
|
+
<p className="text-sm font-medium truncate">{file.name}</p>
|
|
93
|
+
<Button
|
|
94
|
+
variant="ghost"
|
|
95
|
+
size="sm"
|
|
96
|
+
className="h-6 w-6 p-0"
|
|
97
|
+
onClick={() => onRemove(id)}
|
|
98
|
+
>
|
|
99
|
+
<X className="h-3 w-3" />
|
|
100
|
+
</Button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="flex items-center space-x-2 mt-1">
|
|
104
|
+
<p className="text-xs text-muted-foreground">{formatFileSize(file.size)}</p>
|
|
105
|
+
|
|
106
|
+
{/* Status Badge */}
|
|
107
|
+
{status === 'uploading' && (
|
|
108
|
+
<Badge variant="secondary" className="h-5">
|
|
109
|
+
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
|
110
|
+
Uploading
|
|
111
|
+
</Badge>
|
|
112
|
+
)}
|
|
113
|
+
{status === 'success' && (
|
|
114
|
+
<Badge variant="default" className="h-5 bg-green-500">
|
|
115
|
+
<CheckCircle className="h-3 w-3 mr-1" />
|
|
116
|
+
Done
|
|
117
|
+
</Badge>
|
|
118
|
+
)}
|
|
119
|
+
{status === 'error' && (
|
|
120
|
+
<Badge variant="destructive" className="h-5">
|
|
121
|
+
<AlertCircle className="h-3 w-3 mr-1" />
|
|
122
|
+
Error
|
|
123
|
+
</Badge>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Progress Bar */}
|
|
128
|
+
{status === 'uploading' && (
|
|
129
|
+
<div className="mt-2">
|
|
130
|
+
<Progress value={progress} className="h-1" />
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</motion.div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
139
|
+
({
|
|
140
|
+
accept = "*",
|
|
141
|
+
multiple = true,
|
|
142
|
+
maxSize = 10 * 1024 * 1024, // 10MB
|
|
143
|
+
maxFiles = 5,
|
|
144
|
+
disabled = false,
|
|
145
|
+
className,
|
|
146
|
+
onUpload,
|
|
147
|
+
onRemove,
|
|
148
|
+
children,
|
|
149
|
+
...props
|
|
150
|
+
}, ref) => {
|
|
151
|
+
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
|
|
152
|
+
const [isDragOver, setIsDragOver] = useState(false)
|
|
153
|
+
const [error, setError] = useState<string | null>(null)
|
|
154
|
+
|
|
155
|
+
const validateFile = (file: File): string | null => {
|
|
156
|
+
if (file.size > maxSize) {
|
|
157
|
+
return `File size exceeds ${formatFileSize(maxSize)}`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (accept !== "*" && !accept.split(',').some(type => {
|
|
161
|
+
const cleanType = type.trim()
|
|
162
|
+
if (cleanType.includes('*')) {
|
|
163
|
+
return file.type.startsWith(cleanType.replace('*', ''))
|
|
164
|
+
}
|
|
165
|
+
return file.type === cleanType
|
|
166
|
+
})) {
|
|
167
|
+
return `File type not supported`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const processFiles = useCallback(async (files: FileList | File[]) => {
|
|
174
|
+
const fileArray = Array.from(files)
|
|
175
|
+
setError(null)
|
|
176
|
+
|
|
177
|
+
// Check total file count
|
|
178
|
+
if (uploadedFiles.length + fileArray.length > maxFiles) {
|
|
179
|
+
setError(`Maximum ${maxFiles} files allowed`)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const validFiles: File[] = []
|
|
184
|
+
const errors: string[] = []
|
|
185
|
+
|
|
186
|
+
// Validate each file
|
|
187
|
+
fileArray.forEach(file => {
|
|
188
|
+
const validationError = validateFile(file)
|
|
189
|
+
if (validationError) {
|
|
190
|
+
errors.push(`${file.name}: ${validationError}`)
|
|
191
|
+
} else {
|
|
192
|
+
validFiles.push(file)
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
if (errors.length > 0) {
|
|
197
|
+
setError(errors.join(', '))
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Create uploaded file entries
|
|
202
|
+
const newUploadedFiles: UploadedFile[] = validFiles.map((file, index) => ({
|
|
203
|
+
id: `${Date.now()}-${index}-${file.name.replace(/[^a-zA-Z0-9]/g, '')}`,
|
|
204
|
+
file,
|
|
205
|
+
progress: 0,
|
|
206
|
+
status: 'uploading' as const,
|
|
207
|
+
preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined
|
|
208
|
+
}))
|
|
209
|
+
|
|
210
|
+
setUploadedFiles(prev => [...prev, ...newUploadedFiles])
|
|
211
|
+
|
|
212
|
+
// Simulate upload progress
|
|
213
|
+
for (const uploadedFile of newUploadedFiles) {
|
|
214
|
+
let progress = 0
|
|
215
|
+
const interval = setInterval(() => {
|
|
216
|
+
progress += 15 + (uploadedFile.file.size % 10) // Deterministic progress based on file size
|
|
217
|
+
if (progress >= 100) {
|
|
218
|
+
progress = 100
|
|
219
|
+
clearInterval(interval)
|
|
220
|
+
|
|
221
|
+
setUploadedFiles(prev =>
|
|
222
|
+
prev.map(f =>
|
|
223
|
+
f.id === uploadedFile.id
|
|
224
|
+
? { ...f, progress: 100, status: 'success' as const }
|
|
225
|
+
: f
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
} else {
|
|
229
|
+
setUploadedFiles(prev =>
|
|
230
|
+
prev.map(f =>
|
|
231
|
+
f.id === uploadedFile.id
|
|
232
|
+
? { ...f, progress }
|
|
233
|
+
: f
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
}, 200)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Call onUpload if provided
|
|
241
|
+
if (onUpload) {
|
|
242
|
+
try {
|
|
243
|
+
await onUpload(validFiles)
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error('Upload failed:', error)
|
|
246
|
+
setUploadedFiles(prev =>
|
|
247
|
+
prev.map(f =>
|
|
248
|
+
newUploadedFiles.some(nf => nf.id === f.id)
|
|
249
|
+
? { ...f, status: 'error' as const }
|
|
250
|
+
: f
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}, [uploadedFiles.length, maxFiles, maxSize, accept, onUpload])
|
|
256
|
+
|
|
257
|
+
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
258
|
+
e.preventDefault()
|
|
259
|
+
setIsDragOver(false)
|
|
260
|
+
|
|
261
|
+
if (disabled) return
|
|
262
|
+
|
|
263
|
+
const files = e.dataTransfer.files
|
|
264
|
+
if (files.length > 0) {
|
|
265
|
+
processFiles(files)
|
|
266
|
+
}
|
|
267
|
+
}, [processFiles, disabled])
|
|
268
|
+
|
|
269
|
+
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
270
|
+
e.preventDefault()
|
|
271
|
+
if (!disabled) {
|
|
272
|
+
setIsDragOver(true)
|
|
273
|
+
}
|
|
274
|
+
}, [disabled])
|
|
275
|
+
|
|
276
|
+
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
277
|
+
e.preventDefault()
|
|
278
|
+
setIsDragOver(false)
|
|
279
|
+
}, [])
|
|
280
|
+
|
|
281
|
+
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
282
|
+
const files = e.target.files
|
|
283
|
+
if (files && files.length > 0) {
|
|
284
|
+
processFiles(files)
|
|
285
|
+
}
|
|
286
|
+
// Reset input value
|
|
287
|
+
e.target.value = ''
|
|
288
|
+
}, [processFiles])
|
|
289
|
+
|
|
290
|
+
const removeFile = useCallback((fileId: string) => {
|
|
291
|
+
setUploadedFiles(prev => {
|
|
292
|
+
const fileToRemove = prev.find(f => f.id === fileId)
|
|
293
|
+
if (fileToRemove?.preview) {
|
|
294
|
+
URL.revokeObjectURL(fileToRemove.preview)
|
|
295
|
+
}
|
|
296
|
+
return prev.filter(f => f.id !== fileId)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
if (onRemove) {
|
|
300
|
+
onRemove(fileId)
|
|
301
|
+
}
|
|
302
|
+
}, [onRemove])
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<div ref={ref} className={cn("space-y-4", className)} {...props}>
|
|
306
|
+
{/* Upload Area */}
|
|
307
|
+
<motion.div
|
|
308
|
+
className={cn(
|
|
309
|
+
"relative border-2 border-dashed rounded-lg p-8 text-center transition-colors",
|
|
310
|
+
isDragOver && "border-primary bg-primary/5",
|
|
311
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
312
|
+
!disabled && "hover:border-primary/50 cursor-pointer"
|
|
313
|
+
)}
|
|
314
|
+
onDrop={handleDrop}
|
|
315
|
+
onDragOver={handleDragOver}
|
|
316
|
+
onDragLeave={handleDragLeave}
|
|
317
|
+
animate={{
|
|
318
|
+
scale: isDragOver ? 1.02 : 1,
|
|
319
|
+
borderColor: isDragOver ? "hsl(var(--primary))" : "hsl(var(--border))"
|
|
320
|
+
}}
|
|
321
|
+
>
|
|
322
|
+
<input
|
|
323
|
+
type="file"
|
|
324
|
+
accept={accept}
|
|
325
|
+
multiple={multiple}
|
|
326
|
+
disabled={disabled}
|
|
327
|
+
onChange={handleFileSelect}
|
|
328
|
+
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
329
|
+
/>
|
|
330
|
+
|
|
331
|
+
{children || (
|
|
332
|
+
<div className="space-y-4">
|
|
333
|
+
<div className="mx-auto h-12 w-12 text-muted-foreground">
|
|
334
|
+
<Upload className="h-full w-full" />
|
|
335
|
+
</div>
|
|
336
|
+
<div>
|
|
337
|
+
<p className="text-lg font-medium">
|
|
338
|
+
{isDragOver ? 'Drop files here' : 'Upload files'}
|
|
339
|
+
</p>
|
|
340
|
+
<p className="text-sm text-muted-foreground">
|
|
341
|
+
Drag & drop files here, or click to select
|
|
342
|
+
</p>
|
|
343
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
344
|
+
Max {maxFiles} files, {formatFileSize(maxSize)} each
|
|
345
|
+
</p>
|
|
346
|
+
</div>
|
|
347
|
+
<Button variant="outline" disabled={disabled}>
|
|
348
|
+
Select Files
|
|
349
|
+
</Button>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
</motion.div>
|
|
353
|
+
|
|
354
|
+
{/* Error Message */}
|
|
355
|
+
<AnimatePresence>
|
|
356
|
+
{error && (
|
|
357
|
+
<motion.div
|
|
358
|
+
initial={{ opacity: 0, y: -10 }}
|
|
359
|
+
animate={{ opacity: 1, y: 0 }}
|
|
360
|
+
exit={{ opacity: 0, y: -10 }}
|
|
361
|
+
className="flex items-center space-x-2 text-sm text-destructive"
|
|
362
|
+
>
|
|
363
|
+
<AlertCircle className="h-4 w-4" />
|
|
364
|
+
<span>{error}</span>
|
|
365
|
+
</motion.div>
|
|
366
|
+
)}
|
|
367
|
+
</AnimatePresence>
|
|
368
|
+
|
|
369
|
+
{/* Uploaded Files */}
|
|
370
|
+
<AnimatePresence>
|
|
371
|
+
{uploadedFiles.length > 0 && (
|
|
372
|
+
<motion.div
|
|
373
|
+
initial={{ opacity: 0, height: 0 }}
|
|
374
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
375
|
+
exit={{ opacity: 0, height: 0 }}
|
|
376
|
+
className="space-y-2"
|
|
377
|
+
>
|
|
378
|
+
<h4 className="text-sm font-medium">
|
|
379
|
+
Uploaded Files ({uploadedFiles.length})
|
|
380
|
+
</h4>
|
|
381
|
+
<div className="space-y-2">
|
|
382
|
+
<AnimatePresence>
|
|
383
|
+
{uploadedFiles.map(uploadedFile => (
|
|
384
|
+
<FileItem
|
|
385
|
+
key={uploadedFile.id}
|
|
386
|
+
uploadedFile={uploadedFile}
|
|
387
|
+
onRemove={removeFile}
|
|
388
|
+
/>
|
|
389
|
+
))}
|
|
390
|
+
</AnimatePresence>
|
|
391
|
+
</div>
|
|
392
|
+
</motion.div>
|
|
393
|
+
)}
|
|
394
|
+
</AnimatePresence>
|
|
395
|
+
</div>
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
FileUpload.displayName = "FileUpload"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { Star, GitFork, Eye, ExternalLink } from 'lucide-react';
|
|
5
|
+
import { Button } from './button';
|
|
6
|
+
import { cn } from '../../lib/utils';
|
|
7
|
+
|
|
8
|
+
interface GitHubStarsProps {
|
|
9
|
+
className?: string;
|
|
10
|
+
size?: 'sm' | 'md' | 'lg';
|
|
11
|
+
showForks?: boolean;
|
|
12
|
+
showWatchers?: boolean;
|
|
13
|
+
repoUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface GitHubStats {
|
|
17
|
+
stars: number;
|
|
18
|
+
forks: number;
|
|
19
|
+
watchers: number;
|
|
20
|
+
isLive: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function GitHubStars({
|
|
24
|
+
className,
|
|
25
|
+
size = 'md',
|
|
26
|
+
showForks = true,
|
|
27
|
+
showWatchers = false,
|
|
28
|
+
repoUrl = 'https://github.com/moonui/moonui'
|
|
29
|
+
}: GitHubStarsProps) {
|
|
30
|
+
const [stats, setStats] = useState<GitHubStats | null>(null);
|
|
31
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const fetchStats = async () => {
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch('/api/github-stars');
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
setStats(data);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Failed to fetch GitHub stats:', error);
|
|
41
|
+
// Set fallback data
|
|
42
|
+
setStats({
|
|
43
|
+
stars: 1240,
|
|
44
|
+
forks: 89,
|
|
45
|
+
watchers: 156,
|
|
46
|
+
isLive: false
|
|
47
|
+
});
|
|
48
|
+
} finally {
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
fetchStats();
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const formatNumber = (num: number) => {
|
|
57
|
+
if (num >= 1000) {
|
|
58
|
+
return (num / 1000).toFixed(1) + 'k';
|
|
59
|
+
}
|
|
60
|
+
return num.toString();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const sizeClasses = {
|
|
64
|
+
sm: 'text-sm gap-2',
|
|
65
|
+
md: 'text-base gap-3',
|
|
66
|
+
lg: 'text-lg gap-4'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const iconSizes = {
|
|
70
|
+
sm: 14,
|
|
71
|
+
md: 16,
|
|
72
|
+
lg: 20
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (isLoading) {
|
|
76
|
+
return (
|
|
77
|
+
<div className={cn("flex items-center animate-pulse", sizeClasses[size], className)}>
|
|
78
|
+
<div className="flex items-center gap-1">
|
|
79
|
+
<div className="w-4 h-4 bg-gray-300 dark:bg-gray-700 rounded"></div>
|
|
80
|
+
<div className="w-8 h-4 bg-gray-300 dark:bg-gray-700 rounded"></div>
|
|
81
|
+
</div>
|
|
82
|
+
{showForks && (
|
|
83
|
+
<div className="flex items-center gap-1">
|
|
84
|
+
<div className="w-4 h-4 bg-gray-300 dark:bg-gray-700 rounded"></div>
|
|
85
|
+
<div className="w-6 h-4 bg-gray-300 dark:bg-gray-700 rounded"></div>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!stats) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className={cn("flex items-center", sizeClasses[size], className)}>
|
|
98
|
+
<a
|
|
99
|
+
href={repoUrl}
|
|
100
|
+
target="_blank"
|
|
101
|
+
rel="noopener noreferrer"
|
|
102
|
+
className="flex items-center gap-1 text-gray-700 dark:text-gray-300 hover:text-primary transition-colors"
|
|
103
|
+
>
|
|
104
|
+
<Star className="fill-current text-yellow-500" size={iconSizes[size]} />
|
|
105
|
+
<span className="font-medium">{formatNumber(stats.stars)}</span>
|
|
106
|
+
</a>
|
|
107
|
+
|
|
108
|
+
{showForks && (
|
|
109
|
+
<a
|
|
110
|
+
href={`${repoUrl}/network/members`}
|
|
111
|
+
target="_blank"
|
|
112
|
+
rel="noopener noreferrer"
|
|
113
|
+
className="flex items-center gap-1 text-gray-700 dark:text-gray-300 hover:text-primary transition-colors"
|
|
114
|
+
>
|
|
115
|
+
<GitFork size={iconSizes[size]} />
|
|
116
|
+
<span className="font-medium">{formatNumber(stats.forks)}</span>
|
|
117
|
+
</a>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{showWatchers && (
|
|
121
|
+
<a
|
|
122
|
+
href={`${repoUrl}/watchers`}
|
|
123
|
+
target="_blank"
|
|
124
|
+
rel="noopener noreferrer"
|
|
125
|
+
className="flex items-center gap-1 text-gray-700 dark:text-gray-300 hover:text-primary transition-colors"
|
|
126
|
+
>
|
|
127
|
+
<Eye size={iconSizes[size]} />
|
|
128
|
+
<span className="font-medium">{formatNumber(stats.watchers)}</span>
|
|
129
|
+
</a>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{!stats.isLive && (
|
|
133
|
+
<span className="text-xs text-gray-500 dark:text-gray-500 ml-2">
|
|
134
|
+
(demo)
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function GitHubStarButton({
|
|
142
|
+
className,
|
|
143
|
+
size = 'md',
|
|
144
|
+
repoUrl = 'https://github.com/moonui/moonui'
|
|
145
|
+
}: {
|
|
146
|
+
className?: string;
|
|
147
|
+
size?: 'sm' | 'md' | 'lg';
|
|
148
|
+
repoUrl?: string;
|
|
149
|
+
}) {
|
|
150
|
+
const [stars, setStars] = useState<number | null>(null);
|
|
151
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const fetchStats = async () => {
|
|
155
|
+
try {
|
|
156
|
+
const response = await fetch('/api/github-stars');
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
setStars(data.stars);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Failed to fetch GitHub stats:', error);
|
|
161
|
+
setStars(1240);
|
|
162
|
+
} finally {
|
|
163
|
+
setIsLoading(false);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
fetchStats();
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
const formatNumber = (num: number) => {
|
|
171
|
+
if (num >= 1000) {
|
|
172
|
+
return (num / 1000).toFixed(1) + 'k';
|
|
173
|
+
}
|
|
174
|
+
return num.toString();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<Button
|
|
179
|
+
variant="outline"
|
|
180
|
+
size={size}
|
|
181
|
+
className={cn("group", className)}
|
|
182
|
+
asChild
|
|
183
|
+
>
|
|
184
|
+
<a
|
|
185
|
+
href={repoUrl}
|
|
186
|
+
target="_blank"
|
|
187
|
+
rel="noopener noreferrer"
|
|
188
|
+
className="flex items-center gap-2"
|
|
189
|
+
>
|
|
190
|
+
<Star className="fill-current text-yellow-500 group-hover:scale-110 transition-transform" size={16} />
|
|
191
|
+
<span>Star on GitHub</span>
|
|
192
|
+
{!isLoading && stars && (
|
|
193
|
+
<span className="px-2 py-0.5 bg-gray-100 dark:bg-gray-800 rounded-full text-xs font-medium">
|
|
194
|
+
{formatNumber(stars)}
|
|
195
|
+
</span>
|
|
196
|
+
)}
|
|
197
|
+
<ExternalLink size={14} className="opacity-50 group-hover:opacity-100 transition-opacity" />
|
|
198
|
+
</a>
|
|
199
|
+
</Button>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// FREE UI Components - Source of Truth: @moontra/moonui NPM package
|
|
2
|
+
// Ana proje artık NPM paketlerini kullanır (wrapper pattern)
|
|
3
|
+
|
|
4
|
+
// All FREE components from @moontra/moonui package
|
|
5
|
+
export * from "@moontra/moonui"
|
|
6
|
+
|
|
7
|
+
// Local development-only components (not in package)
|
|
8
|
+
export * from "./locked-component"
|
|
9
|
+
export * from "./moon-logo"
|
|
10
|
+
|
|
11
|
+
// Pro Components - Available via @moontra/moonui-pro package
|
|
12
|
+
// Import Pro components via: import { Component } from "@/components/pro"
|