Skip to content

Commit 8e2bdea

Browse files
authored
Merge pull request #226 from janezd/require-chapter-progress
Implement locking chapters before answering all questions in earlier chapters
2 parents b21d7ee + d8d0087 commit 8e2bdea

8 files changed

Lines changed: 77 additions & 19 deletions

File tree

api/book.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use server";
22

33
import db from "@/utils/db";
4-
import { BookFrontmatter, ChapterDef, LinkDesc } from "@/types";
4+
import { BookFrontmatter, ChapterDef, LinkDesc, UnlockChaptersOnAnswersOptions } from "@/types";
55
import { getPublicLink, GroupList, ItemDesc } from "@/api/content";
66
import { getCollection } from "@/api/collection";
77

@@ -104,6 +104,7 @@ export const getBook = async (id: number): Promise<BookProps> => {
104104
frontmatter: {
105105
title: book.title, subTitle: book.subtitle,
106106
requireLogin: book.requireLogin === 1, quizThreshold: book.quizThreshold,
107+
unlockChaptersOnAnswers: UnlockChaptersOnAnswersOptions[book.unlockChaptersOnAnswers],
107108
public: book.public === 1, coverImg: book.coverImg,
108109
groups,
109110
tocInHeader: book.tocInHeader === 1, language: book.language,

components/Book/Book.tsx

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { BookProps, getPublicCollection } from "@/api/book";
1010
import { isAdminFor } from "@/api/user";
1111

1212
import { logger } from "@/utils/logger";
13-
import { getT, IntlContextProvider } from "@/i18n";
13+
import { getT, IntlContextProvider, useIntl } from "@/i18n";
1414
import { UserContext } from "@/context/UserContextProvider";
15-
import { AnswerState, QuizContextProvider } from "@/context/QuizContextProvider";
15+
import { AnswerState, QuizContext, QuizContextProvider } from "@/context/QuizContextProvider";
1616
import Layout from "../Layout/Layout";
1717

1818
import Login from "../Login";
@@ -22,9 +22,51 @@ import { Chapter } from "./Chapter";
2222
import { SidenoteContext } from "@/components/Book/Sidenote";
2323
import { useHasMounted } from "@/hooks/useHasMounted";
2424
import { usePublicProvider } from "@/hooks/usePublicProvider";
25-
import { LinkDesc } from "@/types";
25+
import { ChapterDef, LinkDesc, UnlockChaptersOnAnswersType } from "@/types";
2626

2727

28+
const Chapters = ({chapters: allChapters, bookId, chapterNumbers, unlockChaptersOnAnswers, setIsChapterIndexVisible, allAnswers}:
29+
{chapters: ChapterDef[],
30+
bookId: number,
31+
chapterNumbers: Record<number, number>,
32+
unlockChaptersOnAnswers: UnlockChaptersOnAnswersType,
33+
setIsChapterIndexVisible: React.Dispatch<React.SetStateAction<Record<number, boolean>>>,
34+
allAnswers?: AnswersInBook}
35+
) => {
36+
const {chapterStats} = useContext(QuizContext);
37+
const {t} = useIntl();
38+
const chapters = [];
39+
for(let index = 0; index < allChapters.length; index++) {
40+
chapters.push(allChapters[index]);
41+
const stats = chapterStats(index);
42+
if (unlockChaptersOnAnswers !== "none"
43+
&& stats
44+
&& stats.nQuestions != (unlockChaptersOnAnswers === "attempt" ? stats.answered : stats.correct)) {
45+
break;
46+
}
47+
}
48+
return <>
49+
{chapters.map((chapterDef, index) => (
50+
<Chapter
51+
{...chapterDef}
52+
bookId={bookId}
53+
chapterId={chapterDef.chapterId}
54+
key={chapterDef.chapterPath}
55+
index={index}
56+
setIsChapterIndexVisible={setIsChapterIndexVisible}
57+
chapterNumber={chapterNumbers[index]}
58+
allAnswers={allAnswers}
59+
/>
60+
))}
61+
{ allChapters.length !== chapters.length &&
62+
<div className="chapter locked">
63+
<h2>{t("book.locked-chapter")}</h2>
64+
<p>{t(`book.locked-chapter-msg-${unlockChaptersOnAnswers}`)}</p>
65+
</div>
66+
}
67+
</>
68+
}
69+
2870
export const Book = (
2971
{ frontmatter, content, chapters, slug, bookId, previous, next }: BookProps
3072
) => {
@@ -274,18 +316,14 @@ export const Book = (
274316
/>
275317
}
276318

277-
{chapters.map((chapterDef, index) => (
278-
<Chapter
279-
{...chapterDef}
280-
bookId={bookId}
281-
chapterId={chapterDef.chapterId}
282-
key={chapterDef.chapterPath}
283-
index={index}
284-
setIsChapterIndexVisible={setIsChapterIndexVisible}
285-
chapterNumber={chapterNumbers[index]}
286-
allAnswers={showAnswers && allAnswers || undefined}
319+
<Chapters
320+
chapters={chapters}
321+
bookId={bookId}
322+
chapterNumbers={chapterNumbers}
323+
unlockChaptersOnAnswers={frontmatter.unlockChaptersOnAnswers}
324+
setIsChapterIndexVisible={setIsChapterIndexVisible}
325+
allAnswers={showAnswers && allAnswers || undefined}
287326
/>
288-
))}
289327
{ !!next && <a href={next.href} className="next-book-link">
290328
{t("book.next-in-collection")}{next.title}</a>
291329
}

i18n/dict.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const dict: {[lang: string]: {[key: string]: any}} = {
2424
"collections": "Book Collections",
2525
"book.chapter": "Chapter",
2626
"book.chapters": "Chapters",
27+
"book.locked-chapter": "(Locked chapters)",
28+
"book.locked-chapter-msg-attempt": "The rest of the book will be unlocked when you attempt all shown questions",
29+
"book.locked-chapter-msg-correct": "The rest of the book will be unlocked when you correctly answer all shown questions",
2730
"book.next-in-collection": "Next book in this collection: ",
2831
"chapter.replay": "Replay",
2932
"chapter.showanswer": "Show Answer",
@@ -129,6 +132,9 @@ const dict: {[lang: string]: {[key: string]: any}} = {
129132
"collections": "Zbirke knjig",
130133
"book.chapter": "Poglavje",
131134
"book.chapters": "Poglavja",
135+
"book.locked-chapter": "(Skrita poglavja)",
136+
"book.locked-chapter-msg-attempt": "Ostanek knjige bo prikazan, ko poskusite odgovoriti na vsa prikazana vprašanja",
137+
"book.locked-chapter-msg-correct": "Ostanek knjige bo prikazan, ko pravilno odgovorite na vsa prikazana vprašanja",
132138
"book.next-in-collection": "Naslednja knjiga v tej zbirki: ",
133139
"chapter.replay": "Pokaži ponovno",
134140
"chapter.showanswer": "Pokaži odgovor",

ingest/book.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from "fs";
22
import path from "path";
33

4-
import { RawBookFrontmatter, ChapterDefBase, ChapterFrontmatter } from "@/types";
4+
import { RawBookFrontmatter, ChapterDefBase, ChapterFrontmatter, UnlockChaptersOnAnswersOptions } from "@/types";
55

66
import { pathExists } from "./paths";
77
import { checkedMatter, CollectedDefaults, defaultsFor, getMdFile, isListOfStrings, parseMd, readPublicDirMd } from "./md-helpers";
@@ -23,6 +23,7 @@ const bookFrontmatterDefaults: RawBookFrontmatter = {
2323
coverImg: "",
2424
requireLogin: false,
2525
quizThreshold: 0,
26+
unlockChaptersOnAnswers: UnlockChaptersOnAnswersOptions[0],
2627
} satisfies RawBookFrontmatter & Record<string, unknown>;
2728

2829
const extraBookMatter = {
@@ -46,6 +47,8 @@ export const bookMatter = (indexMd: string, slug: string, defaults: Partial<RawB
4647
: "'groups' must be a list of strings, or string pairs without dashes",
4748
tokens: isListOfStrings,
4849
admins: isListOfStrings,
50+
unlockChaptersOnAnswers: (value) => !["none", "attempt", "correct"].includes(value)
51+
&& `'unlockChaptersOnAnswers' must be one of 'none', 'attempt', or 'correct'`,
4952
}
5053
);
5154

ingest/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export async function updateDb(
5757
driver: sqlite3.Database,
5858
});
5959
await db.exec("PRAGMA foreign_keys = ON");
60+
const { migrate } = await import("../utils/migrateDb.mjs");
61+
await migrate(db);
6062
let anyErrors = false;
6163

6264
let moved: [string, string][] = [];

ingest/updatePaths.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { MailPath } from "@/ingest/mail";
1212
import { InheritableResources } from "@/ingest/inheritables";
1313
import {joinedPath} from "@/ingest/paths";
1414
import {load} from "js-yaml";
15+
import { UnlockChaptersOnAnswersOptions } from "@/types";
1516

1617
const checkMoved = (moved: [string, string][]) => {
1718
moved.forEach(([from]) => {
@@ -393,7 +394,7 @@ const insertBook = async (
393394
mdxContent, chapters, slug,
394395
frontmatter: {
395396
title, subTitle, public: isPublic, language, tocInHeader,
396-
coverImg, requireLogin, quizThreshold, groups, tokens
397+
coverImg, requireLogin, quizThreshold, unlockChaptersOnAnswers, groups, tokens
397398
}
398399
} = book;
399400
const content = await serializedContent(mdxContent, language, slug);
@@ -403,9 +404,9 @@ const insertBook = async (
403404
INSERT INTO books (lastBuildId,
404405
path, title, subtitle,
405406
public, language, tocInHeader,
406-
coverImg, requireLogin, quizThreshold,
407+
coverImg, requireLogin, quizThreshold, unlockChaptersOnAnswers,
407408
content)
408-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
409+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
409410
ON CONFLICT DO UPDATE SET lastBuildId = excluded.lastBuildId,
410411
title = excluded.title,
411412
subtitle = excluded.subtitle,
@@ -415,6 +416,7 @@ const insertBook = async (
415416
coverImg = excluded.coverImg,
416417
requireLogin = excluded.requireLogin,
417418
quizThreshold = excluded.quizThreshold,
419+
unlockChaptersOnAnswers = excluded.unlockChaptersOnAnswers,
418420
content = excluded.content
419421
RETURNING id
420422
`,
@@ -423,6 +425,7 @@ const insertBook = async (
423425
slug, title, subTitle,
424426
isPublic, language, tocInHeader,
425427
coverImg, requireLogin, quizThreshold,
428+
UnlockChaptersOnAnswersOptions.indexOf(unlockChaptersOnAnswers),
426429
content
427430
]
428431
);

types/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export interface ChapterDef extends ChapterDefBase {
3434

3535

3636
/* Books */
37+
export const UnlockChaptersOnAnswersOptions = ["none", "attempt", "correct"] as const
38+
export type UnlockChaptersOnAnswersType = typeof UnlockChaptersOnAnswersOptions[number];
3739

3840
export interface BookFrontmatterBase {
3941
title: string;
@@ -44,6 +46,7 @@ export interface BookFrontmatterBase {
4446
coverImg: string;
4547
requireLogin: boolean;
4648
quizThreshold?: number;
49+
unlockChaptersOnAnswers: UnlockChaptersOnAnswersType;
4750
chapters?: string[];
4851
}
4952

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE books
2+
ADD unlockChaptersOnAnswers INTEGER DEFAULT 0;

0 commit comments

Comments
 (0)