@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.
@@ -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 { FormWizardPro, FormWizardProps, WizardStep } from "../form-wizard"
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 MultiStepFormPro<TFormData extends FieldValues = FieldValues>({
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
- <FormWizardPro
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 { FormWizardPro, WizardStep } from "../form-wizard"
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 QuizFormPro: React.FC<QuizFormProps> = ({
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
- <FormWizardPro
465
+ <MoonUIFormWizardPro
466
466
  steps={wizardSteps}
467
467
  onComplete={handleQuizComplete}
468
468
  allowStepSkip={allowSkip}