Skip to content

Commit 4d81b7d

Browse files
FyreByrdjoslane
andauthored
Migrate Quizzes to Svelte 5 and TS (with minor improvements) (#958)
* Feature/migrate quizzes to Svelte 5 - Replaces run() with effect() for reactivity - Uses $state and $derived for all dynamic values - Handles audio playback, quiz progression, and result saving - Ensures full client-side functionality without server code * Fix quiz type definitions * Convert quiz page load to TS * Convert quiz page to TS * Refactor and simplify quiz methods * CR feedback * Fix svelte-check error * Fix IndexedDB types --------- Co-authored-by: joslane <josiahjlane@gmail.com>
1 parent a512e48 commit 4d81b7d

6 files changed

Lines changed: 303 additions & 417 deletions

File tree

config/index.d.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export type BookConfig = {
6363
hashedFileName?: string; // currently just for HTML books
6464
audio: BookCollectionAudioConfig[];
6565
features: any;
66-
quizFeatures?: any;
66+
quizFeatures?: Record<string, string | boolean | number>;
6767
footer?: HTML;
6868
style?: StyleConfig;
6969
styles?: {
@@ -335,3 +335,47 @@ export type DictionaryConfig = AppConfig & {
335335
};
336336
}[];
337337
};
338+
339+
export type QuizExplanation = {
340+
text?: string;
341+
audio?: string;
342+
};
343+
344+
export type QuizAnswer = {
345+
//\aw or \ar
346+
correct: boolean;
347+
text?: string;
348+
image?: string;
349+
audio?: string;
350+
explanation?: QuizExplanation;
351+
// field is used extensively in UI, adding here for type-safety
352+
clicked?: boolean;
353+
};
354+
355+
export type QuizQuestion = {
356+
//\qu
357+
text: string;
358+
image?: string;
359+
audio?: string;
360+
columns?: number; //\ac
361+
explanation?: QuizExplanation;
362+
answers: QuizAnswer[];
363+
};
364+
365+
export type Quiz = {
366+
id: string; //\id
367+
name?: string; //\qn
368+
shortName?: string; //\qs
369+
rightAnswerAudio?: string[]; //\ra
370+
wrongAnswerAudio?: string[]; //\wa
371+
questions: QuizQuestion[];
372+
scoreMessageBefore?: string; //\sb
373+
scoreMessageAfter?: string; //\sa
374+
commentary?: {
375+
//\sc
376+
rangeMin: number;
377+
rangeMax?: number;
378+
message: string;
379+
}[];
380+
passScore?: number; //\pm
381+
};

convert/convertBooks.ts

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33

44
import * as fs from 'fs';
55
import path, { basename, extname, join } from 'path';
6-
import type { BookConfig, BookTabConfig, ScriptureConfig } from '$config';
6+
import type {
7+
BookConfig,
8+
BookTabConfig,
9+
Quiz,
10+
QuizAnswer,
11+
QuizExplanation,
12+
QuizQuestion,
13+
ScriptureConfig
14+
} from '$config';
715
import { freeze, postQueries, queries } from '../sab-proskomma-tools';
816
import { SABProskomma } from '../src/lib/sab-proskomma';
917
import type { ConfigTaskOutput } from './convertConfig';
@@ -599,48 +607,6 @@ export async function convertBooks(
599607
};
600608
}
601609

602-
export type QuizExplanation = {
603-
text?: string;
604-
audio?: string;
605-
};
606-
607-
export type QuizAnswer = {
608-
//\aw or \ar
609-
correct: boolean;
610-
text?: string;
611-
image?: string;
612-
audio?: string;
613-
explanation?: QuizExplanation;
614-
};
615-
616-
export type QuizQuestion = {
617-
//\qu
618-
text: string;
619-
image?: string;
620-
audio?: string;
621-
columns?: number; //\ac
622-
explanation?: QuizExplanation;
623-
answers: QuizAnswer[];
624-
};
625-
626-
export type Quiz = {
627-
id: string; //\id
628-
name?: string; //\qn
629-
shortName?: string; //\qs
630-
rightAnswerAudio?: string[]; //\ra
631-
wrongAnswerAudio?: string[]; //\wa
632-
questions: QuizQuestion[];
633-
scoreMessageBefore?: string; //\sb
634-
scoreMessageAfter?: string; //\sa
635-
commentary?: {
636-
//\sc
637-
rangeMin: number;
638-
rangeMax?: number;
639-
message: string;
640-
}[];
641-
passScore?: number; //\pm
642-
};
643-
644610
function convertHtmlBook(context: ConvertBookContext, book: BookConfig, files: any[]) {
645611
const srcFile = path.join(context.dataDir, 'books', context.bcId, book.file);
646612

src/lib/data/quiz.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ScriptureConfig } from '$config';
12
import config from '$lib/data/config';
23
import { openDB, type DBSchema } from 'idb';
34

@@ -16,13 +17,13 @@ interface Quiz extends DBSchema {
1617
key: number;
1718
value: QuizScore;
1819
indexes: {
19-
'collection, book': string;
20+
'collection, book': [string, string];
2021
date: number;
2122
};
2223
};
2324
}
2425

25-
let quizDB = null;
26+
let quizDB: Awaited<ReturnType<typeof openDB<Quiz>>> | null = null;
2627
async function openQuiz() {
2728
if (!quizDB) {
2829
quizDB = await openDB<Quiz>('quiz', 1, {
@@ -53,15 +54,18 @@ export async function addQuiz(item: {
5354
return;
5455
}
5556
const date = new Date()[Symbol.toPrimitive]('number');
56-
const bookCollection = config.bookCollections.find((x) => x.id === item.collection);
57-
const bookIndex = bookCollection.books.findIndex((x) => x.id === item.book);
57+
const scriptConfig = config as ScriptureConfig;
58+
const bookCollection = scriptConfig.bookCollections?.find((x) => x.id === item.collection);
59+
const bookIndex = bookCollection?.books.findIndex((x) => x.id === item.book);
5860

59-
const nextItem = {
60-
...item,
61-
date: date,
62-
bookIndex: bookIndex
63-
};
64-
await quiz.add('quiz', nextItem);
61+
if (bookIndex !== undefined && bookIndex >= 0) {
62+
const nextItem = {
63+
...item,
64+
date: date,
65+
bookIndex: bookIndex
66+
};
67+
await quiz.add('quiz', nextItem);
68+
}
6569
} catch (error) {
6670
console.error('Error adding quiz result:', error);
6771
}
@@ -81,8 +85,8 @@ export async function findQuiz(item: { collection: string; book: string }) {
8185
return result;
8286
}
8387

84-
export async function checkQuizAccess(quizId) {
88+
export async function checkQuizAccess(quizId: string) {
8589
const quiz = await openQuiz();
86-
const result = await quiz.getAllFromIndex('quiz', 'collection, book');
90+
const result: QuizScore[] = await quiz.getAllFromIndex('quiz', 'collection, book');
8791
return result.some((item) => item.book === quizId && item.pass);
8892
}

0 commit comments

Comments
 (0)