@moontra/moonui-pro 2.20.2 → 2.20.4
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 +8 -3
- package/plugin/index.d.ts +86 -0
- package/plugin/index.js +308 -0
- package/scripts/postinstall.js +191 -23
- package/src/components/advanced-chart/index.tsx +0 -1246
- package/src/components/advanced-forms/index.tsx +0 -585
- package/src/components/animated-button/index.tsx +0 -385
- package/src/components/calendar/event-dialog.tsx +0 -377
- package/src/components/calendar/index.tsx +0 -1220
- 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 -480
- 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 -225
- package/src/components/dashboard/widgets/activity-feed.tsx +0 -349
- package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
- package/src/components/dashboard/widgets/comparison-widget.tsx +0 -177
- package/src/components/dashboard/widgets/index.ts +0 -5
- package/src/components/dashboard/widgets/metric-card.tsx +0 -363
- package/src/components/dashboard/widgets/progress-widget.tsx +0 -113
- 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/index.tsx +0 -845
- package/src/components/draggable-list/index.tsx +0 -100
- package/src/components/error-boundary/index.tsx +0 -232
- 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 -335
- package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
- package/src/components/form-wizard/form-wizard-progress.tsx +0 -329
- 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 -77
- 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 -517
- 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 -515
- package/src/components/health-check/index.tsx +0 -439
- package/src/components/hover-card-3d/index.tsx +0 -529
- package/src/components/index.ts +0 -130
- 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 -1689
- 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 -31
- package/src/components/magnetic-button/index.tsx +0 -216
- package/src/components/memory-efficient-data/index.tsx +0 -1018
- package/src/components/moonui-quiz-form/index.tsx +0 -817
- package/src/components/navbar/index.tsx +0 -781
- package/src/components/optimized-image/index.tsx +0 -425
- package/src/components/performance-debugger/index.tsx +0 -613
- package/src/components/performance-monitor/index.tsx +0 -808
- package/src/components/phone-number-input/index.tsx +0 -343
- package/src/components/phone-number-input/phone-number-input-simple.tsx +0 -167
- 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.tsx +0 -2322
- package/src/components/rich-text-editor/slash-commands-extension.ts +0 -230
- 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 -884
- 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 -1183
- package/src/components/ui/accordion.tsx +0 -581
- 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 -155
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/breadcrumb.tsx +0 -216
- package/src/components/ui/button.tsx +0 -228
- package/src/components/ui/calendar.tsx +0 -387
- package/src/components/ui/card.tsx +0 -216
- package/src/components/ui/checkbox.tsx +0 -259
- package/src/components/ui/collapsible.tsx +0 -631
- package/src/components/ui/color-picker.tsx +0 -97
- package/src/components/ui/command.tsx +0 -948
- package/src/components/ui/dialog.tsx +0 -752
- package/src/components/ui/dropdown-menu.tsx +0 -706
- 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 -222
- package/src/components/ui/input.tsx +0 -224
- 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 -611
- package/src/components/ui/navigation-menu.tsx +0 -130
- package/src/components/ui/pagination.tsx +0 -125
- 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 -378
- 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 -331
- package/src/components/ui/tabs-pro.tsx +0 -542
- package/src/components/ui/tabs.tsx +0 -54
- 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 -22
- 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 -681
- package/src/styles/tailwind.css +0 -7
- package/src/styles/tokens.css +0 -455
- 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,585 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import React, { useState } from "react"
|
|
4
|
-
import { motion, AnimatePresence } from "framer-motion"
|
|
5
|
-
import { useForm } from "react-hook-form"
|
|
6
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
|
7
|
-
import { Button } from "../ui/button"
|
|
8
|
-
import { Input } from "../ui/input"
|
|
9
|
-
import { Label } from "../ui/label"
|
|
10
|
-
import { Textarea } from "../ui/textarea"
|
|
11
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
|
12
|
-
import { Checkbox } from "../ui/checkbox"
|
|
13
|
-
import { Switch } from "../ui/switch"
|
|
14
|
-
import { MoonUIBadgePro as Badge } from "../ui/badge"
|
|
15
|
-
import { cn } from "../../lib/utils"
|
|
16
|
-
import { Lock, Sparkles, CheckCircle, AlertCircle, Eye, EyeOff, Upload, X } from "lucide-react"
|
|
17
|
-
import { useSubscription } from "../../hooks/use-subscription"
|
|
18
|
-
import { MoonUIPhoneNumberInputPro as PhoneNumberInput } from "../phone-number-input"
|
|
19
|
-
|
|
20
|
-
interface AdvancedFormField {
|
|
21
|
-
name: string
|
|
22
|
-
label: string
|
|
23
|
-
type: "text" | "email" | "password" | "textarea" | "select" | "checkbox" | "switch" | "file" | "number" | "url" | "tel"
|
|
24
|
-
placeholder?: string
|
|
25
|
-
required?: boolean
|
|
26
|
-
options?: Array<{ value: string; label: string }>
|
|
27
|
-
validation?: {
|
|
28
|
-
minLength?: number
|
|
29
|
-
maxLength?: number
|
|
30
|
-
pattern?: RegExp
|
|
31
|
-
custom?: (value: any) => string | true
|
|
32
|
-
}
|
|
33
|
-
description?: string
|
|
34
|
-
defaultValue?: any
|
|
35
|
-
defaultCountry?: string // For phone number fields
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface AdvancedFormsProps {
|
|
39
|
-
fields: AdvancedFormField[]
|
|
40
|
-
onSubmit: (data: any) => void | Promise<void>
|
|
41
|
-
title?: string
|
|
42
|
-
description?: string
|
|
43
|
-
submitText?: string
|
|
44
|
-
enableAutoSave?: boolean
|
|
45
|
-
showProgress?: boolean
|
|
46
|
-
className?: string
|
|
47
|
-
layout?: "vertical" | "horizontal" | "grid"
|
|
48
|
-
columns?: number
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
|
|
52
|
-
fields,
|
|
53
|
-
onSubmit,
|
|
54
|
-
title = "Advanced Form",
|
|
55
|
-
description,
|
|
56
|
-
submitText = "Submit",
|
|
57
|
-
enableAutoSave = false,
|
|
58
|
-
showProgress = true,
|
|
59
|
-
className,
|
|
60
|
-
layout = "vertical",
|
|
61
|
-
columns = 2
|
|
62
|
-
}) => {
|
|
63
|
-
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
64
|
-
const [showPasswords, setShowPasswords] = useState<Record<string, boolean>>({})
|
|
65
|
-
const [uploadedFiles, setUploadedFiles] = useState<Record<string, File[]>>({})
|
|
66
|
-
const [phoneNumbers, setPhoneNumbers] = useState<Record<string, { country: string; number: string }>>(() => {
|
|
67
|
-
// Initialize phone numbers from default values
|
|
68
|
-
const initial: Record<string, { country: string; number: string }> = {}
|
|
69
|
-
fields.forEach(field => {
|
|
70
|
-
if (field.type === "tel" && field.defaultValue) {
|
|
71
|
-
initial[field.name] = {
|
|
72
|
-
country: field.defaultCountry || "US",
|
|
73
|
-
number: field.defaultValue
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
return initial
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
const {
|
|
81
|
-
register,
|
|
82
|
-
handleSubmit,
|
|
83
|
-
watch,
|
|
84
|
-
formState: { errors, isValid, touchedFields },
|
|
85
|
-
setValue,
|
|
86
|
-
getValues,
|
|
87
|
-
setError,
|
|
88
|
-
clearErrors
|
|
89
|
-
} = useForm<any>({
|
|
90
|
-
mode: "onChange",
|
|
91
|
-
defaultValues: fields.reduce((acc, field) => ({
|
|
92
|
-
...acc,
|
|
93
|
-
[field.name]: field.defaultValue || (field.type === "checkbox" || field.type === "switch" ? false : "")
|
|
94
|
-
}), {})
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
const watchedValues = watch()
|
|
98
|
-
|
|
99
|
-
// Calculate progress based on valid and filled fields
|
|
100
|
-
const calculateProgress = () => {
|
|
101
|
-
let validFields = 0
|
|
102
|
-
let totalFields = 0
|
|
103
|
-
|
|
104
|
-
fields.forEach(field => {
|
|
105
|
-
const value = watchedValues[field.name]
|
|
106
|
-
const hasError = errors[field.name]
|
|
107
|
-
|
|
108
|
-
// Only count required fields or optional fields that have been touched
|
|
109
|
-
if (!field.required && !touchedFields[field.name] && !value) {
|
|
110
|
-
return // Skip untouched optional fields
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
totalFields++
|
|
114
|
-
|
|
115
|
-
// Check if field has value and no errors
|
|
116
|
-
if (field.type === "checkbox" || field.type === "switch") {
|
|
117
|
-
if (!field.required || value === true) {
|
|
118
|
-
if (!hasError) validFields++
|
|
119
|
-
}
|
|
120
|
-
} else if (field.type === "file") {
|
|
121
|
-
if (!field.required || uploadedFiles[field.name]?.length > 0) {
|
|
122
|
-
if (!hasError) validFields++
|
|
123
|
-
}
|
|
124
|
-
} else {
|
|
125
|
-
if (value && !hasError && value.toString().trim() !== "") {
|
|
126
|
-
validFields++
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
return totalFields > 0 ? (validFields / totalFields) * 100 : 0
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const progress = calculateProgress()
|
|
135
|
-
|
|
136
|
-
// Check if all required fields are valid
|
|
137
|
-
const isFormValid = () => {
|
|
138
|
-
for (const field of fields) {
|
|
139
|
-
if (field.required) {
|
|
140
|
-
const value = watchedValues[field.name]
|
|
141
|
-
const hasError = errors[field.name]
|
|
142
|
-
|
|
143
|
-
if (hasError) return false
|
|
144
|
-
|
|
145
|
-
if (field.type === "checkbox" && value !== true) return false
|
|
146
|
-
if (field.type === "file" && (!uploadedFiles[field.name] || uploadedFiles[field.name].length === 0)) return false
|
|
147
|
-
if (!value || (typeof value === 'string' && value.trim() === '')) return false
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return true
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const handleFormSubmit = async (data: any) => {
|
|
154
|
-
setIsSubmitting(true)
|
|
155
|
-
try {
|
|
156
|
-
await onSubmit(data)
|
|
157
|
-
} finally {
|
|
158
|
-
setIsSubmitting(false)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const togglePasswordVisibility = (fieldName: string) => {
|
|
163
|
-
setShowPasswords(prev => ({
|
|
164
|
-
...prev,
|
|
165
|
-
[fieldName]: !prev[fieldName]
|
|
166
|
-
}))
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const handleFileUpload = (fieldName: string, files: FileList | null) => {
|
|
170
|
-
if (files) {
|
|
171
|
-
const fileArray = Array.from(files)
|
|
172
|
-
setUploadedFiles(prev => ({
|
|
173
|
-
...prev,
|
|
174
|
-
[fieldName]: fileArray
|
|
175
|
-
}))
|
|
176
|
-
setValue(fieldName as any, fileArray as any)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const removeFile = (fieldName: string, index: number) => {
|
|
181
|
-
setUploadedFiles(prev => {
|
|
182
|
-
const newFiles = [...(prev[fieldName] || [])]
|
|
183
|
-
newFiles.splice(index, 1)
|
|
184
|
-
return {
|
|
185
|
-
...prev,
|
|
186
|
-
[fieldName]: newFiles
|
|
187
|
-
}
|
|
188
|
-
})
|
|
189
|
-
setValue(fieldName as any, uploadedFiles[fieldName]?.filter((_, i) => i !== index) || [] as any)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const renderField = (field: AdvancedFormField, index: number) => {
|
|
193
|
-
const error = errors[field.name]
|
|
194
|
-
const isTouched = touchedFields[field.name]
|
|
195
|
-
|
|
196
|
-
return (
|
|
197
|
-
<motion.div
|
|
198
|
-
key={field.name}
|
|
199
|
-
initial={{ opacity: 0, y: 20 }}
|
|
200
|
-
animate={{ opacity: 1, y: 0 }}
|
|
201
|
-
transition={{ delay: index * 0.1 }}
|
|
202
|
-
className="space-y-2"
|
|
203
|
-
>
|
|
204
|
-
<div className="flex items-center justify-between">
|
|
205
|
-
<Label htmlFor={field.name} className="flex items-center gap-2">
|
|
206
|
-
{field.label}
|
|
207
|
-
{field.required && <span className="text-destructive">*</span>}
|
|
208
|
-
{isTouched && !error && (
|
|
209
|
-
<CheckCircle className="h-4 w-4 text-green-500" />
|
|
210
|
-
)}
|
|
211
|
-
{error && <AlertCircle className="h-4 w-4 text-destructive" />}
|
|
212
|
-
</Label>
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
<div className="relative">
|
|
216
|
-
{(field.type === "text" || field.type === "email" || field.type === "number" || field.type === "url") && (
|
|
217
|
-
<Input
|
|
218
|
-
id={field.name}
|
|
219
|
-
type={field.type}
|
|
220
|
-
placeholder={field.placeholder}
|
|
221
|
-
className={cn(error && "border-destructive")}
|
|
222
|
-
{...register(field.name, {
|
|
223
|
-
required: field.required ? `${field.label} is required` : false,
|
|
224
|
-
minLength: field.validation?.minLength ? {
|
|
225
|
-
value: field.validation.minLength,
|
|
226
|
-
message: `Minimum ${field.validation.minLength} characters required`
|
|
227
|
-
} : undefined,
|
|
228
|
-
maxLength: field.validation?.maxLength ? {
|
|
229
|
-
value: field.validation.maxLength,
|
|
230
|
-
message: `Maximum ${field.validation.maxLength} characters allowed`
|
|
231
|
-
} : undefined,
|
|
232
|
-
pattern: field.validation?.pattern ? {
|
|
233
|
-
value: field.validation.pattern,
|
|
234
|
-
message: "Invalid format"
|
|
235
|
-
} : field.type === "email" ? {
|
|
236
|
-
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
|
237
|
-
message: "Please enter a valid email address"
|
|
238
|
-
} : field.type === "url" ? {
|
|
239
|
-
value: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
|
|
240
|
-
message: "Please enter a valid URL"
|
|
241
|
-
} : undefined,
|
|
242
|
-
validate: field.validation?.custom
|
|
243
|
-
})}
|
|
244
|
-
/>
|
|
245
|
-
)}
|
|
246
|
-
|
|
247
|
-
{field.type === "tel" && (
|
|
248
|
-
<PhoneNumberInput
|
|
249
|
-
id={field.name}
|
|
250
|
-
placeholder={field.placeholder}
|
|
251
|
-
className={cn(error && "border-destructive")}
|
|
252
|
-
value={phoneNumbers[field.name] || undefined}
|
|
253
|
-
onChange={(phoneData: { country: string; number: string; fullNumber: string; isValid: boolean }) => {
|
|
254
|
-
// Update local state
|
|
255
|
-
setPhoneNumbers(prev => ({
|
|
256
|
-
...prev,
|
|
257
|
-
[field.name]: { country: phoneData.country, number: phoneData.number }
|
|
258
|
-
}))
|
|
259
|
-
|
|
260
|
-
// Update form value
|
|
261
|
-
setValue(field.name, phoneData.fullNumber)
|
|
262
|
-
clearErrors(field.name)
|
|
263
|
-
|
|
264
|
-
// Check if required
|
|
265
|
-
if (field.required && !phoneData.fullNumber) {
|
|
266
|
-
setError(field.name, { type: 'required', message: `${field.label} is required` })
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Check custom validation
|
|
270
|
-
if (phoneData.fullNumber && field.validation?.custom) {
|
|
271
|
-
const validationResult = field.validation.custom(phoneData.fullNumber)
|
|
272
|
-
if (validationResult !== true) {
|
|
273
|
-
setError(field.name, { type: 'custom', message: validationResult })
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}}
|
|
277
|
-
defaultCountry={field.defaultCountry || "US"}
|
|
278
|
-
showFlags={true}
|
|
279
|
-
showDialCode={true}
|
|
280
|
-
autoFormat={true}
|
|
281
|
-
validateOnChange={true}
|
|
282
|
-
showValidationIcon={true}
|
|
283
|
-
error={error?.message as string}
|
|
284
|
-
/>
|
|
285
|
-
)}
|
|
286
|
-
|
|
287
|
-
{field.type === "password" && (
|
|
288
|
-
<div className="relative">
|
|
289
|
-
<Input
|
|
290
|
-
id={field.name}
|
|
291
|
-
type={showPasswords[field.name] ? "text" : "password"}
|
|
292
|
-
placeholder={field.placeholder}
|
|
293
|
-
className={cn("pr-10", error && "border-destructive")}
|
|
294
|
-
{...register(field.name, {
|
|
295
|
-
required: field.required ? `${field.label} is required` : false,
|
|
296
|
-
minLength: field.validation?.minLength ? {
|
|
297
|
-
value: field.validation.minLength,
|
|
298
|
-
message: `Minimum ${field.validation.minLength} characters required`
|
|
299
|
-
} : undefined,
|
|
300
|
-
maxLength: field.validation?.maxLength ? {
|
|
301
|
-
value: field.validation.maxLength,
|
|
302
|
-
message: `Maximum ${field.validation.maxLength} characters allowed`
|
|
303
|
-
} : undefined,
|
|
304
|
-
pattern: field.validation?.pattern ? {
|
|
305
|
-
value: field.validation.pattern,
|
|
306
|
-
message: "Invalid format"
|
|
307
|
-
} : undefined,
|
|
308
|
-
validate: field.validation?.custom
|
|
309
|
-
})}
|
|
310
|
-
/>
|
|
311
|
-
<Button
|
|
312
|
-
type="button"
|
|
313
|
-
variant="ghost"
|
|
314
|
-
size="sm"
|
|
315
|
-
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0"
|
|
316
|
-
onClick={() => togglePasswordVisibility(field.name)}
|
|
317
|
-
>
|
|
318
|
-
{showPasswords[field.name] ? (
|
|
319
|
-
<EyeOff className="h-4 w-4" />
|
|
320
|
-
) : (
|
|
321
|
-
<Eye className="h-4 w-4" />
|
|
322
|
-
)}
|
|
323
|
-
</Button>
|
|
324
|
-
</div>
|
|
325
|
-
)}
|
|
326
|
-
|
|
327
|
-
{field.type === "textarea" && (
|
|
328
|
-
<Textarea
|
|
329
|
-
id={field.name}
|
|
330
|
-
placeholder={field.placeholder}
|
|
331
|
-
className={cn(error && "border-destructive")}
|
|
332
|
-
rows={3}
|
|
333
|
-
{...register(field.name, {
|
|
334
|
-
required: field.required ? `${field.label} is required` : false,
|
|
335
|
-
minLength: field.validation?.minLength ? {
|
|
336
|
-
value: field.validation.minLength,
|
|
337
|
-
message: `Minimum ${field.validation.minLength} characters required`
|
|
338
|
-
} : undefined,
|
|
339
|
-
maxLength: field.validation?.maxLength ? {
|
|
340
|
-
value: field.validation.maxLength,
|
|
341
|
-
message: `Maximum ${field.validation.maxLength} characters allowed`
|
|
342
|
-
} : undefined,
|
|
343
|
-
validate: field.validation?.custom
|
|
344
|
-
})}
|
|
345
|
-
/>
|
|
346
|
-
)}
|
|
347
|
-
|
|
348
|
-
{field.type === "select" && (
|
|
349
|
-
<Select
|
|
350
|
-
onValueChange={(value) => {
|
|
351
|
-
setValue(field.name, value)
|
|
352
|
-
if (field.required && !value) {
|
|
353
|
-
setError(field.name, { type: 'required', message: `${field.label} is required` })
|
|
354
|
-
} else {
|
|
355
|
-
clearErrors(field.name)
|
|
356
|
-
}
|
|
357
|
-
}}
|
|
358
|
-
defaultValue={field.defaultValue}
|
|
359
|
-
>
|
|
360
|
-
<SelectTrigger className={cn(error && "border-destructive")}>
|
|
361
|
-
<SelectValue placeholder={field.placeholder} />
|
|
362
|
-
</SelectTrigger>
|
|
363
|
-
<SelectContent>
|
|
364
|
-
{field.options?.map((option) => (
|
|
365
|
-
<SelectItem key={option.value} value={option.value}>
|
|
366
|
-
{option.label}
|
|
367
|
-
</SelectItem>
|
|
368
|
-
))}
|
|
369
|
-
</SelectContent>
|
|
370
|
-
</Select>
|
|
371
|
-
)}
|
|
372
|
-
|
|
373
|
-
{field.type === "checkbox" && (
|
|
374
|
-
<div className="flex items-center space-x-2">
|
|
375
|
-
<Checkbox
|
|
376
|
-
id={field.name}
|
|
377
|
-
{...register(field.name, {
|
|
378
|
-
required: field.required ? `${field.label} must be checked` : false
|
|
379
|
-
})}
|
|
380
|
-
onCheckedChange={(checked: boolean) => {
|
|
381
|
-
setValue(field.name, checked)
|
|
382
|
-
if (field.required && !checked) {
|
|
383
|
-
setError(field.name, { type: 'required', message: `${field.label} must be checked` })
|
|
384
|
-
} else {
|
|
385
|
-
clearErrors(field.name)
|
|
386
|
-
}
|
|
387
|
-
}}
|
|
388
|
-
defaultChecked={field.defaultValue}
|
|
389
|
-
/>
|
|
390
|
-
<Label htmlFor={field.name} className="text-sm cursor-pointer">
|
|
391
|
-
{field.placeholder || "Check this option"}
|
|
392
|
-
</Label>
|
|
393
|
-
</div>
|
|
394
|
-
)}
|
|
395
|
-
|
|
396
|
-
{field.type === "switch" && (
|
|
397
|
-
<div className="flex items-center space-x-2">
|
|
398
|
-
<Switch
|
|
399
|
-
id={field.name}
|
|
400
|
-
{...register(field.name)}
|
|
401
|
-
onCheckedChange={(checked: boolean) => setValue(field.name, checked)}
|
|
402
|
-
defaultChecked={field.defaultValue}
|
|
403
|
-
/>
|
|
404
|
-
<Label htmlFor={field.name} className="text-sm">
|
|
405
|
-
{field.placeholder || "Toggle this option"}
|
|
406
|
-
</Label>
|
|
407
|
-
</div>
|
|
408
|
-
)}
|
|
409
|
-
|
|
410
|
-
{field.type === "file" && (
|
|
411
|
-
<div className="space-y-2">
|
|
412
|
-
<div className="flex items-center justify-center w-full">
|
|
413
|
-
<label
|
|
414
|
-
htmlFor={field.name}
|
|
415
|
-
className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-muted/50 hover:bg-muted/80 transition-colors"
|
|
416
|
-
>
|
|
417
|
-
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
|
418
|
-
<Upload className="w-8 h-8 mb-4 text-muted-foreground" />
|
|
419
|
-
<p className="mb-2 text-sm text-muted-foreground">
|
|
420
|
-
<span className="font-semibold">Click to upload</span> or drag and drop
|
|
421
|
-
</p>
|
|
422
|
-
</div>
|
|
423
|
-
<input
|
|
424
|
-
id={field.name}
|
|
425
|
-
type="file"
|
|
426
|
-
multiple
|
|
427
|
-
className="hidden"
|
|
428
|
-
onChange={(e) => handleFileUpload(field.name, e.target.files)}
|
|
429
|
-
/>
|
|
430
|
-
</label>
|
|
431
|
-
</div>
|
|
432
|
-
|
|
433
|
-
{uploadedFiles[field.name] && uploadedFiles[field.name].length > 0 && (
|
|
434
|
-
<div className="space-y-2">
|
|
435
|
-
{uploadedFiles[field.name].map((file, index) => (
|
|
436
|
-
<div key={index} className="flex items-center justify-between p-2 bg-muted rounded-md">
|
|
437
|
-
<span className="text-sm truncate">{file.name}</span>
|
|
438
|
-
<Button
|
|
439
|
-
type="button"
|
|
440
|
-
variant="ghost"
|
|
441
|
-
size="sm"
|
|
442
|
-
onClick={() => removeFile(field.name, index)}
|
|
443
|
-
>
|
|
444
|
-
<X className="h-4 w-4" />
|
|
445
|
-
</Button>
|
|
446
|
-
</div>
|
|
447
|
-
))}
|
|
448
|
-
</div>
|
|
449
|
-
)}
|
|
450
|
-
</div>
|
|
451
|
-
)}
|
|
452
|
-
</div>
|
|
453
|
-
|
|
454
|
-
{field.description && (
|
|
455
|
-
<p className="text-sm text-muted-foreground">{field.description}</p>
|
|
456
|
-
)}
|
|
457
|
-
|
|
458
|
-
<AnimatePresence>
|
|
459
|
-
{error && (
|
|
460
|
-
<motion.p
|
|
461
|
-
initial={{ opacity: 0, height: 0 }}
|
|
462
|
-
animate={{ opacity: 1, height: "auto" }}
|
|
463
|
-
exit={{ opacity: 0, height: 0 }}
|
|
464
|
-
className="text-sm text-destructive"
|
|
465
|
-
>
|
|
466
|
-
{error.message as string}
|
|
467
|
-
</motion.p>
|
|
468
|
-
)}
|
|
469
|
-
</AnimatePresence>
|
|
470
|
-
</motion.div>
|
|
471
|
-
)
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const gridClassName = layout === "grid" ? `grid grid-cols-1 md:grid-cols-${columns} gap-6` : "space-y-6"
|
|
475
|
-
|
|
476
|
-
return (
|
|
477
|
-
<Card className={cn("w-full max-w-4xl", className)}>
|
|
478
|
-
<CardHeader className={cn(showProgress && "pb-0")}>
|
|
479
|
-
<div className="flex items-center justify-between">
|
|
480
|
-
<div>
|
|
481
|
-
<CardTitle>{title}</CardTitle>
|
|
482
|
-
{description && <CardDescription>{description}</CardDescription>}
|
|
483
|
-
</div>
|
|
484
|
-
<Badge variant="pro" className="ml-2">
|
|
485
|
-
PRO
|
|
486
|
-
</Badge>
|
|
487
|
-
</div>
|
|
488
|
-
|
|
489
|
-
{showProgress && (
|
|
490
|
-
<div className="space-y-2 mt-4">
|
|
491
|
-
<div className="flex justify-between text-sm">
|
|
492
|
-
<span>Progress</span>
|
|
493
|
-
<span>{Math.round(progress)}%</span>
|
|
494
|
-
</div>
|
|
495
|
-
<div className="w-full bg-muted rounded-full h-2">
|
|
496
|
-
<motion.div
|
|
497
|
-
className="bg-primary h-2 rounded-full"
|
|
498
|
-
initial={{ width: 0 }}
|
|
499
|
-
animate={{ width: `${progress}%` }}
|
|
500
|
-
transition={{ duration: 0.3 }}
|
|
501
|
-
/>
|
|
502
|
-
</div>
|
|
503
|
-
</div>
|
|
504
|
-
)}
|
|
505
|
-
</CardHeader>
|
|
506
|
-
|
|
507
|
-
<CardContent className={cn(showProgress && "pt-8")}>
|
|
508
|
-
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
|
509
|
-
<div className={gridClassName}>
|
|
510
|
-
{fields.map((field, index) => renderField(field, index))}
|
|
511
|
-
</div>
|
|
512
|
-
|
|
513
|
-
<div className="flex items-center justify-between pt-6">
|
|
514
|
-
<div className="text-sm text-muted-foreground">
|
|
515
|
-
{(() => {
|
|
516
|
-
const requiredFields = fields.filter(f => f.required).length
|
|
517
|
-
const completedRequired = fields.filter(f => {
|
|
518
|
-
if (!f.required) return false
|
|
519
|
-
const value = watchedValues[f.name]
|
|
520
|
-
const hasError = errors[f.name]
|
|
521
|
-
if (hasError) return false
|
|
522
|
-
if (f.type === "checkbox") return value === true
|
|
523
|
-
if (f.type === "file") return uploadedFiles[f.name]?.length > 0
|
|
524
|
-
return value && value.toString().trim() !== ""
|
|
525
|
-
}).length
|
|
526
|
-
return `${completedRequired}/${requiredFields} required fields completed`
|
|
527
|
-
})()}
|
|
528
|
-
</div>
|
|
529
|
-
|
|
530
|
-
<Button
|
|
531
|
-
type="submit"
|
|
532
|
-
disabled={isSubmitting || !isFormValid()}
|
|
533
|
-
className="min-w-32"
|
|
534
|
-
>
|
|
535
|
-
{isSubmitting ? (
|
|
536
|
-
<motion.div
|
|
537
|
-
animate={{ rotate: 360 }}
|
|
538
|
-
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
|
539
|
-
className="w-4 h-4 border-2 border-white border-t-transparent rounded-full"
|
|
540
|
-
/>
|
|
541
|
-
) : (
|
|
542
|
-
submitText
|
|
543
|
-
)}
|
|
544
|
-
</Button>
|
|
545
|
-
</div>
|
|
546
|
-
</form>
|
|
547
|
-
</CardContent>
|
|
548
|
-
</Card>
|
|
549
|
-
)
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
export const AdvancedForms: React.FC<AdvancedFormsProps> = ({ className, ...props }) => {
|
|
553
|
-
const { hasProAccess, isLoading } = useSubscription()
|
|
554
|
-
|
|
555
|
-
// If no pro access, show upgrade prompt
|
|
556
|
-
if (!isLoading && !hasProAccess) {
|
|
557
|
-
return (
|
|
558
|
-
<Card className={cn("w-fit", className)}>
|
|
559
|
-
<CardContent className="py-6 text-center">
|
|
560
|
-
<div className="space-y-4">
|
|
561
|
-
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
562
|
-
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
563
|
-
</div>
|
|
564
|
-
<div>
|
|
565
|
-
<h3 className="font-semibold text-sm mb-2">Pro Feature</h3>
|
|
566
|
-
<p className="text-muted-foreground text-xs mb-4">
|
|
567
|
-
Advanced Forms is available exclusively to MoonUI Pro subscribers.
|
|
568
|
-
</p>
|
|
569
|
-
<a href="/pricing">
|
|
570
|
-
<Button size="sm">
|
|
571
|
-
<Sparkles className="mr-2 h-4 w-4" />
|
|
572
|
-
Upgrade to Pro
|
|
573
|
-
</Button>
|
|
574
|
-
</a>
|
|
575
|
-
</div>
|
|
576
|
-
</div>
|
|
577
|
-
</CardContent>
|
|
578
|
-
</Card>
|
|
579
|
-
)
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return <AdvancedFormsInternal className={className} {...props} />
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
export type { AdvancedFormField, AdvancedFormsProps }
|