@moontra/moonui-pro 2.6.1 → 2.7.0
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 +304 -20
- package/dist/index.mjs +15467 -2135
- package/package.json +1 -1
- package/src/components/credit-card-input/index.tsx +406 -0
- package/src/components/form-wizard/form-wizard-context.tsx +248 -0
- package/src/components/form-wizard/form-wizard-navigation.tsx +118 -0
- package/src/components/form-wizard/form-wizard-progress.tsx +193 -0
- package/src/components/form-wizard/form-wizard-step.tsx +100 -0
- package/src/components/form-wizard/index.tsx +105 -0
- package/src/components/form-wizard/types.ts +76 -0
- package/src/components/index.ts +16 -1
- package/src/components/multi-step-form/index.tsx +223 -0
- package/src/components/phone-number-input/index.tsx +335 -0
- package/src/components/quiz-form/index.tsx +479 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
import { useForm, FormProvider, UseFormReturn, FieldValues, DefaultValues } from "react-hook-form"
|
|
5
|
+
import { zodResolver } from "@hookform/resolvers/zod"
|
|
6
|
+
import { z } from "zod"
|
|
7
|
+
import { MoonUIFormWizardPro, FormWizardProps, WizardStep } from "../form-wizard"
|
|
8
|
+
import { Alert, AlertDescription } from "../ui/alert"
|
|
9
|
+
import { AlertCircle } from "lucide-react"
|
|
10
|
+
|
|
11
|
+
export interface MultiStepFormField {
|
|
12
|
+
name: string
|
|
13
|
+
label: string
|
|
14
|
+
type: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'textarea' | 'select' | 'checkbox' | 'radio' | 'switch' | 'file' | 'date' | 'time' | 'datetime'
|
|
15
|
+
placeholder?: string
|
|
16
|
+
description?: string
|
|
17
|
+
required?: boolean
|
|
18
|
+
validation?: z.ZodSchema<any>
|
|
19
|
+
options?: Array<{ value: string; label: string }>
|
|
20
|
+
defaultValue?: any
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
autoComplete?: string
|
|
23
|
+
min?: number | string
|
|
24
|
+
max?: number | string
|
|
25
|
+
step?: number | string
|
|
26
|
+
pattern?: string
|
|
27
|
+
multiple?: boolean
|
|
28
|
+
accept?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MultiStepFormStep extends Omit<WizardStep, 'content' | 'validation'> {
|
|
32
|
+
fields: MultiStepFormField[]
|
|
33
|
+
schema?: z.ZodSchema<any>
|
|
34
|
+
submitLabel?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MultiStepFormProps<TFormData extends FieldValues = FieldValues> extends Omit<FormWizardProps, 'steps'> {
|
|
38
|
+
steps: MultiStepFormStep[]
|
|
39
|
+
onSubmit: (data: TFormData) => void | Promise<void>
|
|
40
|
+
defaultValues?: DefaultValues<TFormData>
|
|
41
|
+
showErrorSummary?: boolean
|
|
42
|
+
fieldClassName?: string
|
|
43
|
+
labelClassName?: string
|
|
44
|
+
errorClassName?: string
|
|
45
|
+
descriptionClassName?: string
|
|
46
|
+
renderField?: (field: MultiStepFormField, form: UseFormReturn<TFormData>) => React.ReactNode
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createStepSchema(fields: MultiStepFormField[]): z.ZodSchema<any> {
|
|
50
|
+
const shape: Record<string, z.ZodSchema<any>> = {}
|
|
51
|
+
|
|
52
|
+
fields.forEach(field => {
|
|
53
|
+
if (field.validation) {
|
|
54
|
+
shape[field.name] = field.validation
|
|
55
|
+
} else {
|
|
56
|
+
let schema: z.ZodSchema<any> = z.any()
|
|
57
|
+
|
|
58
|
+
switch (field.type) {
|
|
59
|
+
case 'email':
|
|
60
|
+
schema = z.string().email('Invalid email address')
|
|
61
|
+
break
|
|
62
|
+
case 'number':
|
|
63
|
+
schema = z.number()
|
|
64
|
+
if (field.min !== undefined) schema = (schema as z.ZodNumber).min(Number(field.min))
|
|
65
|
+
if (field.max !== undefined) schema = (schema as z.ZodNumber).max(Number(field.max))
|
|
66
|
+
break
|
|
67
|
+
case 'url':
|
|
68
|
+
schema = z.string().url('Invalid URL')
|
|
69
|
+
break
|
|
70
|
+
case 'tel':
|
|
71
|
+
schema = z.string().regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number')
|
|
72
|
+
break
|
|
73
|
+
case 'checkbox':
|
|
74
|
+
case 'switch':
|
|
75
|
+
schema = z.boolean()
|
|
76
|
+
break
|
|
77
|
+
default:
|
|
78
|
+
schema = z.string()
|
|
79
|
+
if (field.min !== undefined) schema = (schema as z.ZodString).min(Number(field.min))
|
|
80
|
+
if (field.max !== undefined) schema = (schema as z.ZodString).max(Number(field.max))
|
|
81
|
+
if (field.pattern) schema = (schema as z.ZodString).regex(new RegExp(field.pattern))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!field.required) {
|
|
85
|
+
schema = schema.optional()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
shape[field.name] = schema
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return z.object(shape)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function MoonUIMultiStepFormPro<TFormData extends FieldValues = FieldValues>({
|
|
96
|
+
steps,
|
|
97
|
+
onSubmit,
|
|
98
|
+
defaultValues,
|
|
99
|
+
showErrorSummary = true,
|
|
100
|
+
fieldClassName,
|
|
101
|
+
labelClassName,
|
|
102
|
+
errorClassName,
|
|
103
|
+
descriptionClassName,
|
|
104
|
+
renderField,
|
|
105
|
+
...wizardProps
|
|
106
|
+
}: MultiStepFormProps<TFormData>) {
|
|
107
|
+
// Create combined schema from all steps
|
|
108
|
+
const combinedSchema = React.useMemo(() => {
|
|
109
|
+
const shape: Record<string, z.ZodSchema<any>> = {}
|
|
110
|
+
|
|
111
|
+
steps.forEach(step => {
|
|
112
|
+
const stepSchema = step.schema || createStepSchema(step.fields)
|
|
113
|
+
if (stepSchema instanceof z.ZodObject) {
|
|
114
|
+
Object.assign(shape, stepSchema.shape)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return z.object(shape)
|
|
119
|
+
}, [steps])
|
|
120
|
+
|
|
121
|
+
const form = useForm<TFormData>({
|
|
122
|
+
resolver: zodResolver(combinedSchema) as any,
|
|
123
|
+
defaultValues,
|
|
124
|
+
mode: 'onChange'
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const wizardSteps: WizardStep[] = steps.map((step, stepIndex) => ({
|
|
128
|
+
...step,
|
|
129
|
+
validation: async () => {
|
|
130
|
+
// Validate only the fields in the current step
|
|
131
|
+
const currentStepFields = step.fields.map(f => f.name)
|
|
132
|
+
const result = await form.trigger(currentStepFields as any)
|
|
133
|
+
return result
|
|
134
|
+
},
|
|
135
|
+
content: ({ updateStepData }) => {
|
|
136
|
+
const errors = form.formState.errors
|
|
137
|
+
const stepErrors = step.fields
|
|
138
|
+
.map(field => errors[field.name])
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<FormProvider {...form}>
|
|
143
|
+
<form className="space-y-6">
|
|
144
|
+
{showErrorSummary && stepErrors.length > 0 && (
|
|
145
|
+
<Alert variant="error">
|
|
146
|
+
<AlertCircle className="h-4 w-4" />
|
|
147
|
+
<AlertDescription>
|
|
148
|
+
Please fix the errors below before proceeding.
|
|
149
|
+
</AlertDescription>
|
|
150
|
+
</Alert>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{step.fields.map(field => {
|
|
154
|
+
if (renderField) {
|
|
155
|
+
return <div key={field.name}>{renderField(field, form)}</div>
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Default field rendering
|
|
159
|
+
return (
|
|
160
|
+
<div key={field.name} className={fieldClassName}>
|
|
161
|
+
<label
|
|
162
|
+
htmlFor={field.name}
|
|
163
|
+
className={cn(
|
|
164
|
+
"block text-sm font-medium",
|
|
165
|
+
labelClassName
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
{field.label}
|
|
169
|
+
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
170
|
+
</label>
|
|
171
|
+
|
|
172
|
+
{field.description && (
|
|
173
|
+
<p className={cn("text-sm text-muted-foreground mt-1", descriptionClassName)}>
|
|
174
|
+
{field.description}
|
|
175
|
+
</p>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
<div className="mt-2">
|
|
179
|
+
{/* Field rendering would go here - simplified for brevity */}
|
|
180
|
+
<input
|
|
181
|
+
{...form.register(field.name as any)}
|
|
182
|
+
id={field.name}
|
|
183
|
+
type={field.type}
|
|
184
|
+
placeholder={field.placeholder}
|
|
185
|
+
disabled={field.disabled}
|
|
186
|
+
className="w-full px-3 py-2 border rounded-md"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{errors[field.name] && (
|
|
191
|
+
<p className={cn("text-sm text-destructive mt-1", errorClassName)}>
|
|
192
|
+
{errors[field.name]?.message as string}
|
|
193
|
+
</p>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
})}
|
|
198
|
+
</form>
|
|
199
|
+
</FormProvider>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
}))
|
|
203
|
+
|
|
204
|
+
const handleComplete = async (stepData: Record<string, any>) => {
|
|
205
|
+
const isValid = await form.trigger()
|
|
206
|
+
if (isValid) {
|
|
207
|
+
await onSubmit(form.getValues())
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<MoonUIFormWizardPro
|
|
213
|
+
{...wizardProps}
|
|
214
|
+
steps={wizardSteps}
|
|
215
|
+
onComplete={handleComplete}
|
|
216
|
+
/>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Utility function for better imports
|
|
221
|
+
function cn(...classes: (string | undefined | false)[]) {
|
|
222
|
+
return classes.filter(Boolean).join(' ')
|
|
223
|
+
}
|
|
@@ -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 }
|