Add the Next.js web app and shared lesson content.

Move lessons out of the C sources into JSON so the web client can use the same material as the desktop app, and point release builds at desktop/.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
FBukovina 2026-09-17 16:06:15 +02:00
commit f7470d333c
113 changed files with 25817 additions and 9 deletions

114
tools/extract/cetba.mjs Normal file
View file

@ -0,0 +1,114 @@
import { readFileSync } from 'node:fs';
import { parseArray, parseArrayInFunction, trimNulls } from '../lib/cparse.mjs';
/**
* Maturita reading list ("Četba"): one lesson per book.
*
* Each book page shows four reference cards followed by two exercises, so the
* cards become the lesson's theory slides and the quiz and plot ordering
* become its two tasks.
*/
/**
* Lesson numbers continue after Mluvnice, so every Czech lesson has a distinct
* number for the statistics page; the reading path numbers its own nodes.
*/
const BOOKS = [
{
id: 'orwell-1984',
number: 2,
title: 'George Orwell – 1984',
page: 'build_cetba1984_page',
quiz: 'lit_qs',
plot: 'lit_plot_items',
plotMeaning: 'lit_plot_meaning',
cards: [
['about', 'O knize'],
['world', 'Svět a strana'],
['people', 'Postavy'],
['terms', 'Klíčové pojmy'],
],
},
{
id: 'fuks-spalovac-mrtvol',
number: 3,
title: 'Ladislav Fuks – Spalovač mrtvol',
page: 'build_cetba_fuks_page',
quiz: 'fuks_qs',
plot: 'fuks_plot_items',
plotMeaning: 'fuks_plot_meaning',
cards: [
['about', 'O knize'],
['world', 'Doba a svět'],
['people', 'Postavy'],
['terms', 'Klíčové motivy'],
],
},
];
/** `LitQ`: question, opts[4], correct, explanation. */
function litChoice(row, label) {
const [prompt, options, correct, expl] = row;
const list = trimNulls(options ?? []).map(String);
if (typeof correct !== 'number' || correct < 0 || correct >= list.length) {
throw new Error(`${label}: correct index ${correct} out of range for ${list.length} options`);
}
return {
kind: 'choice',
prompt: String(prompt),
options: list,
correct,
...(expl ? { hint: String(expl) } : {}),
};
}
export function extractCetba(desktopDir) {
const src = readFileSync(`${desktopDir}/cetba.c`, 'utf8');
return BOOKS.map((book) => {
const slides = book.cards.map(([arrayName, cardTitle]) => ({
kicker: book.title,
title: cardTitle,
tip: null,
lines: trimNulls(parseArrayInFunction(src, book.page, arrayName)).map(String),
}));
const plotMeanings = parseArray(src, book.plotMeaning).map(String);
return {
id: book.id,
subject: 'czech',
number: book.number,
title: { cs: book.title, en: book.title },
subtitle: {
cs: 'Projděte si přehled a pak si vyzkoušejte kvíz a řazení děje.',
en: 'Read the overview, then try the quiz and the plot ordering.',
},
slides,
tasks: [
{
number: 1,
title: 'Kvíz',
heading: 'Kvíz ke knize',
subtitleKey: 'lit_quiz_sub',
items: parseArray(src, book.quiz)
.map((row, i) => litChoice(row, `${book.id} q${i + 1}`)),
},
{
number: 2,
title: 'Děj',
heading: 'Seřaďte děj',
subtitleKey: 'lit_plot_sub',
items: parseArray(src, book.plot).map(([prompt, words], i) => ({
kind: 'assembly',
prompt: prompt ? String(prompt) : '',
words: trimNulls(words).map(String),
...(plotMeanings[i] ? { meaning: plotMeanings[i] } : {}),
})),
},
],
};
});
}