@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,479 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useCallback } from "react"
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion"
|
|
5
|
+
import { MoonUIFormWizardPro, WizardStep } from "../form-wizard"
|
|
6
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
|
7
|
+
import { Button } from "../ui/button"
|
|
8
|
+
import { RadioGroup, RadioGroupItem } from "../ui/radio-group"
|
|
9
|
+
import { Checkbox } from "../ui/checkbox"
|
|
10
|
+
import { Label } from "../ui/label"
|
|
11
|
+
import { Progress } from "../ui/progress"
|
|
12
|
+
import { Slider } from "../ui/slider"
|
|
13
|
+
import { Textarea } from "../ui/textarea"
|
|
14
|
+
import { Badge } from "../ui/badge"
|
|
15
|
+
import { cn } from "../../lib/utils"
|
|
16
|
+
import { Clock, Trophy, Target, ChevronRight, RotateCcw, Download, Share2 } from "lucide-react"
|
|
17
|
+
|
|
18
|
+
export type QuestionType = 'single-choice' | 'multiple-choice' | 'text' | 'rating' | 'true-false' | 'matching'
|
|
19
|
+
|
|
20
|
+
export interface QuizAnswer {
|
|
21
|
+
value: string | string[] | number | boolean
|
|
22
|
+
label?: string
|
|
23
|
+
isCorrect?: boolean
|
|
24
|
+
points?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface QuizQuestion {
|
|
28
|
+
id: string
|
|
29
|
+
type: QuestionType
|
|
30
|
+
question: string
|
|
31
|
+
description?: string
|
|
32
|
+
image?: string
|
|
33
|
+
options?: QuizAnswer[]
|
|
34
|
+
correctAnswer?: string | string[] | number | boolean
|
|
35
|
+
points?: number
|
|
36
|
+
timeLimit?: number // in seconds
|
|
37
|
+
required?: boolean
|
|
38
|
+
explanation?: string
|
|
39
|
+
hint?: string
|
|
40
|
+
tags?: string[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface QuizResult {
|
|
44
|
+
questionId: string
|
|
45
|
+
answer: any
|
|
46
|
+
isCorrect?: boolean
|
|
47
|
+
points: number
|
|
48
|
+
timeSpent: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface QuizFormProps {
|
|
52
|
+
title: string
|
|
53
|
+
description?: string
|
|
54
|
+
questions: QuizQuestion[]
|
|
55
|
+
onComplete?: (results: QuizResult[], score: number, totalScore: number) => void | Promise<void>
|
|
56
|
+
showTimer?: boolean
|
|
57
|
+
totalTimeLimit?: number // in seconds
|
|
58
|
+
showProgress?: boolean
|
|
59
|
+
showQuestionNumbers?: boolean
|
|
60
|
+
allowReview?: boolean
|
|
61
|
+
allowSkip?: boolean
|
|
62
|
+
shuffleQuestions?: boolean
|
|
63
|
+
shuffleOptions?: boolean
|
|
64
|
+
passingScore?: number // percentage
|
|
65
|
+
instantFeedback?: boolean
|
|
66
|
+
showHints?: boolean
|
|
67
|
+
className?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function shuffleArray<T>(array: T[]): T[] {
|
|
71
|
+
const shuffled = [...array]
|
|
72
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
73
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
74
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
|
|
75
|
+
}
|
|
76
|
+
return shuffled
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const MoonUIQuizFormPro: React.FC<QuizFormProps> = ({
|
|
80
|
+
title,
|
|
81
|
+
description,
|
|
82
|
+
questions: initialQuestions,
|
|
83
|
+
onComplete,
|
|
84
|
+
showTimer = true,
|
|
85
|
+
totalTimeLimit,
|
|
86
|
+
showProgress = true,
|
|
87
|
+
showQuestionNumbers = true,
|
|
88
|
+
allowReview = true,
|
|
89
|
+
allowSkip = true,
|
|
90
|
+
shuffleQuestions = false,
|
|
91
|
+
shuffleOptions = false,
|
|
92
|
+
passingScore = 70,
|
|
93
|
+
instantFeedback = false,
|
|
94
|
+
showHints = false,
|
|
95
|
+
className
|
|
96
|
+
}) => {
|
|
97
|
+
const [questions] = useState(() =>
|
|
98
|
+
shuffleQuestions ? shuffleArray(initialQuestions) : initialQuestions
|
|
99
|
+
)
|
|
100
|
+
const [answers, setAnswers] = useState<Record<string, any>>({})
|
|
101
|
+
const [results, setResults] = useState<QuizResult[]>([])
|
|
102
|
+
const [timeRemaining, setTimeRemaining] = useState(totalTimeLimit || 0)
|
|
103
|
+
const [questionStartTime, setQuestionStartTime] = useState<Record<string, number>>({})
|
|
104
|
+
const [showResults, setShowResults] = useState(false)
|
|
105
|
+
const [currentQuestionTime, setCurrentQuestionTime] = useState(0)
|
|
106
|
+
|
|
107
|
+
// Timer effect
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!showTimer || !totalTimeLimit || showResults) return
|
|
110
|
+
|
|
111
|
+
const interval = setInterval(() => {
|
|
112
|
+
setTimeRemaining(prev => {
|
|
113
|
+
if (prev <= 1) {
|
|
114
|
+
handleQuizComplete()
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
return prev - 1
|
|
118
|
+
})
|
|
119
|
+
}, 1000)
|
|
120
|
+
|
|
121
|
+
return () => clearInterval(interval)
|
|
122
|
+
}, [showTimer, totalTimeLimit, showResults])
|
|
123
|
+
|
|
124
|
+
// Current question timer
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!showTimer || showResults) return
|
|
127
|
+
|
|
128
|
+
const interval = setInterval(() => {
|
|
129
|
+
setCurrentQuestionTime(prev => prev + 1)
|
|
130
|
+
}, 1000)
|
|
131
|
+
|
|
132
|
+
return () => clearInterval(interval)
|
|
133
|
+
}, [showTimer, showResults])
|
|
134
|
+
|
|
135
|
+
const calculateScore = useCallback(() => {
|
|
136
|
+
let score = 0
|
|
137
|
+
let totalScore = 0
|
|
138
|
+
|
|
139
|
+
const quizResults: QuizResult[] = questions.map(question => {
|
|
140
|
+
const answer = answers[question.id]
|
|
141
|
+
const points = question.points || 1
|
|
142
|
+
totalScore += points
|
|
143
|
+
|
|
144
|
+
let isCorrect = false
|
|
145
|
+
if (answer !== undefined && answer !== null && answer !== '') {
|
|
146
|
+
if (question.type === 'single-choice' || question.type === 'true-false') {
|
|
147
|
+
isCorrect = answer === question.correctAnswer
|
|
148
|
+
} else if (question.type === 'multiple-choice') {
|
|
149
|
+
const userAnswers = Array.isArray(answer) ? answer : []
|
|
150
|
+
const correctAnswers = Array.isArray(question.correctAnswer) ? question.correctAnswer : []
|
|
151
|
+
isCorrect = userAnswers.length === correctAnswers.length &&
|
|
152
|
+
userAnswers.every(a => correctAnswers.includes(a))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const earnedPoints = isCorrect ? points : 0
|
|
157
|
+
score += earnedPoints
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
questionId: question.id,
|
|
161
|
+
answer,
|
|
162
|
+
isCorrect,
|
|
163
|
+
points: earnedPoints,
|
|
164
|
+
timeSpent: questionStartTime[question.id] || 0
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return { results: quizResults, score, totalScore }
|
|
169
|
+
}, [questions, answers, questionStartTime])
|
|
170
|
+
|
|
171
|
+
const handleQuizComplete = async () => {
|
|
172
|
+
const { results, score, totalScore } = calculateScore()
|
|
173
|
+
setResults(results)
|
|
174
|
+
setShowResults(true)
|
|
175
|
+
|
|
176
|
+
if (onComplete) {
|
|
177
|
+
await onComplete(results, score, totalScore)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const formatTime = (seconds: number) => {
|
|
182
|
+
const mins = Math.floor(seconds / 60)
|
|
183
|
+
const secs = seconds % 60
|
|
184
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const wizardSteps: WizardStep[] = questions.map((question, index) => ({
|
|
188
|
+
id: question.id,
|
|
189
|
+
title: `Question ${index + 1}`,
|
|
190
|
+
description: question.tags?.join(', '),
|
|
191
|
+
content: ({ updateStepData, goToNext }) => {
|
|
192
|
+
const currentAnswer = answers[question.id]
|
|
193
|
+
const questionOptions = shuffleOptions && question.options
|
|
194
|
+
? shuffleArray(question.options)
|
|
195
|
+
: question.options
|
|
196
|
+
|
|
197
|
+
// Track time for this question
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (!questionStartTime[question.id]) {
|
|
200
|
+
setQuestionStartTime(prev => ({
|
|
201
|
+
...prev,
|
|
202
|
+
[question.id]: currentQuestionTime
|
|
203
|
+
}))
|
|
204
|
+
}
|
|
205
|
+
}, [])
|
|
206
|
+
|
|
207
|
+
const handleAnswerChange = (value: any) => {
|
|
208
|
+
setAnswers(prev => ({ ...prev, [question.id]: value }))
|
|
209
|
+
if (instantFeedback && question.type !== 'text') {
|
|
210
|
+
// Show feedback immediately
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<Card className="border-0 shadow-none">
|
|
216
|
+
<CardHeader>
|
|
217
|
+
<div className="flex items-center justify-between mb-2">
|
|
218
|
+
{showQuestionNumbers && (
|
|
219
|
+
<Badge variant="secondary">
|
|
220
|
+
Question {index + 1} of {questions.length}
|
|
221
|
+
</Badge>
|
|
222
|
+
)}
|
|
223
|
+
{question.points && question.points > 1 && (
|
|
224
|
+
<Badge variant="outline">
|
|
225
|
+
{question.points} points
|
|
226
|
+
</Badge>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
<CardTitle className="text-xl">{question.question}</CardTitle>
|
|
230
|
+
{question.description && (
|
|
231
|
+
<CardDescription>{question.description}</CardDescription>
|
|
232
|
+
)}
|
|
233
|
+
</CardHeader>
|
|
234
|
+
<CardContent className="space-y-4">
|
|
235
|
+
{question.image && (
|
|
236
|
+
<img
|
|
237
|
+
src={question.image}
|
|
238
|
+
alt="Question illustration"
|
|
239
|
+
className="w-full rounded-lg"
|
|
240
|
+
/>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{question.type === 'single-choice' && questionOptions && (
|
|
244
|
+
<RadioGroup
|
|
245
|
+
value={currentAnswer || ''}
|
|
246
|
+
onValueChange={handleAnswerChange}
|
|
247
|
+
>
|
|
248
|
+
{questionOptions.map((option, optionIndex) => (
|
|
249
|
+
<motion.div
|
|
250
|
+
key={String(option.value)}
|
|
251
|
+
initial={{ opacity: 0, x: -20 }}
|
|
252
|
+
animate={{ opacity: 1, x: 0 }}
|
|
253
|
+
transition={{ delay: optionIndex * 0.1 }}
|
|
254
|
+
className="flex items-center space-x-2 p-3 rounded-lg hover:bg-muted/50 transition-colors"
|
|
255
|
+
>
|
|
256
|
+
<RadioGroupItem value={String(option.value)} id={`${question.id}-${option.value}`} />
|
|
257
|
+
<Label
|
|
258
|
+
htmlFor={`${question.id}-${option.value}`}
|
|
259
|
+
className="flex-1 cursor-pointer"
|
|
260
|
+
>
|
|
261
|
+
{option.label || option.value}
|
|
262
|
+
</Label>
|
|
263
|
+
</motion.div>
|
|
264
|
+
))}
|
|
265
|
+
</RadioGroup>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
{question.type === 'multiple-choice' && questionOptions && (
|
|
269
|
+
<div className="space-y-2">
|
|
270
|
+
{questionOptions.map((option, optionIndex) => (
|
|
271
|
+
<motion.div
|
|
272
|
+
key={String(option.value)}
|
|
273
|
+
initial={{ opacity: 0, x: -20 }}
|
|
274
|
+
animate={{ opacity: 1, x: 0 }}
|
|
275
|
+
transition={{ delay: optionIndex * 0.1 }}
|
|
276
|
+
className="flex items-center space-x-2 p-3 rounded-lg hover:bg-muted/50 transition-colors"
|
|
277
|
+
>
|
|
278
|
+
<Checkbox
|
|
279
|
+
id={`${question.id}-${option.value}`}
|
|
280
|
+
checked={(currentAnswer || []).includes(option.value)}
|
|
281
|
+
onCheckedChange={(checked) => {
|
|
282
|
+
const current = currentAnswer || []
|
|
283
|
+
if (checked) {
|
|
284
|
+
handleAnswerChange([...current, option.value])
|
|
285
|
+
} else {
|
|
286
|
+
handleAnswerChange(current.filter((v: string) => v !== option.value))
|
|
287
|
+
}
|
|
288
|
+
}}
|
|
289
|
+
/>
|
|
290
|
+
<Label
|
|
291
|
+
htmlFor={`${question.id}-${option.value}`}
|
|
292
|
+
className="flex-1 cursor-pointer"
|
|
293
|
+
>
|
|
294
|
+
{option.label || option.value}
|
|
295
|
+
</Label>
|
|
296
|
+
</motion.div>
|
|
297
|
+
))}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
|
|
301
|
+
{question.type === 'true-false' && (
|
|
302
|
+
<RadioGroup
|
|
303
|
+
value={currentAnswer?.toString() || ''}
|
|
304
|
+
onValueChange={(value) => handleAnswerChange(value === 'true')}
|
|
305
|
+
>
|
|
306
|
+
<div className="flex items-center space-x-2 p-3 rounded-lg hover:bg-muted/50">
|
|
307
|
+
<RadioGroupItem value="true" id={`${question.id}-true`} />
|
|
308
|
+
<Label htmlFor={`${question.id}-true`} className="flex-1 cursor-pointer">
|
|
309
|
+
True
|
|
310
|
+
</Label>
|
|
311
|
+
</div>
|
|
312
|
+
<div className="flex items-center space-x-2 p-3 rounded-lg hover:bg-muted/50">
|
|
313
|
+
<RadioGroupItem value="false" id={`${question.id}-false`} />
|
|
314
|
+
<Label htmlFor={`${question.id}-false`} className="flex-1 cursor-pointer">
|
|
315
|
+
False
|
|
316
|
+
</Label>
|
|
317
|
+
</div>
|
|
318
|
+
</RadioGroup>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
{question.type === 'text' && (
|
|
322
|
+
<Textarea
|
|
323
|
+
value={currentAnswer || ''}
|
|
324
|
+
onChange={(e) => handleAnswerChange(e.target.value)}
|
|
325
|
+
placeholder="Type your answer here..."
|
|
326
|
+
className="min-h-[120px]"
|
|
327
|
+
/>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{question.type === 'rating' && (
|
|
331
|
+
<div className="space-y-4">
|
|
332
|
+
<Slider
|
|
333
|
+
value={[currentAnswer || 5]}
|
|
334
|
+
onValueChange={([value]) => handleAnswerChange(value)}
|
|
335
|
+
max={10}
|
|
336
|
+
min={1}
|
|
337
|
+
step={1}
|
|
338
|
+
className="w-full"
|
|
339
|
+
/>
|
|
340
|
+
<div className="text-center text-2xl font-bold">
|
|
341
|
+
{currentAnswer || 5}/10
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
|
|
346
|
+
{showHints && question.hint && (
|
|
347
|
+
<Alert>
|
|
348
|
+
<AlertDescription>
|
|
349
|
+
<strong>Hint:</strong> {question.hint}
|
|
350
|
+
</AlertDescription>
|
|
351
|
+
</Alert>
|
|
352
|
+
)}
|
|
353
|
+
</CardContent>
|
|
354
|
+
</Card>
|
|
355
|
+
)
|
|
356
|
+
},
|
|
357
|
+
validation: () => {
|
|
358
|
+
if (question.required && !answers[question.id]) {
|
|
359
|
+
return false
|
|
360
|
+
}
|
|
361
|
+
return true
|
|
362
|
+
},
|
|
363
|
+
isOptional: !question.required
|
|
364
|
+
}))
|
|
365
|
+
|
|
366
|
+
if (showResults) {
|
|
367
|
+
const { score, totalScore } = calculateScore()
|
|
368
|
+
const percentage = Math.round((score / totalScore) * 100)
|
|
369
|
+
const passed = percentage >= passingScore
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<motion.div
|
|
373
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
374
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
375
|
+
className={cn("w-full max-w-2xl mx-auto", className)}
|
|
376
|
+
>
|
|
377
|
+
<Card>
|
|
378
|
+
<CardHeader className="text-center">
|
|
379
|
+
<div className="mx-auto mb-4">
|
|
380
|
+
<Trophy className={cn(
|
|
381
|
+
"w-16 h-16",
|
|
382
|
+
passed ? "text-yellow-500" : "text-muted-foreground"
|
|
383
|
+
)} />
|
|
384
|
+
</div>
|
|
385
|
+
<CardTitle className="text-3xl">Quiz Complete!</CardTitle>
|
|
386
|
+
<CardDescription>
|
|
387
|
+
Here's how you performed
|
|
388
|
+
</CardDescription>
|
|
389
|
+
</CardHeader>
|
|
390
|
+
<CardContent className="space-y-6">
|
|
391
|
+
<div className="text-center">
|
|
392
|
+
<div className="text-5xl font-bold mb-2">
|
|
393
|
+
{percentage}%
|
|
394
|
+
</div>
|
|
395
|
+
<Progress value={percentage} className="h-3 mb-4" />
|
|
396
|
+
<div className="flex justify-between text-sm text-muted-foreground">
|
|
397
|
+
<span>Score: {score}/{totalScore}</span>
|
|
398
|
+
<span>
|
|
399
|
+
{passed ? (
|
|
400
|
+
<Badge variant="success">Passed</Badge>
|
|
401
|
+
) : (
|
|
402
|
+
<Badge variant="destructive">Failed</Badge>
|
|
403
|
+
)}
|
|
404
|
+
</span>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
{allowReview && (
|
|
409
|
+
<div className="space-y-2">
|
|
410
|
+
<h3 className="font-semibold">Review Answers</h3>
|
|
411
|
+
{results.map((result, index) => {
|
|
412
|
+
const question = questions.find(q => q.id === result.questionId)
|
|
413
|
+
return (
|
|
414
|
+
<div key={result.questionId} className="flex items-center justify-between p-2 rounded-lg bg-muted/50">
|
|
415
|
+
<span className="text-sm">
|
|
416
|
+
{showQuestionNumbers && `Q${index + 1}: `}
|
|
417
|
+
{question?.question.substring(0, 50)}...
|
|
418
|
+
</span>
|
|
419
|
+
<Badge variant={result.isCorrect ? "success" : "destructive"}>
|
|
420
|
+
{result.points}/{question?.points || 1}
|
|
421
|
+
</Badge>
|
|
422
|
+
</div>
|
|
423
|
+
)
|
|
424
|
+
})}
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
|
|
428
|
+
<div className="flex gap-2">
|
|
429
|
+
<Button className="flex-1" onClick={() => window.location.reload()}>
|
|
430
|
+
<RotateCcw className="w-4 h-4 mr-2" />
|
|
431
|
+
Retake Quiz
|
|
432
|
+
</Button>
|
|
433
|
+
<Button variant="outline">
|
|
434
|
+
<Download className="w-4 h-4 mr-2" />
|
|
435
|
+
Download Results
|
|
436
|
+
</Button>
|
|
437
|
+
<Button variant="outline">
|
|
438
|
+
<Share2 className="w-4 h-4 mr-2" />
|
|
439
|
+
Share
|
|
440
|
+
</Button>
|
|
441
|
+
</div>
|
|
442
|
+
</CardContent>
|
|
443
|
+
</Card>
|
|
444
|
+
</motion.div>
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<div className={cn("w-full", className)}>
|
|
450
|
+
{showTimer && totalTimeLimit && (
|
|
451
|
+
<div className="mb-6 flex items-center justify-between">
|
|
452
|
+
<div className="flex items-center gap-2">
|
|
453
|
+
<Clock className="w-4 h-4" />
|
|
454
|
+
<span className="text-sm font-medium">
|
|
455
|
+
Time Remaining: {formatTime(timeRemaining)}
|
|
456
|
+
</span>
|
|
457
|
+
</div>
|
|
458
|
+
<Progress
|
|
459
|
+
value={(timeRemaining / totalTimeLimit) * 100}
|
|
460
|
+
className="w-32 h-2"
|
|
461
|
+
/>
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
|
|
465
|
+
<MoonUIFormWizardPro
|
|
466
|
+
steps={wizardSteps}
|
|
467
|
+
onComplete={handleQuizComplete}
|
|
468
|
+
allowStepSkip={allowSkip}
|
|
469
|
+
validateOnStepChange={true}
|
|
470
|
+
showProgressBar={showProgress}
|
|
471
|
+
progressType="dots"
|
|
472
|
+
animationType="slide"
|
|
473
|
+
/>
|
|
474
|
+
</div>
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Additional Alert import since it was missing
|
|
479
|
+
import { Alert, AlertDescription } from "../ui/alert"
|