@schoolio/player 1.2.2 → 1.3.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.mjs CHANGED
@@ -141,15 +141,15 @@ function formatTime(seconds) {
141
141
  }
142
142
 
143
143
  // src/QuizPlayer.tsx
144
- import { jsx, jsxs } from "react/jsx-runtime";
144
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
145
145
  var defaultStyles = {
146
146
  container: {
147
147
  fontFamily: "system-ui, -apple-system, sans-serif",
148
- maxWidth: "800px",
149
- margin: "0 auto",
148
+ width: "100%",
150
149
  padding: "20px",
151
150
  backgroundColor: "#ffffff",
152
- borderRadius: "12px"
151
+ borderRadius: "12px",
152
+ boxSizing: "border-box"
153
153
  },
154
154
  header: {
155
155
  marginBottom: "20px",
@@ -179,7 +179,8 @@ var defaultStyles = {
179
179
  transition: "width 0.3s ease"
180
180
  },
181
181
  question: {
182
- marginBottom: "24px"
182
+ marginBottom: "24px",
183
+ minHeight: "280px"
183
184
  },
184
185
  questionText: {
185
186
  fontSize: "18px",
@@ -196,7 +197,9 @@ var defaultStyles = {
196
197
  border: "2px solid #e5e7eb",
197
198
  borderRadius: "8px",
198
199
  cursor: "pointer",
199
- transition: "all 0.2s ease"
200
+ transition: "all 0.2s ease",
201
+ outline: "none",
202
+ backgroundColor: "#ffffff"
200
203
  },
201
204
  optionSelected: {
202
205
  borderColor: "#6721b0",
@@ -215,7 +218,10 @@ var defaultStyles = {
215
218
  padding: "12px 16px",
216
219
  border: "2px solid #e5e7eb",
217
220
  borderRadius: "8px",
218
- fontSize: "16px"
221
+ fontSize: "16px",
222
+ outline: "none",
223
+ boxSizing: "border-box",
224
+ backgroundColor: "#ffffff"
219
225
  },
220
226
  buttons: {
221
227
  display: "flex",
@@ -244,24 +250,90 @@ var defaultStyles = {
244
250
  color: "#9ca3af",
245
251
  cursor: "not-allowed"
246
252
  },
253
+ buttonAddMore: {
254
+ padding: "12px 24px",
255
+ borderRadius: "8px",
256
+ fontSize: "16px",
257
+ fontWeight: "500",
258
+ cursor: "pointer",
259
+ border: "2px solid #6721b0",
260
+ backgroundColor: "transparent",
261
+ color: "#6721b0",
262
+ transition: "all 0.2s ease",
263
+ display: "flex",
264
+ alignItems: "center",
265
+ justifyContent: "center",
266
+ gap: "8px"
267
+ },
268
+ buttonAddMoreDisabled: {
269
+ border: "2px solid #e5e7eb",
270
+ color: "#9ca3af",
271
+ cursor: "not-allowed"
272
+ },
273
+ buttonsColumn: {
274
+ display: "flex",
275
+ flexDirection: "column",
276
+ gap: "12px",
277
+ marginTop: "24px"
278
+ },
247
279
  timer: {
248
280
  fontSize: "14px",
249
281
  color: "#6b7280",
250
282
  marginLeft: "16px"
251
283
  },
252
284
  results: {
285
+ display: "flex",
286
+ flexDirection: "column",
287
+ alignItems: "center",
288
+ justifyContent: "center",
289
+ padding: "48px 24px",
253
290
  textAlign: "center",
254
- padding: "40px 20px"
291
+ minHeight: "400px",
292
+ position: "relative",
293
+ overflow: "hidden"
255
294
  },
256
- resultScore: {
257
- fontSize: "48px",
258
- fontWeight: "700",
259
- color: "#6721b0",
260
- marginBottom: "8px"
295
+ resultsBackground: {
296
+ position: "absolute",
297
+ inset: "0",
298
+ borderRadius: "16px",
299
+ zIndex: 0
261
300
  },
262
- resultLabel: {
301
+ resultsContent: {
302
+ position: "relative",
303
+ zIndex: 1,
304
+ display: "flex",
305
+ flexDirection: "column",
306
+ alignItems: "center"
307
+ },
308
+ resultDetails: {
263
309
  fontSize: "18px",
264
- color: "#6b7280"
310
+ color: "#6b7280",
311
+ marginBottom: "4px"
312
+ },
313
+ resultStars: {
314
+ display: "flex",
315
+ justifyContent: "center",
316
+ gap: "8px",
317
+ marginBottom: "20px"
318
+ },
319
+ resultStar: {
320
+ fontSize: "32px",
321
+ animation: "starPop 0.5s ease-out forwards",
322
+ opacity: 0
323
+ },
324
+ confettiContainer: {
325
+ position: "absolute",
326
+ inset: "0",
327
+ pointerEvents: "none",
328
+ overflow: "hidden",
329
+ zIndex: 0
330
+ },
331
+ confettiPiece: {
332
+ position: "absolute",
333
+ width: "10px",
334
+ height: "10px",
335
+ top: "-10px",
336
+ animation: "confettiFall 3s ease-out forwards"
265
337
  },
266
338
  loading: {
267
339
  textAlign: "center",
@@ -272,6 +344,80 @@ var defaultStyles = {
272
344
  textAlign: "center",
273
345
  padding: "40px",
274
346
  color: "#ef4444"
347
+ },
348
+ intro: {
349
+ display: "flex",
350
+ flexDirection: "column",
351
+ alignItems: "center",
352
+ justifyContent: "center",
353
+ padding: "48px 24px",
354
+ textAlign: "center",
355
+ background: "linear-gradient(135deg, #f3e8ff 0%, #e0e7ff 50%, #fce7f3 100%)",
356
+ borderRadius: "16px",
357
+ minHeight: "320px"
358
+ },
359
+ introTitle: {
360
+ fontSize: "28px",
361
+ fontWeight: "700",
362
+ color: "#4c1d95",
363
+ marginBottom: "12px"
364
+ },
365
+ introSubtitle: {
366
+ fontSize: "16px",
367
+ color: "#6b7280",
368
+ marginBottom: "8px"
369
+ },
370
+ introQuestionCount: {
371
+ fontSize: "14px",
372
+ color: "#8b5cf6",
373
+ marginBottom: "32px",
374
+ fontWeight: "500"
375
+ },
376
+ startButton: {
377
+ padding: "16px 48px",
378
+ fontSize: "18px",
379
+ fontWeight: "600",
380
+ backgroundColor: "#7c3aed",
381
+ color: "#ffffff",
382
+ border: "none",
383
+ borderRadius: "12px",
384
+ cursor: "pointer",
385
+ transition: "all 0.2s ease",
386
+ boxShadow: "0 4px 14px rgba(124, 58, 237, 0.4)"
387
+ },
388
+ feedback: {
389
+ marginTop: "16px",
390
+ padding: "16px",
391
+ borderRadius: "8px",
392
+ backgroundColor: "#f9fafb",
393
+ border: "1px solid #e5e7eb"
394
+ },
395
+ feedbackCorrect: {
396
+ backgroundColor: "#f0fdf4",
397
+ borderColor: "#22c55e"
398
+ },
399
+ feedbackIncorrect: {
400
+ backgroundColor: "#fef2f2",
401
+ borderColor: "#ef4444"
402
+ },
403
+ feedbackTitle: {
404
+ fontSize: "16px",
405
+ fontWeight: "600",
406
+ marginBottom: "8px",
407
+ display: "flex",
408
+ alignItems: "center",
409
+ gap: "8px"
410
+ },
411
+ feedbackTitleCorrect: {
412
+ color: "#16a34a"
413
+ },
414
+ feedbackTitleIncorrect: {
415
+ color: "#dc2626"
416
+ },
417
+ feedbackExplanation: {
418
+ fontSize: "14px",
419
+ color: "#4b5563",
420
+ lineHeight: "1.5"
275
421
  }
276
422
  };
277
423
  function Spinner({ size = 16, color = "#ffffff" }) {
@@ -303,6 +449,7 @@ function QuizPlayer({
303
449
  onComplete,
304
450
  onError,
305
451
  onProgress,
452
+ onGenerateMoreQuestions,
306
453
  className
307
454
  }) {
308
455
  const [quiz, setQuiz] = useState(null);
@@ -317,9 +464,15 @@ function QuizPlayer({
317
464
  const [error, setError] = useState(null);
318
465
  const [isLoading, setIsLoading] = useState(true);
319
466
  const [elapsedSeconds, setElapsedSeconds] = useState(0);
467
+ const [showIntro, setShowIntro] = useState(true);
468
+ const [timerStarted, setTimerStarted] = useState(false);
469
+ const [showFeedback, setShowFeedback] = useState(false);
470
+ const [currentAnswerDetail, setCurrentAnswerDetail] = useState(null);
471
+ const [extraQuestions, setExtraQuestions] = useState([]);
472
+ const [isGeneratingExtra, setIsGeneratingExtra] = useState(false);
320
473
  const apiClient = useRef(null);
321
474
  const timerRef = useRef(null);
322
- const startTimeRef = useRef(Date.now());
475
+ const startTimeRef = useRef(0);
323
476
  useEffect(() => {
324
477
  apiClient.current = new QuizApiClient({ baseUrl: apiBaseUrl, authToken });
325
478
  }, [apiBaseUrl, authToken]);
@@ -371,7 +524,7 @@ function QuizPlayer({
371
524
  initialize();
372
525
  }, [quizId, lessonId, assignLessonId, courseId, childId, parentId, onError]);
373
526
  useEffect(() => {
374
- if (!isLoading && !isCompleted && !error) {
527
+ if (timerStarted && !isCompleted && !error) {
375
528
  startTimeRef.current = Date.now();
376
529
  timerRef.current = setInterval(() => {
377
530
  setElapsedSeconds(Math.floor((Date.now() - startTimeRef.current) / 1e3));
@@ -382,27 +535,39 @@ function QuizPlayer({
382
535
  clearInterval(timerRef.current);
383
536
  }
384
537
  };
385
- }, [isLoading, isCompleted, error]);
538
+ }, [timerStarted, isCompleted, error]);
539
+ const handleStart = useCallback(() => {
540
+ setShowIntro(false);
541
+ setTimerStarted(true);
542
+ }, []);
543
+ useEffect(() => {
544
+ setShowFeedback(false);
545
+ setCurrentAnswerDetail(null);
546
+ }, [currentQuestionIndex]);
547
+ const allQuestions = quiz ? [...quiz.questions, ...extraQuestions] : [];
548
+ const totalQuestions = allQuestions.length;
549
+ const maxQuestions = 50;
386
550
  useEffect(() => {
387
551
  if (quiz && onProgress) {
388
552
  onProgress({
389
553
  currentQuestion: currentQuestionIndex + 1,
390
- totalQuestions: quiz.questions.length,
554
+ totalQuestions,
391
555
  answeredQuestions: answers.size
392
556
  });
393
557
  }
394
- }, [currentQuestionIndex, answers.size, quiz, onProgress]);
395
- const currentQuestion = quiz?.questions[currentQuestionIndex];
558
+ }, [currentQuestionIndex, answers.size, quiz, onProgress, totalQuestions]);
559
+ const currentQuestion = allQuestions[currentQuestionIndex];
396
560
  const handleAnswerChange = useCallback((value) => {
397
561
  if (!currentQuestion) return;
398
562
  setAnswers((prev) => new Map(prev).set(currentQuestion.id, value));
399
563
  }, [currentQuestion]);
400
- const handleNext = useCallback(async () => {
564
+ const handleCheckAnswer = useCallback(async () => {
401
565
  if (!quiz || !attempt || !currentQuestion || !apiClient.current) return;
402
566
  const selectedAnswer2 = answers.get(currentQuestion.id);
403
567
  if (selectedAnswer2 === void 0) return;
404
568
  setIsNavigating(true);
405
569
  const answerDetail = createAnswerDetail(currentQuestion, selectedAnswer2);
570
+ setCurrentAnswerDetail(answerDetail);
406
571
  const newAnswersDetail = [...answersDetail];
407
572
  const existingIdx = newAnswersDetail.findIndex((a) => a.questionId === currentQuestion.id);
408
573
  if (existingIdx >= 0) {
@@ -420,15 +585,39 @@ function QuizPlayer({
420
585
  } finally {
421
586
  setIsNavigating(false);
422
587
  }
423
- if (currentQuestionIndex < quiz.questions.length - 1) {
588
+ setShowFeedback(true);
589
+ }, [quiz, attempt, currentQuestion, answers, answersDetail]);
590
+ const handleContinue = useCallback(() => {
591
+ if (!quiz) return;
592
+ setShowFeedback(false);
593
+ setCurrentAnswerDetail(null);
594
+ if (currentQuestionIndex < totalQuestions - 1) {
424
595
  setCurrentQuestionIndex((prev) => prev + 1);
425
596
  }
426
- }, [quiz, attempt, currentQuestion, answers, answersDetail, currentQuestionIndex]);
427
- const handlePrevious = useCallback(() => {
428
- if (currentQuestionIndex > 0) {
429
- setCurrentQuestionIndex((prev) => prev - 1);
597
+ }, [quiz, currentQuestionIndex, totalQuestions]);
598
+ const handleAddMoreQuestions = useCallback(async () => {
599
+ if (!attempt || !onGenerateMoreQuestions || isGeneratingExtra) return;
600
+ if (totalQuestions >= maxQuestions) return;
601
+ setIsGeneratingExtra(true);
602
+ try {
603
+ const result2 = await onGenerateMoreQuestions(attempt.id, totalQuestions);
604
+ if (result2.extraQuestions && result2.extraQuestions.length > 0) {
605
+ const slotsAvailable = maxQuestions - totalQuestions;
606
+ const questionsToAppend = result2.extraQuestions.slice(0, slotsAvailable);
607
+ if (questionsToAppend.length > 0) {
608
+ setExtraQuestions((prev) => [...prev, ...questionsToAppend]);
609
+ setCurrentQuestionIndex(totalQuestions);
610
+ setShowFeedback(false);
611
+ setCurrentAnswerDetail(null);
612
+ }
613
+ }
614
+ } catch (err) {
615
+ console.error("Failed to generate extra questions:", err);
616
+ onError?.(err instanceof Error ? err : new Error("Failed to generate extra questions"));
617
+ } finally {
618
+ setIsGeneratingExtra(false);
430
619
  }
431
- }, [currentQuestionIndex]);
620
+ }, [attempt, onGenerateMoreQuestions, isGeneratingExtra, totalQuestions, maxQuestions, onError]);
432
621
  const handleSubmit = useCallback(async () => {
433
622
  if (!quiz || !attempt || !apiClient.current) return;
434
623
  setIsSubmitting(true);
@@ -445,7 +634,7 @@ function QuizPlayer({
445
634
  }
446
635
  }
447
636
  const scoreData = calculateScore(finalAnswersDetail);
448
- const timeSpent = Math.floor((Date.now() - startTimeRef.current) / 1e3);
637
+ const timeSpent = timerStarted && startTimeRef.current > 0 ? Math.floor((Date.now() - startTimeRef.current) / 1e3) : elapsedSeconds;
449
638
  const updatedAttempt = await apiClient.current.updateAttempt(attempt.id, {
450
639
  answers: finalAnswersDetail,
451
640
  status: "completed",
@@ -458,7 +647,7 @@ function QuizPlayer({
458
647
  attemptId: updatedAttempt.id,
459
648
  score: scoreData.score,
460
649
  correctAnswers: scoreData.correctAnswers,
461
- totalQuestions: quiz.questions.length,
650
+ totalQuestions,
462
651
  answers: finalAnswersDetail,
463
652
  timeSpentSeconds: timeSpent
464
653
  };
@@ -474,7 +663,7 @@ function QuizPlayer({
474
663
  } finally {
475
664
  setIsSubmitting(false);
476
665
  }
477
- }, [quiz, attempt, currentQuestion, answers, answersDetail, onComplete, onError]);
666
+ }, [quiz, attempt, currentQuestion, answers, answersDetail, onComplete, onError, totalQuestions, timerStarted, elapsedSeconds]);
478
667
  if (isLoading) {
479
668
  return /* @__PURE__ */ jsx("div", { className, style: defaultStyles.container, children: /* @__PURE__ */ jsx("div", { style: defaultStyles.loading, children: "Loading quiz..." }) });
480
669
  }
@@ -485,67 +674,403 @@ function QuizPlayer({
485
674
  ] }) }) });
486
675
  }
487
676
  if (isCompleted && result) {
488
- return /* @__PURE__ */ jsx("div", { className, style: defaultStyles.container, children: /* @__PURE__ */ jsxs("div", { style: defaultStyles.results, children: [
489
- /* @__PURE__ */ jsxs("div", { style: defaultStyles.resultScore, children: [
490
- result.score,
491
- "%"
492
- ] }),
493
- /* @__PURE__ */ jsxs("div", { style: defaultStyles.resultLabel, children: [
494
- result.correctAnswers,
495
- " of ",
496
- result.totalQuestions,
497
- " correct"
498
- ] }),
499
- /* @__PURE__ */ jsxs("div", { style: { ...defaultStyles.resultLabel, marginTop: "8px" }, children: [
500
- "Time: ",
501
- formatTime(result.timeSpentSeconds)
677
+ const percentage = Math.round(result.correctAnswers / result.totalQuestions * 100);
678
+ const getScoreTheme = (pct) => {
679
+ if (pct >= 80) {
680
+ return {
681
+ color: "#22c55e",
682
+ bgGradient: "linear-gradient(135deg, #dcfce7 0%, #bbf7d0 50%, #86efac 100%)",
683
+ badge: "Quiz Champion!",
684
+ badgeColor: "#fbbf24",
685
+ badgeBg: "linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)",
686
+ mascotMood: "celebrating",
687
+ stars: 3
688
+ };
689
+ } else if (pct >= 60) {
690
+ return {
691
+ color: "#f59e0b",
692
+ bgGradient: "linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, #fcd34d 100%)",
693
+ badge: "Rising Star!",
694
+ badgeColor: "#f59e0b",
695
+ badgeBg: "linear-gradient(135deg, #fed7aa 0%, #fdba74 100%)",
696
+ mascotMood: "happy",
697
+ stars: 2
698
+ };
699
+ } else if (pct >= 40) {
700
+ return {
701
+ color: "#3b82f6",
702
+ bgGradient: "linear-gradient(135deg, #dbeafe 0%, #bfdbfe 50%, #93c5fd 100%)",
703
+ badge: "Great Learner!",
704
+ badgeColor: "#3b82f6",
705
+ badgeBg: "linear-gradient(135deg, #bfdbfe 0%, #93c5fd 100%)",
706
+ mascotMood: "encouraging",
707
+ stars: 1
708
+ };
709
+ } else {
710
+ return {
711
+ color: "#8b5cf6",
712
+ bgGradient: "linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 50%, #d8b4fe 100%)",
713
+ badge: "Keep Growing!",
714
+ badgeColor: "#8b5cf6",
715
+ badgeBg: "linear-gradient(135deg, #e9d5ff 0%, #d8b4fe 100%)",
716
+ mascotMood: "supportive",
717
+ stars: 0
718
+ };
719
+ }
720
+ };
721
+ const theme = getScoreTheme(percentage);
722
+ const confettiColors = ["#f43f5e", "#ec4899", "#8b5cf6", "#3b82f6", "#22c55e", "#f59e0b", "#ef4444"];
723
+ const confettiPieces = Array.from({ length: 50 }, (_, i) => ({
724
+ id: i,
725
+ left: `${Math.random() * 100}%`,
726
+ delay: `${Math.random() * 2}s`,
727
+ duration: `${2 + Math.random() * 2}s`,
728
+ color: confettiColors[Math.floor(Math.random() * confettiColors.length)],
729
+ rotation: Math.random() * 360,
730
+ size: 6 + Math.random() * 8
731
+ }));
732
+ const StarIcon = ({ filled, delay }) => /* @__PURE__ */ jsx(
733
+ "svg",
734
+ {
735
+ width: "36",
736
+ height: "36",
737
+ viewBox: "0 0 24 24",
738
+ style: {
739
+ animation: "starPop 0.5s ease-out forwards",
740
+ animationDelay: `${delay}s`,
741
+ opacity: 0
742
+ },
743
+ children: /* @__PURE__ */ jsx(
744
+ "path",
745
+ {
746
+ d: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z",
747
+ fill: filled ? "#fbbf24" : "#e5e7eb",
748
+ stroke: filled ? "#f59e0b" : "#d1d5db",
749
+ strokeWidth: "1"
750
+ }
751
+ )
752
+ }
753
+ );
754
+ const MascotOwl = ({ mood }) => {
755
+ const getEyeExpression = () => {
756
+ switch (mood) {
757
+ case "celebrating":
758
+ return { leftEye: ">", rightEye: "<", pupilY: 42 };
759
+ // Squinting happy
760
+ case "happy":
761
+ return { leftEye: null, rightEye: null, pupilY: 42 };
762
+ // Normal happy
763
+ case "encouraging":
764
+ return { leftEye: null, rightEye: null, pupilY: 44 };
765
+ // Looking down warmly
766
+ default:
767
+ return { leftEye: null, rightEye: null, pupilY: 42 };
768
+ }
769
+ };
770
+ const eyeExpr = getEyeExpression();
771
+ return /* @__PURE__ */ jsxs(
772
+ "svg",
773
+ {
774
+ width: "120",
775
+ height: "120",
776
+ viewBox: "0 0 100 100",
777
+ style: {
778
+ animation: mood === "celebrating" ? "bounce 0.6s ease-in-out infinite" : "gentleBob 2s ease-in-out infinite"
779
+ },
780
+ children: [
781
+ /* @__PURE__ */ jsx("ellipse", { cx: "50", cy: "60", rx: "35", ry: "30", fill: "#8b5cf6" }),
782
+ /* @__PURE__ */ jsx("ellipse", { cx: "50", cy: "65", rx: "25", ry: "20", fill: "#c4b5fd" }),
783
+ /* @__PURE__ */ jsx("circle", { cx: "50", cy: "35", r: "28", fill: "#a78bfa" }),
784
+ /* @__PURE__ */ jsx("polygon", { points: "28,15 35,30 22,28", fill: "#7c3aed" }),
785
+ /* @__PURE__ */ jsx("polygon", { points: "72,15 65,30 78,28", fill: "#7c3aed" }),
786
+ /* @__PURE__ */ jsx("ellipse", { cx: "38", cy: "38", rx: "10", ry: "12", fill: "white" }),
787
+ /* @__PURE__ */ jsx("ellipse", { cx: "62", cy: "38", rx: "10", ry: "12", fill: "white" }),
788
+ eyeExpr.leftEye ? /* @__PURE__ */ jsx("text", { x: "38", y: "44", textAnchor: "middle", fontSize: "16", fill: "#1f2937", children: eyeExpr.leftEye }) : /* @__PURE__ */ jsx("circle", { cx: "38", cy: eyeExpr.pupilY, r: "5", fill: "#1f2937" }),
789
+ eyeExpr.rightEye ? /* @__PURE__ */ jsx("text", { x: "62", y: "44", textAnchor: "middle", fontSize: "16", fill: "#1f2937", children: eyeExpr.rightEye }) : /* @__PURE__ */ jsx("circle", { cx: "62", cy: eyeExpr.pupilY, r: "5", fill: "#1f2937" }),
790
+ /* @__PURE__ */ jsx("polygon", { points: "50,45 45,52 55,52", fill: "#fbbf24" }),
791
+ (mood === "celebrating" || mood === "happy") && /* @__PURE__ */ jsxs(Fragment, { children: [
792
+ /* @__PURE__ */ jsx("ellipse", { cx: "28", cy: "45", rx: "5", ry: "3", fill: "#fda4af", opacity: "0.6" }),
793
+ /* @__PURE__ */ jsx("ellipse", { cx: "72", cy: "45", rx: "5", ry: "3", fill: "#fda4af", opacity: "0.6" })
794
+ ] }),
795
+ mood === "celebrating" ? /* @__PURE__ */ jsxs(Fragment, { children: [
796
+ /* @__PURE__ */ jsx("ellipse", { cx: "18", cy: "55", rx: "8", ry: "15", fill: "#7c3aed", transform: "rotate(-30 18 55)" }),
797
+ /* @__PURE__ */ jsx("ellipse", { cx: "82", cy: "55", rx: "8", ry: "15", fill: "#7c3aed", transform: "rotate(30 82 55)" })
798
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
799
+ /* @__PURE__ */ jsx("ellipse", { cx: "20", cy: "60", rx: "8", ry: "15", fill: "#7c3aed" }),
800
+ /* @__PURE__ */ jsx("ellipse", { cx: "80", cy: "60", rx: "8", ry: "15", fill: "#7c3aed" })
801
+ ] }),
802
+ /* @__PURE__ */ jsx("ellipse", { cx: "40", cy: "88", rx: "8", ry: "4", fill: "#fbbf24" }),
803
+ /* @__PURE__ */ jsx("ellipse", { cx: "60", cy: "88", rx: "8", ry: "4", fill: "#fbbf24" })
804
+ ]
805
+ }
806
+ );
807
+ };
808
+ return /* @__PURE__ */ jsxs("div", { className, style: defaultStyles.container, children: [
809
+ /* @__PURE__ */ jsx("style", { children: `
810
+ @keyframes confettiFall {
811
+ 0% {
812
+ transform: translateY(-10px) rotate(0deg);
813
+ opacity: 1;
814
+ }
815
+ 100% {
816
+ transform: translateY(500px) rotate(720deg);
817
+ opacity: 0;
818
+ }
819
+ }
820
+ @keyframes starPop {
821
+ 0% {
822
+ transform: scale(0);
823
+ opacity: 0;
824
+ }
825
+ 50% {
826
+ transform: scale(1.3);
827
+ }
828
+ 100% {
829
+ transform: scale(1);
830
+ opacity: 1;
831
+ }
832
+ }
833
+ @keyframes bounce {
834
+ 0%, 100% {
835
+ transform: translateY(0);
836
+ }
837
+ 50% {
838
+ transform: translateY(-10px);
839
+ }
840
+ }
841
+ @keyframes gentleBob {
842
+ 0%, 100% {
843
+ transform: translateY(0);
844
+ }
845
+ 50% {
846
+ transform: translateY(-5px);
847
+ }
848
+ }
849
+ @keyframes badgePop {
850
+ 0% {
851
+ transform: scale(0) rotate(-10deg);
852
+ opacity: 0;
853
+ }
854
+ 60% {
855
+ transform: scale(1.1) rotate(5deg);
856
+ }
857
+ 100% {
858
+ transform: scale(1) rotate(0deg);
859
+ opacity: 1;
860
+ }
861
+ }
862
+ @keyframes scoreSlideIn {
863
+ 0% {
864
+ transform: translateY(20px);
865
+ opacity: 0;
866
+ }
867
+ 100% {
868
+ transform: translateY(0);
869
+ opacity: 1;
870
+ }
871
+ }
872
+ ` }),
873
+ /* @__PURE__ */ jsxs("div", { style: defaultStyles.results, children: [
874
+ percentage >= 60 && /* @__PURE__ */ jsx("div", { style: defaultStyles.confettiContainer, children: confettiPieces.map((piece) => /* @__PURE__ */ jsx(
875
+ "div",
876
+ {
877
+ style: {
878
+ ...defaultStyles.confettiPiece,
879
+ left: piece.left,
880
+ width: `${piece.size}px`,
881
+ height: `${piece.size}px`,
882
+ backgroundColor: piece.color,
883
+ borderRadius: Math.random() > 0.5 ? "50%" : "2px",
884
+ animationDelay: piece.delay,
885
+ animationDuration: piece.duration,
886
+ transform: `rotate(${piece.rotation}deg)`
887
+ }
888
+ },
889
+ piece.id
890
+ )) }),
891
+ /* @__PURE__ */ jsx("div", { style: { ...defaultStyles.resultsBackground, background: theme.bgGradient } }),
892
+ /* @__PURE__ */ jsxs("div", { style: defaultStyles.resultsContent, children: [
893
+ /* @__PURE__ */ jsxs("div", { style: defaultStyles.resultStars, children: [
894
+ /* @__PURE__ */ jsx(StarIcon, { filled: theme.stars >= 1, delay: 0.3 }),
895
+ /* @__PURE__ */ jsx(StarIcon, { filled: theme.stars >= 2, delay: 0.5 }),
896
+ /* @__PURE__ */ jsx(StarIcon, { filled: theme.stars >= 3, delay: 0.7 })
897
+ ] }),
898
+ /* @__PURE__ */ jsx("div", { style: { marginBottom: "16px" }, children: /* @__PURE__ */ jsx(MascotOwl, { mood: theme.mascotMood }) }),
899
+ /* @__PURE__ */ jsx(
900
+ "div",
901
+ {
902
+ style: {
903
+ background: theme.badgeBg,
904
+ padding: "12px 28px",
905
+ borderRadius: "50px",
906
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
907
+ marginBottom: "20px",
908
+ animation: "badgePop 0.6s ease-out 0.2s forwards",
909
+ opacity: 0,
910
+ border: `3px solid ${theme.badgeColor}`
911
+ },
912
+ children: /* @__PURE__ */ jsx(
913
+ "span",
914
+ {
915
+ style: {
916
+ fontSize: "22px",
917
+ fontWeight: "700",
918
+ color: "#1f2937",
919
+ textShadow: "0 1px 2px rgba(255,255,255,0.5)"
920
+ },
921
+ children: theme.badge
922
+ }
923
+ )
924
+ }
925
+ ),
926
+ /* @__PURE__ */ jsxs(
927
+ "div",
928
+ {
929
+ style: {
930
+ animation: "scoreSlideIn 0.5s ease-out 0.4s forwards",
931
+ opacity: 0
932
+ },
933
+ children: [
934
+ /* @__PURE__ */ jsxs(
935
+ "div",
936
+ {
937
+ style: {
938
+ fontSize: "48px",
939
+ fontWeight: "800",
940
+ color: theme.color,
941
+ lineHeight: "1",
942
+ marginBottom: "4px"
943
+ },
944
+ children: [
945
+ result.correctAnswers,
946
+ " of ",
947
+ result.totalQuestions
948
+ ]
949
+ }
950
+ ),
951
+ /* @__PURE__ */ jsx(
952
+ "div",
953
+ {
954
+ style: {
955
+ fontSize: "20px",
956
+ fontWeight: "600",
957
+ color: "#6b7280",
958
+ marginBottom: "12px"
959
+ },
960
+ children: "correct answers"
961
+ }
962
+ )
963
+ ]
964
+ }
965
+ ),
966
+ /* @__PURE__ */ jsxs("div", { style: { ...defaultStyles.resultDetails, marginTop: "8px" }, children: [
967
+ "Time: ",
968
+ formatTime(result.timeSpentSeconds)
969
+ ] })
970
+ ] })
502
971
  ] })
972
+ ] });
973
+ }
974
+ if (quiz && showIntro) {
975
+ return /* @__PURE__ */ jsx("div", { className, style: defaultStyles.container, children: /* @__PURE__ */ jsxs("div", { style: defaultStyles.intro, children: [
976
+ /* @__PURE__ */ jsx("div", { style: defaultStyles.introTitle, children: quiz.title }),
977
+ /* @__PURE__ */ jsx("div", { style: defaultStyles.introSubtitle, children: "Ready to test your knowledge?" }),
978
+ /* @__PURE__ */ jsxs("div", { style: defaultStyles.introQuestionCount, children: [
979
+ quiz.questions.length,
980
+ " question",
981
+ quiz.questions.length !== 1 ? "s" : "",
982
+ " to answer"
983
+ ] }),
984
+ /* @__PURE__ */ jsx(
985
+ "button",
986
+ {
987
+ style: defaultStyles.startButton,
988
+ onClick: handleStart,
989
+ onMouseOver: (e) => {
990
+ e.currentTarget.style.transform = "translateY(-2px)";
991
+ e.currentTarget.style.boxShadow = "0 6px 20px rgba(124, 58, 237, 0.5)";
992
+ },
993
+ onMouseOut: (e) => {
994
+ e.currentTarget.style.transform = "translateY(0)";
995
+ e.currentTarget.style.boxShadow = "0 4px 14px rgba(124, 58, 237, 0.4)";
996
+ },
997
+ "data-testid": "button-start-quiz",
998
+ children: "Let's Start!"
999
+ }
1000
+ )
503
1001
  ] }) });
504
1002
  }
505
1003
  if (!quiz || !currentQuestion) {
506
1004
  return /* @__PURE__ */ jsx("div", { className, style: defaultStyles.container, children: /* @__PURE__ */ jsx("div", { style: defaultStyles.error, children: "No quiz data available" }) });
507
1005
  }
508
1006
  const selectedAnswer = answers.get(currentQuestion.id);
509
- const isLastQuestion = currentQuestionIndex === quiz.questions.length - 1;
510
- const progressPercent = (currentQuestionIndex + 1) / quiz.questions.length * 100;
1007
+ const isLastQuestion = currentQuestionIndex === totalQuestions - 1;
1008
+ const progressPercent = (currentQuestionIndex + 1) / totalQuestions * 100;
1009
+ const remainingSlots = maxQuestions - totalQuestions;
1010
+ const questionsToAdd = Math.min(5, remainingSlots);
1011
+ const canAddMore = onGenerateMoreQuestions && remainingSlots > 0;
511
1012
  return /* @__PURE__ */ jsxs("div", { className, style: defaultStyles.container, children: [
512
1013
  /* @__PURE__ */ jsxs("div", { style: defaultStyles.header, children: [
513
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
514
- /* @__PURE__ */ jsx("div", { style: defaultStyles.title, children: quiz.title }),
515
- /* @__PURE__ */ jsx("div", { style: defaultStyles.timer, children: formatTime(elapsedSeconds) })
516
- ] }),
1014
+ /* @__PURE__ */ jsx("div", { style: defaultStyles.title, children: quiz.title }),
517
1015
  /* @__PURE__ */ jsxs("div", { style: defaultStyles.progress, children: [
518
1016
  "Question ",
519
1017
  currentQuestionIndex + 1,
520
1018
  " of ",
521
- quiz.questions.length
1019
+ totalQuestions
522
1020
  ] }),
523
1021
  /* @__PURE__ */ jsx("div", { style: defaultStyles.progressBar, children: /* @__PURE__ */ jsx("div", { style: { ...defaultStyles.progressFill, width: `${progressPercent}%` } }) })
524
1022
  ] }),
525
1023
  /* @__PURE__ */ jsxs("div", { style: defaultStyles.question, children: [
526
1024
  /* @__PURE__ */ jsx("div", { style: defaultStyles.questionText, children: currentQuestion.question }),
527
- (currentQuestion.type === "single" || currentQuestion.type === "true-false") && /* @__PURE__ */ jsx("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => /* @__PURE__ */ jsx(
528
- "div",
529
- {
530
- style: {
531
- ...defaultStyles.option,
532
- ...selectedAnswer === option ? defaultStyles.optionSelected : {}
1025
+ (currentQuestion.type === "single" || currentQuestion.type === "true-false") && /* @__PURE__ */ jsx("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
1026
+ const isSelected = selectedAnswer === option;
1027
+ const isCorrectOption = currentQuestion.correctAnswer === option;
1028
+ let optionStyle = { ...defaultStyles.option };
1029
+ if (showFeedback) {
1030
+ if (isCorrectOption) {
1031
+ optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
1032
+ } else if (isSelected && !isCorrectOption) {
1033
+ optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
1034
+ }
1035
+ } else if (isSelected) {
1036
+ optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
1037
+ }
1038
+ return /* @__PURE__ */ jsx(
1039
+ "div",
1040
+ {
1041
+ style: {
1042
+ ...optionStyle,
1043
+ cursor: showFeedback ? "default" : "pointer"
1044
+ },
1045
+ onClick: () => !showFeedback && handleAnswerChange(option),
1046
+ children: option
533
1047
  },
534
- onClick: () => handleAnswerChange(option),
535
- children: option
536
- },
537
- idx
538
- )) }),
1048
+ idx
1049
+ );
1050
+ }) }),
539
1051
  currentQuestion.type === "multiple" && /* @__PURE__ */ jsx("div", { style: defaultStyles.options, children: currentQuestion.options?.map((option, idx) => {
540
1052
  const selected = Array.isArray(selectedAnswer) && selectedAnswer.includes(option);
1053
+ const correctAnswers = Array.isArray(currentQuestion.correctAnswer) ? currentQuestion.correctAnswer : currentQuestion.correctAnswer ? [currentQuestion.correctAnswer] : [];
1054
+ const isCorrectOption = correctAnswers.includes(option);
1055
+ let optionStyle = { ...defaultStyles.option };
1056
+ if (showFeedback) {
1057
+ if (isCorrectOption) {
1058
+ optionStyle = { ...optionStyle, ...defaultStyles.optionCorrect };
1059
+ } else if (selected && !isCorrectOption) {
1060
+ optionStyle = { ...optionStyle, ...defaultStyles.optionIncorrect };
1061
+ }
1062
+ } else if (selected) {
1063
+ optionStyle = { ...optionStyle, ...defaultStyles.optionSelected };
1064
+ }
541
1065
  return /* @__PURE__ */ jsx(
542
1066
  "div",
543
1067
  {
544
1068
  style: {
545
- ...defaultStyles.option,
546
- ...selected ? defaultStyles.optionSelected : {}
1069
+ ...optionStyle,
1070
+ cursor: showFeedback ? "default" : "pointer"
547
1071
  },
548
1072
  onClick: () => {
1073
+ if (showFeedback) return;
549
1074
  const current = Array.isArray(selectedAnswer) ? selectedAnswer : [];
550
1075
  if (selected) {
551
1076
  handleAnswerChange(current.filter((o) => o !== option));
@@ -564,7 +1089,8 @@ function QuizPlayer({
564
1089
  style: { ...defaultStyles.input, minHeight: currentQuestion.type === "essay" ? "150px" : "60px" },
565
1090
  value: selectedAnswer || "",
566
1091
  onChange: (e) => handleAnswerChange(e.target.value),
567
- placeholder: "Type your answer here..."
1092
+ placeholder: "Type your answer here...",
1093
+ disabled: showFeedback
568
1094
  }
569
1095
  ),
570
1096
  currentQuestion.type === "fill" && /* @__PURE__ */ jsx("div", { style: defaultStyles.options, children: currentQuestion.blanks?.map((_, idx) => /* @__PURE__ */ jsx(
@@ -577,47 +1103,86 @@ function QuizPlayer({
577
1103
  current[idx] = e.target.value;
578
1104
  handleAnswerChange(current);
579
1105
  },
580
- placeholder: `Blank ${idx + 1}`
1106
+ placeholder: `Blank ${idx + 1}`,
1107
+ disabled: showFeedback
581
1108
  },
582
1109
  idx
583
- )) })
1110
+ )) }),
1111
+ showFeedback && currentAnswerDetail && /* @__PURE__ */ jsxs("div", { style: {
1112
+ ...defaultStyles.feedback,
1113
+ ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackCorrect : defaultStyles.feedbackIncorrect
1114
+ }, children: [
1115
+ /* @__PURE__ */ jsx("div", { style: {
1116
+ ...defaultStyles.feedbackTitle,
1117
+ ...currentAnswerDetail.isCorrect ? defaultStyles.feedbackTitleCorrect : defaultStyles.feedbackTitleIncorrect
1118
+ }, children: currentAnswerDetail.isCorrect ? "\u2713 Correct!" : "\u2717 Incorrect" }),
1119
+ currentQuestion.explanation && /* @__PURE__ */ jsx("div", { style: defaultStyles.feedbackExplanation, children: currentQuestion.explanation })
1120
+ ] })
584
1121
  ] }),
585
- /* @__PURE__ */ jsxs("div", { style: defaultStyles.buttons, children: [
586
- /* @__PURE__ */ jsx(
1122
+ /* @__PURE__ */ jsxs("div", { style: defaultStyles.buttonsColumn, children: [
1123
+ showFeedback && isLastQuestion && canAddMore && /* @__PURE__ */ jsx(
587
1124
  "button",
588
1125
  {
589
1126
  style: {
590
- ...defaultStyles.button,
591
- ...currentQuestionIndex > 0 ? defaultStyles.buttonSecondary : defaultStyles.buttonDisabled
1127
+ ...defaultStyles.buttonAddMore,
1128
+ ...isGeneratingExtra ? defaultStyles.buttonAddMoreDisabled : {}
592
1129
  },
593
- onClick: handlePrevious,
594
- disabled: currentQuestionIndex === 0,
595
- children: "Previous"
1130
+ onClick: handleAddMoreQuestions,
1131
+ disabled: isGeneratingExtra,
1132
+ "data-testid": "button-add-more-questions",
1133
+ children: isGeneratingExtra ? /* @__PURE__ */ jsxs(Fragment, { children: [
1134
+ /* @__PURE__ */ jsx(Spinner, { size: 16, color: "#9ca3af" }),
1135
+ "Generating Questions..."
1136
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1137
+ "+ Add ",
1138
+ questionsToAdd,
1139
+ " More Question",
1140
+ questionsToAdd !== 1 ? "s" : ""
1141
+ ] })
596
1142
  }
597
1143
  ),
598
- isLastQuestion ? /* @__PURE__ */ jsx(
599
- "button",
600
- {
601
- style: {
602
- ...defaultStyles.button,
603
- ...isSubmitting ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
604
- },
605
- onClick: handleSubmit,
606
- disabled: isSubmitting || selectedAnswer === void 0,
607
- children: isSubmitting ? /* @__PURE__ */ jsx(Spinner, { size: 16, color: "#9ca3af" }) : "Submit Quiz"
608
- }
609
- ) : /* @__PURE__ */ jsx(
610
- "button",
611
- {
612
- style: {
613
- ...defaultStyles.button,
614
- ...isNavigating || selectedAnswer === void 0 ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
615
- },
616
- onClick: handleNext,
617
- disabled: isNavigating || selectedAnswer === void 0,
618
- children: isNavigating ? /* @__PURE__ */ jsx(Spinner, { size: 16, color: "#9ca3af" }) : "Next"
619
- }
620
- )
1144
+ /* @__PURE__ */ jsx("div", { style: { ...defaultStyles.buttons, justifyContent: "flex-end" }, children: showFeedback ? (
1145
+ // After viewing feedback
1146
+ isLastQuestion ? /* @__PURE__ */ jsx(
1147
+ "button",
1148
+ {
1149
+ style: {
1150
+ ...defaultStyles.button,
1151
+ ...isSubmitting || isGeneratingExtra ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
1152
+ },
1153
+ onClick: handleSubmit,
1154
+ disabled: isSubmitting || isGeneratingExtra,
1155
+ "data-testid": "button-submit-quiz",
1156
+ children: isSubmitting ? /* @__PURE__ */ jsx(Spinner, { size: 16, color: "#9ca3af" }) : "Submit Quiz"
1157
+ }
1158
+ ) : /* @__PURE__ */ jsx(
1159
+ "button",
1160
+ {
1161
+ style: {
1162
+ ...defaultStyles.button,
1163
+ ...defaultStyles.buttonPrimary
1164
+ },
1165
+ onClick: handleContinue,
1166
+ "data-testid": "button-continue",
1167
+ children: "Continue"
1168
+ }
1169
+ )
1170
+ ) : (
1171
+ // Before checking answer
1172
+ /* @__PURE__ */ jsx(
1173
+ "button",
1174
+ {
1175
+ style: {
1176
+ ...defaultStyles.button,
1177
+ ...isNavigating || selectedAnswer === void 0 ? defaultStyles.buttonDisabled : defaultStyles.buttonPrimary
1178
+ },
1179
+ onClick: handleCheckAnswer,
1180
+ disabled: isNavigating || selectedAnswer === void 0,
1181
+ "data-testid": "button-check-answer",
1182
+ children: isNavigating ? /* @__PURE__ */ jsx(Spinner, { size: 16, color: "#9ca3af" }) : "Check Answer"
1183
+ }
1184
+ )
1185
+ ) })
621
1186
  ] })
622
1187
  ] });
623
1188
  }