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.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",