payload-quiz-plugin 1.1.0 → 1.2.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/README.md +85 -7
- package/dist/client.d.mts +4 -2
- package/dist/client.d.ts +4 -2
- package/dist/client.js +146 -107
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +155 -108
- package/dist/client.mjs.map +1 -1
- package/dist/quiz-theme.css +96 -0
- package/package.json +18 -6
package/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
A comprehensive quiz and test system plugin for [Payload CMS](https://payloadcms.com) v3. Create timed quizzes with multiple choice questions, automatic grading, and detailed results.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/payload-quiz-plugin)
|
|
6
|
+
[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
|
|
7
|
+
|
|
5
8
|
## Features
|
|
6
9
|
|
|
7
10
|
- **Collections**: Questions, Tests, and Certificate Types
|
|
@@ -402,15 +405,77 @@ export const Pages = {
|
|
|
402
405
|
|
|
403
406
|
## Styling
|
|
404
407
|
|
|
405
|
-
The
|
|
408
|
+
The plugin ships with a CSS theme file that defines all quiz colors as CSS custom properties with sensible defaults and dark mode support.
|
|
409
|
+
|
|
410
|
+
### Setup
|
|
411
|
+
|
|
412
|
+
Import the theme in your `globals.css`:
|
|
413
|
+
|
|
414
|
+
```css
|
|
415
|
+
@import 'payload-quiz-plugin/styles';
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
That's it. The import provides `:root` defaults and a `.dark` / `[data-theme="dark"]` override block. No Tailwind preset or `@theme` block is needed.
|
|
406
419
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
- `--primary`, `--
|
|
410
|
-
|
|
411
|
-
|
|
420
|
+
### Host project variables
|
|
421
|
+
|
|
422
|
+
Components also reference standard shadcn/ui-style variables (`--primary`, `--muted`, `--border`, etc.). Make sure your project defines those.
|
|
423
|
+
|
|
424
|
+
### Overriding colors
|
|
425
|
+
|
|
426
|
+
Override any variable in your own CSS:
|
|
427
|
+
|
|
428
|
+
```css
|
|
429
|
+
:root {
|
|
430
|
+
--quiz-success: oklch(0.72 0.19 160);
|
|
431
|
+
--quiz-error: hsl(25 95% 53%);
|
|
432
|
+
}
|
|
433
|
+
```
|
|
412
434
|
|
|
413
|
-
|
|
435
|
+
For dark mode or multi-theme setups, scope overrides to the matching selector:
|
|
436
|
+
|
|
437
|
+
```css
|
|
438
|
+
[data-theme="dark"] {
|
|
439
|
+
--quiz-success: oklch(0.55 0.15 150);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
[data-theme="ocean"] {
|
|
443
|
+
--quiz-success: oklch(0.65 0.18 180);
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### CSS variable reference
|
|
448
|
+
|
|
449
|
+
| Variable | Purpose |
|
|
450
|
+
|----------|---------|
|
|
451
|
+
| `--quiz-success` | Correct answer accent |
|
|
452
|
+
| `--quiz-success-foreground` | Text on success background |
|
|
453
|
+
| `--quiz-success-light` | Light success background |
|
|
454
|
+
| `--quiz-success-border` | Success border color |
|
|
455
|
+
| `--quiz-success-text` | Success text color |
|
|
456
|
+
| `--quiz-success-muted` | Muted success background |
|
|
457
|
+
| `--quiz-success-muted-border` | Muted success border |
|
|
458
|
+
| `--quiz-error` | Incorrect answer accent |
|
|
459
|
+
| `--quiz-error-foreground` | Text on error background |
|
|
460
|
+
| `--quiz-error-light` | Light error background |
|
|
461
|
+
| `--quiz-error-border` | Error border color |
|
|
462
|
+
| `--quiz-error-text` | Error text color |
|
|
463
|
+
| `--quiz-error-muted` | Muted error background |
|
|
464
|
+
| `--quiz-error-muted-border` | Muted error border |
|
|
465
|
+
| `--quiz-info` | Info badge / explanation accent |
|
|
466
|
+
| `--quiz-info-foreground` | Text on info background |
|
|
467
|
+
| `--quiz-info-light` | Light info background |
|
|
468
|
+
| `--quiz-info-border` | Info border color |
|
|
469
|
+
| `--quiz-info-text` | Info text color |
|
|
470
|
+
| `--quiz-warning-light` | Timer low-time background |
|
|
471
|
+
| `--quiz-warning-text` | Timer low-time text |
|
|
472
|
+
| `--quiz-choice-background` | Choice button background |
|
|
473
|
+
| `--quiz-choice-foreground` | Choice button text |
|
|
474
|
+
| `--quiz-choice-select` | Selected choice accent |
|
|
475
|
+
| `--quiz-choice-border` | Choice border color |
|
|
476
|
+
| `--quiz-choice-text` | Choice label text |
|
|
477
|
+
| `--quiz-choice-muted` | Unselected choice fill |
|
|
478
|
+
| `--quiz-choice-muted-border` | Choice indicator border |
|
|
414
479
|
|
|
415
480
|
## TypeScript
|
|
416
481
|
|
|
@@ -428,6 +493,12 @@ import type {
|
|
|
428
493
|
} from 'payload-quiz-plugin'
|
|
429
494
|
```
|
|
430
495
|
|
|
496
|
+
## Author
|
|
497
|
+
|
|
498
|
+
**Alexander Sedeke** - [@alexandrstudio](https://alexandr.studio)
|
|
499
|
+
|
|
500
|
+
Created for [Alexandr Studio](https://alexandr.studio)
|
|
501
|
+
|
|
431
502
|
## License
|
|
432
503
|
|
|
433
504
|
This project is licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/).
|
|
@@ -436,6 +507,13 @@ This project is licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/lic
|
|
|
436
507
|
- **NonCommercial** — You may not use this for commercial purposes
|
|
437
508
|
- **ShareAlike** — Derivatives must use the same license
|
|
438
509
|
|
|
510
|
+
## Support
|
|
511
|
+
|
|
512
|
+
Need help?
|
|
513
|
+
- 📖 Check the [documentation](https://github.com/alexandrstudio/payload-quiz-plugin/wiki)
|
|
514
|
+
- 🐛 Found a bug? [Open an issue](https://github.com/alexandrstudio/payload-quiz-plugin/issues)
|
|
515
|
+
- 💬 Have questions? [Discussions](https://github.com/alexandrstudio/payload-quiz-plugin/discussions)
|
|
516
|
+
|
|
439
517
|
## Contributing
|
|
440
518
|
|
|
441
519
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/client.d.mts
CHANGED
|
@@ -205,11 +205,13 @@ declare function ChoiceOption({ id, text, isSelected, isMultiple, onClick, disab
|
|
|
205
205
|
type Props = {
|
|
206
206
|
result: QuizResult;
|
|
207
207
|
testSlug: string;
|
|
208
|
+
/** Base route path for quiz pages (e.g. "/quiz", "/tests"). Defaults to "/tests" */
|
|
209
|
+
basePath?: string;
|
|
208
210
|
className?: string;
|
|
209
211
|
/** Optional custom component for rendering rich text explanations */
|
|
210
212
|
RichTextComponent?: RichTextRenderer;
|
|
211
213
|
};
|
|
212
|
-
declare function QuizResults({ result, testSlug, className, RichTextComponent }: Props): react_jsx_runtime.JSX.Element;
|
|
214
|
+
declare function QuizResults({ result, testSlug, basePath, className, RichTextComponent, }: Props): react_jsx_runtime.JSX.Element;
|
|
213
215
|
|
|
214
216
|
/**
|
|
215
217
|
* Types for TestCard component
|
|
@@ -291,7 +293,7 @@ type TestCardProps = {
|
|
|
291
293
|
* />
|
|
292
294
|
* ```
|
|
293
295
|
*/
|
|
294
|
-
declare function TestCard({ doc, title: titleFromProps, className, MediaComponent }: TestCardProps): react_jsx_runtime.JSX.Element;
|
|
296
|
+
declare function TestCard({ doc, title: titleFromProps, className, MediaComponent, }: TestCardProps): react_jsx_runtime.JSX.Element;
|
|
295
297
|
|
|
296
298
|
/**
|
|
297
299
|
* Utility function for merging Tailwind CSS classes
|
package/dist/client.d.ts
CHANGED
|
@@ -205,11 +205,13 @@ declare function ChoiceOption({ id, text, isSelected, isMultiple, onClick, disab
|
|
|
205
205
|
type Props = {
|
|
206
206
|
result: QuizResult;
|
|
207
207
|
testSlug: string;
|
|
208
|
+
/** Base route path for quiz pages (e.g. "/quiz", "/tests"). Defaults to "/tests" */
|
|
209
|
+
basePath?: string;
|
|
208
210
|
className?: string;
|
|
209
211
|
/** Optional custom component for rendering rich text explanations */
|
|
210
212
|
RichTextComponent?: RichTextRenderer;
|
|
211
213
|
};
|
|
212
|
-
declare function QuizResults({ result, testSlug, className, RichTextComponent }: Props): react_jsx_runtime.JSX.Element;
|
|
214
|
+
declare function QuizResults({ result, testSlug, basePath, className, RichTextComponent, }: Props): react_jsx_runtime.JSX.Element;
|
|
213
215
|
|
|
214
216
|
/**
|
|
215
217
|
* Types for TestCard component
|
|
@@ -291,7 +293,7 @@ type TestCardProps = {
|
|
|
291
293
|
* />
|
|
292
294
|
* ```
|
|
293
295
|
*/
|
|
294
|
-
declare function TestCard({ doc, title: titleFromProps, className, MediaComponent }: TestCardProps): react_jsx_runtime.JSX.Element;
|
|
296
|
+
declare function TestCard({ doc, title: titleFromProps, className, MediaComponent, }: TestCardProps): react_jsx_runtime.JSX.Element;
|
|
295
297
|
|
|
296
298
|
/**
|
|
297
299
|
* Utility function for merging Tailwind CSS classes
|
package/dist/client.js
CHANGED
|
@@ -296,7 +296,7 @@ function QuizTimer({ timeRemaining, className }) {
|
|
|
296
296
|
{
|
|
297
297
|
className: cn(
|
|
298
298
|
"flex items-center gap-2 px-4 py-2 rounded-lg font-mono text-lg font-medium",
|
|
299
|
-
isLowTime ? "bg-
|
|
299
|
+
isLowTime ? "bg-[var(--quiz-warning-light)] text-[var(--quiz-warning-text)]" : "bg-muted",
|
|
300
300
|
isCriticalTime && "animate-pulse",
|
|
301
301
|
className
|
|
302
302
|
),
|
|
@@ -387,11 +387,11 @@ function ChoiceOption({
|
|
|
387
387
|
onClick,
|
|
388
388
|
disabled,
|
|
389
389
|
className: cn(
|
|
390
|
-
"w-full flex items-start gap-4 p-4 rounded-xl border
|
|
391
|
-
isSelected ? "border-
|
|
390
|
+
"w-full text-left flex items-start gap-4 p-4 rounded-xl border transition-all cursor-pointer",
|
|
391
|
+
isSelected ? "border-[var(--quiz-choice-select)] bg-[var(--quiz-choice-select)]" : "border-[var(--quiz-choice-border)] hover:border-[var(--quiz-choice-select)]",
|
|
392
392
|
disabled && "cursor-not-allowed opacity-60",
|
|
393
|
-
showResult && isCorrect && "border-
|
|
394
|
-
showResult && isSelected && !isCorrect && "border-
|
|
393
|
+
showResult && isCorrect && "border-[var(--quiz-success-border)] bg-[var(--quiz-success-light)]",
|
|
394
|
+
showResult && isSelected && !isCorrect && "border-[var(--quiz-error-border)] bg-[var(--quiz-error-light)]",
|
|
395
395
|
className
|
|
396
396
|
),
|
|
397
397
|
children: [
|
|
@@ -399,15 +399,15 @@ function ChoiceOption({
|
|
|
399
399
|
"div",
|
|
400
400
|
{
|
|
401
401
|
className: cn(
|
|
402
|
-
"
|
|
402
|
+
"border size-6 rounded-full flex items-center justify-center bg-[var(--quiz-choice-muted-border)]",
|
|
403
403
|
isSelected ? "border-primary bg-primary text-primary-foreground" : "border-muted-foreground/30",
|
|
404
|
-
showResult && isCorrect && "border-
|
|
405
|
-
showResult && isSelected && !isCorrect && "border-
|
|
404
|
+
showResult && isCorrect && "border-[var(--quiz-success)] bg-[var(--quiz-success)] text-[var(--quiz-success-foreground)]",
|
|
405
|
+
showResult && isSelected && !isCorrect && "border-[var(--quiz-error)] bg-[var(--quiz-error)] text-[var(--quiz-error-foreground)]"
|
|
406
406
|
),
|
|
407
407
|
children: (isSelected || showResult && isCorrect) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.Check, { className: "size-4" })
|
|
408
408
|
}
|
|
409
409
|
),
|
|
410
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("
|
|
410
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex-1 text-base", children: text })
|
|
411
411
|
]
|
|
412
412
|
}
|
|
413
413
|
);
|
|
@@ -441,13 +441,13 @@ function QuestionCard({
|
|
|
441
441
|
{
|
|
442
442
|
className: cn(
|
|
443
443
|
"px-4 py-1 text-md font-normal rounded-xl",
|
|
444
|
-
isMultiple ? "bg-
|
|
444
|
+
isMultiple ? "bg-[var(--quiz-info)] text-[var(--quiz-info-foreground)]" : "bg-[var(--quiz-success)] text-[var(--quiz-success-foreground)]"
|
|
445
445
|
),
|
|
446
446
|
children: isMultiple ? `Select ${requiredAnswers} answers` : "Select the best answer"
|
|
447
447
|
}
|
|
448
448
|
) }),
|
|
449
449
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "prose dark:prose-invert max-w-none", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "py-8 text-2xl font-medium leading-relaxed", children: question.question }) }),
|
|
450
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "space-y-3", children: question.choices?.map((choice) => {
|
|
450
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "space-y-3 flex flex-col gap-2", children: question.choices?.map((choice) => {
|
|
451
451
|
const choiceId = choice.id || "";
|
|
452
452
|
const isSelected = selectedChoiceIds.includes(choiceId);
|
|
453
453
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
@@ -470,8 +470,16 @@ var import_react2 = require("react");
|
|
|
470
470
|
var import_lucide_react3 = require("lucide-react");
|
|
471
471
|
var import_link = __toESM(require("next/link"));
|
|
472
472
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
473
|
-
function QuizResults({
|
|
474
|
-
|
|
473
|
+
function QuizResults({
|
|
474
|
+
result,
|
|
475
|
+
testSlug,
|
|
476
|
+
basePath = "/tests",
|
|
477
|
+
className,
|
|
478
|
+
RichTextComponent
|
|
479
|
+
}) {
|
|
480
|
+
const [expandedQuestions, setExpandedQuestions] = (0, import_react2.useState)(
|
|
481
|
+
/* @__PURE__ */ new Set()
|
|
482
|
+
);
|
|
475
483
|
const toggleQuestion = (questionId) => {
|
|
476
484
|
const newExpanded = new Set(expandedQuestions);
|
|
477
485
|
if (newExpanded.has(questionId)) {
|
|
@@ -492,17 +500,17 @@ function QuizResults({ result, testSlug, className, RichTextComponent }) {
|
|
|
492
500
|
"div",
|
|
493
501
|
{
|
|
494
502
|
className: cn(
|
|
495
|
-
"rounded-2xl p-8 text-center",
|
|
496
|
-
result.passed ? "bg-
|
|
503
|
+
"rounded-2xl p-8 text-center border",
|
|
504
|
+
result.passed ? "bg-[var(--quiz-success-light)] border-[var(--quiz-success-muted-border)]" : "bg-[var(--quiz-error-light)] border-[var(--quiz-error-muted-border)]"
|
|
497
505
|
),
|
|
498
506
|
children: [
|
|
499
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mb-4", children: result.passed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Trophy, { className: "size-16 mx-auto text-
|
|
507
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mb-4", children: result.passed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Trophy, { className: "size-16 mx-auto text-[var(--quiz-success)]" }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Target, { className: "size-16 mx-auto text-[var(--quiz-error)]" }) }),
|
|
500
508
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
501
509
|
"h2",
|
|
502
510
|
{
|
|
503
511
|
className: cn(
|
|
504
512
|
"text-3xl font-bold mb-2",
|
|
505
|
-
result.passed ? "text-
|
|
513
|
+
result.passed ? "text-[var(--quiz-success-text)]" : "text-[var(--quiz-error-text)]"
|
|
506
514
|
),
|
|
507
515
|
children: result.passed ? "Congratulations!" : "Keep Practicing!"
|
|
508
516
|
}
|
|
@@ -526,7 +534,7 @@ function QuizResults({ result, testSlug, className, RichTextComponent }) {
|
|
|
526
534
|
{
|
|
527
535
|
className: cn(
|
|
528
536
|
"stroke-current",
|
|
529
|
-
result.passed ? "text-
|
|
537
|
+
result.passed ? "text-[var(--quiz-success)]" : "text-[var(--quiz-error)]"
|
|
530
538
|
),
|
|
531
539
|
strokeWidth: "8",
|
|
532
540
|
strokeLinecap: "round",
|
|
@@ -549,19 +557,19 @@ function QuizResults({ result, testSlug, className, RichTextComponent }) {
|
|
|
549
557
|
] })
|
|
550
558
|
] }),
|
|
551
559
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4 max-w-2xl mx-auto", children: [
|
|
552
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "bg-
|
|
553
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-2xl font-bold text-
|
|
560
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "bg-[var(--quiz-choice-background)] rounded-lg p-4", children: [
|
|
561
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-2xl font-bold text-[var(--quiz-success-text)]", children: result.correctAnswers }),
|
|
554
562
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-sm text-muted-foreground", children: "Correct" })
|
|
555
563
|
] }),
|
|
556
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "bg-
|
|
557
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-2xl font-bold text-
|
|
564
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "bg-[var(--quiz-choice-background)] rounded-lg p-4", children: [
|
|
565
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-2xl font-bold text-[var(--quiz-error-text)]", children: result.incorrectAnswers }),
|
|
558
566
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-sm text-muted-foreground", children: "Incorrect" })
|
|
559
567
|
] }),
|
|
560
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "bg-
|
|
568
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "bg-[var(--quiz-choice-background)] rounded-lg p-4", children: [
|
|
561
569
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-2xl font-bold text-muted-foreground", children: result.unanswered }),
|
|
562
570
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-sm text-muted-foreground", children: "Unanswered" })
|
|
563
571
|
] }),
|
|
564
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "bg-
|
|
572
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "bg-[var(--quiz-choice-background)] rounded-lg p-4", children: [
|
|
565
573
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "text-2xl font-bold flex items-center justify-center gap-1", children: [
|
|
566
574
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Clock, { className: "size-5" }),
|
|
567
575
|
formatTime(result.timeTaken)
|
|
@@ -576,7 +584,7 @@ function QuizResults({ result, testSlug, className, RichTextComponent }) {
|
|
|
576
584
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
577
585
|
import_link.default,
|
|
578
586
|
{
|
|
579
|
-
href:
|
|
587
|
+
href: `${basePath}/${testSlug}/start`,
|
|
580
588
|
className: "inline-flex items-center justify-center gap-2 px-6 py-3 bg-primary text-primary-foreground font-medium rounded-xl hover:bg-primary/90 transition-colors",
|
|
581
589
|
children: "Try Again"
|
|
582
590
|
}
|
|
@@ -584,14 +592,14 @@ function QuizResults({ result, testSlug, className, RichTextComponent }) {
|
|
|
584
592
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
585
593
|
import_link.default,
|
|
586
594
|
{
|
|
587
|
-
href:
|
|
595
|
+
href: basePath,
|
|
588
596
|
className: "inline-flex items-center justify-center gap-2 px-6 py-3 bg-muted text-foreground font-medium rounded-xl hover:bg-muted/80 transition-colors",
|
|
589
597
|
children: "Back to Tests"
|
|
590
598
|
}
|
|
591
599
|
)
|
|
592
600
|
] }),
|
|
593
601
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "space-y-4", children: [
|
|
594
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "text-xl font-semibold", children: "Review Your Answers" }),
|
|
602
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "text-xl font-semibold mb-4", children: "Review Your Answers" }),
|
|
595
603
|
result.questionResults.map((questionResult, index) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
596
604
|
QuestionReview,
|
|
597
605
|
{
|
|
@@ -613,82 +621,103 @@ function QuestionReview({
|
|
|
613
621
|
onToggle,
|
|
614
622
|
RichTextComponent
|
|
615
623
|
}) {
|
|
616
|
-
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
625
|
+
"div",
|
|
626
|
+
{
|
|
627
|
+
className: cn(
|
|
628
|
+
"border border-border rounded-xl overflow-hidden",
|
|
629
|
+
isExpanded ? "bg-[var(--quiz-choice-background)]" : "bg-transparent"
|
|
630
|
+
),
|
|
631
|
+
children: [
|
|
632
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
633
|
+
"button",
|
|
634
|
+
{
|
|
635
|
+
onClick: onToggle,
|
|
636
|
+
className: cn(
|
|
637
|
+
"w-full flex items-center gap-4 p-4 text-left transition-colors",
|
|
638
|
+
isExpanded ? "bg-[var(--quiz-choise-muted)]" : "bg-[var(--quiz-choice-background)] hover:bg-[var(--quiz-choise-muted)]"
|
|
639
|
+
),
|
|
640
|
+
children: [
|
|
641
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
642
|
+
"div",
|
|
643
|
+
{
|
|
644
|
+
className: cn(
|
|
645
|
+
"flex-shrink-0 size-8 rounded-full flex items-center justify-center",
|
|
646
|
+
questionResult.isCorrect ? "bg-[var(--quiz-success-muted)] text-[var(--quiz-success-text)]" : "bg-[var(--quiz-error-muted)] text-[var(--quiz-error-text)]"
|
|
647
|
+
),
|
|
648
|
+
children: questionResult.isCorrect ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Check, { className: "size-5" }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.X, { className: "size-5" })
|
|
649
|
+
}
|
|
650
|
+
),
|
|
651
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex-1 min-w-0", children: [
|
|
652
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "text-sm text-muted-foreground mb-1", children: [
|
|
653
|
+
"Question ",
|
|
654
|
+
index + 1
|
|
655
|
+
] }),
|
|
656
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "font-medium truncate", children: questionResult.question })
|
|
657
|
+
] }),
|
|
658
|
+
isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.ChevronUp, { className: "size-5 text-muted-foreground" }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.ChevronDown, { className: "size-5 text-muted-foreground" })
|
|
659
|
+
]
|
|
660
|
+
}
|
|
624
661
|
),
|
|
625
|
-
children: [
|
|
626
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
662
|
+
isExpanded && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "p-4 border-t border-border space-y-4 ", children: [
|
|
663
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mt-4 mb-8 text-lg", children: questionResult.question }),
|
|
664
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex flex-col gap-4", children: questionResult.choices.map((choice) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
627
665
|
"div",
|
|
628
666
|
{
|
|
629
667
|
className: cn(
|
|
630
|
-
"flex-
|
|
631
|
-
|
|
668
|
+
"flex items-start gap-3 p-3 rounded-lg border",
|
|
669
|
+
choice.isCorrect && "bg-[var(--quiz-success-light)] border-[var(--quiz-success-muted-border)]",
|
|
670
|
+
choice.wasSelected && !choice.isCorrect && "bg-[var(--quiz-error-light)] border-[var(--quiz-error-muted-border)]",
|
|
671
|
+
!choice.isCorrect && !choice.wasSelected && "bg-transparent border-transparent"
|
|
632
672
|
),
|
|
633
|
-
children:
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-lg", children: questionResult.question }),
|
|
649
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "space-y-2", children: questionResult.choices.map((choice) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
650
|
-
"div",
|
|
651
|
-
{
|
|
652
|
-
className: cn(
|
|
653
|
-
"flex items-start gap-3 p-3 rounded-lg border",
|
|
654
|
-
choice.isCorrect && "bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800",
|
|
655
|
-
choice.wasSelected && !choice.isCorrect && "bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800",
|
|
656
|
-
!choice.isCorrect && !choice.wasSelected && "bg-muted/30 border-border"
|
|
657
|
-
),
|
|
658
|
-
children: [
|
|
659
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
660
|
-
"div",
|
|
661
|
-
{
|
|
662
|
-
className: cn(
|
|
663
|
-
"flex-shrink-0 size-6 rounded-full flex items-center justify-center",
|
|
664
|
-
choice.isCorrect && "bg-green-500 text-white",
|
|
665
|
-
choice.wasSelected && !choice.isCorrect && "bg-red-500 text-white",
|
|
666
|
-
!choice.isCorrect && !choice.wasSelected && "bg-muted"
|
|
673
|
+
children: [
|
|
674
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
675
|
+
"div",
|
|
676
|
+
{
|
|
677
|
+
className: cn(
|
|
678
|
+
"flex-shrink-0 size-6 rounded-full flex items-center justify-center",
|
|
679
|
+
choice.isCorrect && "bg-[var(--quiz-success)] text-[var(--quiz-success-foreground)]",
|
|
680
|
+
choice.wasSelected && !choice.isCorrect && "bg-[var(--quiz-error)] text-[var(--quiz-error-foreground)]",
|
|
681
|
+
!choice.isCorrect && !choice.wasSelected && "bg-[var(--quiz-choice-muted)]"
|
|
682
|
+
),
|
|
683
|
+
children: [
|
|
684
|
+
choice.isCorrect && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Check, { className: "size-4" }),
|
|
685
|
+
choice.wasSelected && !choice.isCorrect && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.X, { className: "size-4" })
|
|
686
|
+
]
|
|
687
|
+
}
|
|
667
688
|
),
|
|
668
|
-
children: [
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
689
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex-1", children: [
|
|
690
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
691
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: choice.text }),
|
|
692
|
+
choice.wasSelected && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-xs px-2 py-0.5 bg-muted rounded-full text-nowrap", children: "Your answer" }),
|
|
693
|
+
choice.isCorrect && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-xs px-2 py-0.5 bg-[var(--quiz-success-muted)] text-[var(--quiz-success-text)] rounded-full", children: "Correct" })
|
|
694
|
+
] }),
|
|
695
|
+
choice.explanation && RichTextComponent && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mt-2 text-sm text-muted-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
696
|
+
RichTextComponent,
|
|
697
|
+
{
|
|
698
|
+
data: choice.explanation,
|
|
699
|
+
enableGutter: false
|
|
700
|
+
}
|
|
701
|
+
) })
|
|
702
|
+
] })
|
|
703
|
+
]
|
|
704
|
+
},
|
|
705
|
+
choice.id
|
|
706
|
+
)) }),
|
|
707
|
+
questionResult.explanation && RichTextComponent && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-4 p-4 bg-[var(--quiz-info-light)] rounded-lg border border-[var(--quiz-info-border)]", children: [
|
|
708
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "font-medium text-[var(--quiz-info-text)] mb-2", children: "Explanation" }),
|
|
709
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-sm", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
710
|
+
RichTextComponent,
|
|
711
|
+
{
|
|
712
|
+
data: questionResult.explanation,
|
|
713
|
+
enableGutter: false
|
|
672
714
|
}
|
|
673
|
-
)
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
] }),
|
|
680
|
-
choice.explanation && RichTextComponent && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mt-2 text-sm text-muted-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichTextComponent, { data: choice.explanation, enableGutter: false }) })
|
|
681
|
-
] })
|
|
682
|
-
]
|
|
683
|
-
},
|
|
684
|
-
choice.id
|
|
685
|
-
)) }),
|
|
686
|
-
questionResult.explanation && RichTextComponent && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800", children: [
|
|
687
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "font-medium text-blue-700 dark:text-blue-400 mb-2", children: "Explanation" }),
|
|
688
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-sm", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichTextComponent, { data: questionResult.explanation, enableGutter: false }) })
|
|
689
|
-
] })
|
|
690
|
-
] })
|
|
691
|
-
] });
|
|
715
|
+
) })
|
|
716
|
+
] })
|
|
717
|
+
] })
|
|
718
|
+
]
|
|
719
|
+
}
|
|
720
|
+
);
|
|
692
721
|
}
|
|
693
722
|
|
|
694
723
|
// src/components/TestCard/TestCard.tsx
|
|
@@ -698,18 +727,28 @@ var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
|
698
727
|
function DefaultMediaPlaceholder() {
|
|
699
728
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "h-48 bg-muted flex items-center justify-center" });
|
|
700
729
|
}
|
|
701
|
-
function TestCard({
|
|
730
|
+
function TestCard({
|
|
731
|
+
doc,
|
|
732
|
+
title: titleFromProps,
|
|
733
|
+
className,
|
|
734
|
+
MediaComponent
|
|
735
|
+
}) {
|
|
702
736
|
const cardRef = (0, import_react3.useRef)(null);
|
|
703
737
|
const linkRef = (0, import_react3.useRef)(null);
|
|
704
|
-
const handleCardClick = (0, import_react3.useCallback)(
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
738
|
+
const handleCardClick = (0, import_react3.useCallback)((e) => {
|
|
739
|
+
const target = e.target;
|
|
740
|
+
if (target.tagName === "A" || target.tagName === "BUTTON") return;
|
|
741
|
+
linkRef.current?.click();
|
|
742
|
+
}, []);
|
|
743
|
+
const {
|
|
744
|
+
slug,
|
|
745
|
+
certificateType,
|
|
746
|
+
meta,
|
|
747
|
+
title,
|
|
748
|
+
questionCount,
|
|
749
|
+
timeLimit,
|
|
750
|
+
passMark
|
|
751
|
+
} = doc || {};
|
|
713
752
|
const { description, image: metaImage } = meta || {};
|
|
714
753
|
const titleToUse = titleFromProps || title;
|
|
715
754
|
const sanitizedDescription = description?.replace(/\s/g, " ");
|
|
@@ -720,7 +759,7 @@ function TestCard({ doc, title: titleFromProps, className, MediaComponent }) {
|
|
|
720
759
|
"article",
|
|
721
760
|
{
|
|
722
761
|
className: cn(
|
|
723
|
-
"border border-border rounded-lg overflow-hidden bg-
|
|
762
|
+
"border border-border rounded-lg overflow-hidden bg-[var(--quiz-choice-background)] hover:cursor-pointer",
|
|
724
763
|
className
|
|
725
764
|
),
|
|
726
765
|
ref: cardRef,
|