question-bank-framework 0.3.0 → 0.4.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.
|
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
4
4
|
import { Link, useParams } from '@tanstack/react-router';
|
|
5
5
|
import { Check, Pencil, X } from 'lucide-react';
|
|
6
6
|
import { useBank } from '../../core/config.tsx';
|
|
7
|
-
import type { QuestionStatus, QuizAnswer, QuizSession } from '../../core/types.ts';
|
|
7
|
+
import type { QuestionStatus, QuestionStatusMap, QuizAnswer, QuizSession } from '../../core/types.ts';
|
|
8
8
|
import {
|
|
9
9
|
getCorrectIds,
|
|
10
10
|
isAnswerCorrect,
|
|
@@ -19,12 +19,25 @@ import { Badge } from './ui/badge';
|
|
|
19
19
|
import { Button } from './ui/button';
|
|
20
20
|
import { Input } from './ui/input';
|
|
21
21
|
import { MarkdownContent } from './markdown-content';
|
|
22
|
-
import { setQuestionStatus } from '../lib/question-status-storage.ts';
|
|
22
|
+
import { setQuestionStatus, getQuestionStatuses } from '../lib/question-status-storage.ts';
|
|
23
|
+
import { questionStatusLabels } from '../lib/question-filter.ts';
|
|
23
24
|
import { getQuiz, saveQuizLocally, saveQuizProgress } from '../lib/quiz-storage.ts';
|
|
24
25
|
import { cn } from '../lib/utils.ts';
|
|
25
26
|
|
|
26
27
|
const optionLetter = (index: number): string => String.fromCharCode(65 + index);
|
|
27
28
|
|
|
29
|
+
const questionStatusStyles: Record<QuestionStatus, string> = {
|
|
30
|
+
unanswered: 'border-gray-300 bg-gray-50 text-gray-700',
|
|
31
|
+
revisit: 'border-amber-300 bg-amber-50 text-amber-900',
|
|
32
|
+
complete: 'border-green-300 bg-green-50 text-green-800',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const questionStatusDotStyles: Record<QuestionStatus, string> = {
|
|
36
|
+
unanswered: 'border-gray-300 bg-white text-gray-600 hover:border-gray-500',
|
|
37
|
+
revisit: 'border-amber-400 bg-amber-100 text-amber-900 hover:border-amber-600',
|
|
38
|
+
complete: 'border-green-500 bg-green-100 text-green-900 hover:border-green-700',
|
|
39
|
+
};
|
|
40
|
+
|
|
28
41
|
export function QuizPage() {
|
|
29
42
|
const bank = useBank();
|
|
30
43
|
const { quizId } = useParams({ from: '/quiz/$quizId' });
|
|
@@ -35,6 +48,7 @@ export function QuizPage() {
|
|
|
35
48
|
const [checked, setChecked] = useState<Record<string, boolean>>({});
|
|
36
49
|
const [editingTitle, setEditingTitle] = useState(false);
|
|
37
50
|
const [titleDraft, setTitleDraft] = useState('');
|
|
51
|
+
const [statusMap, setStatusMap] = useState<QuestionStatusMap>({});
|
|
38
52
|
const titleInputRef = useRef<HTMLInputElement>(null);
|
|
39
53
|
const quizQuestions = useMemo(
|
|
40
54
|
() =>
|
|
@@ -53,6 +67,9 @@ export function QuizPage() {
|
|
|
53
67
|
}).finally(() => {
|
|
54
68
|
if (active) setLoading(false);
|
|
55
69
|
});
|
|
70
|
+
getQuestionStatuses(bank.namespace).then((statuses) => {
|
|
71
|
+
if (active) setStatusMap(statuses);
|
|
72
|
+
});
|
|
56
73
|
return () => {
|
|
57
74
|
active = false;
|
|
58
75
|
};
|
|
@@ -123,6 +140,7 @@ export function QuizPage() {
|
|
|
123
140
|
const updateQuestionStatus = (status: QuestionStatus) => {
|
|
124
141
|
if (!activeQuestion) return;
|
|
125
142
|
void setQuestionStatus(bank.namespace, activeQuestion.id, status);
|
|
143
|
+
setStatusMap((current) => ({ ...current, [activeQuestion.id]: status }));
|
|
126
144
|
setShownAnswerId(null);
|
|
127
145
|
setSession((current) =>
|
|
128
146
|
current
|
|
@@ -172,6 +190,7 @@ export function QuizPage() {
|
|
|
172
190
|
const isChecked = checked[activeQuestion.id] === true;
|
|
173
191
|
const correctness = isChecked ? isAnswerCorrect(activeQuestion, answer) : null;
|
|
174
192
|
const correctIds = getCorrectIds(activeQuestion);
|
|
193
|
+
const activeStatus: QuestionStatus = statusMap[activeQuestion.id] ?? 'unanswered';
|
|
175
194
|
const score = scoreAnswers(quizQuestions, session.answers);
|
|
176
195
|
|
|
177
196
|
return (
|
|
@@ -233,6 +252,35 @@ export function QuizPage() {
|
|
|
233
252
|
)}
|
|
234
253
|
</div>
|
|
235
254
|
|
|
255
|
+
{quizQuestions.length > 1 && (
|
|
256
|
+
<nav aria-label="Questions in this quiz">
|
|
257
|
+
<ol className="m-0 flex list-none flex-wrap gap-1.5 p-0">
|
|
258
|
+
{quizQuestions.map((question, index) => {
|
|
259
|
+
const status = statusMap[question.id] ?? 'unanswered';
|
|
260
|
+
const isActive = index === activeIndex;
|
|
261
|
+
return (
|
|
262
|
+
<li key={question.id}>
|
|
263
|
+
<button
|
|
264
|
+
type="button"
|
|
265
|
+
onClick={() => goToQuestion(index)}
|
|
266
|
+
aria-label={`Go to question ${index + 1}: ${question.title} — ${questionStatusLabels[status]}${isActive ? ' (current question)' : ''}`}
|
|
267
|
+
aria-current={isActive ? 'true' : undefined}
|
|
268
|
+
title={`${question.title} — ${questionStatusLabels[status]}`}
|
|
269
|
+
className={cn(
|
|
270
|
+
'flex size-8 items-center justify-center rounded-md border text-xs font-semibold tabular-nums transition-colors',
|
|
271
|
+
questionStatusDotStyles[status],
|
|
272
|
+
isActive && 'ring-2 ring-gray-900 ring-offset-2',
|
|
273
|
+
)}
|
|
274
|
+
>
|
|
275
|
+
{index + 1}
|
|
276
|
+
</button>
|
|
277
|
+
</li>
|
|
278
|
+
);
|
|
279
|
+
})}
|
|
280
|
+
</ol>
|
|
281
|
+
</nav>
|
|
282
|
+
)}
|
|
283
|
+
|
|
236
284
|
{session.completed && score.gradeable > 0 && (
|
|
237
285
|
<section aria-label="Quiz results" className="rounded-xl border border-gray-200 bg-gray-50 p-5">
|
|
238
286
|
<h2 className="m-0 text-lg font-semibold text-gray-900">
|
|
@@ -276,6 +324,9 @@ export function QuizPage() {
|
|
|
276
324
|
<div className="flex flex-wrap items-center gap-2">
|
|
277
325
|
<h2 className="m-0 flex-1 text-xl font-semibold text-gray-900">{activeQuestion.title}</h2>
|
|
278
326
|
<Badge variant="outline">{questionKindLabels[kind]}</Badge>
|
|
327
|
+
<Badge variant="outline" className={questionStatusStyles[activeStatus]}>
|
|
328
|
+
{questionStatusLabels[activeStatus]}
|
|
329
|
+
</Badge>
|
|
279
330
|
</div>
|
|
280
331
|
<p className="mt-1 font-mono text-xs text-gray-500">{activeQuestion.id}</p>
|
|
281
332
|
{activeQuestion.tags.length > 0 && (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "question-bank-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Framework-style question bank: TanStack Start + TanStack Table grid, quizzes, and filter DSL. Bring your own bank.config.ts + content/. Eject any framework/ file with `qb reveal` (Vike-style).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|