@moontra/moonui-pro 2.6.2 → 2.7.1
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 +148 -8
- package/dist/index.mjs +1092 -328
- package/package.json +1 -1
- package/src/components/credit-card-input/index.tsx +406 -0
- package/src/components/form-wizard/index.tsx +2 -2
- package/src/components/index.ts +18 -1
- package/src/components/moonui-quiz-form/index.tsx +817 -0
- package/src/components/multi-step-form/index.tsx +3 -3
- package/src/components/phone-number-input/index.tsx +335 -0
- package/src/components/quiz-form/index.tsx +3 -3
|
@@ -4,7 +4,7 @@ import React from "react"
|
|
|
4
4
|
import { useForm, FormProvider, UseFormReturn, FieldValues, DefaultValues } from "react-hook-form"
|
|
5
5
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
6
6
|
import { z } from "zod"
|
|
7
|
-
import {
|
|
7
|
+
import { MoonUIFormWizardPro, FormWizardProps, WizardStep } from "../form-wizard"
|
|
8
8
|
import { Alert, AlertDescription } from "../ui/alert"
|
|
9
9
|
import { AlertCircle } from "lucide-react"
|
|
10
10
|
|
|
@@ -92,7 +92,7 @@ function createStepSchema(fields: MultiStepFormField[]): z.ZodSchema<any> {
|
|
|
92
92
|
return z.object(shape)
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export function
|
|
95
|
+
export function MoonUIMultiStepFormPro<TFormData extends FieldValues = FieldValues>({
|
|
96
96
|
steps,
|
|
97
97
|
onSubmit,
|
|
98
98
|
defaultValues,
|
|
@@ -209,7 +209,7 @@ export function MultiStepFormPro<TFormData extends FieldValues = FieldValues>({
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
return (
|
|
212
|
-
<
|
|
212
|
+
<MoonUIFormWizardPro
|
|
213
213
|
{...wizardProps}
|
|
214
214
|
steps={wizardSteps}
|
|
215
215
|
onComplete={handleComplete}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useEffect } from "react"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
import { Input } from "../ui/input"
|
|
6
|
+
import { Label } from "../ui/label"
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
|
8
|
+
import { Phone, Globe, Check, X } from "lucide-react"
|
|
9
|
+
import { motion, AnimatePresence } from "framer-motion"
|
|
10
|
+
|
|
11
|
+
// Ülke kodları ve bayrakları
|
|
12
|
+
const countries = [
|
|
13
|
+
{ code: 'US', name: 'United States', dialCode: '+1', flag: '🇺🇸', format: '(xxx) xxx-xxxx' },
|
|
14
|
+
{ code: 'GB', name: 'United Kingdom', dialCode: '+44', flag: '🇬🇧', format: 'xxxx xxxxxx' },
|
|
15
|
+
{ code: 'TR', name: 'Turkey', dialCode: '+90', flag: '🇹🇷', format: '(xxx) xxx xx xx' },
|
|
16
|
+
{ code: 'DE', name: 'Germany', dialCode: '+49', flag: '🇩🇪', format: 'xxx xxxxxxxx' },
|
|
17
|
+
{ code: 'FR', name: 'France', dialCode: '+33', flag: '🇫🇷', format: 'x xx xx xx xx' },
|
|
18
|
+
{ code: 'IT', name: 'Italy', dialCode: '+39', flag: '🇮🇹', format: 'xxx xxxxxxx' },
|
|
19
|
+
{ code: 'ES', name: 'Spain', dialCode: '+34', flag: '🇪🇸', format: 'xxx xxx xxx' },
|
|
20
|
+
{ code: 'CN', name: 'China', dialCode: '+86', flag: '🇨🇳', format: 'xxx xxxx xxxx' },
|
|
21
|
+
{ code: 'JP', name: 'Japan', dialCode: '+81', flag: '🇯🇵', format: 'xx xxxx xxxx' },
|
|
22
|
+
{ code: 'KR', name: 'South Korea', dialCode: '+82', flag: '🇰🇷', format: 'xx xxxx xxxx' },
|
|
23
|
+
{ code: 'IN', name: 'India', dialCode: '+91', flag: '🇮🇳', format: 'xxxxx xxxxx' },
|
|
24
|
+
{ code: 'BR', name: 'Brazil', dialCode: '+55', flag: '🇧🇷', format: '(xx) xxxxx-xxxx' },
|
|
25
|
+
{ code: 'MX', name: 'Mexico', dialCode: '+52', flag: '🇲🇽', format: 'xxx xxx xxxx' },
|
|
26
|
+
{ code: 'CA', name: 'Canada', dialCode: '+1', flag: '🇨🇦', format: '(xxx) xxx-xxxx' },
|
|
27
|
+
{ code: 'AU', name: 'Australia', dialCode: '+61', flag: '🇦🇺', format: 'xxx xxx xxx' },
|
|
28
|
+
{ code: 'RU', name: 'Russia', dialCode: '+7', flag: '🇷🇺', format: '(xxx) xxx-xx-xx' },
|
|
29
|
+
{ code: 'NL', name: 'Netherlands', dialCode: '+31', flag: '🇳🇱', format: 'x xxxxxxxx' },
|
|
30
|
+
{ code: 'SE', name: 'Sweden', dialCode: '+46', flag: '🇸🇪', format: 'xx xxx xx xx' },
|
|
31
|
+
{ code: 'NO', name: 'Norway', dialCode: '+47', flag: '🇳🇴', format: 'xxx xx xxx' },
|
|
32
|
+
{ code: 'FI', name: 'Finland', dialCode: '+358', flag: '🇫🇮', format: 'xx xxxxxxx' }
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
export interface PhoneNumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
|
36
|
+
value?: {
|
|
37
|
+
country: string
|
|
38
|
+
number: string
|
|
39
|
+
}
|
|
40
|
+
onChange?: (value: { country: string; number: string; fullNumber: string; isValid: boolean }) => void
|
|
41
|
+
defaultCountry?: string
|
|
42
|
+
countries?: typeof countries
|
|
43
|
+
showFlags?: boolean
|
|
44
|
+
showDialCode?: boolean
|
|
45
|
+
autoFormat?: boolean
|
|
46
|
+
validateOnChange?: boolean
|
|
47
|
+
showValidationIcon?: boolean
|
|
48
|
+
label?: string
|
|
49
|
+
error?: string
|
|
50
|
+
helperText?: string
|
|
51
|
+
required?: boolean
|
|
52
|
+
className?: string
|
|
53
|
+
inputClassName?: string
|
|
54
|
+
selectClassName?: string
|
|
55
|
+
labelClassName?: string
|
|
56
|
+
errorClassName?: string
|
|
57
|
+
allowInternationalFormat?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Telefon numarası formatlama
|
|
61
|
+
function formatPhoneNumber(number: string, format: string): string {
|
|
62
|
+
const cleaned = number.replace(/\D/g, '')
|
|
63
|
+
let formatted = ''
|
|
64
|
+
let digitIndex = 0
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < format.length && digitIndex < cleaned.length; i++) {
|
|
67
|
+
if (format[i] === 'x') {
|
|
68
|
+
formatted += cleaned[digitIndex]
|
|
69
|
+
digitIndex++
|
|
70
|
+
} else {
|
|
71
|
+
formatted += format[i]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return formatted
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Telefon numarası doğrulama
|
|
79
|
+
function validatePhoneNumber(number: string, countryCode: string): boolean {
|
|
80
|
+
const cleaned = number.replace(/\D/g, '')
|
|
81
|
+
const country = countries.find(c => c.code === countryCode)
|
|
82
|
+
|
|
83
|
+
if (!country) return false
|
|
84
|
+
|
|
85
|
+
// Format'taki x sayısını say
|
|
86
|
+
const expectedLength = country.format.split('').filter(c => c === 'x').length
|
|
87
|
+
|
|
88
|
+
return cleaned.length === expectedLength
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Uluslararası format dönüştürme
|
|
92
|
+
function toInternationalFormat(countryCode: string, phoneNumber: string): string {
|
|
93
|
+
const country = countries.find(c => c.code === countryCode)
|
|
94
|
+
if (!country) return phoneNumber
|
|
95
|
+
|
|
96
|
+
const cleaned = phoneNumber.replace(/\D/g, '')
|
|
97
|
+
return `${country.dialCode}${cleaned}`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const MoonUIPhoneNumberInputPro = React.forwardRef<HTMLDivElement, PhoneNumberInputProps>(({
|
|
101
|
+
value = { country: 'US', number: '' },
|
|
102
|
+
onChange,
|
|
103
|
+
defaultCountry = 'US',
|
|
104
|
+
countries: customCountries,
|
|
105
|
+
showFlags = true,
|
|
106
|
+
showDialCode = true,
|
|
107
|
+
autoFormat = true,
|
|
108
|
+
validateOnChange = true,
|
|
109
|
+
showValidationIcon = true,
|
|
110
|
+
label,
|
|
111
|
+
error,
|
|
112
|
+
helperText,
|
|
113
|
+
required,
|
|
114
|
+
className,
|
|
115
|
+
inputClassName,
|
|
116
|
+
selectClassName,
|
|
117
|
+
labelClassName,
|
|
118
|
+
errorClassName,
|
|
119
|
+
disabled,
|
|
120
|
+
allowInternationalFormat = true,
|
|
121
|
+
...props
|
|
122
|
+
}, ref) => {
|
|
123
|
+
const [isValid, setIsValid] = useState(false)
|
|
124
|
+
const [isFocused, setIsFocused] = useState(false)
|
|
125
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
126
|
+
|
|
127
|
+
const countryList = customCountries || countries
|
|
128
|
+
const selectedCountry = countryList.find(c => c.code === value.country) || countryList[0]
|
|
129
|
+
|
|
130
|
+
// Doğrulama
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (validateOnChange && value.number) {
|
|
133
|
+
const valid = validatePhoneNumber(value.number, value.country)
|
|
134
|
+
setIsValid(valid)
|
|
135
|
+
}
|
|
136
|
+
}, [value, validateOnChange])
|
|
137
|
+
|
|
138
|
+
const handleCountryChange = (countryCode: string) => {
|
|
139
|
+
const fullNumber = allowInternationalFormat
|
|
140
|
+
? toInternationalFormat(countryCode, value.number)
|
|
141
|
+
: value.number
|
|
142
|
+
|
|
143
|
+
onChange?.({
|
|
144
|
+
country: countryCode,
|
|
145
|
+
number: value.number,
|
|
146
|
+
fullNumber,
|
|
147
|
+
isValid: validatePhoneNumber(value.number, countryCode)
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
152
|
+
let newValue = e.target.value
|
|
153
|
+
|
|
154
|
+
if (autoFormat) {
|
|
155
|
+
// Sadece sayıları al
|
|
156
|
+
const cleaned = newValue.replace(/\D/g, '')
|
|
157
|
+
// Format uygula
|
|
158
|
+
newValue = formatPhoneNumber(cleaned, selectedCountry.format)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const valid = validatePhoneNumber(newValue, value.country)
|
|
162
|
+
const fullNumber = allowInternationalFormat
|
|
163
|
+
? toInternationalFormat(value.country, newValue)
|
|
164
|
+
: newValue
|
|
165
|
+
|
|
166
|
+
onChange?.({
|
|
167
|
+
country: value.country,
|
|
168
|
+
number: newValue,
|
|
169
|
+
fullNumber,
|
|
170
|
+
isValid: valid
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
175
|
+
if (!autoFormat) return
|
|
176
|
+
|
|
177
|
+
e.preventDefault()
|
|
178
|
+
const pastedText = e.clipboardData.getData('text')
|
|
179
|
+
const cleaned = pastedText.replace(/\D/g, '')
|
|
180
|
+
const formatted = formatPhoneNumber(cleaned, selectedCountry.format)
|
|
181
|
+
|
|
182
|
+
const valid = validatePhoneNumber(formatted, value.country)
|
|
183
|
+
const fullNumber = allowInternationalFormat
|
|
184
|
+
? toInternationalFormat(value.country, formatted)
|
|
185
|
+
: formatted
|
|
186
|
+
|
|
187
|
+
onChange?.({
|
|
188
|
+
country: value.country,
|
|
189
|
+
number: formatted,
|
|
190
|
+
fullNumber,
|
|
191
|
+
isValid: valid
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div ref={ref} className={cn("space-y-2", className)} {...props}>
|
|
197
|
+
{label && (
|
|
198
|
+
<Label htmlFor="phone-number" className={labelClassName}>
|
|
199
|
+
{label}
|
|
200
|
+
{required && <span className="text-destructive ml-1">*</span>}
|
|
201
|
+
</Label>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
<div className="flex gap-2">
|
|
205
|
+
{/* Ülke Seçici */}
|
|
206
|
+
<Select
|
|
207
|
+
value={value.country}
|
|
208
|
+
onValueChange={handleCountryChange}
|
|
209
|
+
disabled={disabled}
|
|
210
|
+
>
|
|
211
|
+
<SelectTrigger
|
|
212
|
+
className={cn(
|
|
213
|
+
"w-[140px]",
|
|
214
|
+
error && "border-destructive",
|
|
215
|
+
selectClassName
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
<SelectValue>
|
|
219
|
+
<div className="flex items-center gap-2">
|
|
220
|
+
{showFlags && <span className="text-lg">{selectedCountry.flag}</span>}
|
|
221
|
+
{showDialCode && <span className="text-sm">{selectedCountry.dialCode}</span>}
|
|
222
|
+
{!showFlags && !showDialCode && <span className="text-sm">{selectedCountry.code}</span>}
|
|
223
|
+
</div>
|
|
224
|
+
</SelectValue>
|
|
225
|
+
</SelectTrigger>
|
|
226
|
+
<SelectContent>
|
|
227
|
+
{countryList.map((country) => (
|
|
228
|
+
<SelectItem key={country.code} value={country.code}>
|
|
229
|
+
<div className="flex items-center gap-2">
|
|
230
|
+
{showFlags && <span className="text-lg">{country.flag}</span>}
|
|
231
|
+
<span className="text-sm">{country.name}</span>
|
|
232
|
+
{showDialCode && <span className="text-muted-foreground text-sm">{country.dialCode}</span>}
|
|
233
|
+
</div>
|
|
234
|
+
</SelectItem>
|
|
235
|
+
))}
|
|
236
|
+
</SelectContent>
|
|
237
|
+
</Select>
|
|
238
|
+
|
|
239
|
+
{/* Telefon Numarası Input */}
|
|
240
|
+
<div className="relative flex-1">
|
|
241
|
+
<Input
|
|
242
|
+
ref={inputRef}
|
|
243
|
+
id="phone-number"
|
|
244
|
+
type="tel"
|
|
245
|
+
value={value.number}
|
|
246
|
+
onChange={handleNumberChange}
|
|
247
|
+
onPaste={handlePaste}
|
|
248
|
+
onFocus={() => setIsFocused(true)}
|
|
249
|
+
onBlur={() => setIsFocused(false)}
|
|
250
|
+
placeholder={selectedCountry.format.replace(/x/g, '•')}
|
|
251
|
+
className={cn(
|
|
252
|
+
"pr-10",
|
|
253
|
+
error && "border-destructive",
|
|
254
|
+
inputClassName
|
|
255
|
+
)}
|
|
256
|
+
disabled={disabled}
|
|
257
|
+
/>
|
|
258
|
+
|
|
259
|
+
{/* Doğrulama İkonu */}
|
|
260
|
+
{showValidationIcon && value.number && (
|
|
261
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
262
|
+
<AnimatePresence mode="wait">
|
|
263
|
+
{isValid ? (
|
|
264
|
+
<motion.div
|
|
265
|
+
key="valid"
|
|
266
|
+
initial={{ scale: 0, opacity: 0 }}
|
|
267
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
268
|
+
exit={{ scale: 0, opacity: 0 }}
|
|
269
|
+
transition={{ duration: 0.2 }}
|
|
270
|
+
>
|
|
271
|
+
<Check className="w-4 h-4 text-green-500" />
|
|
272
|
+
</motion.div>
|
|
273
|
+
) : (
|
|
274
|
+
<motion.div
|
|
275
|
+
key="invalid"
|
|
276
|
+
initial={{ scale: 0, opacity: 0 }}
|
|
277
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
278
|
+
exit={{ scale: 0, opacity: 0 }}
|
|
279
|
+
transition={{ duration: 0.2 }}
|
|
280
|
+
>
|
|
281
|
+
<X className="w-4 h-4 text-destructive" />
|
|
282
|
+
</motion.div>
|
|
283
|
+
)}
|
|
284
|
+
</AnimatePresence>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Helper Text veya Error */}
|
|
291
|
+
{(error || helperText) && (
|
|
292
|
+
<AnimatePresence mode="wait">
|
|
293
|
+
{error ? (
|
|
294
|
+
<motion.p
|
|
295
|
+
key="error"
|
|
296
|
+
initial={{ opacity: 0, height: 0 }}
|
|
297
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
298
|
+
exit={{ opacity: 0, height: 0 }}
|
|
299
|
+
className={cn("text-sm text-destructive", errorClassName)}
|
|
300
|
+
>
|
|
301
|
+
{error}
|
|
302
|
+
</motion.p>
|
|
303
|
+
) : (
|
|
304
|
+
<motion.p
|
|
305
|
+
key="helper"
|
|
306
|
+
initial={{ opacity: 0, height: 0 }}
|
|
307
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
308
|
+
exit={{ opacity: 0, height: 0 }}
|
|
309
|
+
className="text-sm text-muted-foreground"
|
|
310
|
+
>
|
|
311
|
+
{helperText}
|
|
312
|
+
</motion.p>
|
|
313
|
+
)}
|
|
314
|
+
</AnimatePresence>
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{/* Uluslararası Format Gösterimi */}
|
|
318
|
+
{allowInternationalFormat && value.number && isValid && (
|
|
319
|
+
<motion.div
|
|
320
|
+
initial={{ opacity: 0, y: -10 }}
|
|
321
|
+
animate={{ opacity: 1, y: 0 }}
|
|
322
|
+
className="flex items-center gap-2 text-sm text-muted-foreground"
|
|
323
|
+
>
|
|
324
|
+
<Globe className="w-4 h-4" />
|
|
325
|
+
<span>International: {toInternationalFormat(value.country, value.number)}</span>
|
|
326
|
+
</motion.div>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
MoonUIPhoneNumberInputPro.displayName = "MoonUIPhoneNumberInputPro"
|
|
333
|
+
|
|
334
|
+
// Ülke listesini de export edelim
|
|
335
|
+
export { countries as phoneCountries }
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback } from "react"
|
|
4
4
|
import { motion, AnimatePresence } from "framer-motion"
|
|
5
|
-
import {
|
|
5
|
+
import { MoonUIFormWizardPro, WizardStep } from "../form-wizard"
|
|
6
6
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
|
7
7
|
import { Button } from "../ui/button"
|
|
8
8
|
import { RadioGroup, RadioGroupItem } from "../ui/radio-group"
|
|
@@ -76,7 +76,7 @@ function shuffleArray<T>(array: T[]): T[] {
|
|
|
76
76
|
return shuffled
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
export const
|
|
79
|
+
export const MoonUIQuizFormPro: React.FC<QuizFormProps> = ({
|
|
80
80
|
title,
|
|
81
81
|
description,
|
|
82
82
|
questions: initialQuestions,
|
|
@@ -462,7 +462,7 @@ export const QuizFormPro: React.FC<QuizFormProps> = ({
|
|
|
462
462
|
</div>
|
|
463
463
|
)}
|
|
464
464
|
|
|
465
|
-
<
|
|
465
|
+
<MoonUIFormWizardPro
|
|
466
466
|
steps={wizardSteps}
|
|
467
467
|
onComplete={handleQuizComplete}
|
|
468
468
|
allowStepSkip={allowSkip}
|