mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
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:
parent
4708c69d9c
commit
f7470d333c
113 changed files with 25817 additions and 9 deletions
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
|
|
@ -27,6 +27,7 @@ jobs:
|
|||
- name: Build AppImage
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
working-directory: desktop
|
||||
run: |
|
||||
chmod +x scripts/bundle-linux.sh
|
||||
./scripts/bundle-linux.sh
|
||||
|
|
@ -35,7 +36,7 @@ jobs:
|
|||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maturita-linux
|
||||
path: dist/maturita-linux-*.AppImage
|
||||
path: desktop/dist/maturita-linux-*.AppImage
|
||||
|
||||
windows:
|
||||
name: Windows zip
|
||||
|
|
@ -59,6 +60,7 @@ jobs:
|
|||
mingw-w64-ucrt-x86_64-make
|
||||
|
||||
- name: Build and bundle
|
||||
working-directory: desktop
|
||||
run: |
|
||||
make bundle
|
||||
cd dist
|
||||
|
|
@ -71,7 +73,7 @@ jobs:
|
|||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maturita-windows
|
||||
path: dist-out/maturita-windows-x64.zip
|
||||
path: desktop/dist-out/maturita-windows-x64.zip
|
||||
|
||||
macos:
|
||||
name: macOS app
|
||||
|
|
@ -86,6 +88,7 @@ jobs:
|
|||
- name: Build .app zip
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
working-directory: desktop
|
||||
run: |
|
||||
chmod +x scripts/bundle-macos.sh
|
||||
./scripts/bundle-macos.sh
|
||||
|
|
@ -94,7 +97,7 @@ jobs:
|
|||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maturita-macos
|
||||
path: dist/maturita-macos-*.zip
|
||||
path: desktop/dist/maturita-macos-*.zip
|
||||
|
||||
publish:
|
||||
name: Publish GitHub Release
|
||||
|
|
|
|||
13
.gitignore
vendored
13
.gitignore
vendored
|
|
@ -18,6 +18,19 @@ build/
|
|||
|
||||
# Generated from assets/app-icon.png (Makefile / CMake POST_BUILD)
|
||||
assets/icons/
|
||||
desktop/assets/icons/
|
||||
desktop/maturita
|
||||
|
||||
# ---> Node / Next.js (web/)
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
.turbo/
|
||||
*.tsbuildinfo
|
||||
.env*.local
|
||||
.vercel
|
||||
# Generated by `next dev` for AI agents (same reason CLAUDE.md is ignored above)
|
||||
web/AGENTS.md
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
|
|
|
|||
84
README.md
84
README.md
|
|
@ -20,7 +20,7 @@ GTK 4 interface.
|
|||
|
||||
| Welcome screen | Subjects ("Předměty") | German learning roadmap |
|
||||
| --------------------------------------- | ---------------------------------------- | --------------------------------------- |
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|
||||
### Features
|
||||
|
||||
|
|
@ -218,10 +218,30 @@ sudo pacman -S base-devel pkgconf
|
|||
Windows – `gcc`, GTK 4 and `pkg-config` are installed in step 1 of the
|
||||
Windows guide below. Visual Studio is not required.
|
||||
|
||||
#### Building the application
|
||||
#### Running the web app
|
||||
|
||||
The web client needs Node.js 20 or newer and no GTK at all:
|
||||
|
||||
```bash
|
||||
npm run web:install # installs web/ dependencies
|
||||
npm run web # dev server on http://localhost:3000
|
||||
npm run web:build # production build
|
||||
```
|
||||
|
||||
It reads its lessons from `content/`, which is exported from the C sources by
|
||||
`npm run content`. Themes, translations, exercises and progress tracking all
|
||||
mirror the desktop app; progress is kept in the browser's localStorage instead of
|
||||
`progress/*.conf`.
|
||||
|
||||
Coverage matches the desktop client: German units 1–3 with their vocabulary
|
||||
trainers, all 27 network lessons, the three hardware lessons, the 20 Czech
|
||||
grammar exercises and both reading-list books.
|
||||
|
||||
#### Building the desktop application
|
||||
|
||||
1. Clone or download this repository
|
||||
2. Navigate to the project directory
|
||||
2. Navigate to the `desktop/` directory — every command in this section runs
|
||||
there, since the C sources and the Makefile live inside it
|
||||
3. Build with the Makefile:
|
||||
|
||||
```bash
|
||||
|
|
@ -378,6 +398,22 @@ Completed network units are saved to `progress/net.conf`; hardware units to
|
|||
|
||||
### Project structure
|
||||
|
||||
The repository holds three pieces: the GTK desktop app, a Next.js web app, and
|
||||
the shared lesson content both read from.
|
||||
|
||||
```
|
||||
desktop/ GTK 4 desktop app (C)
|
||||
web/ Next.js web app (TypeScript / React)
|
||||
content/ shared lesson content as JSON — see content/README.md
|
||||
tools/ exports content/ from the desktop C sources
|
||||
```
|
||||
|
||||
Run `npm run content` from the repo root to regenerate `content/` after editing
|
||||
lesson data in `desktop/`. Run `npm run web` for the web app (after
|
||||
`npm run web:install`) and `npm run desktop` for the GTK client.
|
||||
|
||||
#### desktop/
|
||||
|
||||
```
|
||||
maturita.h shared types, macros, globals and prototypes
|
||||
globals.c shared global state (window, stack, units, theme, …)
|
||||
|
|
@ -494,7 +530,7 @@ moderním GTK 4 rozhraním stylovaným přes CSS.
|
|||
|
||||
| Úvodní obrazovka | Předměty | Německá učební cesta |
|
||||
| --------------------------------------- | ---------------------------------------- | --------------------------------------- |
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|
||||
### Funkce
|
||||
|
||||
|
|
@ -680,10 +716,30 @@ sudo pacman -S base-devel pkgconf
|
|||
Windows – `gcc`, GTK 4 i `pkg-config` se nainstalují v kroku 1 návodu níže.
|
||||
Visual Studio není potřeba.
|
||||
|
||||
#### Sestavení aplikace
|
||||
#### Spuštění webové aplikace
|
||||
|
||||
Webový klient potřebuje Node.js 20 nebo novější a žádné GTK:
|
||||
|
||||
```bash
|
||||
npm run web:install # instalace závislostí ve web/
|
||||
npm run web # vývojový server na http://localhost:3000
|
||||
npm run web:build # produkční build
|
||||
```
|
||||
|
||||
Lekce čte z `content/`, který se z C zdrojů exportuje příkazem
|
||||
`npm run content`. Témata, překlady, cvičení i sledování postupu odpovídají
|
||||
desktopové aplikaci; postup se ukládá do localStorage prohlížeče místo do
|
||||
`progress/*.conf`.
|
||||
|
||||
Obsah odpovídá desktopovému klientovi: německé jednotky 1–3 včetně slovníkového
|
||||
tréninku, všech 27 lekcí sítí, tři lekce technického vybavení, 20 cvičení
|
||||
mluvnice a obě knihy maturitní četby.
|
||||
|
||||
#### Sestavení desktopové aplikace
|
||||
|
||||
1. Naklonujte nebo stáhněte tento repozitář
|
||||
2. Přejděte do složky projektu
|
||||
2. Přejděte do složky `desktop/` — všechny příkazy v této části se spouštějí
|
||||
tam, protože C zdroje i Makefile jsou uvnitř
|
||||
3. Sestavte pomocí Makefile:
|
||||
|
||||
```bash
|
||||
|
|
@ -836,6 +892,22 @@ ukládají do `progress/net.conf`, hardwarové do `progress/hw.conf`.
|
|||
|
||||
### Struktura projektu
|
||||
|
||||
Repozitář obsahuje tři části: desktopovou aplikaci v GTK, webovou aplikaci
|
||||
v Next.js a společný obsah lekcí, který obě čtou.
|
||||
|
||||
```
|
||||
desktop/ desktopová aplikace GTK 4 (C)
|
||||
web/ webová aplikace Next.js (TypeScript / React)
|
||||
content/ společný obsah lekcí v JSON — viz content/README.md
|
||||
tools/ export content/ z C zdrojů desktopové aplikace
|
||||
```
|
||||
|
||||
Po úpravě obsahu lekcí v `desktop/` spusťte v hlavní složce repozitáře
|
||||
`npm run content` a `content/` se přegeneruje. Webovou aplikaci spustíte přes
|
||||
`npm run web` (po `npm run web:install`), desktopovou přes `npm run desktop`.
|
||||
|
||||
#### desktop/
|
||||
|
||||
```
|
||||
maturita.h sdílené typy, makra, globální proměnné a prototypy
|
||||
globals.c sdílený globální stav (okno, stack, jednotky, téma, …)
|
||||
|
|
|
|||
80
content/README.md
Normal file
80
content/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# content/
|
||||
|
||||
Shared lesson content for the whole project. One folder, one file per lesson —
|
||||
instead of lesson text being buried inside each unit's C source file.
|
||||
|
||||
`schema.ts` is the single description of every shape here. Both the extractor in
|
||||
`tools/` and the web app in `web/` type against it, so a change there shows up as
|
||||
a type error on both sides.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
content/
|
||||
schema.ts TypeScript types for everything below
|
||||
themes.json 10 palettes, dark + light (24 colors each)
|
||||
subjects.json the 13 subjects, their i18n keys and routes
|
||||
paths.json serpentine unit paths per subject
|
||||
manifest.json subject and lesson ids
|
||||
i18n/
|
||||
ui.json UI chrome strings, keyed by identifier
|
||||
content.json lesson strings, keyed by the Czech text
|
||||
lessons/
|
||||
deutsch/unit-01.json … unit-03.json
|
||||
networks/unit-01.json … unit-27.json
|
||||
hardware/unit-01.json … unit-03.json
|
||||
czech/mluvnice.json, orwell-1984.json, fuks-spalovac-mrtvol.json
|
||||
```
|
||||
|
||||
`paths.json` holds one entry per path. A subject usually has exactly one, but
|
||||
Czech has two: the top-level area picker and the `reading` branch listing the
|
||||
books, distinguished by the `branch` field.
|
||||
|
||||
## Lesson shape
|
||||
|
||||
A lesson holds theory `slides` and a list of `tasks`. Each task is one bubble on
|
||||
the unit map and contains `items` of a single `kind`:
|
||||
|
||||
| kind | what it is |
|
||||
| ---- | ---------- |
|
||||
| `choice` | multiple choice, `correct` indexes `options` |
|
||||
| `fill` | sentence split into `segments` with a gap between each; `input` picks dropdown or text |
|
||||
| `dialogue` | dialogue lines with one missing word each |
|
||||
| `assembly` | build a sentence from scrambled words |
|
||||
| `assign` | sort labelled cards into named groups |
|
||||
| `free` | open question with a model answer |
|
||||
| `typed` | type the answer, `answers` lists accepted spellings |
|
||||
| `fields` | one prompt, several labelled fields (Czech grammatical categories) |
|
||||
| `reveal` | self-assessed: write an answer, then compare it with the model |
|
||||
| `hangman` | guess the word letter by letter |
|
||||
| `profile` | "Steckbrief" line with a model answer |
|
||||
| `table` | editable grid, some cells prefilled |
|
||||
| `subnet` | VLSM subnetting worksheet |
|
||||
|
||||
`fill` absorbs every gap variant in the desktop app. A few flags steer it:
|
||||
`letters` marks gaps holding missing letters inside a word rather than whole
|
||||
words, `gapPool` picks the task's `pool` or `altPool` per gap (the "Ordne zu"
|
||||
question-word and answer columns), and `heading` starts a labelled group of rows.
|
||||
|
||||
Two conventions carried over from the desktop app:
|
||||
|
||||
- **Pipe-separated answers.** `"bin|heiße"` means either spelling is accepted.
|
||||
- **Lenient comparison.** Answers are matched after lowercasing, expanding
|
||||
`ä ö ü ß` to `ae oe ue ss`, dropping accents and ignoring punctuation, so Czech
|
||||
diacritics and German umlauts are optional. See `web/src/lib/answers.ts`.
|
||||
|
||||
Titles come either as a literal `title` or as a `titleKey` to look up in the
|
||||
i18n tables, because German unit names are identical in both languages while
|
||||
network lesson names are translated.
|
||||
|
||||
## Regenerating
|
||||
|
||||
The C sources in `desktop/` are still where content is authored, so this folder
|
||||
is a generated one-way export:
|
||||
|
||||
```bash
|
||||
npm run content # from the repo root, or: node tools/build-content.mjs
|
||||
```
|
||||
|
||||
The extractor parses the C initializers directly and throws on any shape it does
|
||||
not recognise, so a bad export fails loudly rather than emitting empty lessons.
|
||||
1248
content/i18n/content.json
Normal file
1248
content/i18n/content.json
Normal file
File diff suppressed because it is too large
Load diff
541
content/i18n/ui.json
Normal file
541
content/i18n/ui.json
Normal file
|
|
@ -0,0 +1,541 @@
|
|||
{
|
||||
"cs": {
|
||||
"back": "Zpět",
|
||||
"settings": "Nastavení",
|
||||
"settings_title": "Nastavení",
|
||||
"mode": "REŽIM",
|
||||
"mode_dark": "Tmavý",
|
||||
"mode_light": "Světlý",
|
||||
"theme": "TÉMA",
|
||||
"language": "JAZYK",
|
||||
"welcome_body": "maturita.C je vzdělávací program pro studenty středních škol a gymnázií na přípravu k maturitě.\n\nProgram je napsaný v Céčku studentama ze SSŠVT!",
|
||||
"continue": "Pokračuj",
|
||||
"roadmap_title": "Učební plán",
|
||||
"roadmap_sub": "Vyberte jednotku na cestě a začněte procvičovat.",
|
||||
"unit1_sub": "Vyberte cvičení a dokončete je.",
|
||||
"check": "Zkontrolovat",
|
||||
"finish": "Dokončit",
|
||||
"show_sample": "Ukázat vzor",
|
||||
"your_answer": "Vaše odpověď…",
|
||||
"feedback_ok": "Výborně!",
|
||||
"feedback_retry": "Ještě to není správně. Zkuste to znovu.",
|
||||
"feedback_retry_short": "Ještě to není správně.",
|
||||
"feedback_sentences": "Některé věty nejsou správně. Zkuste to znovu.",
|
||||
"tip_ss": "Písmeno „ß“ se dá na klávesnici zaměnit za „ss“!",
|
||||
"hint_umlauts": "Na klávesnici nemáte německé znaky? Můžete psát ä jako ae, ö jako oe, ü jako ue a ß jako ss.",
|
||||
"sample_fmt": "Otázka: %s\nVzor: %s (česky: %s)",
|
||||
"sub_dialog": "Doplňte chybějící slova v dialogu.",
|
||||
"sub_assembly": "Přetáhněte slova do správného pořadí.",
|
||||
"sub_choice_num": "Vyberte správnou číslovku.",
|
||||
"sub_free": "Odpovězte vlastními slovy, poté klikněte na Dokončit.",
|
||||
"sub_zahlen": "Vyberte správné slovo pro dané číslo.",
|
||||
"sub_wieviel": "Spočítejte předměty a vyberte počet.",
|
||||
"sub_reihe": "Doplňte chybějící číslovku.",
|
||||
"sub_verb": "Vyberte správný tvar slovesa.",
|
||||
"sub_wer": "Vyberte správné tázací slovo.",
|
||||
"sub_bild": "Vyberte správný tvar slovesa podle obrázku.",
|
||||
"sub_gruss": "Roztřiďte pozdravy na pozdravy a rozloučení.",
|
||||
"sub_land": "Přiřaďte symboly ke správné zemi.",
|
||||
"sub_translate": "Přeložte slovo. Špatná odpověď se vrátí na konec.",
|
||||
"trans_wrong": "Špatně. Správně: %s",
|
||||
"trans_incomplete": "Nepřesné – něco chybí.",
|
||||
"trans_progress": "Hotovo: %d / %d",
|
||||
"trans_done": "Hotovo! Zvládl(a) jsi všechna slova.",
|
||||
"assign_hint": "Vyberte skupinu a klikněte na kartu. Kliknutím na kartu ve skupině ji vrátíte zpět.",
|
||||
"unit2_sub": "Vyberte cvičení a dokončete je.",
|
||||
"Bei uns zu Hause": "Bei uns zu Hause",
|
||||
"unit3_sub": "Vyberte cvičení a dokončete je.",
|
||||
"sub3_pair": "Přiřaďte každému slovu jeho protějšek.",
|
||||
"sub3_poss": "Doplňte přivlastňovací zájmena (1. pád).",
|
||||
"sub3_haustier": "Doplňte ein/kein ve správném tvaru.",
|
||||
"sub3_buchst": "Doplňte chybějící písmena.",
|
||||
"sub3_marco": "Doplňte slova z nabídky.",
|
||||
"sub3_sort": "Roztřiďte slova do dvou skupin.",
|
||||
"sub3_tabelle": "Doplňte přivlastňovací zájmena do tabulky.",
|
||||
"sub3_akk": "Doplňte přivlastňovací zájmena (4. pád).",
|
||||
"sub3_nicht": "Vyberte kein/keine/keinen/nicht.",
|
||||
"sub3_satz": "Sestavte správné věty.",
|
||||
"sub3_sehen": "Napište větu „Ich sehe…“.",
|
||||
"sub3_wem": "Komu to patří? Doplňte zájmeno.",
|
||||
"sub3_gibt": "Doplňte „Es gibt“ + ein/eine/einen.",
|
||||
"sub3_saetze": "Doplňte věty podle vzoru.",
|
||||
"sub3_wochen": "Doplňte chybějící písmena.",
|
||||
"wordbank": "Nabídka slov:",
|
||||
"sample_line": "Příklad: %s",
|
||||
"sub_verben": "Doplňte sloveso ve správném tvaru.",
|
||||
"sub_ausin": "Vyberte správnou předložku: aus nebo in.",
|
||||
"sub_sprich": "Doplňte sloveso „sprechen“.",
|
||||
"sub_nation": "Doplňte národnost – použijte slova z nabídky.",
|
||||
"sub_woher": "Odkud lidé pocházejí? Napište zemi.",
|
||||
"sub_laender": "Doplňte chybějící písmena v názvech zemí.",
|
||||
"sub_verb2": "Vyberte správný tvar slovesa.",
|
||||
"sub_euro": "Napište částku slovy.",
|
||||
"sub_wortsuchen": "Najděte skryté slovo a napište ho.",
|
||||
"sub_ordne": "Přiřaďte správné tázací slovo a odpověď.",
|
||||
"sub_luecke": "Doplňte chybějící slova do textu.",
|
||||
"sub_verbinde": "Spojte každé podstatné jméno se správným slovesem.",
|
||||
"sub_zahlpaar": "Spojte čísla s německými slovy.",
|
||||
"sub_steckbrief": "Doplňte údaje o sobě.",
|
||||
"sub_berufe": "Hádejte povolání dřív, než bude oběšenec hotový.",
|
||||
"sub_bistdu": "Odpovězte podle vlajky – jakou máte národnost?",
|
||||
"hm_progress": "Slovo %d / %d",
|
||||
"hm_hint": "Nápověda",
|
||||
"hm_wrong": "Slovo je uhodnuto!",
|
||||
"hm_fail": "Oběšenec! Slovo bylo „%s“.",
|
||||
"hm_retry": "Zkusit znovu",
|
||||
"hm_done": "Výborně! Uhodli jste všechna povolání.",
|
||||
"hm_guess": "Hádejte písmeno",
|
||||
"stats": "Statistiky",
|
||||
"stats_title": "Statistiky",
|
||||
"stats_sub": "Přehled vašeho pokroku.",
|
||||
"stats_ex_label": "Cvičení",
|
||||
"stats_pct_label": "Hotovo",
|
||||
"stats_units_label": "Jednotky",
|
||||
"stats_section": "Předměty",
|
||||
"stats_locked": "Zamčeno",
|
||||
"stats_complete": "Hotovo",
|
||||
"stats_ex_fmt": "%d / %d",
|
||||
"stats_pct_fmt": "%d %%",
|
||||
"stats_units_fmt": "%d / %d",
|
||||
"subjects_title": "Předměty",
|
||||
"Český jazyk a literatura": "Český jazyk a literatura",
|
||||
"Občanská nauka": "Občanská nauka",
|
||||
"English": "English",
|
||||
"Deutsch": "Deutsch",
|
||||
"Matematika": "Matematika",
|
||||
"Fyzika": "Fyzika",
|
||||
"Základy Přírodopisných věd": "Základy Přírodopisných věd",
|
||||
"Technická grafika": "Technická grafika",
|
||||
"Prezentační grafika": "Prezentační grafika",
|
||||
"Správa počítačových sítí": "Správa počítačových sítí",
|
||||
"Programování": "Programování",
|
||||
"Technické vybavení": "Technické vybavení",
|
||||
"Programové vybavení": "Programové vybavení",
|
||||
"net_sub": "Vyberte lekci na cestě a začněte procvičovat.",
|
||||
"net_years_title": "Správa počítačových sítí",
|
||||
"net_years_sub": "Vyberte ročník a začněte procvičovat.",
|
||||
"net_year1": "1. ročník",
|
||||
"net_year2": "2. ročník",
|
||||
"net_year3": "3. ročník",
|
||||
"net_year4": "4. ročník",
|
||||
"net_year_locked_sub": "Zatím bez obsahu",
|
||||
"net_unit1": "Výpočet IP adres",
|
||||
"net_unit1_sub": "4 snímky s postupem a pak cvičení.",
|
||||
"net_ex_title": "Cvičení: výpočet IP adres",
|
||||
"net_unit2": "Základní pojmy a rozdělení sítí",
|
||||
"net_unit2_sub": "5 snímků s pojmy a pak kvíz.",
|
||||
"net_ex2_title": "Kvíz: základní pojmy sítí",
|
||||
"net_unit3": "Sítě Klient–Server a Cloud",
|
||||
"net_unit3_sub": "4 snímky o serverech a cloudu a pak kvíz.",
|
||||
"net_ex3_title": "Kvíz: klient–server a cloud",
|
||||
"net_unit4": "Přenos dat a jeho zabezpečení",
|
||||
"net_unit4_sub": "4 snímky o přenosu a zabezpečení a pak kvíz.",
|
||||
"net_ex4_title": "Kvíz: přenos dat a zabezpečení",
|
||||
"net_unit5": "Signály a modulace",
|
||||
"net_unit5_sub": "4 snímky o signálech a modulaci a pak kvíz.",
|
||||
"net_ex5_title": "Kvíz: signály a modulace",
|
||||
"net_unit6": "Cesta, kanál a okruh",
|
||||
"net_unit6_sub": "4 snímky o cestě, kanálu a okruhu a pak kvíz.",
|
||||
"net_ex6_title": "Kvíz: cesta, kanál a okruh",
|
||||
"net_unit7": "Multiplex a směry přenosu",
|
||||
"net_unit7_sub": "4 snímky o multiplexu a směrech a pak kvíz.",
|
||||
"net_ex7_title": "Kvíz: multiplex a směry přenosu",
|
||||
"net_unit8": "Přepojování",
|
||||
"net_unit8_sub": "4 snímky o přepojování a pak kvíz.",
|
||||
"net_ex8_title": "Kvíz: přepojování",
|
||||
"net_unit9": "Vrstevnaté modely a protokoly",
|
||||
"net_unit9_sub": "4 snímky o vrstvách a protokolech a pak kvíz.",
|
||||
"net_ex9_title": "Kvíz: vrstevnaté modely a protokoly",
|
||||
"net_unit10": "Model ISO/OSI a TCP/IP",
|
||||
"net_unit10_sub": "3 snímky o ISO/OSI a TCP/IP a pak kvíz.",
|
||||
"net_ex10_title": "Kvíz: ISO/OSI a TCP/IP",
|
||||
"net_unit11": "Podrobný rozbor 7 vrstev ISO/OSI",
|
||||
"net_unit11_sub": "8 snímků o jednotlivých vrstvách a pak kvíz.",
|
||||
"net_ex11_title": "Kvíz: 7 vrstev ISO/OSI",
|
||||
"net_unit12": "Základy architektury TCP/IP",
|
||||
"net_unit12_sub": "6 snímků o TCP/IP a protokolech a pak kvíz.",
|
||||
"net_ex12_title": "Kvíz: architektura TCP/IP",
|
||||
"net_unit13": "Topologie sítě",
|
||||
"net_unit13_sub": "4 snímky o sběrnici, hvězdě a kruhu a pak kvíz.",
|
||||
"net_ex13_title": "Kvíz: topologie sítě",
|
||||
"net_unit14": "Optika",
|
||||
"net_unit14_sub": "5 snímků o optických vláknech a pak kvíz.",
|
||||
"net_ex14_title": "Kvíz: optika",
|
||||
"net_unit15": "Ochrana a montáž optiky",
|
||||
"net_unit15_sub": "3 snímky o ochraně a montáži a pak kvíz.",
|
||||
"net_ex15_title": "Kvíz: ochrana a montáž optiky",
|
||||
"net_unit16": "Metody přístupu",
|
||||
"net_unit16_sub": "4 snímky o kolizích a CSMA a pak kvíz.",
|
||||
"net_ex16_title": "Kvíz: metody přístupu",
|
||||
"net_unit17": "Aktivní prvky",
|
||||
"net_unit17_sub": "7 snímků o hubu, switchi a routeru a pak kvíz.",
|
||||
"net_ex17_title": "Kvíz: aktivní prvky",
|
||||
"net_unit18": "Switch (podrobně)",
|
||||
"net_unit18_sub": "5 snímků o CAM a přepínání a pak kvíz.",
|
||||
"net_ex18_title": "Kvíz: switch podrobně",
|
||||
"net_unit19": "Směrování (Routing)",
|
||||
"net_unit19_sub": "7 snímků o směrovacích algoritmech a pak kvíz.",
|
||||
"net_ex19_title": "Kvíz: směrování",
|
||||
"net_unit20": "Směrovač a brána",
|
||||
"net_unit20_sub": "7 snímků o routeru a gateway a pak kvíz.",
|
||||
"net_ex20_title": "Kvíz: směrovač a brána",
|
||||
"net_unit21": "Adresy a rámce v ethernetu",
|
||||
"net_unit21_sub": "8 snímků o MAC a typech rámců a pak kvíz.",
|
||||
"net_ex21_title": "Kvíz: ethernet adresy a rámce",
|
||||
"net_unit22": "Paket IP",
|
||||
"net_unit22_sub": "8 snímků o IP datagramu a hlavičce a pak kvíz.",
|
||||
"net_ex22_title": "Kvíz: paket IP",
|
||||
"net_unit23": "ICMP",
|
||||
"net_unit23_sub": "5 snímků o ICMP zprávách a traceroute a pak kvíz.",
|
||||
"net_ex23_title": "Kvíz: ICMP",
|
||||
"net_unit24": "ARP",
|
||||
"net_unit24_sub": "5 snímků o ARP/RARP a cache a pak kvíz.",
|
||||
"net_ex24_title": "Kvíz: ARP",
|
||||
"net_unit25": "TCP a UDP",
|
||||
"net_unit25_sub": "6 snímků o transportní vrstvě a pak kvíz.",
|
||||
"net_ex25_title": "Kvíz: TCP a UDP",
|
||||
"net_unit26": "Aplikační protokoly",
|
||||
"net_unit26_sub": "7 snímků o portech a službách a pak kvíz.",
|
||||
"net_ex26_title": "Kvíz: Aplikační protokoly",
|
||||
"net_unit27": "Bezdrátové sítě",
|
||||
"net_unit27_sub": "8 snímků o družicích, Wi-Fi a zabezpečení a pak kvíz.",
|
||||
"net_ex27_title": "Kvíz: Bezdrátové sítě",
|
||||
"net_quiz_intro": "Vyberte u každé otázky jednu správnou odpověď a stiskněte Zkontrolovat.",
|
||||
"net_quiz2_head": "Kvíz k základním pojmům sítí",
|
||||
"net_quiz3_head": "Kvíz ke klient–server a cloudu",
|
||||
"net_quiz4_head": "Kvíz k přenosu dat a zabezpečení",
|
||||
"net_quiz5_head": "Kvíz k signálům a modulaci",
|
||||
"net_quiz6_head": "Kvíz k cestě, kanálu a okruhu",
|
||||
"net_quiz7_head": "Kvíz k multiplexu a směrům přenosu",
|
||||
"net_quiz8_head": "Kvíz k přepojování",
|
||||
"net_quiz9_head": "Kvíz k vrstevnatým modelům a protokolům",
|
||||
"net_quiz10_head": "Kvíz k ISO/OSI a TCP/IP",
|
||||
"net_quiz11_head": "Kvíz k 7 vrstvám ISO/OSI",
|
||||
"net_quiz12_head": "Kvíz k architektuře TCP/IP",
|
||||
"net_quiz13_head": "Kvíz k topologiím sítě",
|
||||
"net_quiz14_head": "Kvíz k optice",
|
||||
"net_quiz15_head": "Kvíz k ochraně a montáži optiky",
|
||||
"net_quiz16_head": "Kvíz k metodám přístupu",
|
||||
"net_quiz17_head": "Kvíz k aktivním prvkům",
|
||||
"net_quiz18_head": "Kvíz ke switchi podrobně",
|
||||
"net_quiz19_head": "Kvíz ke směrování",
|
||||
"net_quiz20_head": "Kvíz ke směrovači a bráně",
|
||||
"net_quiz21_head": "Kvíz k ethernetovým adresám a rámcům",
|
||||
"net_quiz22_head": "Kvíz k IP paketu",
|
||||
"net_quiz23_head": "Kvíz k ICMP",
|
||||
"net_quiz24_head": "Kvíz k ARP",
|
||||
"net_quiz25_head": "Kvíz k TCP a UDP",
|
||||
"net_quiz26_head": "Kvíz k aplikačním protokolům",
|
||||
"net_quiz27_head": "Kvíz k bezdrátovým sítím",
|
||||
"net_slide_next": "Další",
|
||||
"net_slide_prev": "Zpět",
|
||||
"net_slide_start": "Přejít na cvičení",
|
||||
"net_solution": "Ukázat řešení",
|
||||
"hw_sub": "Vyberte lekci na cestě a začněte procvičovat.",
|
||||
"hw_unit1": "Architektura počítače",
|
||||
"hw_unit1_sub": "3 snímky o Von Neumannovi a schématu a pak kvíz.",
|
||||
"hw_ex1_title": "Kvíz: architektura počítače",
|
||||
"hw_quiz1_head": "Kvíz k architektuře počítače",
|
||||
"hw_unit2": "Historie počítačů",
|
||||
"hw_unit2_sub": "6 snímků o generacích počítačů a pak kvíz.",
|
||||
"hw_ex2_title": "Kvíz: historie počítačů",
|
||||
"hw_quiz2_head": "Kvíz k historii počítačů",
|
||||
"hw_unit3": "Zobrazování dat v počítači",
|
||||
"hw_unit3_sub": "2 snímky o bitu a bytu a pak kvíz.",
|
||||
"hw_ex3_title": "Kvíz: zobrazování dat",
|
||||
"hw_quiz3_head": "Kvíz k zobrazování dat v počítači",
|
||||
"czech_sub": "Vyberte oblast a začněte procvičovat.",
|
||||
"Literatura": "Literatura",
|
||||
"Mluvnice": "Mluvnice",
|
||||
"Maturitní četba": "Maturitní četba",
|
||||
"reading_sub": "Vyberte knihu, kterou chcete studovat.",
|
||||
"book_1984_sub": "Antiutopický román • 1949",
|
||||
"cetba1984_sub": "Přehled knihy, klíčové pojmy a cvičení.",
|
||||
"lit_quiz_title": "Kvíz: George Orwell – 1984",
|
||||
"lit_quiz_sub": "U každé otázky vyberte jednu správnou odpověď.",
|
||||
"lit_plot_title": "Sestavte děj",
|
||||
"lit_plot_sub": "Přetáhněte části příběhu do správného pořadí.",
|
||||
"lit_next": "Další",
|
||||
"lit_show_result": "Zobrazit výsledek",
|
||||
"lit_again": "Zkusit znovu",
|
||||
"lit_question_fmt": "Otázka %d / %d",
|
||||
"lit_score_fmt": "Skóre: %d / %d",
|
||||
"lit_finished": "Hotovo!",
|
||||
"lit_finished_text": "Prošli jste všemi otázkami.",
|
||||
"cetbaFuks_sub": "Přehled knihy, klíčové pojmy a cvičení.",
|
||||
"lit_fuks_quiz_title": "Kvíz: Spalovač mrtvol",
|
||||
"lit_fuks_quiz_sub": "U každé otázky vyberte jednu správnou odpověď.",
|
||||
"lit_fuks_plot_title": "Sestavte děj",
|
||||
"lit_fuks_plot_sub": "Přetáhněte části příběhu do správného pořadí."
|
||||
},
|
||||
"en": {
|
||||
"back": "Back",
|
||||
"settings": "Settings",
|
||||
"settings_title": "Settings",
|
||||
"mode": "MODE",
|
||||
"mode_dark": "Dark",
|
||||
"mode_light": "Light",
|
||||
"theme": "THEME",
|
||||
"language": "LANGUAGE",
|
||||
"welcome_body": "maturita.C is an educational app for high-school and gymnasium students to prepare for their maturita exam.\n\nThe program is written in C by students from SSŠVT!",
|
||||
"continue": "Continue",
|
||||
"roadmap_title": "Learning path",
|
||||
"roadmap_sub": "Pick a unit on the path and start practicing.",
|
||||
"unit1_sub": "Choose an exercise and complete it.",
|
||||
"check": "Check",
|
||||
"finish": "Finish",
|
||||
"show_sample": "Show sample",
|
||||
"your_answer": "Your answer…",
|
||||
"feedback_ok": "Great!",
|
||||
"feedback_retry": "Not quite right yet. Try again.",
|
||||
"feedback_retry_short": "Not quite right yet.",
|
||||
"feedback_sentences": "Some sentences are not correct. Try again.",
|
||||
"tip_ss": "You can type “ss” instead of “ß” on the keyboard!",
|
||||
"hint_umlauts": "No German keyboard? You can type ä as ae, ö as oe, ü as ue and ß as ss.",
|
||||
"sample_fmt": "Question: %s\nSample: %s (English: %s)",
|
||||
"sub_dialog": "Fill in the missing words in the dialogue.",
|
||||
"sub_assembly": "Drag the words into the correct order.",
|
||||
"sub_choice_num": "Choose the correct numeral.",
|
||||
"sub_free": "Answer in your own words, then click Finish.",
|
||||
"sub_zahlen": "Choose the correct word for the given number.",
|
||||
"sub_wieviel": "Count the items and choose the amount.",
|
||||
"sub_reihe": "Fill in the missing numeral.",
|
||||
"sub_verb": "Choose the correct verb form.",
|
||||
"sub_wer": "Choose the correct question word.",
|
||||
"sub_bild": "Choose the correct verb form for the picture.",
|
||||
"sub_gruss": "Sort the phrases into greetings and farewells.",
|
||||
"sub_land": "Match the symbols to the correct country.",
|
||||
"Vokabeltraining": "Vocabulary training",
|
||||
"sub_translate": "Translate the word. Wrong answers come back at the end.",
|
||||
"trans_wrong": "Wrong. Correct: %s",
|
||||
"trans_incomplete": "Almost – something is missing.",
|
||||
"trans_progress": "Done: %d / %d",
|
||||
"trans_done": "Done! You mastered all the words.",
|
||||
"assign_hint": "Pick a group and click a card. Click a card inside a group to send it back.",
|
||||
"unit2_sub": "Choose an exercise and complete it.",
|
||||
"Bei uns zu Hause": "At Home with Us",
|
||||
"unit3_sub": "Choose an exercise and complete it.",
|
||||
"sub3_pair": "Match each word with its counterpart.",
|
||||
"sub3_poss": "Fill in the possessive pronouns (nominative).",
|
||||
"sub3_haustier": "Fill in ein/kein in the correct form.",
|
||||
"sub3_buchst": "Fill in the missing letters.",
|
||||
"sub3_marco": "Fill in the words from the box.",
|
||||
"sub3_sort": "Sort the words into two groups.",
|
||||
"sub3_tabelle": "Fill the possessive pronouns into the table.",
|
||||
"sub3_akk": "Fill in the possessive pronouns (accusative).",
|
||||
"sub3_nicht": "Choose kein/keine/keinen/nicht.",
|
||||
"sub3_satz": "Build the correct sentences.",
|
||||
"sub3_sehen": "Write an “Ich sehe…” sentence.",
|
||||
"sub3_wem": "Whose is it? Fill in the pronoun.",
|
||||
"sub3_gibt": "Fill in “Es gibt” + ein/eine/einen.",
|
||||
"sub3_saetze": "Complete the sentences following the example.",
|
||||
"sub3_wochen": "Fill in the missing letters.",
|
||||
"wordbank": "Word bank:",
|
||||
"sample_line": "Sample: %s",
|
||||
"sub_verben": "Fill in the verb in the correct form.",
|
||||
"sub_ausin": "Choose the correct preposition: aus or in.",
|
||||
"sub_sprich": "Fill in the verb “sprechen”.",
|
||||
"sub_nation": "Fill in the nationality – use the words in the box.",
|
||||
"sub_woher": "Where are the people from? Type the country.",
|
||||
"sub_laender": "Type the full country name (the missing letters are shown as _).",
|
||||
"sub_verb2": "Choose the correct verb form.",
|
||||
"sub_euro": "Write the amount in words.",
|
||||
"sub_wortsuchen": "Find the hidden word and type it.",
|
||||
"sub_ordne": "Match each phrase with the right question word and answer.",
|
||||
"sub_luecke": "Fill the missing words into the text.",
|
||||
"sub_verbinde": "Match each noun with the right verb.",
|
||||
"sub_zahlpaar": "Match the numbers with the German words.",
|
||||
"sub_steckbrief": "Complete the information about yourself.",
|
||||
"sub_berufe": "Guess each job before the hangman drawing is finished.",
|
||||
"sub_bistdu": "Answer according to the flag – what is your nationality?",
|
||||
"hm_progress": "Word %d / %d",
|
||||
"hm_hint": "Hint",
|
||||
"hm_wrong": "Word guessed!",
|
||||
"hm_fail": "Hanged! The word was “%s”.",
|
||||
"hm_retry": "Try again",
|
||||
"hm_done": "Great! You guessed all the jobs.",
|
||||
"hm_guess": "Guess a letter",
|
||||
"stats": "Statistics",
|
||||
"stats_title": "Statistics",
|
||||
"stats_sub": "Overview of your progress.",
|
||||
"stats_ex_label": "Exercises",
|
||||
"stats_pct_label": "Done",
|
||||
"stats_units_label": "Units",
|
||||
"stats_section": "Subjects",
|
||||
"stats_locked": "Locked",
|
||||
"stats_complete": "Complete",
|
||||
"stats_ex_fmt": "%d / %d",
|
||||
"stats_pct_fmt": "%d %%",
|
||||
"stats_units_fmt": "%d / %d",
|
||||
"subjects_title": "Subjects",
|
||||
"Český jazyk a literatura": "Czech Language and Literature",
|
||||
"Občanská nauka": "Civics",
|
||||
"English": "English",
|
||||
"Deutsch": "Deutsch",
|
||||
"Matematika": "Mathematics",
|
||||
"Fyzika": "Physics",
|
||||
"Základy Přírodopisných věd": "Fundamentals of Natural Sciences",
|
||||
"Technická grafika": "Technical Drawing",
|
||||
"Prezentační grafika": "Presentation Graphics",
|
||||
"Správa počítačových sítí": "Computer Network Administration",
|
||||
"Programování": "Programming",
|
||||
"Technické vybavení": "Computer Hardware",
|
||||
"Programové vybavení": "Software",
|
||||
"net_sub": "Pick a unit on the path and start practicing.",
|
||||
"net_years_title": "Computer Network Administration",
|
||||
"net_years_sub": "Pick a school year and start practicing.",
|
||||
"net_year1": "Year 1",
|
||||
"net_year2": "Year 2",
|
||||
"net_year3": "Year 3",
|
||||
"net_year4": "Year 4",
|
||||
"net_year_locked_sub": "No content yet",
|
||||
"net_unit1": "IP Address Calculation",
|
||||
"net_unit1_sub": "4 slides with the method, then an exercise.",
|
||||
"net_ex_title": "Exercise: IP address calculation",
|
||||
"net_unit2": "Basic concepts and network types",
|
||||
"net_unit2_sub": "5 slides with the concepts, then a quiz.",
|
||||
"net_ex2_title": "Quiz: basic networking concepts",
|
||||
"net_unit3": "Client–Server networks and Cloud",
|
||||
"net_unit3_sub": "4 slides about servers and cloud, then a quiz.",
|
||||
"net_ex3_title": "Quiz: client–server and cloud",
|
||||
"net_unit4": "Data transmission and security",
|
||||
"net_unit4_sub": "4 slides about transmission and security, then a quiz.",
|
||||
"net_ex4_title": "Quiz: data transmission and security",
|
||||
"net_unit5": "Signals and modulation",
|
||||
"net_unit5_sub": "4 slides about signals and modulation, then a quiz.",
|
||||
"net_ex5_title": "Quiz: signals and modulation",
|
||||
"net_unit6": "Path, channel and circuit",
|
||||
"net_unit6_sub": "4 slides about path, channel and circuit, then a quiz.",
|
||||
"net_ex6_title": "Quiz: path, channel and circuit",
|
||||
"net_unit7": "Multiplexing and transmission directions",
|
||||
"net_unit7_sub": "4 slides about multiplexing and directions, then a quiz.",
|
||||
"net_ex7_title": "Quiz: multiplexing and transmission directions",
|
||||
"net_unit8": "Switching",
|
||||
"net_unit8_sub": "4 slides about switching, then a quiz.",
|
||||
"net_ex8_title": "Quiz: switching",
|
||||
"net_unit9": "Layered models and protocols",
|
||||
"net_unit9_sub": "4 slides about layers and protocols, then a quiz.",
|
||||
"net_ex9_title": "Quiz: layered models and protocols",
|
||||
"net_unit10": "ISO/OSI and TCP/IP models",
|
||||
"net_unit10_sub": "3 slides about ISO/OSI and TCP/IP, then a quiz.",
|
||||
"net_ex10_title": "Quiz: ISO/OSI and TCP/IP",
|
||||
"net_unit11": "Detailed review of the 7 ISO/OSI layers",
|
||||
"net_unit11_sub": "8 slides about each layer, then a quiz.",
|
||||
"net_ex11_title": "Quiz: 7 ISO/OSI layers",
|
||||
"net_unit12": "Basics of TCP/IP architecture",
|
||||
"net_unit12_sub": "6 slides about TCP/IP and protocols, then a quiz.",
|
||||
"net_ex12_title": "Quiz: TCP/IP architecture",
|
||||
"net_unit13": "Network topologies",
|
||||
"net_unit13_sub": "4 slides about bus, star and ring, then a quiz.",
|
||||
"net_ex13_title": "Quiz: network topologies",
|
||||
"net_unit14": "Fiber optics",
|
||||
"net_unit14_sub": "5 slides about optical fibers, then a quiz.",
|
||||
"net_ex14_title": "Quiz: fiber optics",
|
||||
"net_unit15": "Fiber protection and installation",
|
||||
"net_unit15_sub": "3 slides about protection and installation, then a quiz.",
|
||||
"net_ex15_title": "Quiz: fiber protection and installation",
|
||||
"net_unit16": "Access methods",
|
||||
"net_unit16_sub": "4 slides about collisions and CSMA, then a quiz.",
|
||||
"net_ex16_title": "Quiz: access methods",
|
||||
"net_unit17": "Active network devices",
|
||||
"net_unit17_sub": "7 slides about hubs, switches and routers, then a quiz.",
|
||||
"net_ex17_title": "Quiz: active network devices",
|
||||
"net_unit18": "Switch in detail",
|
||||
"net_unit18_sub": "5 slides about CAM and switching modes, then a quiz.",
|
||||
"net_ex18_title": "Quiz: switch in detail",
|
||||
"net_unit19": "Routing",
|
||||
"net_unit19_sub": "7 slides about routing algorithms, then a quiz.",
|
||||
"net_ex19_title": "Quiz: routing",
|
||||
"net_unit20": "Router and gateway",
|
||||
"net_unit20_sub": "7 slides about routers and gateways, then a quiz.",
|
||||
"net_ex20_title": "Quiz: router and gateway",
|
||||
"net_unit21": "Ethernet addresses and frames",
|
||||
"net_unit21_sub": "8 slides about MAC and frame types, then a quiz.",
|
||||
"net_ex21_title": "Quiz: Ethernet addresses and frames",
|
||||
"net_unit22": "IP packet",
|
||||
"net_unit22_sub": "8 slides about the IP datagram and header, then a quiz.",
|
||||
"net_ex22_title": "Quiz: IP packet",
|
||||
"net_unit23": "ICMP",
|
||||
"net_unit23_sub": "5 slides about ICMP messages and traceroute, then a quiz.",
|
||||
"net_ex23_title": "Quiz: ICMP",
|
||||
"net_unit24": "ARP",
|
||||
"net_unit24_sub": "5 slides about ARP/RARP and cache, then a quiz.",
|
||||
"net_ex24_title": "Quiz: ARP",
|
||||
"net_unit25": "TCP and UDP",
|
||||
"net_unit25_sub": "6 slides about the transport layer, then a quiz.",
|
||||
"net_ex25_title": "Quiz: TCP and UDP",
|
||||
"net_unit26": "Application protocols",
|
||||
"net_unit26_sub": "7 slides about ports and services, then a quiz.",
|
||||
"net_ex26_title": "Quiz: Application protocols",
|
||||
"net_unit27": "Wireless networks",
|
||||
"net_unit27_sub": "8 slides about satellites, Wi-Fi and security, then a quiz.",
|
||||
"net_ex27_title": "Quiz: Wireless networks",
|
||||
"net_quiz_intro": "Choose one correct answer for each question and press Check.",
|
||||
"net_quiz2_head": "Quiz on basic networking concepts",
|
||||
"net_quiz3_head": "Quiz on client–server and cloud",
|
||||
"net_quiz4_head": "Quiz on data transmission and security",
|
||||
"net_quiz5_head": "Quiz on signals and modulation",
|
||||
"net_quiz6_head": "Quiz on path, channel and circuit",
|
||||
"net_quiz7_head": "Quiz on multiplexing and transmission directions",
|
||||
"net_quiz8_head": "Quiz on switching",
|
||||
"net_quiz9_head": "Quiz on layered models and protocols",
|
||||
"net_quiz10_head": "Quiz on ISO/OSI and TCP/IP",
|
||||
"net_quiz11_head": "Quiz on the 7 ISO/OSI layers",
|
||||
"net_quiz12_head": "Quiz on TCP/IP architecture",
|
||||
"net_quiz13_head": "Quiz on network topologies",
|
||||
"net_quiz14_head": "Quiz on fiber optics",
|
||||
"net_quiz15_head": "Quiz on fiber protection and installation",
|
||||
"net_quiz16_head": "Quiz on access methods",
|
||||
"net_quiz17_head": "Quiz on active network devices",
|
||||
"net_quiz18_head": "Quiz on switches in detail",
|
||||
"net_quiz19_head": "Quiz on routing",
|
||||
"net_quiz20_head": "Quiz on router and gateway",
|
||||
"net_quiz21_head": "Quiz on Ethernet addresses and frames",
|
||||
"net_quiz22_head": "Quiz on the IP packet",
|
||||
"net_quiz23_head": "Quiz on ICMP",
|
||||
"net_quiz24_head": "Quiz on ARP",
|
||||
"net_quiz25_head": "Quiz on TCP and UDP",
|
||||
"net_quiz26_head": "Quiz on application protocols",
|
||||
"net_quiz27_head": "Quiz on wireless networks",
|
||||
"net_slide_next": "Next",
|
||||
"net_slide_prev": "Back",
|
||||
"net_slide_start": "Go to the exercise",
|
||||
"net_solution": "Show solution",
|
||||
"hw_sub": "Pick a unit on the path and start practicing.",
|
||||
"hw_unit1": "Computer architecture",
|
||||
"hw_unit1_sub": "3 slides about von Neumann and the model, then a quiz.",
|
||||
"hw_ex1_title": "Quiz: computer architecture",
|
||||
"hw_quiz1_head": "Quiz on computer architecture",
|
||||
"hw_unit2": "History of computers",
|
||||
"hw_unit2_sub": "6 slides about computer generations, then a quiz.",
|
||||
"hw_ex2_title": "Quiz: history of computers",
|
||||
"hw_quiz2_head": "Quiz on the history of computers",
|
||||
"hw_unit3": "Data representation",
|
||||
"hw_unit3_sub": "2 slides about bits and bytes, then a quiz.",
|
||||
"hw_ex3_title": "Quiz: data representation",
|
||||
"hw_quiz3_head": "Quiz on data representation in computers",
|
||||
"czech_sub": "Pick an area and start practicing.",
|
||||
"Literatura": "Literature",
|
||||
"Mluvnice": "Grammar",
|
||||
"Maturitní četba": "Maturita reading list",
|
||||
"reading_sub": "Pick a book to study.",
|
||||
"book_1984_sub": "Dystopian novel • 1949",
|
||||
"cetba1984_sub": "Book overview, key terms and exercises.",
|
||||
"lit_quiz_title": "Quiz: George Orwell – 1984",
|
||||
"lit_quiz_sub": "Pick one correct answer for each question.",
|
||||
"lit_plot_title": "Order the plot",
|
||||
"lit_plot_sub": "Drag the story parts into the correct order.",
|
||||
"lit_next": "Next",
|
||||
"lit_show_result": "Show result",
|
||||
"lit_again": "Try again",
|
||||
"lit_question_fmt": "Question %d / %d",
|
||||
"lit_score_fmt": "Score: %d / %d",
|
||||
"lit_finished": "Done!",
|
||||
"lit_finished_text": "You have finished all the questions.",
|
||||
"cetbaFuks_sub": "Book overview, key terms and exercises.",
|
||||
"lit_fuks_quiz_title": "Quiz: The Cremator",
|
||||
"lit_fuks_quiz_sub": "Pick one correct answer for each question.",
|
||||
"lit_fuks_plot_title": "Order the plot",
|
||||
"lit_fuks_plot_sub": "Drag the story parts into the correct order."
|
||||
}
|
||||
}
|
||||
539
content/lessons/czech/fuks-spalovac-mrtvol.json
Normal file
539
content/lessons/czech/fuks-spalovac-mrtvol.json
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
{
|
||||
"id": "fuks-spalovac-mrtvol",
|
||||
"subject": "czech",
|
||||
"number": 3,
|
||||
"title": {
|
||||
"cs": "Ladislav Fuks – Spalovač mrtvol",
|
||||
"en": "Ladislav Fuks – Spalovač mrtvol"
|
||||
},
|
||||
"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": [
|
||||
{
|
||||
"kicker": "Ladislav Fuks – Spalovač mrtvol",
|
||||
"title": "O knize",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Autor: Ladislav Fuks (1923–1994)",
|
||||
"Žánr: psychologický román s prvky grotesky a hororu",
|
||||
"Poprvé vydáno: 1967",
|
||||
"Děj: Praha v době protektorátu (konec 30. let)",
|
||||
"Vyprávění: ich-forma – vypráví sám pan Kopfrkingl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "Ladislav Fuks – Spalovač mrtvol",
|
||||
"title": "Doba a svět",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Děj se odehrává v protektorátu Čechy a Morava za nastupujícího nacismu.",
|
||||
"Atmosféru tvoří strach, přizpůsobování a touha zalíbit se moci.",
|
||||
"Krematorium je symbolem smrti a odlidštění.",
|
||||
"Antisemitismus a árijská ideologie postupně prostupují společnost.",
|
||||
"Obyčejný úředník se mění v nástroj zla."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "Ladislav Fuks – Spalovač mrtvol",
|
||||
"title": "Postavy",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Pan Kopfrkingl – zaměstnanec krematoria, vypravěč a vrah své rodiny.",
|
||||
"Lakmé – jeho žena, židovského původu.",
|
||||
"Zina a Mil – jeho děti.",
|
||||
"Willi Reinke – Němec, který Kopfrkingla přivede k nacismu.",
|
||||
"Dr. Bettelheim – lékař, s nímž Kopfrkingl rozmlouvá o smrti."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "Ladislav Fuks – Spalovač mrtvol",
|
||||
"title": "Klíčové motivy",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Krematorium – symbol smrti a zvrácené „očisty“.",
|
||||
"Kniha mrtvých – tibetský text, který si Kopfrkingl překrucuje.",
|
||||
"Reinkarnace a „osvobození“ – záminka pro vraždy.",
|
||||
"Masky a přetvářka – zlo se tváří jako zdvořilost a řád.",
|
||||
"Groteska a černý humor – hrůza podaná klidným, slavnostním tónem.",
|
||||
"Voda a Vltava – motiv smrti a plynutí."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Kvíz",
|
||||
"heading": "Kvíz ke knize",
|
||||
"subtitleKey": "lit_quiz_sub",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kdo je hlavní postava románu a jaké je jeho povolání?",
|
||||
"options": [
|
||||
"Pan Kopfrkingl – zaměstnanec krematoria",
|
||||
"Willi Reinke – nacistický důstojník",
|
||||
"Dr. Bettelheim – psychiatr",
|
||||
"Pan Kopfrkingl – ředitel školy"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Hlavní postavou je pan Kopfrkingl, spořádaný zaměstnanec krematoria, který svou práci vnímá jako poslání."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "V jakém prostředí pan Kopfrkingl pracuje?",
|
||||
"options": [
|
||||
"V nemocnici",
|
||||
"V krematoriu",
|
||||
"V divadle",
|
||||
"Ve škole"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Kopfrkingl pracuje v krematoriu, kde má na starosti zpopelňování zemřelých."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "V jaké historické době se děj románu odehrává?",
|
||||
"options": [
|
||||
"Za první republiky",
|
||||
"Za protektorátu Čechy a Morava (před a během 2. světové války)",
|
||||
"Po roce 1989",
|
||||
"V 19. století"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Román se odehrává v době nastupujícího nacismu a protektorátu, který umožňuje Kopfrkinglovu proměnu."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak se proměňuje hlavní hrdina v průběhu příběhu?",
|
||||
"options": [
|
||||
"Z laskavého spořádaného muže v chladnokrevného vraha a fanatika",
|
||||
"Z vraha v kajícníka",
|
||||
"Z chudáka v boháče",
|
||||
"Vůbec se nemění"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Kopfrkingl se postupně mění v člověka, který vraždí své blízké a ospravedlňuje to vyšším dobrem."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaký je vztah pana Kopfrkingla k jeho rodině na začátku knihy?",
|
||||
"options": [
|
||||
"Nenávidí je",
|
||||
"Je starostlivý, zdvořilý a především spořádaný",
|
||||
"S rodinou se nezná",
|
||||
"Týrá je"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Na začátku působí jako vzorný manžel a otec, posedlý řádem a zdvořilostí."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak se jmenuje manželka pana Kopfrkingla a jaký je jejich vztah?",
|
||||
"options": [
|
||||
"Marie – milují se",
|
||||
"Lakmé – formálně zdvořilý, chladný vztah",
|
||||
"Zina – nenávidí se",
|
||||
"Julie – jsou rozvedení"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Manželka se jmenuje Lakmé. Jejich vztah je spíš zdvořilá forma než opravdová láska."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaký je vztah pana Kopfrkingla k dětem, Zině a Mílovi?",
|
||||
"options": [
|
||||
"Miluje je víc než sebe",
|
||||
"Chová se k nim odtažitě, jako k součásti řádu",
|
||||
"Nevidí je",
|
||||
"Jsou adoptované"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "K dětem je odměřený; vnímá je spíš jako součást svého spořádaného světa než jako milované bytosti."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kdo je Willi Reinke a jakou roli hraje v proměně hlavního hrdiny?",
|
||||
"options": [
|
||||
"Soused z krematoria",
|
||||
"Německý úředník a bývalý voják, který Kopfrkingla ovlivní",
|
||||
"Kopfrkinglův bratr",
|
||||
"Lékař v nemocnici"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Reinke je Němec, který v Kopfrkinglovi probudí touhu po árijském původu a kariéře."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak Willi Reinke ovlivňuje Kopfrkinglovo smýšlení?",
|
||||
"options": [
|
||||
"Přesvědčí ho, aby se přidal k nacistům a byl „lepší“",
|
||||
"Nabádá ho k odchodu do zahraničí",
|
||||
"Učí ho tibetštinu",
|
||||
"Nijak ho neovlivní"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Reinke v něm posiluje antisemitismus a touhu po moci a uznání v novém režimu."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaký je Kopfrkinglův vztah ke krematoriu a smrti obecně?",
|
||||
"options": [
|
||||
"Bojí se jich",
|
||||
"Má je rád, kremaci vnímá jako krásný obřad osvobození",
|
||||
"Nechce tam pracovat",
|
||||
"Je mu to lhostejné"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Smrt a kremace jsou pro něj estetickým a filozofickým rituálem, ne něčím hrůzným."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak pan Kopfrkingl vnímá a popisuje proces kremace?",
|
||||
"options": [
|
||||
"Jako nutné zlo",
|
||||
"Jako vznešený, očistný obřad, o kterém rád mluví",
|
||||
"Jako hroznou povinnost",
|
||||
"Nikdy o něm nemluví"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Kopfrkingl o kremaci hovoří básnivě a s nadšením, což ostře kontrastuje s hrůzou jeho činů."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaká je symbolika krematoria v celém románu?",
|
||||
"options": [
|
||||
"Symbol naděje",
|
||||
"Smrt, odlidštění a zvrácená ideologie, která z lidí dělá jen těla",
|
||||
"Symbol lásky",
|
||||
"Symbol mládí"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Krematorium je ústředním symbolem – odhaluje chladnou mašinerii smrti, do níž se ideologie snaží proměnit člověka."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co symbolizuje Kopfrkinglova záliba v tibetské Knize mrtvých?",
|
||||
"options": [
|
||||
"Jeho touhu po vědění a zvrácenou víru ve vlastní výjimečnost",
|
||||
"Historický zájem o Asii",
|
||||
"Strach ze smrti",
|
||||
"Lásku k manželce"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Kopfrkingl si z ní vybírá jen to, co se hodí k ospravedlnění jeho činů a jeho pocitu vyvolenosti."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak se v knize projevuje motiv buddhismu a reinkarnace?",
|
||||
"options": [
|
||||
"Kopfrkingl jimi ospravedlňuje smrt jako „osvobození duše“",
|
||||
"Postavy se modlí k Bohu",
|
||||
"Nikde se neobjevuje",
|
||||
"Je to hlavní téma manželky"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Reinkarnace a „osvobození od utrpení“ se v jeho mysli zvrhávají v právo zabíjet."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak Kopfrkingl zneužívá filozofické a náboženské myšlenky?",
|
||||
"options": [
|
||||
"Ospravedlňuje jimi vlastní vraždy jako službu vyššímu dobru",
|
||||
"Pomáhá jimi druhým",
|
||||
"Odmítá je",
|
||||
"Používá je jen v práci"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Z vznešených idejí si staví masku, za kterou skrývá krutost a touhu po moci."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak se postupně mění Kopfrkinglova mluva a způsob vyjadřování?",
|
||||
"options": [
|
||||
"Začíná mluvit vulgárně",
|
||||
"Mění se v patetickou, ráznou řeč plnou frází o řádu a osvobození",
|
||||
"Přestane mluvit",
|
||||
"Od začátku mluví jen německy"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Jeho zdvořilá mluvnost se postupně mění v ideologické fráze a bezcitný patos."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaké opakující se fráze nebo návyky pana Kopfrkingla si všímáš?",
|
||||
"options": [
|
||||
"Neustále se ptá na počasí",
|
||||
"Opakuje „to je takové“, „vidíte“ a lpí na zdvořilosti",
|
||||
"Zpívá si",
|
||||
"Mluví jen o jídle"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Opakující se fráze a zdvořilostní obraty vytvářejí rytmus, který je čím dál tím děsivější."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaký je vypravěčský styl románu (ich-forma, er-forma)?",
|
||||
"options": [
|
||||
"Er-forma bez vnitřního světa",
|
||||
"Ich-forma – vypráví sám Kopfrkingl",
|
||||
"Deníková forma",
|
||||
"Forma dopisů"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Ich-forma je klíčová: čtenář sleduje svět zkreslený Kopfrkinglovou myslí a sám postupně propadá jeho logice."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak se v knize projevuje černý humor a groteska?",
|
||||
"options": [
|
||||
"Vážné scény jsou líčeny s odstupem a absurditou",
|
||||
"Kniha je čistě vážná",
|
||||
"Humor je jediný žánr",
|
||||
"V knize se neprojevuje"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Groteskní kontrast mezi slavnostním vyprávěním a hrůzou vytváří mrazivý černý humor."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak kontrastuje banalita každodenního života s hrůzností činů?",
|
||||
"options": [
|
||||
"Vůbec nesouvisí",
|
||||
"Všední detaily a zdvořilost činí hrůzu ještě děsivější",
|
||||
"Hrůza je potlačena",
|
||||
"Den je popsán jen jednou"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Fuks zesiluje otřes tím, že vraždění je popisováno stejně klidně jako ranní káva nebo kremace."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak se v knize projevuje antisemitismus?",
|
||||
"options": [
|
||||
"Od začátku otevřeně",
|
||||
"Postupně, nejdřív v náznacích a vlivem Reinkeho",
|
||||
"Nikdy",
|
||||
"Jen mimochodem v závěru"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Kopfrkinglův antisemitismus se rodí pozvolna a je živen dobovou atmosférou i Reinkem."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak nacistická ideologie postupně prostupuje Kopfrkinglovo myšlení?",
|
||||
"options": [
|
||||
"Odmítá ji",
|
||||
"Přijímá ji jako vyšší řád a ospravedlnění vlastní výjimečnosti",
|
||||
"Nerozumí jí",
|
||||
"Zajímá ho jen hudba"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Ideologie se mu stává nástrojem kariéry i vnitřním ospravedlněním vražd."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak se mění Kopfrkinglův vztah k manželce kvůli jejímu původu?",
|
||||
"options": [
|
||||
"Začne ji milovat víc",
|
||||
"Odcizí se jí, protože je židovského původu",
|
||||
"Nijak se nemění",
|
||||
"Odstěhují se od sebe"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Kvůli jejímu původu ji přestane vnímat jako rovnocennou a nakonec ji zabije."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co se stane s Kopfrkinglovou rodinou ve druhé polovině románu?",
|
||||
"options": [
|
||||
"Odstěhují se",
|
||||
"Kopfrkingl svou ženu i děti zabije",
|
||||
"Všichni přežijí",
|
||||
"Zachrání je Reinke"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Pod vlivem ideologie Kopfrkingl vyvraždí vlastní rodinu a považuje to za „osvobození“."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak Kopfrkingl zdůvodňuje vraždu vlastní ženy?",
|
||||
"options": [
|
||||
"Zlobila se na něj",
|
||||
"Že je židovského původu a překáží jeho kariéře",
|
||||
"Že ho podváděla",
|
||||
"Nijak ji nezdůvodňuje"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Svou ženu zabije mimo jiné proto, že je židovského původu, tedy „méněcenná“ pro nový režim."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jakým způsobem Kopfrkingl vraždí své blízké?",
|
||||
"options": [
|
||||
"Ve zlosti a afektu",
|
||||
"Chladně a s pocitem, že je „osvobozuje od utrpení“",
|
||||
"Omylem",
|
||||
"Na příkaz úřadů"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Vraždí klidně, jako by vykonával bohulibý úkon, a svou krutost si vykládá jako soucit."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak souvisí přesvědčení o „osvobození“ s jeho vražednými činy?",
|
||||
"options": [
|
||||
"Dává mu zdání vyššího smyslu, kterým si vraždy ospravedlňuje",
|
||||
"Nemá s nimi nic společného",
|
||||
"Brání mu v zabíjení",
|
||||
"Je to jen náhodná fráze"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Zvrácená víra v osvobození od utrpení mu slouží jako mravní alibi pro vraždy."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaký je vztah pana Kopfrkingla k postavě Dr. Bettelheima?",
|
||||
"options": [
|
||||
"Obdivuje ho a rád s ním mluví",
|
||||
"Nenávidí ho",
|
||||
"Nikdy se nepotkají",
|
||||
"Je to jeho nadřízený"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "S doktorem Bettelheimem vede Kopfrkingl své „filozofické“ rozhovory o smrti a lidskosti."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak se mění společenské postavení pana Kopfrkingla v době okupace?",
|
||||
"options": [
|
||||
"Upadá",
|
||||
"Stoupá – díky režimu a touze po árijském původu",
|
||||
"Nemění se",
|
||||
"Odchází do penze"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Spolu s nacistickou mocí roste i Kopfrkinglova kariéra a sebevědomí."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaký je symbolický význam jména Kopfrkingl?",
|
||||
"options": [
|
||||
"Zdánlivě úřednické, ale zní hrozivě – naznačuje zvrácenost",
|
||||
"Je to běžné české jméno",
|
||||
"Znamená „laskavý“",
|
||||
"Je to pseudonym"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Zvukomalebné jméno působí úředně a zároveň hrozivě, což podtrhuje dvojí tvář postavy."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak funguje motiv masek, divadla a přetvářky?",
|
||||
"options": [
|
||||
"Postavy nosí masky dobroty, pod nimiž se skrývá krutost",
|
||||
"V knize jsou jen skutečné divadelní masky",
|
||||
"Motiv se neobjevuje",
|
||||
"Masky symbolizují lásku"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Přetvářka a předstíraná spořádanost jsou ústřední – zlo se tváří jako slušnost."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaký je vztah mezi soukromým a kolektivním šílenstvím doby?",
|
||||
"options": [
|
||||
"Kopfrkinglovo osobní šílenství roste spolu s ideologií doby",
|
||||
"Nijak nesouvisí",
|
||||
"Doba je klidná a šťastná",
|
||||
"Lidé režim jednomyslně odmítají"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Fuks ukazuje, jak se individuální porucha napojí na kolektivní ideologii a čerpá z ní sílu."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak končí román a co se stane s panem Kopfrkinglem na konci?",
|
||||
"options": [
|
||||
"Uteče do zahraničí",
|
||||
"Je zatčen a zničen",
|
||||
"Stane se hrdinou",
|
||||
"Věří, že je vtělením Dalajlámy, a zůstává zaslepený"
|
||||
],
|
||||
"correct": 3,
|
||||
"hint": "Kopfrkinglova zaslepenost vrcholí přesvědčením o vlastní božskosti; jeho zvrácená víra ho neopouští."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak je Spalovač mrtvol adaptován filmově?",
|
||||
"options": [
|
||||
"Film Juraje Herze je vizuálně expresivní a patří k vrcholům české kinematografie",
|
||||
"Byl zfilmován jen v zahraničí",
|
||||
"Film neexistuje",
|
||||
"Vznikl jako animovaný seriál"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Známá filmová verze je od režiséra Juraje Herze z roku 1968."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Které další dílo Ladislava Fukse se dá srovnat s touto knihou?",
|
||||
"options": [
|
||||
"Pan Theodor Mundstock nebo Myši Natálie Mooshabrové",
|
||||
"Farma zvířat",
|
||||
"R.U.R.",
|
||||
"Babička"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Fuks se opakovaně zabýval osudem židovských postav a atmosférou strachu; srovnat lze např. Pana Theodora Mundstocka."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaká jsou hlavní témata románu?",
|
||||
"options": [
|
||||
"Moc, manipulace, ideologie, násilí a přetvářka",
|
||||
"Láska a dobrodružství",
|
||||
"Sport a přátelství",
|
||||
"Příroda a vesmír"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Kniha zkoumá, jak se obyčejný člověk přizpůsobí zlu a stane se jeho nástrojem."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaké poselství podle tebe chtěl Ladislav Fuks knihou předat?",
|
||||
"options": [
|
||||
"Varování před manipulací a tím, jak se zlo rodí z banality a lhostejnosti",
|
||||
"Oslavu války",
|
||||
"Návod k podnikání",
|
||||
"Kritiku školství"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Fuks ukazuje, že hrůza může vzniknout z poslušnosti a touhy být „lepší“ než ostatní."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"title": "Děj",
|
||||
"heading": "Seřaďte děj",
|
||||
"subtitleKey": "lit_plot_sub",
|
||||
"items": [
|
||||
{
|
||||
"kind": "assembly",
|
||||
"prompt": "Přetáhněte části příběhu do správného pořadí:",
|
||||
"words": [
|
||||
"Kopfrkingl je spořádaným zaměstnancem krematoria",
|
||||
"Seznámí se s Willim Reinkem",
|
||||
"Začne věřit v árijskou nadřazenost",
|
||||
"Postupně se odcizí své rodině",
|
||||
"Zabije ženu a děti jako „osvobození“",
|
||||
"Na konci se považuje za vtělení Dalajlámy"
|
||||
],
|
||||
"meaning": "Správné pořadí: spořádaný úředník → setkání s Reinkem → árijská ideologie → odcizení rodině → vraždy → přesvědčení o vlastní božskosti."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1657
content/lessons/czech/mluvnice.json
Normal file
1657
content/lessons/czech/mluvnice.json
Normal file
File diff suppressed because it is too large
Load diff
481
content/lessons/czech/orwell-1984.json
Normal file
481
content/lessons/czech/orwell-1984.json
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
{
|
||||
"id": "orwell-1984",
|
||||
"subject": "czech",
|
||||
"number": 2,
|
||||
"title": {
|
||||
"cs": "George Orwell – 1984",
|
||||
"en": "George Orwell – 1984"
|
||||
},
|
||||
"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": [
|
||||
{
|
||||
"kicker": "George Orwell – 1984",
|
||||
"title": "O knize",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Autor: George Orwell (vl. jm. Eric Arthur Blair)",
|
||||
"Žánr: antiutopický (dystopický) román",
|
||||
"Poprvé vydáno: 1949",
|
||||
"Děj: Londýn, provincie Letopočet 1, stát Oceánie",
|
||||
"Vyprávění: v er-formě, po deníkových zápiscích Winstona Smithe"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "George Orwell – 1984",
|
||||
"title": "Svět a strana",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Svět je rozdělen na tři supervelmoci: Oceánii, Eurasii a Východasii, které spolu vedou trvalou válku.",
|
||||
"Vládne jediná strana (Angsoc) v čele s kultem Velkého bratra.",
|
||||
"Ministerstvo pravdy – přepisuje historii a vyrábí „pravdu“.",
|
||||
"Ministerstvo lásky – mučírny a likvidace nepohodlných.",
|
||||
"Ministerstvo hojnosti – plánuje hospodářství a drží lidi v nedostatku.",
|
||||
"Ministerstvo míru – vede válku."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "George Orwell – 1984",
|
||||
"title": "Postavy",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Winston Smith – hlavní hrdina, přepisuje historii, touží po svobodě.",
|
||||
"Julie – mladá členka strany, Winstonova tajná láska.",
|
||||
"O'Brien – člen vnitřní strany, předstírá odboj a Winstona zničí.",
|
||||
"Emmanuel Goldstein – nepřítel státu, údajný vůdce Bratrstva.",
|
||||
"Velký bratr – symbol všemocné moci strany.",
|
||||
"Pan Charrington – zdánlivý obchodník, ve skutečnosti agent."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "George Orwell – 1984",
|
||||
"title": "Klíčové pojmy",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Newspeak – uměle zjednodušovaný jazyk, který omezuje myšlení.",
|
||||
"Dvojité myšlení – ochota věřit dvěma protikladným pravdám.",
|
||||
"Zločin myšlení – už samotná zakázaná myšlenka.",
|
||||
"Policie myšlení – tajná policie, která zatýká a mučí.",
|
||||
"Bratrstvo – možná neexistující odbojová organizace.",
|
||||
"Místnost 101 – nejhlubší mučírna, pro každého jeho největší strach."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Kvíz",
|
||||
"heading": "Kvíz ke knize",
|
||||
"subtitleKey": "lit_quiz_sub",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kdo je hlavní hrdina románu a co dělá?",
|
||||
"options": [
|
||||
"Winston Smith – přepisuje historii na Ministerstvu pravdy",
|
||||
"O'Brien – řídí Ministerstvo lásky",
|
||||
"Emmanuel Goldstein – vede Bratrstvo",
|
||||
"Winston Smith – velí Policii myšlení"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Winston Smith je vnější člen strany, který na Ministerstvu pravdy upravuje staré záznamy a novinové články."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Ve kterém fiktivním státě se děj odehrává?",
|
||||
"options": [
|
||||
"V Eurasii",
|
||||
"V Oceánii",
|
||||
"Ve Východasii",
|
||||
"V Angsocu"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Příběh se odehrává v Oceánii, v provincii Letopočet 1 (bývalá Anglie), jejímž hlavním městem je Londýn."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co znamená slogan „VÁLKA JE MÍR, SVOBODA JE OTROCTVÍ, NEVĚDOMOST JE SÍLA“?",
|
||||
"options": [
|
||||
"Jsou to tři zásady Bratrstva",
|
||||
"Jsou to názvy ministerstev",
|
||||
"Jde o hesla prolety",
|
||||
"Jsou to paradoxní hesla, kterými strana ospravedlňuje útlak"
|
||||
],
|
||||
"correct": 3,
|
||||
"hint": "Strana používá dvojité myšlení: hesla znějí vznešeně, ale ve skutečnosti obhajují válku, otroctví a nevědomost."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kdo je Velký bratr a je reálnou osobou?",
|
||||
"options": [
|
||||
"Skutečný vůdce Bratrstva",
|
||||
"Winstonův nadřízený na ministerstvu",
|
||||
"Symbol všemocné moci a kultu osobnosti; možná vůbec neexistuje",
|
||||
"O'Brienův pseudonym"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Velký bratr je tvář režimu. Není jasné, zda jde o živou osobu, nebo jen o symbol, který drží strach a poslušnost."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je Newspeak (novořeč) a proč ho strana zavádí?",
|
||||
"options": [
|
||||
"Tajná řeč Bratrstva",
|
||||
"Zjednodušená angličtina omezující slovní zásobu, aby nešlo vyslovit zakázané myšlenky",
|
||||
"Úřední jazyk prolety",
|
||||
"Šifra Policie myšlení"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Newspeak neustále ubírá slova. Když pro svobodu nebo odpor neexistuje slovo, nelze o nich ani myslet."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je teleobrazovka (telescreen) a jak funguje?",
|
||||
"options": [
|
||||
"Televize určená jen pro zábavu",
|
||||
"Zařízení, které vysílá i sleduje; nelze ji úplně vypnout",
|
||||
"Rozhlasová stanice",
|
||||
"Počítač Ministerstva pravdy"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Telescreen funguje obousměrně – neustále sleduje občany. Vypnout lze jen zvuk, obraz běží dál."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak se jmenují tři supervelmoci světa 1984?",
|
||||
"options": [
|
||||
"Oceánie, Eurasie a Východasie",
|
||||
"Oceánie, Evropa a Asie",
|
||||
"Anglie, Eurasie a Ostasie",
|
||||
"Oceánie, Východasie a Afrika"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Svět je rozdělen na tři státy ve věčném, ale nerozhodném konfliktu: Oceánii, Eurasii a Východasii."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Čím se zabývá Ministerstvo pravdy?",
|
||||
"options": [
|
||||
"Vede válku",
|
||||
"Dohlíží na morálku a sňatky",
|
||||
"Upravuje a přepisuje historii, noviny a archivy",
|
||||
"Rozděluje potraviny"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Ministerstvo pravdy vyrábí „pravdu“ – Winston v něm přepisuje staré záznamy tak, aby vždy odpovídaly současné linii strany."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "K čemu slouží Ministerstvo lásky?",
|
||||
"options": [
|
||||
"Zajišťuje sňatky a lásku",
|
||||
"Je to budova bez oken, kde se mučí a likvidují nepřátelé strany",
|
||||
"Vyrábí zbraně",
|
||||
"Školí děti"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Ministerstvo lásky je opakem svého názvu – sídlí tam mučírny a nachází se v něm i obávaná místnost 101."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "V čem je paradox Ministerstva hojnosti?",
|
||||
"options": [
|
||||
"Má na starosti hospodářství, ale ve skutečnosti drží lidi v nedostatku",
|
||||
"Rozdává jídlo a zboží zdarma",
|
||||
"Zajišťuje dostatek pro všechny",
|
||||
"Starají se o něj prolety"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Ministerstvo hojnosti plánuje výrobu, ale ekonomika je záměrně nastavena tak, aby lidé žili v trvalém nedostatku."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je Ministerstvo míru a čím se zabývá?",
|
||||
"options": [
|
||||
"Uzavírá mírové smlouvy",
|
||||
"Dojednává příměří",
|
||||
"Ve skutečnosti vede válku",
|
||||
"Velí Policii myšlení"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Název je přesně opačný: Ministerstvo míru řídí válku, protože trvalý válečný stav pomáhá udržet moc strany."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kdo je Julie a jaký vztah s ní Winston naváže?",
|
||||
"options": [
|
||||
"Winstonova žena",
|
||||
"Mladá členka strany, s níž Winston prožije tajnou milostnou aféru",
|
||||
"O'Brienova asistentka",
|
||||
"Proletka a obchodnice"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Julie pracuje v oddělení beletrie a naoko je vzornou členkou strany. S Winstonem vytvoří tajný milostný vztah."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Proč je vztah Winstona a Julie zakázaný?",
|
||||
"options": [
|
||||
"Strana zakazuje lásku a sexualitu jako zločin; vztah je navíc tajný",
|
||||
"Julie je vdaná za člena vnitřní strany",
|
||||
"Winston je jejím nadřízeným",
|
||||
"Julie patří k proletům"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Strana potlačuje citové a sexuální vazby, protože oddanost má patřit jen straně. Tajný vztah je proto dvojnásob nebezpečný."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kdo je O'Brien a jakou roli hraje v příběhu?",
|
||||
"options": [
|
||||
"Skutečný vůdce Bratrstva",
|
||||
"Vysoce postavený člen vnitřní strany, který Winstona nakonec mučí a „vyléčí“",
|
||||
"Winstonův přítel z dětství",
|
||||
"Proletářský básník"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Winston věří, že O'Brien je odbojář. Ve skutečnosti je to věrný člen vnitřní strany, který ho dostane do místnosti 101."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je Bratrstvo a existuje vůbec?",
|
||||
"options": [
|
||||
"Skutečná armáda Oceánie",
|
||||
"Náboženská organizace",
|
||||
"Údajná tajná organizace odporu; není jisté, zda existuje",
|
||||
"Odbory prolety"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Bratrstvo zmiňuje Goldstein i O'Brien, ale kniha nikdy nepotvrdí, že doopravdy existuje – může jít o past strany."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kdo je Emmanuel Goldstein?",
|
||||
"options": [
|
||||
"Ministr pravdy",
|
||||
"Oficiální nepřítel státu a údajný vůdce Bratrstva",
|
||||
"Winstonův kolega",
|
||||
"Velký bratrův dvojník"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Goldstein je terčem dvouminutovky nenávisti. Má ztělesňovat nepřítele, proti kterému strana sjednocuje občany."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je dvojité myšlení (doublethink)?",
|
||||
"options": [
|
||||
"Schopnost myslet ve dvou jazycích",
|
||||
"Zdvojení osobnosti",
|
||||
"Schopnost věřit dvěma protikladným tvrzením zároveň",
|
||||
"Tajné zapisování myšlenek"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Dvojité myšlení je jádro stranické ideologie – člověk přijme i zjevné rozpory, jako „válka je mír“."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Proč strana neustále přepisuje minulost?",
|
||||
"options": [
|
||||
"Aby opravila chyby v archivech",
|
||||
"Aby měla vždy pravdu a nepřítel jí nemohl dokázat opak",
|
||||
"Aby ušetřila papír",
|
||||
"Aby potěšila prolety"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Kdo ovládá minulost, ovládá i budoucnost. Přepisováním historie strana udržuje absolutní kontrolu nad pravdou."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je zločin myšlení (thoughtcrime)?",
|
||||
"options": [
|
||||
"Pouhá myšlenka nebo názor, který se nelíbí straně",
|
||||
"Krádež státního majetku",
|
||||
"Nelegální obchod",
|
||||
"Opomenutí dvouminutovky"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Strana trestá už samotné myšlenky. Winston se proviní tím, že si do deníku napíše „Dolů s Velkým bratrem“."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kdo je pan Charrington a jaké je jeho skutečné postavení?",
|
||||
"options": [
|
||||
"Winstonův přítel a majitel hospody",
|
||||
"Proletářský básník",
|
||||
"Zdánlivě starý majitel obchodu, ve skutečnosti agent Policie myšlení",
|
||||
"Člen Bratrstva"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Charrington Winstonovi pronajme pokoj bez teleobrazovky, ale právě on nakonec oba milence udá."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je místnost 101?",
|
||||
"options": [
|
||||
"Winstonova cela na policii",
|
||||
"Skladiště knih",
|
||||
"Místnost, ve které každého čeká jeho největší strach",
|
||||
"Zasedací místnost vnitřní strany"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Místnost 101 je vrcholný mučicí nástroj. Winstonovi v ní hrozí krysy – jeho největší fobie."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak skončí vztah Winstona a Julie?",
|
||||
"options": [
|
||||
"Utečou spolu do Eurasie",
|
||||
"Vezmou se a žijí svobodně",
|
||||
"Winston se stane členem Bratrstva",
|
||||
"Zradí se a nakonec se přestanou milovat"
|
||||
],
|
||||
"correct": 3,
|
||||
"hint": "Oba jsou po mučení zlomení a vzájemně se zradí. Na konci už k sobě nic necítí."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co znamená závěrečná věta „Miloval Velkého bratra“?",
|
||||
"options": [
|
||||
"Winstonův vzdor se změnil v pokornou lásku k režimu",
|
||||
"Winston se stal novým vůdcem",
|
||||
"Byl to jen Winstonův sen",
|
||||
"Julie mu to řekla"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Winston byl vnitřně zlomen. Přestal být sám sebou a nakonec režim opravdu přijal za svůj."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Které tvrzení NENÍ jedním z hlavních témat románu?",
|
||||
"options": [
|
||||
"Moc a absolutní kontrola",
|
||||
"Pravda a přepisování historie",
|
||||
"Svoboda a identita",
|
||||
"Idylický venkovský život"
|
||||
],
|
||||
"correct": 3,
|
||||
"hint": "1984 je temná antiutopie. Hlavními tématy jsou moc, kontrola, manipulace s pravdou a ztráta svobody, ne venkovská idyla."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "V jakém roce byl román 1984 poprvé vydán?",
|
||||
"options": [
|
||||
"1936",
|
||||
"1949",
|
||||
"1954",
|
||||
"1984"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Román vyšel v roce 1949; letopočet 1984 označuje blízkou budoucnost, do níž Orwell příběh zasadil."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Které další Orwellovo dílo se dá s 1984 srovnat?",
|
||||
"options": [
|
||||
"Farma zvířat",
|
||||
"Velký Gatsby",
|
||||
"Zločin a trest",
|
||||
"Babička"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Obě Orwellova díla varují před totalitou. Farma zvířat ji líčí alegoricky pomocí zvířat, 1984 očima jedince."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je „dvouminutovka nenávisti“?",
|
||||
"options": [
|
||||
"Krátká každodenní akce, při níž občané společně nenávidí Goldsteina a nepřítele",
|
||||
"Pravidelná modlitba ke Velkému bratrovi",
|
||||
"Sportovní přestávka ve škole",
|
||||
"Porada vedení ministerstva"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Slouží k uvolnění a přesměrování agrese občanů na nepřítele státu, čímž strana posiluje svou moc."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Proč je podle knihy válka pro systém nezbytná?",
|
||||
"options": [
|
||||
"Přináší mír a blahobyt",
|
||||
"Udržuje strach, spotřebovává výrobu a zachovává chudobu i hierarchii",
|
||||
"Rozvíjí vědu a techniku",
|
||||
"Sjednocuje vládu s prolety"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Trvalá válka pohlcuje nadbytečnou produkci a udržuje lidi ve strachu i chudobě, takže se nemohou vzbouřit."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaký je rozdíl mezi vnějšími členy strany a prolety?",
|
||||
"options": [
|
||||
"Proleti mají více práv než členové strany",
|
||||
"Vnější členové jsou dozíráni, kdežto proleti žijí v chudobě a strana o ně nestojí",
|
||||
"Není mezi nimi žádný rozdíl",
|
||||
"Proleti tvoří vedení strany"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Proleti tvoří asi 85 % obyvatel. Žijí v bídě a jsou považováni za neškodné, dokud si neuvědomí svou sílu."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jak strana kontroluje sexualitu a osobní vztahy?",
|
||||
"options": [
|
||||
"Podporuje volnou lásku a nezávislost",
|
||||
"Potlačuje lásku a sexualitu, děti vede k oddanosti straně",
|
||||
"Zakazuje veškerá manželství",
|
||||
"Povoluje vztahy jen s prolety"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Strana chce, aby veškerá oddanost a energie patřily jí. Proto podporuje zdrženlivost a vede děti v Antisexuální lize mládeže."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co symbolizuje papírové těžítko, které si Winston koupí?",
|
||||
"options": [
|
||||
"Jeho nenávist ke Goldsteinovi",
|
||||
"Kousek krásy a minulosti, kterou už nelze vrátit",
|
||||
"Moc Velkého bratra",
|
||||
"Lásku k Julii"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Těžítko je drobný, krásný předmět z minulosti. Pro Winstona ztělesňuje svět, který strana zničila."
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaké poselství chtěl Orwell románem nejspíš předat?",
|
||||
"options": [
|
||||
"Varování před totalitou a ztrátou svobody i pravdy",
|
||||
"Oslavu technického pokroku",
|
||||
"Návod, jak založit stát",
|
||||
"Kritiku sportu a zábavy"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "1984 je varování: když lidé přestanou bránit pravdu a svobodu, mohou o ně nenávratně přijít."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"title": "Děj",
|
||||
"heading": "Seřaďte děj",
|
||||
"subtitleKey": "lit_plot_sub",
|
||||
"items": [
|
||||
{
|
||||
"kind": "assembly",
|
||||
"prompt": "Přetáhněte části příběhu do správného pořadí:",
|
||||
"words": [
|
||||
"Winston upravuje historii na Ministerstvu pravdy",
|
||||
"Seznámí se s Julií a začnou se tajně scházet",
|
||||
"Pronajme si pokoj nad obchodem pana Charringtona",
|
||||
"Přečte Goldsteinovu knihu u O'Briena",
|
||||
"Jsou zatčeni Policií myšlení",
|
||||
"Winston se zlomí v místnosti 101"
|
||||
],
|
||||
"meaning": "Správné pořadí: přepisování historie → seznámení s Julií → pronájem pokoje → četba Goldsteinovy knihy → zatčení → místnost 101."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
2200
content/lessons/deutsch/unit-01.json
Normal file
2200
content/lessons/deutsch/unit-01.json
Normal file
File diff suppressed because it is too large
Load diff
2788
content/lessons/deutsch/unit-02.json
Normal file
2788
content/lessons/deutsch/unit-02.json
Normal file
File diff suppressed because it is too large
Load diff
2731
content/lessons/deutsch/unit-03.json
Normal file
2731
content/lessons/deutsch/unit-03.json
Normal file
File diff suppressed because it is too large
Load diff
125
content/lessons/hardware/unit-01.json
Normal file
125
content/lessons/hardware/unit-01.json
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
{
|
||||
"id": "unit-01",
|
||||
"subject": "hardware",
|
||||
"number": 1,
|
||||
"titleKey": "hw_unit1",
|
||||
"subtitleKey": "hw_unit1_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 3 • Úvod",
|
||||
"title": "Architektura počítače",
|
||||
"tip": "Architektura popisuje, z jakých částí se počítač skládá a jak spolu spolupracují.",
|
||||
"lines": [
|
||||
"Architektura počítače je základní model, podle kterého jsou počítače navrhovány.",
|
||||
"Nejznámější model je Von Neumannovo schéma.",
|
||||
"Používá se dodnes u většiny běžných počítačů."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 3 • Osobnost",
|
||||
"title": "John von Neumann",
|
||||
"tip": "Tip: zapamatuj si jméno – podle něj se jmenuje celé schéma.",
|
||||
"lines": [
|
||||
"John von Neumann byl americký matematik.",
|
||||
"Pracoval na poli digitálních počítačů a operační teorie kvantové mechaniky.",
|
||||
"Je autorem Von Neumannovy algebry.",
|
||||
"Byl tvůrcem teorie her.",
|
||||
"Podílel se na vývoji atomové a vodíkové bomby."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 3 • Schéma",
|
||||
"title": "Von Neumannovo schéma",
|
||||
"tip": "ALU = aritmeticko-logická jednotka",
|
||||
"lines": [
|
||||
"Von Neumannovo schéma má pět hlavních částí:",
|
||||
"ALU – aritmeticko-logická jednotka (výpočty a logika)",
|
||||
"Řadič – řídí činnost ostatních částí",
|
||||
"Operační paměť – uchovává program i data",
|
||||
"Vstupní jednotka – přivádí data do počítače",
|
||||
"Výstupní jednotka – předává výsledky ven"
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "hw_quiz1_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kdo je autorem Von Neumannova schématu?",
|
||||
"options": [
|
||||
"Alan Turing",
|
||||
"John von Neumann",
|
||||
"Bill Gates",
|
||||
"Steve Jobs"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "John von Neumann – americký matematik"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Čím byl John von Neumann?",
|
||||
"options": [
|
||||
"Americký matematik",
|
||||
"Německý fyzik",
|
||||
"Anglický chemik",
|
||||
"Francouzský lékař"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Americký matematik s objevy v digitálních počítačích"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co znamená zkratka ALU?",
|
||||
"options": [
|
||||
"Automatická logická síť",
|
||||
"Aritmeticko-logická jednotka",
|
||||
"Analogová linková sestava",
|
||||
"Adresová listová sekce"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "ALU = aritmeticko-logická jednotka"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Která část Von Neumannova schématu řídí ostatní části?",
|
||||
"options": [
|
||||
"Operační paměť",
|
||||
"Vstupní jednotka",
|
||||
"Řadič",
|
||||
"Výstupní jednotka"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Řadič řídí činnost ostatních částí"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kde se podle Von Neumannova schématu uchovává program i data?",
|
||||
"options": [
|
||||
"Jen na pevném disku",
|
||||
"V operační paměti",
|
||||
"Jen ve vstupní jednotce",
|
||||
"Jen v ALU"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Operační paměť uchovává program i data"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Čím se John von Neumann také zabýval?",
|
||||
"options": [
|
||||
"Jen malířstvím",
|
||||
"Teorií her",
|
||||
"Jen botanikou",
|
||||
"Jen hudbou"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Byl tvůrcem teorie her"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
177
content/lessons/hardware/unit-02.json
Normal file
177
content/lessons/hardware/unit-02.json
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"id": "unit-02",
|
||||
"subject": "hardware",
|
||||
"number": 2,
|
||||
"titleKey": "hw_unit2",
|
||||
"subtitleKey": "hw_unit2_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 6 • Přehled",
|
||||
"title": "Historie počítačů",
|
||||
"tip": "Generace se dělí podle klíčové technologie své doby.",
|
||||
"lines": [
|
||||
"1. Předchůdci – do 30. let 19. století",
|
||||
"2. Nultá generace – 1938–1944",
|
||||
"3. První generace – 1944–1955",
|
||||
"4. Druhá generace – 1955–1964",
|
||||
"5. Třetí generace – 1964–1971",
|
||||
"Další snímky shrnují znaky jednotlivých generací."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 6 • 0. generace",
|
||||
"title": "Počítače 0. generace (1938–1944)",
|
||||
"tip": "Tip: počítačů bylo málo a často sloužily armádě.",
|
||||
"lines": [
|
||||
"Existovalo jen několik počítačů na světě.",
|
||||
"Většinou měly vojenský účel."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 6 • 1. generace",
|
||||
"title": "Počítače 1. generace (1944–1955)",
|
||||
"tip": "Klíčový objev: elektronka",
|
||||
"lines": [
|
||||
"Objev elektronky umožnil tuto generaci.",
|
||||
"Ještě neexistoval software v dnešním smyslu.",
|
||||
"Data a programy se zadávaly pomocí děrných štítků."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 6 • 2. generace",
|
||||
"title": "Počítače 2. generace (1955–1964)",
|
||||
"tip": "Klíčový objev: tranzistor",
|
||||
"lines": [
|
||||
"Objev tranzistoru nahradil elektronky.",
|
||||
"Používala se hlavní externí paměť.",
|
||||
"Počítače měly menší rozměry než v 1. generaci."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 6 • 3. generace",
|
||||
"title": "Počítače 3. generace (1964–1971)",
|
||||
"tip": "Klíčový objev: integrovaný obvod",
|
||||
"lines": [
|
||||
"Objev integrovaného obvodu.",
|
||||
"Stále se používala hlavní externí paměť.",
|
||||
"Rozměry počítačů se dál zmenšovaly."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "6 / 6 • 4. generace",
|
||||
"title": "Počítače 4. generace",
|
||||
"tip": "Klíčové: programovatelné mikroprocesory",
|
||||
"lines": [
|
||||
"Nastupují programovatelné mikroprocesory.",
|
||||
"1972 – první počítačová hra",
|
||||
"1973 – první disketa",
|
||||
"1979 – první CD"
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "hw_quiz2_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kdy probíhala nultá generace počítačů?",
|
||||
"options": [
|
||||
"1938–1944",
|
||||
"1944–1955",
|
||||
"1955–1964",
|
||||
"1964–1971"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Nultá generace: 1938–1944"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co bylo typické pro počítače 0. generace?",
|
||||
"options": [
|
||||
"Byly v každé domácnosti",
|
||||
"Jen několik kusů, často vojenský účel",
|
||||
"Měly mikroprocesory",
|
||||
"Používaly CD"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Jen několik počítačů, většinou vojenský účel"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Která technologie patří k 1. generaci?",
|
||||
"options": [
|
||||
"Tranzistor",
|
||||
"Elektronka",
|
||||
"Integrovaný obvod",
|
||||
"Mikroprocesor"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "1. generace – objev elektronky"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Čím se zadávaly data v 1. generaci?",
|
||||
"options": [
|
||||
"USB fleškou",
|
||||
"Děrnými štítky",
|
||||
"CD",
|
||||
"Disketou"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "1. generace – děrné štítky"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Která technologie patří k 2. generaci?",
|
||||
"options": [
|
||||
"Elektronka",
|
||||
"Tranzistor",
|
||||
"Mikroprocesor",
|
||||
"CD"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "2. generace – objev tranzistoru"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Která technologie patří k 3. generaci?",
|
||||
"options": [
|
||||
"Elektronka",
|
||||
"Tranzistor",
|
||||
"Integrovaný obvod",
|
||||
"Disketa"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "3. generace – integrovaný obvod"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je typické pro 4. generaci?",
|
||||
"options": [
|
||||
"Jen děrné štítky",
|
||||
"Programovatelné mikroprocesory",
|
||||
"Jen vojenské použití",
|
||||
"Žádný software"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "4. generace – programovatelné mikroprocesory"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kdy vznikla první počítačová hra (podle lekce)?",
|
||||
"options": [
|
||||
"1964",
|
||||
"1972",
|
||||
"1973",
|
||||
"1979"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "1972 – první počítačová hra"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
86
content/lessons/hardware/unit-03.json
Normal file
86
content/lessons/hardware/unit-03.json
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"id": "unit-03",
|
||||
"subject": "hardware",
|
||||
"number": 3,
|
||||
"titleKey": "hw_unit3",
|
||||
"subtitleKey": "hw_unit3_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 2 • Bit",
|
||||
"title": "Bit – nejmenší jednotka informace",
|
||||
"tip": "Bit = binary digit (dvojková číslice)",
|
||||
"lines": [
|
||||
"Bit je dvojková číslice – nabývá hodnoty 0 nebo 1.",
|
||||
"Je to nejmenší zobrazitelná jednotka informace.",
|
||||
"Z bitů se skládají větší jednotky (např. byte)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 2 • Byte",
|
||||
"title": "Byte – adresovatelná jednotka paměti",
|
||||
"tip": "Tip: 1 B = 8 b",
|
||||
"lines": [
|
||||
"Byte je nejmenší adresovatelná jednotka paměti.",
|
||||
"Počítač tedy paměť adresuje po bytech, ne po jednotlivých bitech.",
|
||||
"Jeden byte obvykle obsahuje 8 bitů."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "hw_quiz3_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je bit?",
|
||||
"options": [
|
||||
"Největší jednotka paměti",
|
||||
"Dvojková číslice – nejmenší jednotka informace",
|
||||
"Jen klávesa na klávesnici",
|
||||
"Typ procesoru"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Bit = dvojková číslice, nejmenší jednotka informace"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jaké hodnoty může nabývat bit?",
|
||||
"options": [
|
||||
"Jen 0",
|
||||
"Jen 1",
|
||||
"0 nebo 1",
|
||||
"0 až 255"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Bit nabývá hodnoty 0 nebo 1"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je byte?",
|
||||
"options": [
|
||||
"Nejmenší adresovatelná jednotka paměti",
|
||||
"Nejmenší zobrazitelná jednotka informace",
|
||||
"Jen síťový kabel",
|
||||
"Jen grafická karta"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Byte = nejmenší adresovatelná jednotka paměti"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kolik bitů má obvykle jeden byte?",
|
||||
"options": [
|
||||
"2",
|
||||
"4",
|
||||
"8",
|
||||
"16"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "1 B = 8 b"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
178
content/lessons/networks/unit-01.json
Normal file
178
content/lessons/networks/unit-01.json
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
{
|
||||
"id": "unit-01",
|
||||
"subject": "networks",
|
||||
"number": 1,
|
||||
"titleKey": "net_unit1",
|
||||
"subtitleKey": "net_unit1_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 4 • VLSM",
|
||||
"title": "Podsíťování sítí",
|
||||
"tip": "Tip: prefix /n = počet jedniček v masce. /24 = 255.255.255.0",
|
||||
"lines": [
|
||||
"Cíl: rozdělit jednu IP síť na menší podsítě podle počtu uzlů.",
|
||||
"Pamatuj si tři role adres v každém bloku:",
|
||||
"síťová = první adresa bloku",
|
||||
"broadcast = poslední adresa bloku",
|
||||
"uzly = vše mezi nimi (síť + 1 až broadcast − 1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 4 • Velikosti",
|
||||
"title": "Jak velký blok potřebuji?",
|
||||
"tip": "Počet bitů pro uzly h = log₂(velikost), prefix = 32 − h",
|
||||
"lines": [
|
||||
"Pro X uzlů potřebuješ blok, do kterého se vejde X + 2 adresy (síť a broadcast).",
|
||||
"Velikost bloku je vždy mocnina dvojky – vezmi nejmenší blok, který je větší nebo roven X + 2.",
|
||||
"60 uzlů → 62 → blok 64 → /26",
|
||||
"30 uzlů → 32 → blok 32 → /27",
|
||||
"14 uzlů → 16 → blok 16 → /28",
|
||||
"7 uzlů → 9 → blok 16 → /28"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 4 • Postup",
|
||||
"title": "Jak na to krok za krokem",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"1. Seřaď podsítě od největší po nejmenší.",
|
||||
"2. Pro každou najdi potřebný prefix.",
|
||||
"3. Začni na síťové adrese ze zadání.",
|
||||
"4. Rozsah podsítě = od síťové adresy po broadcast.",
|
||||
"5. Rozsah uzlů = síťová + 1 až broadcast − 1.",
|
||||
"6. Posuň se hned za broadcast a pokračuj dál."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 4 • Příklad",
|
||||
"title": "Ukázka: 10.0.0.0/24",
|
||||
"tip": "A: 60 uzlů, B: 30, C: 14, D: 7",
|
||||
"lines": [
|
||||
"A → 10.0.0.0/26 uzly 10.0.0.1–10.0.0.62 broadcast 10.0.0.63",
|
||||
"B → 10.0.0.64/27 uzly 10.0.0.65–10.0.0.94 broadcast 10.0.0.95",
|
||||
"C → 10.0.0.96/28 uzly 10.0.0.97–10.0.0.110 broadcast 10.0.0.111",
|
||||
"D → 10.0.0.112/28 uzly 10.0.0.113–10.0.0.126 broadcast 10.0.0.127"
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "VLSM",
|
||||
"subtitleKey": "net_ex1_title",
|
||||
"items": [
|
||||
{
|
||||
"kind": "subnet",
|
||||
"scenario": "1.) Síť má adresu 10.0.0.0/24. Rozdělte ji do podsítí s následujícími požadavky:\n\nPodsíť A: 60 uzlů\nPodsíť B: 30 uzlů\nPodsíť C: 14 uzlů\nPodsíť D: 7 uzlů",
|
||||
"solution": "A: 10.0.0.0/26 síť 10.0.0.0 uzly 10.0.0.1–10.0.0.62 broadcast 10.0.0.63\nB: 10.0.0.64/27 síť 10.0.0.64 uzly 10.0.0.65–10.0.0.94 broadcast 10.0.0.95\nC: 10.0.0.96/28 síť 10.0.0.96 uzly 10.0.0.97–10.0.0.110 broadcast 10.0.0.111\nD: 10.0.0.112/28 síť 10.0.0.112 uzly 10.0.0.113–10.0.0.126 broadcast 10.0.0.127",
|
||||
"blocks": [
|
||||
{
|
||||
"prefix": 26,
|
||||
"network": "10.0.0.0",
|
||||
"broadcast": "10.0.0.63",
|
||||
"firstHost": "10.0.0.1",
|
||||
"lastHost": "10.0.0.62"
|
||||
},
|
||||
{
|
||||
"prefix": 27,
|
||||
"network": "10.0.0.64",
|
||||
"broadcast": "10.0.0.95",
|
||||
"firstHost": "10.0.0.65",
|
||||
"lastHost": "10.0.0.94"
|
||||
},
|
||||
{
|
||||
"prefix": 28,
|
||||
"network": "10.0.0.96",
|
||||
"broadcast": "10.0.0.111",
|
||||
"firstHost": "10.0.0.97",
|
||||
"lastHost": "10.0.0.110"
|
||||
},
|
||||
{
|
||||
"prefix": 28,
|
||||
"network": "10.0.0.112",
|
||||
"broadcast": "10.0.0.127",
|
||||
"firstHost": "10.0.0.113",
|
||||
"lastHost": "10.0.0.126"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "subnet",
|
||||
"scenario": "2.) Síť má adresu 172.16.0.0/16. Rozdělte ji do podsítí s následujícími požadavky:\n\nPodsíť A: 500 uzlů\nPodsíť B: 200 uzlů\nPodsíť C: 100 uzlů\nPodsíť D: 50 uzlů",
|
||||
"solution": "A: 172.16.0.0/23 síť 172.16.0.0 uzly 172.16.0.1–172.16.1.254 broadcast 172.16.1.255\nB: 172.16.2.0/24 síť 172.16.2.0 uzly 172.16.2.1–172.16.2.254 broadcast 172.16.2.255\nC: 172.16.3.0/25 síť 172.16.3.0 uzly 172.16.3.1–172.16.3.126 broadcast 172.16.3.127\nD: 172.16.3.128/26 síť 172.16.3.128 uzly 172.16.3.129–172.16.3.190 broadcast 172.16.3.191",
|
||||
"blocks": [
|
||||
{
|
||||
"prefix": 23,
|
||||
"network": "172.16.0.0",
|
||||
"broadcast": "172.16.1.255",
|
||||
"firstHost": "172.16.0.1",
|
||||
"lastHost": "172.16.1.254"
|
||||
},
|
||||
{
|
||||
"prefix": 24,
|
||||
"network": "172.16.2.0",
|
||||
"broadcast": "172.16.2.255",
|
||||
"firstHost": "172.16.2.1",
|
||||
"lastHost": "172.16.2.254"
|
||||
},
|
||||
{
|
||||
"prefix": 25,
|
||||
"network": "172.16.3.0",
|
||||
"broadcast": "172.16.3.127",
|
||||
"firstHost": "172.16.3.1",
|
||||
"lastHost": "172.16.3.126"
|
||||
},
|
||||
{
|
||||
"prefix": 26,
|
||||
"network": "172.16.3.128",
|
||||
"broadcast": "172.16.3.191",
|
||||
"firstHost": "172.16.3.129",
|
||||
"lastHost": "172.16.3.190"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "subnet",
|
||||
"scenario": "3.) Síť má adresu 192.168.10.0/24. Rozdělte ji do podsítí s následujícími požadavky:\n\nPodsíť A: 120 uzlů\nPodsíť B: 70 uzlů\nPodsíť C: 35 uzlů\nPodsíť D: 15 uzlů",
|
||||
"solution": "Do sítě /24 se tyto požadavky nevejdou: A i B potřebují /25 (2 × 128 = 256 adres), na C a D už nezbude žádný blok. Při zachování velikostí by bylo nutné větší síť (např. /23).",
|
||||
"blocks": null
|
||||
},
|
||||
{
|
||||
"kind": "subnet",
|
||||
"scenario": "4.) Síť má adresu 192.168.2.0/25. Rozdělte ji do podsítí s následujícími požadavky:\n\nPodsíť A: 50 uzlů\nPodsíť B: 25 uzlů\nPodsíť C: 12 uzlů\nPodsíť D: 5 uzlů",
|
||||
"solution": "A: 192.168.2.0/26 síť 192.168.2.0 uzly 192.168.2.1–192.168.2.62 broadcast 192.168.2.63\nB: 192.168.2.64/27 síť 192.168.2.64 uzly 192.168.2.65–192.168.2.94 broadcast 192.168.2.95\nC: 192.168.2.96/28 síť 192.168.2.96 uzly 192.168.2.97–192.168.2.110 broadcast 192.168.2.111\nD: 192.168.2.112/28 síť 192.168.2.112 uzly 192.168.2.113–192.168.2.126 broadcast 192.168.2.127",
|
||||
"blocks": [
|
||||
{
|
||||
"prefix": 26,
|
||||
"network": "192.168.2.0",
|
||||
"broadcast": "192.168.2.63",
|
||||
"firstHost": "192.168.2.1",
|
||||
"lastHost": "192.168.2.62"
|
||||
},
|
||||
{
|
||||
"prefix": 27,
|
||||
"network": "192.168.2.64",
|
||||
"broadcast": "192.168.2.95",
|
||||
"firstHost": "192.168.2.65",
|
||||
"lastHost": "192.168.2.94"
|
||||
},
|
||||
{
|
||||
"prefix": 28,
|
||||
"network": "192.168.2.96",
|
||||
"broadcast": "192.168.2.111",
|
||||
"firstHost": "192.168.2.97",
|
||||
"lastHost": "192.168.2.110"
|
||||
},
|
||||
{
|
||||
"prefix": 28,
|
||||
"network": "192.168.2.112",
|
||||
"broadcast": "192.168.2.127",
|
||||
"firstHost": "192.168.2.113",
|
||||
"lastHost": "192.168.2.126"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
173
content/lessons/networks/unit-02.json
Normal file
173
content/lessons/networks/unit-02.json
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
{
|
||||
"id": "unit-02",
|
||||
"subject": "networks",
|
||||
"number": 2,
|
||||
"titleKey": "net_unit2",
|
||||
"subtitleKey": "net_unit2_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 5 • Jednotky",
|
||||
"title": "Bit, byte a počítačová síť",
|
||||
"tip": "Tip: 1 B = 8 b → 2⁸ = 256 možných hodnot",
|
||||
"lines": [
|
||||
"Bit (1 b): základní jednotka informace – 0 nebo 1.",
|
||||
"0 = nevede proud, 1 = vede proud.",
|
||||
"Byte (1 B): 1 B = 8 b, nabývá 256 hodnot.",
|
||||
"Počítačová síť: propojení uzlů (PC a dalších prvků).",
|
||||
"Vyžaduje hardwarové i softwarové prostředky."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 5 • Propojení",
|
||||
"title": "Trvalé, dočasné a důvody zapojení",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Trvalé propojení: kabely – síť zůstává připojená i po přenosu.",
|
||||
"Dočasné propojení: modem, Wi‑Fi – po přenosu se odpojuje.",
|
||||
"Důvody zapojení do sítě:",
|
||||
"sdílení dat (nutná podpora aplikací)",
|
||||
"sdílení HW (tiskárny, disky) a SW (databáze)",
|
||||
"vyšší spolehlivost, bezpečnost (přístupová práva)",
|
||||
"komunikace (e‑mail, hlas)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 5 • Pojmy",
|
||||
"title": "Terminologie sítí",
|
||||
"tip": "Architektura = topologie + standard",
|
||||
"lines": [
|
||||
"Uzel: jakékoli zařízení (stanice, server, aktivní prvek).",
|
||||
"Topologie: způsob propojení (hvězda, kruh, sběrnice).",
|
||||
"Standard: normy pro zpracování dat.",
|
||||
"Architektura: topologie + standard."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 5 • Rozlehlost",
|
||||
"title": "WAN, MAN a LAN",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"WAN: rozsáhlé sítě (státy, kontinenty), např. internet.",
|
||||
"MAN: městské sítě – propojují lokální sítě.",
|
||||
"LAN: lokální sítě (budova), kabeláž v řádu km,",
|
||||
"jednotná architektura."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 5 • Uzly",
|
||||
"title": "Server, stanice a peer‑to‑peer",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Server: nadřazený, řídící, poskytuje služby, vyšší výkon.",
|
||||
"Pracovní stanice: pro uživatele, jednodušší konfigurace.",
|
||||
"Peer‑to‑peer: síť bez serverů – jen stanice,",
|
||||
"které sdílejí data a tiskárny."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz2_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kolik bitů má jeden byte?",
|
||||
"options": [
|
||||
"4",
|
||||
"8",
|
||||
"16",
|
||||
"32"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "1 B = 8 b"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kolik různých hodnot nabývá jeden byte?",
|
||||
"options": [
|
||||
"128",
|
||||
"255",
|
||||
"256",
|
||||
"512"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "2⁸ = 256 hodnot (0–255)"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je trvalé propojení sítě?",
|
||||
"options": [
|
||||
"Modem",
|
||||
"Wi‑Fi",
|
||||
"Kabely",
|
||||
"Bluetooth"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Kabely – síť zůstává připojená i po přenosu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co označuje zkratka WAN?",
|
||||
"options": [
|
||||
"Lokální síť v budově",
|
||||
"Městskou síť",
|
||||
"Rozsáhlou síť (státy, kontinenty)",
|
||||
"Bezdrátovou síť"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "WAN = Wide Area Network, např. internet"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co je topologie sítě?",
|
||||
"options": [
|
||||
"Norma pro zpracování dat",
|
||||
"Způsob propojení uzlů",
|
||||
"Název serveru",
|
||||
"Rychlost přenosu"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Např. hvězda, kruh, sběrnice"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Architektura sítě je:",
|
||||
"options": [
|
||||
"Jen topologie",
|
||||
"Jen standard",
|
||||
"Topologie + standard",
|
||||
"Pouze typ kabelu"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Architektura = topologie + standard"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Peer‑to‑peer síť znamená:",
|
||||
"options": [
|
||||
"Síť s jedním výkonným serverem",
|
||||
"Síť bez serverů – stanice sdílejí data",
|
||||
"Jen internetovou síť",
|
||||
"Síť pouze s tiskárnami"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Bez centrálního serveru – stanice si sdílejí data a tiskárny"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Server v síti je:",
|
||||
"options": [
|
||||
"Jednoduchá stanice pro uživatele",
|
||||
"Nadřazený uzel poskytující služby",
|
||||
"Jen aktivní prvek kabeláže",
|
||||
"Dočasné Wi‑Fi připojení"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Server řídí a poskytuje služby, má vyšší výkon"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
161
content/lessons/networks/unit-03.json
Normal file
161
content/lessons/networks/unit-03.json
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
{
|
||||
"id": "unit-03",
|
||||
"subject": "networks",
|
||||
"number": 3,
|
||||
"titleKey": "net_unit3",
|
||||
"subtitleKey": "net_unit3_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 4 • Servery",
|
||||
"title": "Role serverů a typy podle služby",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Servery ověřují uživatele a povolují přístup.",
|
||||
"Souborový server (fileserver): sdílení souborů.",
|
||||
"Tiskový server: sdílení tiskáren.",
|
||||
"Poštovní server: e‑mailové služby.",
|
||||
"Databázový server: správa a přístup k databázím."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 4 • Rozložení",
|
||||
"title": "Centralizovaný a distribuovaný server",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Centralizovaný: jeden server v síti.",
|
||||
"Výpadek = nedostupnost služby.",
|
||||
"Distribuovaný: více serverů, každý obsluhuje část.",
|
||||
"Vyšší spolehlivost a rychlost."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 4 • Využití",
|
||||
"title": "Vyhrazený, nevyhrazený a mainframe",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Nevyhrazený: slouží i jako stanice (levnější).",
|
||||
"Vyhrazený: jen síťové služby – vysoký výkon a bezpečnost,",
|
||||
"typicky v racku.",
|
||||
"Mainframe: obří sálové počítače (IBM, Siemens),",
|
||||
"stovky procesorů, OS Unix (AIX, Solaris).",
|
||||
"Použití: banky a státní správa."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 4 • Cloud",
|
||||
"title": "Cloudové služby",
|
||||
"tip": "Tip: SaaS = software jako služba",
|
||||
"lines": [
|
||||
"Cloud: globální síť vzdálených serverů propojených internetem.",
|
||||
"Fungují jako jeden systém: ukládání dat, SaaS,",
|
||||
"streamování, webová pošta.",
|
||||
"Přístup online z jakéhokoli zařízení."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz3_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Co typicky dělají servery v síti?",
|
||||
"options": [
|
||||
"Jen tisknou dokumenty",
|
||||
"Ověřují uživatele a povolují přístup",
|
||||
"Nahrazují kabely",
|
||||
"Slouží jen jako Wi‑Fi"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Ověřují uživatele a povolují přístup ke službám"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Fileserver je:",
|
||||
"options": [
|
||||
"Tiskový server",
|
||||
"Poštovní server",
|
||||
"Souborový server",
|
||||
"Databázový server"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Fileserver = souborový server"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Centralizovaný server znamená:",
|
||||
"options": [
|
||||
"Více serverů v síti",
|
||||
"Jeden server – výpadek = nedostupnost služby",
|
||||
"Server jen pro tisk",
|
||||
"Cloud bez internetu"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Jeden server – při výpadku je služba nedostupná"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Distribuovaný model serverů nabízí hlavně:",
|
||||
"options": [
|
||||
"Nižší spolehlivost",
|
||||
"Vyšší spolehlivost a rychlost",
|
||||
"Jen levnější kabely",
|
||||
"Odstranění uživatelů"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Více serverů, každý obsluhuje část – vyšší spolehlivost"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Nevyhrazený server:",
|
||||
"options": [
|
||||
"Slouží jen síťovým službám",
|
||||
"Slouží i jako pracovní stanice",
|
||||
"Je vždy mainframe",
|
||||
"Nemůže ověřovat uživatele"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Slouží zároveň i jako pracovní stanice"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Vyhrazený server je určen:",
|
||||
"options": [
|
||||
"Jen jako stanice uživatele",
|
||||
"Jen pro síťové služby",
|
||||
"Jen pro Bluetooth",
|
||||
"Jen pro dočasné Wi‑Fi"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Jen síťové služby – vysoký výkon a bezpečnost, často v racku"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Mainframe se typicky používá:",
|
||||
"options": [
|
||||
"Jen v domácnostech",
|
||||
"V bankách a státní správě",
|
||||
"Jen pro streamování hudby",
|
||||
"Jen jako tiskárna"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Obří sálové počítače (IBM, Siemens) pro banky a státní správu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Cloud a SaaS znamenají především:",
|
||||
"options": [
|
||||
"Lokální síť bez internetu",
|
||||
"Vzdálené servery a software jako služba online",
|
||||
"Jen jeden kabel v budově",
|
||||
"Peer‑to‑peer bez serverů"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "SaaS = software jako služba, přístup online z libovolného zařízení"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
156
content/lessons/networks/unit-04.json
Normal file
156
content/lessons/networks/unit-04.json
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
{
|
||||
"id": "unit-04",
|
||||
"subject": "networks",
|
||||
"number": 4,
|
||||
"titleKey": "net_unit4",
|
||||
"subtitleKey": "net_unit4_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 4 • Přenos",
|
||||
"title": "Paralelní a sériový přenos",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Paralelní: více bitů současně po více vodičích.",
|
||||
"Problém: přeslechy (rušení) při delších kabelech",
|
||||
"a vysokých frekvencích.",
|
||||
"Sériový: bit po bitu po jednom vodiči.",
|
||||
"Umožňuje vyšší frekvence i délky – celkově rychlejší."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 4 • Časování",
|
||||
"title": "Asynchronní přenos",
|
||||
"tip": "Tip: Start-bit = 0, Stop-bit = 1",
|
||||
"lines": [
|
||||
"Data se posílají v blocích (znaky 5–8 bitů).",
|
||||
"Start-bit (0) synchronizuje začátek znaku.",
|
||||
"Stop-bit (1) znak ukončuje."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 4 • Časování",
|
||||
"title": "Synchronní přenos",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Řízeno společným hodinovým signálem.",
|
||||
"Přesné časování mezi vysílačem a přijímačem."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 4 • Zabezpečení",
|
||||
"title": "Parita, checksum a CRC",
|
||||
"tip": "Tip: CRC je nejbezpečnější z těchto tří metod",
|
||||
"lines": [
|
||||
"Parita: nejslabší – přidá bit pro sudý/lichý počet jedniček.",
|
||||
"Checksum: součet znaků jako dvojkových čísel.",
|
||||
"CRC (cyklické kódy): nejbezpečnější,",
|
||||
"počítá se z jednotlivých bitů v bloku."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz4_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Paralelní přenos znamená:",
|
||||
"options": [
|
||||
"Bit po bitu po jednom vodiči",
|
||||
"Více bitů současně po více vodičích",
|
||||
"Jen Wi‑Fi bez kabelů",
|
||||
"Jen cloudové ukládání"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Více bitů najednou po více vodičích; problém jsou přeslechy"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Hlavní problém paralelního přenosu při delších kabelech je:",
|
||||
"options": [
|
||||
"Absence serveru",
|
||||
"Přeslechy (rušení)",
|
||||
"Chybějící SaaS",
|
||||
"Peer‑to‑peer režim"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Přeslechy vznikají při delších kabelech a vysokých frekvencích"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Sériový přenos je oproti paralelnímu celkově:",
|
||||
"options": [
|
||||
"Vždy pomalejší",
|
||||
"Rychlejší díky vyšším frekvencím a délkám",
|
||||
"Jen pro tiskárny",
|
||||
"Bez jakéhokoli kabelu"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Bit po bitu po jednom vodiči – vyšší frekvence i délky"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "V asynchronním přenosu start-bit má hodnotu:",
|
||||
"options": [
|
||||
"1",
|
||||
"0",
|
||||
"8",
|
||||
"255"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Start-bit (0) synchronizuje, Stop-bit (1) ukončuje znak"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Synchronní přenos je řízen:",
|
||||
"options": [
|
||||
"Jen stop-bitem",
|
||||
"Společným hodinovým signálem",
|
||||
"Jen paritou",
|
||||
"Jen checksumem"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Řízeno společným hodinovým signálem a přesným časováním"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Nejslabší metoda zabezpečení z uvedených je:",
|
||||
"options": [
|
||||
"CRC",
|
||||
"Checksum",
|
||||
"Parita",
|
||||
"Mainframe"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Nejslabší – přidá bit pro sudý/lichý počet jedniček"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Checksum spočívá v:",
|
||||
"options": [
|
||||
"Přidání start-bitu",
|
||||
"Součtu znaků jako dvojkových čísel",
|
||||
"Jen Wi‑Fi šifrování",
|
||||
"Výměně kabelů"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Součet znaků jako dvojkových čísel"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "CRC (cyklické kódy) je z uvedených metod:",
|
||||
"options": [
|
||||
"Nejslabší",
|
||||
"Stejně slabá jako parita",
|
||||
"Nejbezpečnější",
|
||||
"Jen pro paralelní přenos"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Nejbezpečnější – počítá se z jednotlivých bitů v bloku"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
156
content/lessons/networks/unit-05.json
Normal file
156
content/lessons/networks/unit-05.json
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
{
|
||||
"id": "unit-05",
|
||||
"subject": "networks",
|
||||
"number": 5,
|
||||
"titleKey": "net_unit5",
|
||||
"subtitleKey": "net_unit5_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 4 • Signál",
|
||||
"title": "Analogový a digitální signál",
|
||||
"tip": "Tip: f = 1/T",
|
||||
"lines": [
|
||||
"Analogový: spojitý, nabývá libovolných hodnot.",
|
||||
"Digitální: diskrétní, hodnoty 0 a 1.",
|
||||
"Parametry signálu:",
|
||||
"perioda T, frekvence f = 1/T,",
|
||||
"amplituda A, fázový posun Φ."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 4 • Modulace",
|
||||
"title": "Základní typy modulace",
|
||||
"tip": "Modulace = přenos v přeloženém pásmu",
|
||||
"lines": [
|
||||
"AM – amplitudová modulace.",
|
||||
"FM – frekvenční modulace.",
|
||||
"PM – fázová modulace."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 4 • Kombinované",
|
||||
"title": "QPSK a 256-QAM",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"QPSK: 2 bity v jednom prvku (symbolu).",
|
||||
"256-QAM: 8 bitů v jednom prvku."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 4 • Rychlosti",
|
||||
"title": "Modulační, přenosová a šířka pásma",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Modulační rychlost: počet změn signálu za sekundu [Baud, Bd/s].",
|
||||
"Přenosová rychlost: velikost přenesené informace [bit/s, bps].",
|
||||
"Šířka pásma: rozsah frekvencí –",
|
||||
"určuje maximální možnou rychlost."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz5_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Digitální signál nabývá:",
|
||||
"options": [
|
||||
"Libovolných spojitých hodnot",
|
||||
"Diskrétních hodnot 0 a 1",
|
||||
"Jen frekvence FM",
|
||||
"Jen šířky pásma"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Analogový je spojitý, digitální má diskrétní hodnoty 0 a 1"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Frekvence f je:",
|
||||
"options": [
|
||||
"Stejná jako amplituda",
|
||||
"f = 1/T",
|
||||
"Jen fázový posun",
|
||||
"Počet bitů v QPSK"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Frekvence f = 1/T, kde T je perioda"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "AM, FM a PM jsou:",
|
||||
"options": [
|
||||
"Typy kabelů",
|
||||
"Základní typy modulace",
|
||||
"Jen CRC metody",
|
||||
"Typy serverů"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "AM mění amplitudu, FM frekvenci, PM fázi"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "QPSK přenáší v jednom prvku:",
|
||||
"options": [
|
||||
"1 bit",
|
||||
"2 bity",
|
||||
"8 bitů",
|
||||
"256 bitů"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "QPSK nese 2 bity v prvku, 256-QAM nese 8 bitů"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "256-QAM přenáší v jednom prvku:",
|
||||
"options": [
|
||||
"2 bity",
|
||||
"4 bity",
|
||||
"8 bitů",
|
||||
"16 bitů"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "QPSK nese 2 bity v prvku, 256-QAM nese 8 bitů"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Modulační rychlost se udává v:",
|
||||
"options": [
|
||||
"Jen bit/s",
|
||||
"Baud (Bd/s)",
|
||||
"Jen metrech",
|
||||
"Jen hertzech amplitudy"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Modulační rychlost = počet změn signálu za sekundu [Bd/s]"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Přenosová rychlost vyjadřuje:",
|
||||
"options": [
|
||||
"Počet změn signálu za sekundu",
|
||||
"Velikost přenesené informace [bit/s]",
|
||||
"Jen fázový posun",
|
||||
"Jen periodu T"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Přenosová rychlost = množství informace za sekundu [bit/s]"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Šířka pásma určuje:",
|
||||
"options": [
|
||||
"Jen barvu kabelu",
|
||||
"Maximální možnou přenosovou rychlost",
|
||||
"Jen počet serverů",
|
||||
"Jen start-bit"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Šířka pásma určuje maximální možnou přenosovou rychlost"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
139
content/lessons/networks/unit-06.json
Normal file
139
content/lessons/networks/unit-06.json
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
{
|
||||
"id": "unit-06",
|
||||
"subject": "networks",
|
||||
"number": 6,
|
||||
"titleKey": "net_unit6",
|
||||
"subtitleKey": "net_unit6_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 4 • Spoj",
|
||||
"title": "Datový spoj",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Datový spoj umožňuje výměnu informací.",
|
||||
"Může být dvoubodový nebo vícebodový."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 4 • Cesta",
|
||||
"title": "Přenosová cesta",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Přenosová cesta je fyzické médium.",
|
||||
"Příklady: metalické nebo optické kabely,",
|
||||
"mikrovlny, družice."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 4 • Kanál a okruh",
|
||||
"title": "Přenosový kanál a okruh",
|
||||
"tip": "Tip: okruh = dva kanály → obousměrný přenos",
|
||||
"lines": [
|
||||
"Přenosový kanál: jednosměrný souhrn prostředků pro spojení.",
|
||||
"Okruh: tvořen dvěma kanály – umožňuje obousměrný přenos."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 4 • Trunking",
|
||||
"title": "Spojování cest",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Trunking spojuje více cest do jednoho okruhu.",
|
||||
"Cíl: zvýšení šířky pásma."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz6_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Datový spoj slouží k:",
|
||||
"options": [
|
||||
"Jen měření amplitudy",
|
||||
"Výměně informací",
|
||||
"Jen FM modulaci",
|
||||
"Jen tisku dokumentů"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Datový spoj umožňuje výměnu informací mezi uzly"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Přenosová cesta je:",
|
||||
"options": [
|
||||
"Jen softwarová služba",
|
||||
"Fyzické médium přenosu",
|
||||
"Jen CRC kód",
|
||||
"Jen peer‑to‑peer účet"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Fyzické médium: kabely, mikrovlny nebo družice"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Mezi přenosová média patří:",
|
||||
"options": [
|
||||
"Jen parita",
|
||||
"Kabely, mikrovlny a družice",
|
||||
"Jen Baud",
|
||||
"Jen start-bit"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Fyzické médium: kabely, mikrovlny nebo družice"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Přenosový kanál je:",
|
||||
"options": [
|
||||
"Obousměrný vždy",
|
||||
"Jednosměrný souhrn prostředků pro spojení",
|
||||
"Jen název serveru",
|
||||
"Jen typ QPSK"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Kanál je jednosměrný souhrn prostředků pro spojení"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Okruh vzniká:",
|
||||
"options": [
|
||||
"Z jednoho kanálu",
|
||||
"Ze dvou kanálů pro obousměrný přenos",
|
||||
"Jen z parity",
|
||||
"Jen z Wi‑Fi hesla"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Okruh tvoří dva kanály pro obousměrný přenos"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Trunking znamená:",
|
||||
"options": [
|
||||
"Odpojení všech kabelů",
|
||||
"Spojení více cest do jednoho okruhu",
|
||||
"Jen asynchronní stop-bit",
|
||||
"Jen digitální 0 a 1"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Trunking spojuje více cest do jednoho okruhu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Hlavní cíl trunkingu je:",
|
||||
"options": [
|
||||
"Snížit šířku pásma",
|
||||
"Zvýšit šířku pásma",
|
||||
"Zrušit kanály",
|
||||
"Nahradit mainframe"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Trunking zvyšuje šířku pásma spojením cest"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
152
content/lessons/networks/unit-07.json
Normal file
152
content/lessons/networks/unit-07.json
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
{
|
||||
"id": "unit-07",
|
||||
"subject": "networks",
|
||||
"number": 7,
|
||||
"titleKey": "net_unit7",
|
||||
"subtitleKey": "net_unit7_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 4 • Multiplex",
|
||||
"title": "Rozdělení kanálu",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Multiplex: rozdělení jednoho kanálu",
|
||||
"na více logických podkanálů."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 4 • Typy",
|
||||
"title": "FDM, TDM a STDM",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"FDM (frekvenční): každému kanálu část frekvenčního pásma.",
|
||||
"TDM (časový): každému kanálu vyhrazený časový slot.",
|
||||
"STDM (statistický): kapacita dle potřeby (paketový přenos),",
|
||||
"negarantuje 100% dostupnost."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 4 • Směry",
|
||||
"title": "Simplex, poloduplex a duplex",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Simplex: jen jeden směr (např. optické vlákno).",
|
||||
"Poloduplex (half-duplex): oběma směry, ne současně.",
|
||||
"Duplex (full-duplex): oběma směry současně."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 4 • Agregace",
|
||||
"title": "Sdílení kapacity",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Agregace: sdílení kapacity kanálu více uživateli."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz7_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Multiplex znamená:",
|
||||
"options": [
|
||||
"Jen jeden uživatel na kanál",
|
||||
"Rozdělení jednoho kanálu na více logických podkanálů",
|
||||
"Jen CRC kontrolu",
|
||||
"Jen digitální 0 a 1"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Multiplex dělí jeden kanál na více logických podkanálů"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "FDM přiděluje každému kanálu:",
|
||||
"options": [
|
||||
"Časový slot",
|
||||
"Část frekvenčního pásma",
|
||||
"Jen start-bit",
|
||||
"Jen mainframe"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "FDM: každému kanálu část frekvenčního pásma"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "TDM přiděluje každému kanálu:",
|
||||
"options": [
|
||||
"Část frekvenčního pásma",
|
||||
"Vyhrazený časový slot",
|
||||
"Jen SaaS účet",
|
||||
"Jen paritu"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "TDM: každému kanálu vyhrazený časový slot"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "STDM:",
|
||||
"options": [
|
||||
"Garantuje vždy 100% dostupnost",
|
||||
"Přiděluje kapacitu dle potřeby a negarantuje 100%",
|
||||
"Je jen simplex",
|
||||
"Je jen trunking kabelů"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "STDM přiděluje kapacitu dle potřeby, negarantuje 100 %"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Simplex umožňuje přenos:",
|
||||
"options": [
|
||||
"Oběma směry současně",
|
||||
"Jen jedním směrem",
|
||||
"Oběma směry, ale ne současně",
|
||||
"Jen přes CRC"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Simplex = jen jeden směr přenosu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Half-duplex znamená:",
|
||||
"options": [
|
||||
"Jen jeden směr",
|
||||
"Oběma směry, ne současně",
|
||||
"Oběma směry současně",
|
||||
"Jen FDM"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Half-duplex = oběma směry, ale ne současně"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Full-duplex znamená:",
|
||||
"options": [
|
||||
"Jen jeden směr",
|
||||
"Oběma směry, ne současně",
|
||||
"Oběma směry současně",
|
||||
"Jen STDM"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Full-duplex = oběma směry současně"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Agregace je:",
|
||||
"options": [
|
||||
"Sdílení kapacity kanálu více uživateli",
|
||||
"Jen fázová modulace",
|
||||
"Jen dvoubodový spoj",
|
||||
"Jen stop-bit"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Agregace = sdílení kapacity kanálu více uživateli"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
153
content/lessons/networks/unit-08.json
Normal file
153
content/lessons/networks/unit-08.json
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
{
|
||||
"id": "unit-08",
|
||||
"subject": "networks",
|
||||
"number": 8,
|
||||
"titleKey": "net_unit8",
|
||||
"subtitleKey": "net_unit8_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 4 • Okruhy",
|
||||
"title": "Přepojování okruhů",
|
||||
"tip": "Tip: spojovaná služba – jako telefon",
|
||||
"lines": [
|
||||
"Cesta se vytýčí předem (spojovaná služba).",
|
||||
"Kapacita je garantována.",
|
||||
"Přenos probíhá v reálném čase."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 4 • Pakety",
|
||||
"title": "Přepojování paketů",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Data se dělí na pakety s adresami.",
|
||||
"Každý paket může jít jinou cestou."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 4 • Hodnocení",
|
||||
"title": "Výhody a nevýhody paketů",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Výhody: reakce na zátěž, lepší využití kapacity.",
|
||||
"Nevýhody: negarantuje pořadí ani plynulost.",
|
||||
"Nevhodné pro video v reálném čase."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 4 • Virtuální",
|
||||
"title": "Virtuální spoje",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Cesta je vytýčena předem,",
|
||||
"ale prostředky se využívají jen při průchodu paketu.",
|
||||
"Výsledek: zrychlení datagramů."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz8_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Přepojování okruhů znamená:",
|
||||
"options": [
|
||||
"Pakety jdou vždy jinou cestou",
|
||||
"Cesta se vytýčí předem a kapacita je garantována",
|
||||
"Jen FDM multiplex",
|
||||
"Jen half-duplex"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Cesta se vytýčí předem, kapacita je garantována"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Přepojování okruhů je vhodné pro:",
|
||||
"options": [
|
||||
"Jen náhodné datagramy bez cesty",
|
||||
"Přenos v reálném čase (jako telefon)",
|
||||
"Jen CRC",
|
||||
"Jen peer‑to‑peer tisk"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Cesta se vytýčí předem, kapacita je garantována"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Při přepojování paketů:",
|
||||
"options": [
|
||||
"Vždy existuje jedna pevná cesta",
|
||||
"Každý paket může jít jinou cestou",
|
||||
"Kapacita je vždy garantována",
|
||||
"Neexistují adresy"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Data se dělí na pakety, každý může jít jinou cestou"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Výhoda přepojování paketů je:",
|
||||
"options": [
|
||||
"Garantované pořadí vždy",
|
||||
"Reakce na zátěž a využití kapacity",
|
||||
"Jen simplex",
|
||||
"Jen trunking"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Výhody: reakce na zátěž a lepší využití kapacity"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Nevýhoda přepojování paketů je:",
|
||||
"options": [
|
||||
"Nereaguje na zátěž",
|
||||
"Negarantuje pořadí ani plynulost",
|
||||
"Nelze použít adresy",
|
||||
"Vždy blokuje kanál jako telefon"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Negarantuje pořadí ani plynulost – nevhodné pro živé video"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Pro video v reálném čase je přepojování paketů:",
|
||||
"options": [
|
||||
"Ideální vždy",
|
||||
"Spíše nevhodné kvůli plynulosti",
|
||||
"Jediná možná metoda",
|
||||
"Stejné jako AM modulace"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Negarantuje pořadí ani plynulost – nevhodné pro živé video"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Virtuální spoje:",
|
||||
"options": [
|
||||
"Nemají předem žádnou cestu",
|
||||
"Mají cestu předem, prostředky jen při průchodu paketu",
|
||||
"Jsou jen FDM",
|
||||
"Jsou jen parita"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Cesta je předem, prostředky se použijí jen při průchodu paketu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Virtuální spoje slouží hlavně k:",
|
||||
"options": [
|
||||
"Zrušení paketů",
|
||||
"Zrychlení datagramů",
|
||||
"Jen simplexu",
|
||||
"Jen analogovému signálu"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Virtuální spoje zrychlují přenos datagramů"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
151
content/lessons/networks/unit-09.json
Normal file
151
content/lessons/networks/unit-09.json
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
{
|
||||
"id": "unit-09",
|
||||
"subject": "networks",
|
||||
"number": 9,
|
||||
"titleKey": "net_unit9",
|
||||
"subtitleKey": "net_unit9_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 4 • Proč",
|
||||
"title": "Standardizace a dekompozice",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Standardizace zajišťuje kompatibilitu mezi výrobci.",
|
||||
"Dekompozice: rozklad složitého problému přenosu na vrstvy."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 4 • Vrstvy",
|
||||
"title": "Komunikace mezi vrstvami",
|
||||
"tip": "Tip: SAP = Service Access Point",
|
||||
"lines": [
|
||||
"Vrstvy komunikují jen se sousedy přes rozhraní (SAP)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 4 • Protokol",
|
||||
"title": "Pravidla komunikace",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Protokol: pravidla komunikace mezi stejnými vrstvami",
|
||||
"na různých uzlech."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 4 • Jednotky",
|
||||
"title": "Hlavičky a PDU",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Každá vrstva přidá k datům hlavičku (header).",
|
||||
"Rámec – linková vrstva.",
|
||||
"Paket – síťová vrstva.",
|
||||
"Segment – transportní vrstva."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz9_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Standardizace v sítích především:",
|
||||
"options": [
|
||||
"Zvyšuje nekompatibilitu",
|
||||
"Zajišťuje kompatibilitu mezi výrobci",
|
||||
"Ruší protokoly",
|
||||
"Nahrazuje kabely"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Standardizace zajišťuje kompatibilitu mezi výrobci"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Dekompozice znamená:",
|
||||
"options": [
|
||||
"Spojení všeho do jedné vrstvy",
|
||||
"Rozklad složitého přenosu na vrstvy",
|
||||
"Jen FDM",
|
||||
"Jen CRC"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Dekompozice rozkládá přenos na jednotlivé vrstvy"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Vrstvy komunikují:",
|
||||
"options": [
|
||||
"Se všemi vrstvami najednou",
|
||||
"Jen se sousedy přes rozhraní (SAP)",
|
||||
"Jen přes satelit",
|
||||
"Jen bez protokolu"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Vrstvy komunikují jen se sousedy přes rozhraní SAP"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Protokol je:",
|
||||
"options": [
|
||||
"Fyzický kabel",
|
||||
"Pravidla komunikace mezi stejnými vrstvami",
|
||||
"Jen amplituda signálu",
|
||||
"Jen tiskový server"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Protokol = pravidla mezi stejnými vrstvami na různých uzlech"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Každá vrstva k datům typicky přidá:",
|
||||
"options": [
|
||||
"Jen stop-bit",
|
||||
"Hlavičku (header)",
|
||||
"Jen šířku pásma",
|
||||
"Jen mainframe"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Každá vrstva přidává k datům hlavičku (header)"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jednotka linkové vrstvy se nazývá:",
|
||||
"options": [
|
||||
"Segment",
|
||||
"Paket",
|
||||
"Rámec",
|
||||
"Baud"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Rámec = linková, paket = síťová, segment = transportní"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jednotka síťové vrstvy se nazývá:",
|
||||
"options": [
|
||||
"Rámec",
|
||||
"Paket",
|
||||
"Segment",
|
||||
"Simplex"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Rámec = linková, paket = síťová, segment = transportní"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jednotka transportní vrstvy se nazývá:",
|
||||
"options": [
|
||||
"Rámec",
|
||||
"Paket",
|
||||
"Segment",
|
||||
"Trunk"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Rámec = linková, paket = síťová, segment = transportní"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
120
content/lessons/networks/unit-10.json
Normal file
120
content/lessons/networks/unit-10.json
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
{
|
||||
"id": "unit-10",
|
||||
"subject": "networks",
|
||||
"number": 10,
|
||||
"titleKey": "net_unit10",
|
||||
"subtitleKey": "net_unit10_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 3 • ISO/OSI",
|
||||
"title": "Sedmivrstvý model",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"ISO/OSI: 7vrstvý model.",
|
||||
"Je univerzální – popisuje komunikaci obecně.",
|
||||
"V praxi je ale pomalejší: každá vrstva řešila spolehlivost."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 3 • TCP/IP",
|
||||
"title": "Praktická architektura",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"TCP/IP: architektura, která zvítězila v praxi.",
|
||||
"Vycházela ze skutečných potřeb sítí.",
|
||||
"Spolehlivost řeší až vyšší vrstvy."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 3 • Srovnání",
|
||||
"title": "Proč zvítězil TCP/IP",
|
||||
"tip": "Tip: méně režie na nižších vrstvách = rychlejší provoz",
|
||||
"lines": [
|
||||
"ISO/OSI: teoretický, univerzální, těžší a pomalejší.",
|
||||
"TCP/IP: praktický, jednodušší dělení odpovědností.",
|
||||
"Proto se TCP/IP stal základem dnešního internetu."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz10_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Model ISO/OSI má:",
|
||||
"options": [
|
||||
"4 vrstvy",
|
||||
"5 vrstev",
|
||||
"7 vrstev",
|
||||
"10 vrstev"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "ISO/OSI má 7 vrstev a je univerzální model"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "ISO/OSI je především:",
|
||||
"options": [
|
||||
"Jen typ kabelu",
|
||||
"Univerzální referenční model",
|
||||
"Jen tiskový server",
|
||||
"Jen FDM"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "ISO/OSI má 7 vrstev a je univerzální model"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Proč je ISO/OSI v praxi pomalejší?",
|
||||
"options": [
|
||||
"Nemá žádné vrstvy",
|
||||
"Každá vrstva řešila spolehlivost",
|
||||
"Nepoužívá protokoly",
|
||||
"Nemá SAP"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "V praxi je ISO/OSI pomalejší – spolehlivost řeší každá vrstva"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "TCP/IP v praxi:",
|
||||
"options": [
|
||||
"Prohrál proti ISO/OSI",
|
||||
"Zvítězil jako praktická architektura",
|
||||
"Je jen analogový signál",
|
||||
"Je jen half-duplex"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "TCP/IP zvítězil, protože vycházel ze skutečných potřeb"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "TCP/IP vycházel z:",
|
||||
"options": [
|
||||
"Jen teoretických schémat bez praxe",
|
||||
"Skutečných potřeb sítí",
|
||||
"Jen parity",
|
||||
"Jen trunkingu"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "TCP/IP zvítězil, protože vycházel ze skutečných potřeb"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "V TCP/IP spolehlivost řeší:",
|
||||
"options": [
|
||||
"Vždy jen fyzická vrstva",
|
||||
"Až vyšší vrstvy",
|
||||
"Jen metalické kabely",
|
||||
"Jen mainframe"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "V TCP/IP řeší spolehlivost až vyšší vrstvy"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
181
content/lessons/networks/unit-11.json
Normal file
181
content/lessons/networks/unit-11.json
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
{
|
||||
"id": "unit-11",
|
||||
"subject": "networks",
|
||||
"number": 11,
|
||||
"titleKey": "net_unit11",
|
||||
"subtitleKey": "net_unit11_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 8 • Přehled",
|
||||
"title": "Tři skupiny vrstev",
|
||||
"tip": "Tip: přenos → přizpůsobení → aplikace",
|
||||
"lines": [
|
||||
"Vrstvy orientované na přenos: fyzická, spojová, síťová.",
|
||||
"Přizpůsobovací vrstva: transportní.",
|
||||
"Vrstvy orientované na aplikace: relační, prezentační,",
|
||||
"aplikační."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 8 • Fyzická",
|
||||
"title": "Přenos bitů",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Přenos bitů po médiu.",
|
||||
"Definuje napětí, kabely, konektory, kódování a modulaci.",
|
||||
"Řeší také duplex / simplex."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 8 • Spojová",
|
||||
"title": "Rámce a MAC",
|
||||
"tip": "Tip: podvrstvy MAC (přístup k médiu) a LLC (logické řízení)",
|
||||
"lines": [
|
||||
"Tvorba rámců a fyzické (MAC) adresy.",
|
||||
"Kontrola chyb (CRC) a řízení toku mezi sousedními uzly.",
|
||||
"Podvrstvy: MAC a LLC."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 8 • Síťová",
|
||||
"title": "Směrování paketů",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Směrování (routing) paketů.",
|
||||
"Funguje i v sítích bez přímého spojení mezi uzly."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 8 • Transportní",
|
||||
"title": "Segmenty end-to-end",
|
||||
"tip": "Tip: v routerech transportní vrstva není",
|
||||
"lines": [
|
||||
"Rozklad zpráv na segmenty a jejich zpětné složení.",
|
||||
"Kontrola pořadí a náprava chyb.",
|
||||
"Existuje jen v koncových uzlech – není v routerech."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "6 / 8 • Relační",
|
||||
"title": "Relace a dialog",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Navazuje, udržuje a ukončuje relace.",
|
||||
"Řídí dialog („neskákat si do řeči“).",
|
||||
"Synchronizace – navázání po přerušení."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "7 / 8 • Prezentační",
|
||||
"title": "Formát dat",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Formátování dat (např. ASCII).",
|
||||
"Komprese a šifrování.",
|
||||
"Převod mezi různými standardy."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "8 / 8 • Aplikační",
|
||||
"title": "Rozhraní pro programy",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Rozhraní pro aplikace a služby.",
|
||||
"Například e-mail, HTTP, FTP nebo terminál."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz11_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Fyzická vrstva se stará o:",
|
||||
"options": [
|
||||
"Přenos bitů",
|
||||
"Směrování paketů",
|
||||
"Navazování relací",
|
||||
"Formátování ASCII"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Fyzická vrstva přenáší bity a řeší médium"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "MAC a LLC jsou podvrstvy:",
|
||||
"options": [
|
||||
"Síťové vrstvy",
|
||||
"Spojové (linkové) vrstvy",
|
||||
"Transportní vrstvy",
|
||||
"Aplikační vrstvy"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Spojová vrstva má podvrstvy MAC a LLC"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Směrování paketů v sítích bez přímého spojení řeší:",
|
||||
"options": [
|
||||
"Fyzická vrstva",
|
||||
"Relační vrstva",
|
||||
"Síťová vrstva",
|
||||
"Prezentační vrstva"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Síťová vrstva směruje pakety i bez přímého spojení"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Transportní vrstva existuje:",
|
||||
"options": [
|
||||
"Jen v koncových uzlech",
|
||||
"I v routerech",
|
||||
"Jen ve switchech",
|
||||
"Jen na hubu"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Transportní vrstva je jen v koncových uzlech, ne v routerech"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Relační vrstva především:",
|
||||
"options": [
|
||||
"Definuje napětí a kabely",
|
||||
"Navazuje, udržuje a ukončuje relace",
|
||||
"Přidává MAC adresy",
|
||||
"Směruje pakety"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Relační vrstva řídí relace, dialog a synchronizaci"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Komprese a šifrování patří do vrstvy:",
|
||||
"options": [
|
||||
"Fyzické",
|
||||
"Spojové",
|
||||
"Prezentační",
|
||||
"Síťové"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Prezentační vrstva formátuje data, komprimuje a šifruje"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Aplikační vrstva poskytuje:",
|
||||
"options": [
|
||||
"CRC mezi sousedy",
|
||||
"Rozhraní pro programy (HTTP, FTP, e-mail…)",
|
||||
"Jen duplex/simplex",
|
||||
"Jen směrování"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Aplikační vrstva je rozhraní pro programy a služby"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
172
content/lessons/networks/unit-12.json
Normal file
172
content/lessons/networks/unit-12.json
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
{
|
||||
"id": "unit-12",
|
||||
"subject": "networks",
|
||||
"number": 12,
|
||||
"titleKey": "net_unit12",
|
||||
"subtitleKey": "net_unit12_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 6 • Mapování",
|
||||
"title": "TCP/IP vs ISO/OSI",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Aplikační TCP/IP = aplikační + prezentační + relační ISO/OSI.",
|
||||
"Transportní TCP/IP = transportní ISO/OSI.",
|
||||
"Síťová (internetová) = síťová ISO/OSI.",
|
||||
"Vrstva síťového rozhraní = linková + fyzická ISO/OSI."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 6 • Vznik",
|
||||
"title": "Nejprve protokoly",
|
||||
"tip": "Tip: opačný postup než u ISO/OSI",
|
||||
"lines": [
|
||||
"Nejprve vznikly protokoly pro Internet.",
|
||||
"Až poté byl definován model TCP/IP.",
|
||||
"U ISO/OSI to bylo naopak: nejdřív model, pak protokoly."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 6 • Aplikace",
|
||||
"title": "Aplikační protokoly",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"HTTP, HTTPS, FTP, DHCP, DNS.",
|
||||
"Pošta: POP3, IMAP, SMTP."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 6 • Transport",
|
||||
"title": "TCP a UDP",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"TCP: spolehlivý, spojovaný.",
|
||||
"UDP: nespolehlivý, rychlý."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 6 • Síť",
|
||||
"title": "Internetové protokoly",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"IP – základní přenos paketů.",
|
||||
"ICMP, ARP.",
|
||||
"Směrování: RIP, OSPF."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "6 / 6 • Rozhraní",
|
||||
"title": "Síťové rozhraní",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Ethernet, Token Ring, PPP, ATM.",
|
||||
"Odpovídá linkové a fyzické vrstvě ISO/OSI."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz12_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Aplikační vrstva TCP/IP odpovídá v ISO/OSI:",
|
||||
"options": [
|
||||
"Jen aplikační vrstvě",
|
||||
"Aplikační + prezentační + relační",
|
||||
"Jen fyzické vrstvě",
|
||||
"Linkové + síťové"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Aplikační TCP/IP = aplikační + prezentační + relační ISO/OSI"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Vrstva síťového rozhraní TCP/IP odpovídá:",
|
||||
"options": [
|
||||
"Jen síťové ISO/OSI",
|
||||
"Linkové + fyzické ISO/OSI",
|
||||
"Jen transportní ISO/OSI",
|
||||
"Jen aplikační ISO/OSI"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Síťové rozhraní TCP/IP = linková + fyzická ISO/OSI"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Model TCP/IP vznikl:",
|
||||
"options": [
|
||||
"Nejdřív model, pak protokoly",
|
||||
"Nejdřív protokoly, pak model",
|
||||
"Současně s Token Ring",
|
||||
"Jen jako náhrada za ATM"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "U TCP/IP vznikly nejdřív protokoly, až pak model"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "TCP je:",
|
||||
"options": [
|
||||
"Nespolehlivý a rychlý",
|
||||
"Spolehlivý a spojovaný",
|
||||
"Jen směrovací protokol",
|
||||
"Jen fyzické kódování"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "TCP je spolehlivý a spojovaný"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "UDP je:",
|
||||
"options": [
|
||||
"Spolehlivý a spojovaný",
|
||||
"Nespolehlivý a rychlý",
|
||||
"Jen e-mailový protokol",
|
||||
"Jen ARP"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "UDP je nespolehlivý a rychlý"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "HTTP, DNS a SMTP patří do vrstvy:",
|
||||
"options": [
|
||||
"Aplikační",
|
||||
"Transportní",
|
||||
"Síťové",
|
||||
"Síťového rozhraní"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "HTTP, DNS a SMTP jsou aplikační protokoly"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "IP, ICMP a OSPF patří do vrstvy:",
|
||||
"options": [
|
||||
"Aplikační",
|
||||
"Transportní",
|
||||
"Síťové (internetové)",
|
||||
"Síťového rozhraní"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "IP, ICMP a OSPF patří na síťovou (internetovou) vrstvu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Ethernet a PPP patří do vrstvy:",
|
||||
"options": [
|
||||
"Aplikační",
|
||||
"Transportní",
|
||||
"Síťové",
|
||||
"Síťového rozhraní"
|
||||
],
|
||||
"correct": 3,
|
||||
"hint": "Ethernet a PPP patří na vrstvu síťového rozhraní"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
130
content/lessons/networks/unit-13.json
Normal file
130
content/lessons/networks/unit-13.json
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
"id": "unit-13",
|
||||
"subject": "networks",
|
||||
"number": 13,
|
||||
"titleKey": "net_unit13",
|
||||
"subtitleKey": "net_unit13_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 4 • Přehled",
|
||||
"title": "Topologie sítě",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Topologie popisuje, jak jsou uzly propojené.",
|
||||
"Základní typy: sběrnice, hvězda a kruh."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 4 • Sběrnice",
|
||||
"title": "Bus",
|
||||
"tip": "Tip: dříve typicky koaxiální kabel",
|
||||
"lines": [
|
||||
"Médium sdílí všichni – jednoduchá a levná topologie.",
|
||||
"Malá délka kabelů.",
|
||||
"Nevýhody: nízká bezpečnost (všichni slyší vše).",
|
||||
"Přerušení kabelu nebo terminátoru vyřadí celou síť."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 4 • Hvězda",
|
||||
"title": "Star",
|
||||
"tip": "Tip: kroucená dvoulinka nebo optika",
|
||||
"lines": [
|
||||
"Uzly jdou přes centrální prvek (hub nebo switch).",
|
||||
"Výhody: odolnost proti poruchám kabelů k uzlům,",
|
||||
"snadná diagnostika.",
|
||||
"Nevýhody: aktivní prvky a více kabeláže."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 4 • Kruh",
|
||||
"title": "Ring",
|
||||
"tip": "Tip: často zdvojený kruh pro odolnost",
|
||||
"lines": [
|
||||
"Kabely tvoří souvislý kruh.",
|
||||
"Zprávy obíhají, dokud nenajdou adresáta."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz13_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Topologie sběrnice (bus) znamená, že:",
|
||||
"options": [
|
||||
"Každý uzel má vlastní centrální switch",
|
||||
"Médium sdílí všichni",
|
||||
"Kabely tvoří jen zdvojený kruh",
|
||||
"Používá jen OSPF"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Sběrnice: médium sdílí všichni"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Nevýhoda sběrnice je hlavně:",
|
||||
"options": [
|
||||
"Že potřebuje hub u každého uzlu",
|
||||
"Nízká bezpečnost a náchylnost na poruchu kabelu",
|
||||
"Že nelze použít koaxiál",
|
||||
"Že zprávy neobíhají"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Sběrnice: nízká bezpečnost a porucha kabelu vyřadí síť"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Topologie hvězda propojuje uzly přes:",
|
||||
"options": [
|
||||
"Jen jeden společný koaxiál bez centra",
|
||||
"Centrální prvek (hub nebo switch)",
|
||||
"Jen Token Ring",
|
||||
"Jen ATM"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Hvězda: uzly jdou přes hub nebo switch"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Výhoda hvězdy je:",
|
||||
"options": [
|
||||
"Že přerušení jednoho kabelu k uzlu nevyřadí celou síť",
|
||||
"Že všichni slyší vše",
|
||||
"Že nepotřebuje žádnou kabeláž",
|
||||
"Že nemá aktivní prvky"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Hvězda: porucha kabelu k uzlu nevyřadí celou síť"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Hvězda typicky používá:",
|
||||
"options": [
|
||||
"Jen koaxiální kabel",
|
||||
"Kroucenou dvoulinku nebo optiku",
|
||||
"Jen simplexní rádio",
|
||||
"Jen FDM"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Hvězda používá kroucenou dvoulinku nebo optiku"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "V topologii kruh (ring):",
|
||||
"options": [
|
||||
"Zprávy obíhají, dokud nenajdou adresáta",
|
||||
"Každý paket jde jen přes DNS",
|
||||
"Médium sdílí hub bez kabelů",
|
||||
"Neexistuje žádná kabeláž"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "V kruhu zprávy obíhají, dokud nenajdou adresáta"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
140
content/lessons/networks/unit-14.json
Normal file
140
content/lessons/networks/unit-14.json
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
{
|
||||
"id": "unit-14",
|
||||
"subject": "networks",
|
||||
"number": 14,
|
||||
"titleKey": "net_unit14",
|
||||
"subtitleKey": "net_unit14_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 5 • Princip",
|
||||
"title": "Světlo místo proudu",
|
||||
"tip": "Tip: obousměrný provoz = dvě vlákna",
|
||||
"lines": [
|
||||
"Elektrické impulsy se převádí na světlo (LED nebo laser).",
|
||||
"Zpět je zachytí fotodioda.",
|
||||
"Přenos jedním vláknem je jednosměrný."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 5 • Výhody",
|
||||
"title": "Proč optika",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Imunita vůči elektromagnetickému rušení.",
|
||||
"Obrovská šířka pásma a dosah až kolem 100 km.",
|
||||
"Lepší bezpečnost proti odposlechu.",
|
||||
"Malý průměr a nízká váha."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 5 • Nevýhody",
|
||||
"title": "Cena a montáž",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Náročná montáž.",
|
||||
"Dražší aktivní prvky a konektory."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 5 • MM",
|
||||
"title": "Mnohovidová vlákna",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Větší jádro: 50 / 62,5 µm.",
|
||||
"Více cest světla (vidů).",
|
||||
"Levnější zdroje – typicky LED."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 5 • SM",
|
||||
"title": "Jednovidová vlákna",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Malé jádro: 7–10 µm.",
|
||||
"Jen jeden paprsek osou vlákna.",
|
||||
"Velký dosah, vyžaduje laser."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz14_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Princip optiky je:",
|
||||
"options": [
|
||||
"Převod el. impulsů na světlo a zpět fotodiodou",
|
||||
"Jen směrování paketů OSPF",
|
||||
"Jen sdílená sběrnice",
|
||||
"Jen UDP spojení"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Optika: el. impulsy → světlo (LED/laser) → fotodioda"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jedním vláknem je přenos:",
|
||||
"options": [
|
||||
"Vždy plně duplexní bez výjimky",
|
||||
"Jednosměrný (obousměrně dvě vlákna)",
|
||||
"Jen poloviční kruh",
|
||||
"Jen Token Ring"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Jedno vlákno = jednosměrný přenos; obousměrně dvě vlákna"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Výhoda optiky není:",
|
||||
"options": [
|
||||
"Imunita vůči el.mag. rušení",
|
||||
"Velký dosah a šířka pásma",
|
||||
"Levná a snadná montáž konektorů",
|
||||
"Lepší ochrana proti odposlechu"
|
||||
],
|
||||
"correct": 2,
|
||||
"hint": "Nevýhody: náročná montáž a cena prvků/konektorů"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Mnohovidové vlákno (MM) má:",
|
||||
"options": [
|
||||
"Jádro 7–10 µm a vyžaduje laser",
|
||||
"Větší jádro 50/62,5 µm a více vidů",
|
||||
"Jen koaxiální stínění",
|
||||
"Jen jednu MAC adresu"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "MM: jádro 50/62,5 µm a více vidů"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Jednovidové vlákno (SM):",
|
||||
"options": [
|
||||
"Má větší jádro a LED zdroj",
|
||||
"Má malé jádro, jeden paprsek a laser",
|
||||
"Nepřenáší světlo",
|
||||
"Funguje jen jako hub"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "SM: jádro 7–10 µm, jeden paprsek, laser"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Levnější zdroje (LED) typicky patří k:",
|
||||
"options": [
|
||||
"Jednovidovým vláknům",
|
||||
"Mnohovidovým vláknům",
|
||||
"Jen kruhové topologii",
|
||||
"Jen ATM"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "MM typicky používá levnější LED zdroje"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
118
content/lessons/networks/unit-15.json
Normal file
118
content/lessons/networks/unit-15.json
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
{
|
||||
"id": "unit-15",
|
||||
"subject": "networks",
|
||||
"number": 15,
|
||||
"titleKey": "net_unit15",
|
||||
"subtitleKey": "net_unit15_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 3 • Primární",
|
||||
"title": "Ochrana 250 µm",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Primární ochrana má průměr 250 µm.",
|
||||
"Chrání samotné vlákno hned po výrobě."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 3 • Sekundární",
|
||||
"title": "Ochrana 900 µm",
|
||||
"tip": "Tip: těsná pro svislé, volná pro vodorovné instalace",
|
||||
"lines": [
|
||||
"Sekundární ochrana má průměr 900 µm.",
|
||||
"Těsná varianta: vhodné pro svislé instalace.",
|
||||
"Volná varianta: vhodné pro vodorovné instalace."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 3 • Montáž",
|
||||
"title": "Lámání a sváření",
|
||||
"tip": "Tip: kolmost lomu do 0,5°",
|
||||
"lines": [
|
||||
"Vyžaduje precizní lámání lámačkou.",
|
||||
"Spojování se dělá svářečkou optických vláken."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz15_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Primární ochrana optického vlákna má průměr:",
|
||||
"options": [
|
||||
"900 µm",
|
||||
"250 µm",
|
||||
"50 µm",
|
||||
"7–10 µm"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Primární ochrana: 250 µm"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Sekundární ochrana má průměr:",
|
||||
"options": [
|
||||
"250 µm",
|
||||
"900 µm",
|
||||
"62,5 µm",
|
||||
"1 km"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Sekundární ochrana: 900 µm"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Těsná sekundární ochrana je vhodná hlavně:",
|
||||
"options": [
|
||||
"Pro svislé instalace",
|
||||
"Jen pro kruhovou topologii",
|
||||
"Jen pro UDP",
|
||||
"Jen pro hub"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Těsná sekundární ochrana: svislé instalace"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Volná sekundární ochrana je vhodná hlavně:",
|
||||
"options": [
|
||||
"Pro vodorovné instalace",
|
||||
"Jen pro ATM",
|
||||
"Jen pro LED bez vlákna",
|
||||
"Jen pro sběrnici"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Volná sekundární ochrana: vodorovné instalace"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Montáž optiky vyžaduje:",
|
||||
"options": [
|
||||
"Jen nůžky na koaxiál",
|
||||
"Precizní lámání a svářečku vláken",
|
||||
"Jen hub bez konektorů",
|
||||
"Jen Token Ring"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Montáž: lámačka a svářečka optických vláken"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Při lámání se požaduje kolmost zhruba:",
|
||||
"options": [
|
||||
"Do 0,5°",
|
||||
"Do 45°",
|
||||
"Přesně 90° bez tolerance",
|
||||
"Bez významu"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Lámání vyžaduje kolmost asi 0,5°"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
129
content/lessons/networks/unit-16.json
Normal file
129
content/lessons/networks/unit-16.json
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
{
|
||||
"id": "unit-16",
|
||||
"subject": "networks",
|
||||
"number": 16,
|
||||
"titleKey": "net_unit16",
|
||||
"subtitleKey": "net_unit16_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 4 • Kolize",
|
||||
"title": "Sdílené médium",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Kolize nastane, když na sdíleném médiu vysílá více uzlů najednou."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 4 • Aloha",
|
||||
"title": "Vysílej kdykoliv",
|
||||
"tip": "Tip: časté kolize, primitivní metoda",
|
||||
"lines": [
|
||||
"Uzel vysílá kdykoliv chce.",
|
||||
"Když nepřijde potvrzení, pošle data znovu.",
|
||||
"Kolize jsou časté."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 4 • CSMA/CD",
|
||||
"title": "Ethernet",
|
||||
"tip": "Tip: Carrier Sense + Collision Detection",
|
||||
"lines": [
|
||||
"Nejdřív se poslouchá nosná (Carrier Sense).",
|
||||
"Je-li ticho, uzel vysílá.",
|
||||
"Při kolizi se vysílání zastaví a zkusí se znovu",
|
||||
"po náhodném čase."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 4 • CSMA/CA",
|
||||
"title": "Wi-Fi",
|
||||
"tip": "Tip: Collision Avoidance přes RTS/CTS",
|
||||
"lines": [
|
||||
"Předchází kolizím (Collision Avoidance).",
|
||||
"RTS (Request to Send) a CTS (Clear to Send)",
|
||||
"rezervují médium před vysíláním."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz16_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Kolize nastane, když:",
|
||||
"options": [
|
||||
"Na sdíleném médiu vysílá více uzlů najednou",
|
||||
"Router vypne OSPF",
|
||||
"Vlákno má ochranu 250 µm",
|
||||
"Použijeme jen TCP"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Kolize = více uzlů vysílá na sdíleném médiu najednou"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Metoda Aloha znamená především:",
|
||||
"options": [
|
||||
"Vysílej kdykoliv; bez potvrzení pošli znovu",
|
||||
"Vždy rezervuj médium RTS/CTS",
|
||||
"Nikdy nevysílej při tichu",
|
||||
"Jen směrování RIP"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Aloha: vysílej kdykoliv, při chybě znovu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "CSMA/CD se typicky pojí s:",
|
||||
"options": [
|
||||
"Wi-Fi",
|
||||
"Ethernetem",
|
||||
"Jen SMTP",
|
||||
"Jen ATM"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "CSMA/CD patří k Ethernetu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Při CSMA/CD uzel před vysíláním:",
|
||||
"options": [
|
||||
"Ignoruje médium",
|
||||
"Monitoruje nosnou (Carrier Sense)",
|
||||
"Vždy čeká přesně 1 hodinu",
|
||||
"Použije jen laser"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "CSMA/CD nejdřív poslouchá nosnou (Carrier Sense)"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Když CSMA/CD detekuje kolizi:",
|
||||
"options": [
|
||||
"Pokračuje vysíláním bez změny",
|
||||
"Zastaví vysílání a zkusí to po náhodném čase",
|
||||
"Přepne síť na sběrnici",
|
||||
"Smaže IP adresu"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Po kolizi CSMA/CD čeká náhodný čas a zkusí znovu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "CSMA/CA na Wi-Fi předchází kolizím pomocí:",
|
||||
"options": [
|
||||
"RTS a CTS",
|
||||
"Jen CRC bez nosné",
|
||||
"Jen hubu",
|
||||
"Jen koaxiálu"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "CSMA/CA rezervuje médium přes RTS/CTS"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
162
content/lessons/networks/unit-17.json
Normal file
162
content/lessons/networks/unit-17.json
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
{
|
||||
"id": "unit-17",
|
||||
"subject": "networks",
|
||||
"number": 17,
|
||||
"titleKey": "net_unit17",
|
||||
"subtitleKey": "net_unit17_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 7 • Přehled",
|
||||
"title": "Aktivní prvky",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Aktivní prvky zpracovávají nebo převádí signál / data.",
|
||||
"Liší se podle vrstvy ISO/OSI, na které pracují."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 7 • Repeater",
|
||||
"title": "Opakovač",
|
||||
"tip": "Tip: fyzická vrstva",
|
||||
"lines": [
|
||||
"Zesiluje signál a tím zvyšuje dosah."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 7 • Transceiver",
|
||||
"title": "Konvertor",
|
||||
"tip": "Tip: fyzická vrstva",
|
||||
"lines": [
|
||||
"Převádí signál mezi různými médii.",
|
||||
"Například metaliku na optiku."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 7 • Hub",
|
||||
"title": "Rozbočovač",
|
||||
"tip": "Tip: fyzická vrstva – „hloupý“ prvek",
|
||||
"lines": [
|
||||
"Co přijme na jednom portu, rozešle do všech ostatních."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 7 • Bridge/Switch",
|
||||
"title": "Most a přepínač",
|
||||
"tip": "Tip: linková vrstva – rámce a MAC",
|
||||
"lines": [
|
||||
"Pracují s rámci a MAC adresami.",
|
||||
"Switch dělí síť na kolizní domény."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "6 / 7 • Router",
|
||||
"title": "Směrovač",
|
||||
"tip": "Tip: síťová vrstva – pakety a IP",
|
||||
"lines": [
|
||||
"Pracuje s pakety a IP adresami.",
|
||||
"Propojuje různé sítě."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "7 / 7 • Gateway",
|
||||
"title": "Brána",
|
||||
"tip": "Tip: vyšší vrstvy",
|
||||
"lines": [
|
||||
"Převádí mezi odlišnými architekturami nebo protokoly."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz17_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Repeater (opakovač) hlavně:",
|
||||
"options": [
|
||||
"Zesiluje signál a zvyšuje dosah",
|
||||
"Směruje IP pakety",
|
||||
"Převádí mezi protokoly vyšších vrstev",
|
||||
"Rezervuje médium RTS/CTS"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Repeater zesiluje signál na fyzické vrstvě"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Transceiver typicky:",
|
||||
"options": [
|
||||
"Převádí signál mezi různými médii",
|
||||
"Dělí síť jen podle DNS",
|
||||
"Je vždy jen brána",
|
||||
"Pracuje jen s UDP"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Transceiver převádí mezi médii (např. metalika ↔ optika)"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Hub (rozbočovač) je:",
|
||||
"options": [
|
||||
"„Hloupý“ prvek – data z jednoho portu pošle všem ostatním",
|
||||
"Směrovač na síťové vrstvě",
|
||||
"Jen laserový zdroj",
|
||||
"Jen MAC filtr bez portů"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Hub rozešle přijatá data do všech ostatních portů"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Bridge a switch pracují na vrstvě:",
|
||||
"options": [
|
||||
"Fyzické",
|
||||
"Linkové (rámce a MAC)",
|
||||
"Aplikační",
|
||||
"Jen transportní"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Bridge/switch = linková vrstva, rámce a MAC"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Switch mimo jiné:",
|
||||
"options": [
|
||||
"Dělí síť na kolizní domény",
|
||||
"Nikdy nepoužívá MAC adresy",
|
||||
"Funguje jen jako hub bez portů",
|
||||
"Převádí jen ASCII"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Switch dělí síť na kolizní domény"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Router pracuje s:",
|
||||
"options": [
|
||||
"Pakety a IP adresami a propojuje sítě",
|
||||
"Jen koaxiálem bez IP",
|
||||
"Jen huby bez směrování",
|
||||
"Jen RTS/CTS"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Router = síťová vrstva, pakety a IP, propojuje sítě"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Gateway (brána) především:",
|
||||
"options": [
|
||||
"Převádí mezi odlišnými architekturami/protokoly",
|
||||
"Jen zesiluje bitový signál",
|
||||
"Jen tvoří kolize",
|
||||
"Jen lámání vlákna"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Gateway převádí mezi odlišnými architekturami/protokoly"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
139
content/lessons/networks/unit-18.json
Normal file
139
content/lessons/networks/unit-18.json
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
{
|
||||
"id": "unit-18",
|
||||
"subject": "networks",
|
||||
"number": 18,
|
||||
"titleKey": "net_unit18",
|
||||
"subtitleKey": "net_unit18_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 5 • Funkce",
|
||||
"title": "Učení MAC adres",
|
||||
"tip": "Tip: CAM tabulka = adresa → port",
|
||||
"lines": [
|
||||
"Switch se učí MAC adresy odesílatelů.",
|
||||
"Ukládá je do CAM tabulky.",
|
||||
"Rámce pak posílá jen na konkrétní port."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 5 • Metody",
|
||||
"title": "Tři způsoby přepínání",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Cut-through, store-and-forward a fragment-free.",
|
||||
"Liší se rychlostí a kontrolou chyb."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 5 • Cut-through",
|
||||
"title": "Hned po adrese",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Čte jen adresu příjemce a hned posílá dál.",
|
||||
"Nejrychlejší metoda.",
|
||||
"Nevýhoda: posílá i chybné rámce."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 5 • Store-and-forward",
|
||||
"title": "Celý rámec + CRC",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Přijme celý rámec a zkontroluje CRC.",
|
||||
"Teprve pak rámec pošle.",
|
||||
"Nejpomalejší, ale nejbezpečnější metoda."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 5 • Fragment-free",
|
||||
"title": "Modifikovaný cut-through",
|
||||
"tip": "Tip: prvních 64 bytů = detekce kolizí",
|
||||
"lines": [
|
||||
"Čte prvních 64 bytů rámce.",
|
||||
"To stačí k odhalení kolizních fragmentů.",
|
||||
"Kompromis mezi rychlostí a bezpečností."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz18_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Switch se učí MAC adresy a ukládá je do:",
|
||||
"options": [
|
||||
"CAM tabulky",
|
||||
"Jen DNS cache",
|
||||
"Jen ARP bez portů",
|
||||
"Jen OSPF"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "MAC adresy switch ukládá do CAM tabulky"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Díky CAM tabulce switch posílá rámce:",
|
||||
"options": [
|
||||
"Jen na konkrétní port",
|
||||
"Vždy na všechny porty jako hub",
|
||||
"Jen na routery",
|
||||
"Jen přes laser"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Podle CAM tabulky posílá rámec jen na cílový port"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Cut-through přepínání:",
|
||||
"options": [
|
||||
"Čte jen adresu příjemce a hned posílá",
|
||||
"Vždy čeká na celé CRC",
|
||||
"Nikdy neposílá rámce",
|
||||
"Pracuje jen s IP směrováním"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Cut-through čte adresu příjemce a hned přeposílá"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Nevýhoda cut-through je:",
|
||||
"options": [
|
||||
"Že posílá i chybné rámce",
|
||||
"Že je nejpomalejší",
|
||||
"Že nemá MAC adresy",
|
||||
"Že vyžaduje gateway"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Cut-through je rychlý, ale propustí i chyby"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Store-and-forward:",
|
||||
"options": [
|
||||
"Přijme celý rámec, zkontroluje CRC a pak pošle",
|
||||
"Čte jen první 2 byty",
|
||||
"Nikdy nekontroluje chyby",
|
||||
"Funguje jen jako Aloha"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Store-and-forward kontroluje celý rámec včetně CRC"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Fragment-free (modifikovaný cut-through) čte:",
|
||||
"options": [
|
||||
"Prvních 64 bytů (detekce kolizí)",
|
||||
"Jen poslední byte",
|
||||
"Celý paket IP včetně směrování",
|
||||
"Jen SMTP hlavičku"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Fragment-free čte prvních 64 bytů kvůli kolizím"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
173
content/lessons/networks/unit-19.json
Normal file
173
content/lessons/networks/unit-19.json
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
{
|
||||
"id": "unit-19",
|
||||
"subject": "networks",
|
||||
"number": 19,
|
||||
"titleKey": "net_unit19",
|
||||
"subtitleKey": "net_unit19_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 7 • Princip",
|
||||
"title": "Směrování paketů",
|
||||
"tip": "Tip: 3. vrstva ISO/OSI – síťová",
|
||||
"lines": [
|
||||
"Rozhodování o dalším směru přenosu paketů do cílové sítě.",
|
||||
"Probíhá na síťové vrstvě modelu ISO/OSI.",
|
||||
"K rozhodování se používají směrovací algoritmy."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 7 • Adaptivní",
|
||||
"title": "Dynamické algoritmy",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Reagují na změny v síti (výpadek linky, přetížení).",
|
||||
"Vyžadují pravidelnou výměnu informací mezi směrovači."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 7 • Neadaptivní",
|
||||
"title": "Statické algoritmy",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Nereagují na změny v síti.",
|
||||
"Cesty pevně definuje správce.",
|
||||
"Při výpadku části sítě může spojení spadnout."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 7 • Řízení",
|
||||
"title": "Centralizované a izolované",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Centralizované: centrum počítá cesty pro všechny směrovače.",
|
||||
"Výpadek centra = pád směrování; v praxi skoro nepoužívané.",
|
||||
"Izolované: směrovač se rozhoduje sám z lokálních informací.",
|
||||
"Izolované se používá spíš jako doplněk při přetížení."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 7 • Distribuované",
|
||||
"title": "Výměna mezi směrovači",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Směrovače si vyměňují informace.",
|
||||
"Každý si udržuje vlastní směrovací tabulku."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "6 / 7 • Tabulka",
|
||||
"title": "Next Hop a metrika",
|
||||
"tip": "Tip: nižší metrika = lepší / rychlejší trasa",
|
||||
"lines": [
|
||||
"Záznam: cílová síť, next hop a metrika.",
|
||||
"Next hop = nejbližší soused, kterému paket předáme.",
|
||||
"Metrika číselně ohodnocuje cestu.",
|
||||
"Při více cestách se volí ta s nejnižší metrikou."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "7 / 7 • Distance Vector",
|
||||
"title": "Příklad algoritmu",
|
||||
"tip": "Tip: např. výměna tabulek každých 30 s",
|
||||
"lines": [
|
||||
"Směrovače znají jen své přímé sousedy.",
|
||||
"Pravidelně si posílají kopie směrovacích tabulek.",
|
||||
"Informace se šíří krok za krokem po celé síti.",
|
||||
"Každý směrovač si spočítá nejlepší trasu do známých sítí."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz19_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Směrování probíhá na vrstvě:",
|
||||
"options": [
|
||||
"Fyzické",
|
||||
"Síťové (3. ISO/OSI)",
|
||||
"Jen aplikační",
|
||||
"Jen linkové"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Směrování běží na síťové (3.) vrstvě ISO/OSI"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Adaptivní (dynamické) algoritmy:",
|
||||
"options": [
|
||||
"Reagují na změny a vyměňují informace mezi směrovači",
|
||||
"Nikdy nemění cesty",
|
||||
"Fungují jen na fyzické vrstvě",
|
||||
"Nepoužívají metriku"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Adaptivní algoritmy reagují na změny a sdílí stav sítě"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Neadaptivní (statické) směrování:",
|
||||
"options": [
|
||||
"Má cesty pevně nastavené správcem",
|
||||
"Samo opraví každý výpadek",
|
||||
"Nepotřebuje směrovací tabulku",
|
||||
"Běží jen na Wi-Fi"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Statické cesty nastaví správce a samy se nemění"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Centralizované směrování v praxi:",
|
||||
"options": [
|
||||
"Se skoro nepoužívá – výpadek centra shodí směrování",
|
||||
"Je jediný standard internetu",
|
||||
"Neexistuje žádné centrum",
|
||||
"Pracuje jen s MAC adresami"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Centralizované směrování padá s výpadkem centra"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Distribuované směrování znamená, že:",
|
||||
"options": [
|
||||
"Směrovače si vyměňují informace a každý má vlastní tabulku",
|
||||
"Jedno centrum řídí všechny cesty",
|
||||
"Směrovač nezná žádné sousedy",
|
||||
"Pakety jdou jen přes hub"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Distribuované: výměna informací a vlastní tabulky"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Metrika ve směrovací tabulce:",
|
||||
"options": [
|
||||
"Číselně hodnotí cestu – nižší je lepší",
|
||||
"Je vždy jen MAC adresa",
|
||||
"Nemá vliv na výběr trasy",
|
||||
"Nahrazuje next hop"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Nižší metrika = lepší / rychlejší trasa"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Distance Vector typicky:",
|
||||
"options": [
|
||||
"Posílá sousedům kopie tabulek a šíří informace krok za krokem",
|
||||
"Počítá cesty jen v jednom centru",
|
||||
"Ignoruje přímé sousedy",
|
||||
"Pracuje jen na aplikační vrstvě"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Distance Vector: výměna tabulek se sousedy (např. každých 30 s)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
172
content/lessons/networks/unit-20.json
Normal file
172
content/lessons/networks/unit-20.json
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
{
|
||||
"id": "unit-20",
|
||||
"subject": "networks",
|
||||
"number": 20,
|
||||
"titleKey": "net_unit20",
|
||||
"subtitleKey": "net_unit20_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 7 • Router",
|
||||
"title": "Síťová vrstva",
|
||||
"tip": "Tip: má vlastní IP a je vidět např. pingen",
|
||||
"lines": [
|
||||
"Pracuje na 3. (síťové) vrstvě.",
|
||||
"Segmentuje síť na podsítě nebo propojuje LAN do většího celku.",
|
||||
"Umožňuje přístup na WAN (např. internet).",
|
||||
"Podporuje soustavy protokolů (např. TCP/IP) a má vlastní adresu."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 7 • Bezpečnost",
|
||||
"title": "Firewall na routeru",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Router s firewallem filtruje pakety.",
|
||||
"Může zakázat nebo povolit určité síťové služby."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 7 • Multiprotokol",
|
||||
"title": "Různé standardy",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Multiprotokolový router spojuje různé soustavy protokolů.",
|
||||
"Například TCP/IP se sítěmi IPX/SPX.",
|
||||
"Umí i různý síťový hardware (Ethernet ↔ Token Ring).",
|
||||
"Konvertuje formát rámců i paketů."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 7 • Routing",
|
||||
"title": "Směrování a tabulky",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Každému paketu hledá vhodnou cestu k cíli.",
|
||||
"Zohledňuje např. okamžité zatížení sítí.",
|
||||
"Používá směrovací tabulky a musí znát topologii.",
|
||||
"Směrovače si tabulky vyměňují (např. broadcastem)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 7 • Broadcast",
|
||||
"title": "Broadcastové domény",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Router rozděluje síť na broadcastové domény.",
|
||||
"Broadcasty lze filtrovat – nešíří se na všechny porty."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "6 / 7 • Gateway",
|
||||
"title": "Brána",
|
||||
"tip": "Tip: pracuje až na aplikační vrstvě (i nižších)",
|
||||
"lines": [
|
||||
"Připojuje LAN na zcela odlišné prostředí.",
|
||||
"Například mainframe nebo GSM síť.",
|
||||
"Často PC s kartou/emulací terminálu nebo s modemem na WAN."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "7 / 7 • Poznámka",
|
||||
"title": "Gateway vs router",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Někdy se pod pojmem brána rozumí spíše router.",
|
||||
"Gateway také označuje připojení LAN na WAN / internet."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz20_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Směrovač (router) pracuje na vrstvě:",
|
||||
"options": [
|
||||
"Fyzické",
|
||||
"Síťové (3.)",
|
||||
"Jen aplikační",
|
||||
"Jen prezentační"
|
||||
],
|
||||
"correct": 1,
|
||||
"hint": "Router pracuje na síťové (3.) vrstvě"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Router mimo jiné umožňuje:",
|
||||
"options": [
|
||||
"Segmentaci na podsítě a přístup na WAN",
|
||||
"Jen lámání optiky",
|
||||
"Jen Aloha bez IP",
|
||||
"Jen cut-through bez MAC"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Router segmentuje podsítě a zajišťuje WAN přístup"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Firewall na routeru dokáže:",
|
||||
"options": [
|
||||
"Filtrovat pakety a řídit služby",
|
||||
"Jen zesílit bitový signál",
|
||||
"Jen uložit CAM bez IP",
|
||||
"Jen nahradit fotodiodu"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Firewall filtruje pakety a služby"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Multiprotokolový router:",
|
||||
"options": [
|
||||
"Propojí sítě různých protokolů/standardů",
|
||||
"Umí jen jeden protokol",
|
||||
"Nepracuje s IP adresou",
|
||||
"Nikdy nekonvertuje rámce"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Multiprotokolový router spojuje různé protokoly/standardy"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Router rozděluje síť na:",
|
||||
"options": [
|
||||
"Broadcastové domény",
|
||||
"Jen fyzické kabely bez domén",
|
||||
"Jen aplikační servery",
|
||||
"Jen Token Ring bez Ethernetu"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Router dělí síť na broadcastové domény"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Brána (gateway) typicky:",
|
||||
"options": [
|
||||
"Připojuje LAN na odlišné prostředí (mainframe, GSM, WAN)",
|
||||
"Pracuje jen jako hub",
|
||||
"Nikdy nemá software",
|
||||
"Funguje jen na fyzické vrstvě"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Gateway připojuje LAN na odlišné prostředí"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Poznámka k pojmu brána:",
|
||||
"options": [
|
||||
"Někdy se jí myslí spíše router / LAN–WAN připojení",
|
||||
"Vždy znamená jen fotodiodu",
|
||||
"Nikdy nesouvisí s internetem",
|
||||
"Nahrazuje jen lámačku vláken"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Pojem brána někdy označuje router nebo LAN–WAN připojení"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
180
content/lessons/networks/unit-21.json
Normal file
180
content/lessons/networks/unit-21.json
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
{
|
||||
"id": "unit-21",
|
||||
"subject": "networks",
|
||||
"number": 21,
|
||||
"titleKey": "net_unit21",
|
||||
"subtitleKey": "net_unit21_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 8 • Ethernet",
|
||||
"title": "Síťový hardware",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Jeden ze standardů síťového hardware.",
|
||||
"Dnes nejpoužívanější."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 8 • Adresy",
|
||||
"title": "48bitová MAC",
|
||||
"tip": "Tip: IEEE přidělí první 3 B, výrobce další 3 B",
|
||||
"lines": [
|
||||
"Adresa má 48 bitů (6 bytů) a je celosvětově jedinečná.",
|
||||
"Je pevně v síťové kartě už od výroby.",
|
||||
"Výrobci dostávají bloky adres od IEEE."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 8 • Rámce",
|
||||
"title": "Typy hlaviček",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Rámec = skupina bitů na linkové vrstvě.",
|
||||
"Hlavička má adresy odesílatele/příjemce a typ obsahu.",
|
||||
"Typy: Ethernet II, IEEE 802.3, 802.3 SNAP, raw 802.3."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 8 • Ethernet II",
|
||||
"title": "DIX + EtherType",
|
||||
"tip": "Tip: EtherType > 1500 (např. IP = 0800h)",
|
||||
"lines": [
|
||||
"Původní DIX Ethernet.",
|
||||
"Hlavička: příjemce 6 B, odesílatel 6 B, EtherType 2 B.",
|
||||
"EtherType identifikuje protokol (IP, IPX…)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 8 • IEEE 802.3",
|
||||
"title": "Délka místo typu",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Místo EtherType je údaj o délce (vždy ≤ 1500).",
|
||||
"Uvnitř je rámec 802.2 (stejný i pro Token Ring).",
|
||||
"Typ protokolu je v SAP mezi linkovou a síťovou vrstvou."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "6 / 8 • Raw 802.3",
|
||||
"title": "Novell / IPX",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"„Holý“ rámec 802.3 bez 802.2 (Novell).",
|
||||
"Funguje jen v prostředí IPX.",
|
||||
"Paket začíná dvěma byty FFFFh."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "7 / 8 • SNAP",
|
||||
"title": "802.2 SNAP",
|
||||
"tip": "Tip: byty 15–16 za délkou = AAAAh",
|
||||
"lines": [
|
||||
"Vložen do rámce 802.3.",
|
||||
"Rozšiřuje identifikaci protokolu až na 5 bytů."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "8 / 8 • Velikost",
|
||||
"title": "Min / max / MTU",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Max. Ethernet II: 1500 + 18 = 1518 B.",
|
||||
"Min. velikost: 64 B (kolizní okénko).",
|
||||
"MTU je obvyklé omezení velikosti na routerech."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz21_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Ethernetová adresa má:",
|
||||
"options": [
|
||||
"48 bitů (6 bytů) a je jedinečná",
|
||||
"Jen 16 bitů",
|
||||
"Jen 32 bitů IP",
|
||||
"Jen 8 bitů SAP"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "MAC adresa Ethernetu má 48 bitů a je unikátní"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "První 3 byty MAC adresy přiděluje:",
|
||||
"options": [
|
||||
"IEEE výrobci",
|
||||
"Jen uživatel v BIOS",
|
||||
"Jen DNS server",
|
||||
"Jen Aloha"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "IEEE přidělí výrobci první 3 byty adresy"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Ethernet II v hlavičce používá:",
|
||||
"options": [
|
||||
"EtherType (např. IP = 0800h, hodnota > 1500)",
|
||||
"Jen délku ≤ 1500 bez typu",
|
||||
"Jen FFFFh bez adres",
|
||||
"Jen AAAAh bez MAC"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Ethernet II: EtherType > 1500 (IP = 0800h)"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "IEEE 802.3 místo EtherType má:",
|
||||
"options": [
|
||||
"Údaj o délce (≤ 1500) a uvnitř 802.2",
|
||||
"Jen cut-through",
|
||||
"Jen 64 B MTU bez délky",
|
||||
"Jen laserovou hlavičku"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "IEEE 802.3: délka ≤ 1500 a rámec 802.2"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Raw 802.3 (Novell):",
|
||||
"options": [
|
||||
"Bez 802.2, jen IPX, začíná FFFFh",
|
||||
"Funguje jen s HTTP",
|
||||
"Má vždy EtherType 0800h",
|
||||
"Nemá žádné adresy"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Raw 802.3: Novell/IPX, začíná FFFFh"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Maximální rámec Ethernet II je:",
|
||||
"options": [
|
||||
"1518 B (1500 + 18)",
|
||||
"64 B",
|
||||
"900 µm",
|
||||
"250 µm"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Max. Ethernet II = 1518 B"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Minimální velikost rámce plyne z:",
|
||||
"options": [
|
||||
"Kolizního okénka (64 B)",
|
||||
"Jen z MTU routeru",
|
||||
"Jen z SNAP AAAAh",
|
||||
"Jen z Token Ring"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Min. rámec 64 B kvůli koliznímu okénku"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
198
content/lessons/networks/unit-22.json
Normal file
198
content/lessons/networks/unit-22.json
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
{
|
||||
"id": "unit-22",
|
||||
"subject": "networks",
|
||||
"number": 22,
|
||||
"titleKey": "net_unit22",
|
||||
"subtitleKey": "net_unit22_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 8 • Paket",
|
||||
"title": "Header a data",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Paket/segment: hlavička (pro partnerskou vrstvu) + data.",
|
||||
"Protokol určuje strukturu a pravidla stejnolehlých vrstev.",
|
||||
"Může běžet na různých platformách."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 8 • TCP/IP",
|
||||
"title": "Protokoly po vrstvách",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Aplikační: aplikační protokoly.",
|
||||
"Transportní: TCP, UDP.",
|
||||
"Síťová: IP, ARP, RARP, ICMP, IGMP, RIP, OSPF.",
|
||||
"Síťové rozhraní: Ethernet, Token Ring, ATM, PPP…"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 8 • IP",
|
||||
"title": "Univerzální přenos",
|
||||
"tip": "Tip: nespojovaný a nespolehlivý",
|
||||
"lines": [
|
||||
"Jediný přenosový protokol TCP/IP nad libovolnou technologií.",
|
||||
"Pracuje s virtuálními pakety – IP datagramy.",
|
||||
"Stará se o směrování a přenos; velikost volí odesílatel.",
|
||||
"Nezaručuje pořadí, dobu ani nepoškozené doručení."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 8 • Routing",
|
||||
"title": "Routing vs forwarding",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Routing: rozhodnutí o dalším směru datagramu.",
|
||||
"Forwarding: vložení do linkového rámce a odeslání.",
|
||||
"Linkové adresy se mění podle konkrétní sítě."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 8 • Datagram",
|
||||
"title": "Velikost a HLEN",
|
||||
"tip": "Tip: Length 5 = 20 B hlavičky",
|
||||
"lines": [
|
||||
"Velikost: od 576 B do 64 kB.",
|
||||
"Části: hlavička + data.",
|
||||
"HLEN: délka hlavičky ve 32bitových slovech.",
|
||||
"TOTAL LENGTH: délka celého paketu v bytech."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "6 / 8 • Hlavička",
|
||||
"title": "Klíčová pole",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"VERSION (IPv4 = 4), LENGTH, TOTAL LENGTH.",
|
||||
"IDENTIFICATION + FLAGS + OFFSET = fragmentace.",
|
||||
"TTL: čítač směrovačů proti zacyklení.",
|
||||
"PROTOCOL, HEADER CHECKSUM, zdrojová/cílová IP."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "7 / 8 • PROTOCOL",
|
||||
"title": "Čísla protokolů",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"1 = ICMP, 2 = IGMP, 6 = TCP.",
|
||||
"17 = UDP, 89 = OSPF.",
|
||||
"Položka je 8bitová → max. 256 hodnot."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "8 / 8 • Fragmentace",
|
||||
"title": "MTU a Ethernet",
|
||||
"tip": "Tip: Ethernet MTU = 1500 B",
|
||||
"lines": [
|
||||
"Fragmentace, když se datagram nevejde do menšího rámce.",
|
||||
"MTU udává max. velikost rámce podle hardwaru.",
|
||||
"DF = nefragmentuj; MF = další fragmenty."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz22_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "IP protokol je:",
|
||||
"options": [
|
||||
"Nespojovaný a nespolehlivý přenos datagramů",
|
||||
"Spolehlivý a spojovaný jako TCP",
|
||||
"Jen fyzické kódování",
|
||||
"Jen EtherType bez IP"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "IP je nespojovaný a nespolehlivý"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Routing u IP znamená:",
|
||||
"options": [
|
||||
"Rozhodnutí o dalším směru datagramu",
|
||||
"Jen CRC na lince",
|
||||
"Jen MAC učení ve switchi",
|
||||
"Jen lámání vlákna"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Routing = rozhodnutí o směru datagramu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Forwarding u IP znamená:",
|
||||
"options": [
|
||||
"Vložení do linkového rámce a odeslání",
|
||||
"Jen výpočet metriky OSPF",
|
||||
"Jen DNS překlad",
|
||||
"Jen Aloha"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Forwarding = vložení do rámce a odeslání"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "HLEN (LENGTH) s hodnotou 5 znamená:",
|
||||
"options": [
|
||||
"Hlavičku dlouhou 20 B",
|
||||
"TTL 5",
|
||||
"PROTOCOL = 5",
|
||||
"MTU 5"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "HLEN 5 = 5 × 4 B = 20 B hlavičky"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "TTL v IP hlavičce:",
|
||||
"options": [
|
||||
"Počítá průchody směrovači a brání zacyklení",
|
||||
"Udává EtherType",
|
||||
"Nahrazuje MAC adresu",
|
||||
"Je vždy 1500"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "TTL snižují směrovače proti zacyklení"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Číslo protokolu 6 v IP hlavičce znamená:",
|
||||
"options": [
|
||||
"TCP",
|
||||
"UDP",
|
||||
"ICMP",
|
||||
"OSPF"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "PROTOCOL 6 = TCP"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Číslo protokolu 17 znamená:",
|
||||
"options": [
|
||||
"UDP",
|
||||
"TCP",
|
||||
"IGMP",
|
||||
"ARP"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "PROTOCOL 17 = UDP"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Ethernet MTU je typicky:",
|
||||
"options": [
|
||||
"1500 B",
|
||||
"64 kB",
|
||||
"576 B bez dat",
|
||||
"20 B hlavičky"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Ethernet MTU = 1500 B"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
139
content/lessons/networks/unit-23.json
Normal file
139
content/lessons/networks/unit-23.json
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
{
|
||||
"id": "unit-23",
|
||||
"subject": "networks",
|
||||
"number": 23,
|
||||
"titleKey": "net_unit23",
|
||||
"subtitleKey": "net_unit23_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 5 • ICMP",
|
||||
"title": "Řídicí zprávy",
|
||||
"tip": "Tip: síťová vrstva, ale nese se v IP datagramu",
|
||||
"lines": [
|
||||
"Zasílá informace o chybách a stavu přenosu.",
|
||||
"ICMP paket jede přímo v IP datagramu.",
|
||||
"Směrovače s ním pracují i bez transportní vrstvy."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 5 • Zprávy I",
|
||||
"title": "Quench, TTL, Unreachable",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Source Quench: hrozí zahlcení – zpomal odesílání.",
|
||||
"Time Exceeded: TTL kleslo na 0, paket zahozen (tracert).",
|
||||
"Destination Unreachable: cíl (síť/uzel/protokol/port)",
|
||||
"není dostupný."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 5 • Zprávy II",
|
||||
"title": "Redirect a Echo",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Redirect: přesměruj přenos na jiný (rychlejší) router.",
|
||||
"Echo Request / Echo Reply: test dostupnosti (ping)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 5 • Traceroute",
|
||||
"title": "Princip TTL",
|
||||
"tip": "Tip: typicky max. 30 skoků",
|
||||
"lines": [
|
||||
"Odešle paket s TTL = 1; první router odpoví Time Exceeded.",
|
||||
"Pak TTL = 2, 3… a tak se zjišťují skoky na cestě.",
|
||||
"Pokračuje, dokud paket nedorazí do cíle nebo nevyprší limit."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 5 • Shrnutí",
|
||||
"title": "K čemu ICMP slouží",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Diagnostika a hlášení chyb na síťové vrstvě.",
|
||||
"Základ příkazů ping a traceroute / tracert."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz23_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "ICMP slouží především k:",
|
||||
"options": [
|
||||
"Zasílání informací o chybách a stavu přenosu",
|
||||
"Jen směrování OSPF tabulek",
|
||||
"Jen lámání optiky",
|
||||
"Jen EtherType 0800h"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "ICMP posílá informace o chybách a stavu přenosu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "ICMP paket se přenáší:",
|
||||
"options": [
|
||||
"Přímo v IP datagramu",
|
||||
"Jen jako Ethernet II bez IP",
|
||||
"Jen v UDP bez IP",
|
||||
"Jen jako MAC bez datagramu"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "ICMP jede uvnitř IP datagramu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Source Quench znamená:",
|
||||
"options": [
|
||||
"Varování před zahlcením – zpomal odesílání",
|
||||
"TTL kleslo na 0",
|
||||
"Cíl je nedostupný",
|
||||
"Echo Reply"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Source Quench = zpomal odesílání kvůli zahlcení"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Time Exceeded nastane, když:",
|
||||
"options": [
|
||||
"TTL v IP hlavičce klesne na 0",
|
||||
"Router nemá firewall",
|
||||
"Použijeme jen SNAP",
|
||||
"MTU je 1500"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Time Exceeded = TTL kleslo na 0"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Echo Request / Reply využívá příkaz:",
|
||||
"options": [
|
||||
"ping",
|
||||
"lámačka",
|
||||
"cut-through",
|
||||
"hub"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Echo Request/Reply = ping"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Traceroute zjišťuje cestu tak, že:",
|
||||
"options": [
|
||||
"Postupně zvyšuje TTL a čeká na Time Exceeded",
|
||||
"Posílá jen ARP bez IP",
|
||||
"Ignoruje směrovače",
|
||||
"Používá jen CAM tabulku"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Traceroute zvyšuje TTL a čte Time Exceeded"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
138
content/lessons/networks/unit-24.json
Normal file
138
content/lessons/networks/unit-24.json
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
{
|
||||
"id": "unit-24",
|
||||
"subject": "networks",
|
||||
"number": 24,
|
||||
"titleKey": "net_unit24",
|
||||
"subtitleKey": "net_unit24_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 5 • ARP / RARP",
|
||||
"title": "Převod adres",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"ARP: z IP adresy na MAC (fyzickou) adresu.",
|
||||
"RARP: z MAC adresy na IP adresu."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 5 • Způsoby",
|
||||
"title": "Jak zjistit MAC",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Tabulkový převod: ruční správa (malé sítě).",
|
||||
"Výpočet: funkce, kde MAC plyne z IP.",
|
||||
"Dotaz a odpověď: dynamické zjišťování (nejčastější)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 5 • Mechanismus",
|
||||
"title": "Broadcast a unicast",
|
||||
"tip": "Tip: „Kdo má IP x.x.x.x?“",
|
||||
"lines": [
|
||||
"ARP využívá broadcast v Ethernetu.",
|
||||
"Odpoví jen uzel s danou IP.",
|
||||
"Odpověď jde unicastem a obsahuje MAC adresu."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 5 • Cache",
|
||||
"title": "Dočasná paměť",
|
||||
"tip": "Tip: zobrazení příkazem arp -a",
|
||||
"lines": [
|
||||
"Výsledky se ukládají do ARP cache.",
|
||||
"Dotazy se tak nemusí stále opakovat.",
|
||||
"Položky: dynamické (učené) nebo statické (ruční)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 5 • Shrnutí",
|
||||
"title": "K čemu ARP slouží",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Propojuje síťovou adresu (IP) s linkovou (MAC).",
|
||||
"Bez ARP by IP datagram nešel vložit do Ethernet rámce."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz24_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "ARP převádí:",
|
||||
"options": [
|
||||
"IP adresu na MAC adresu",
|
||||
"MAC adresu na IP adresu",
|
||||
"TCP na UDP",
|
||||
"TTL na MTU"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "ARP: IP → MAC"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "RARP převádí:",
|
||||
"options": [
|
||||
"MAC adresu na IP adresu",
|
||||
"IP adresu na MAC adresu",
|
||||
"EtherType na SAP",
|
||||
"OSPF na RIP"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "RARP: MAC → IP"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Nejčastější způsob ARP je:",
|
||||
"options": [
|
||||
"Dotaz a odpověď v síti",
|
||||
"Jen ruční tabulka bez sítě",
|
||||
"Jen cut-through",
|
||||
"Jen ICMP Echo"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Nejčastější je dynamický dotaz a odpověď"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "ARP dotaz v Ethernetu používá:",
|
||||
"options": [
|
||||
"Broadcast",
|
||||
"Jen unicast bez broadcastu",
|
||||
"Jen laser",
|
||||
"Jen Token Ring bez IP"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "ARP dotaz jde broadcastem"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "ARP odpověď typicky přichází jako:",
|
||||
"options": [
|
||||
"Unicast s MAC adresou uzlu",
|
||||
"Broadcast bez MAC",
|
||||
"Jen Time Exceeded",
|
||||
"Jen Source Quench"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "ARP odpověď je unicast s MAC"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "ARP cache slouží k tomu, aby:",
|
||||
"options": [
|
||||
"Se nemusely stále opakovat stejné dotazy",
|
||||
"Se nahradil směrovač",
|
||||
"Se zrušila IP adresa",
|
||||
"Se vypnul Ethernet"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "ARP cache ukládá výsledky dočasně (arp -a)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
164
content/lessons/networks/unit-25.json
Normal file
164
content/lessons/networks/unit-25.json
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
{
|
||||
"id": "unit-25",
|
||||
"subject": "networks",
|
||||
"number": 25,
|
||||
"titleKey": "net_unit25",
|
||||
"subtitleKey": "net_unit25_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 6 • TCP vs UDP",
|
||||
"title": "Základní rozdíly",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"TCP: spolehlivý a spojovaný přenos.",
|
||||
"Potvrzování, kontrola pořadí a řízení toku.",
|
||||
"UDP: nespolehlivý a nespojovaný.",
|
||||
"Minimální režie – neřeší potvrzení ani pořadí."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 6 • Porty",
|
||||
"title": "Multiplex a demultiplex",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Porty rozlišují aplikace (procesy) na jednom uzlu.",
|
||||
"Multiplex: sběr dat od aplikací do segmentů/datagramů.",
|
||||
"Demultiplex: doručení příchozích dat správné aplikaci",
|
||||
"podle čísla portu."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 6 • TCP",
|
||||
"title": "Spojení a vlastnosti",
|
||||
"tip": "Tip: three-way handshake",
|
||||
"lines": [
|
||||
"Navázání spojení: třícestné podání ruky.",
|
||||
"Streamově orientovaný a full duplex.",
|
||||
"Jen unicast – bez multicastu a broadcastu."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 6 • Nevýhody TCP",
|
||||
"title": "Režie a real-time",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Větší režie: hlavička 20 B (UDP má 8 B).",
|
||||
"Nevhodný pro real-time média: ztráta segmentu",
|
||||
"zastaví proud a čeká se na znovudoručení.",
|
||||
"Bez multihomingu – výpadek rozhraní shodí spojení."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 6 • Alternativy",
|
||||
"title": "SCTP a DCCP",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"SCTP: multihoming a multistreaming (až 64k proudů).",
|
||||
"DCCP: datagramy s řízením zahlcení",
|
||||
"(streaming, IP telefonie)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "6 / 6 • Shrnutí",
|
||||
"title": "Kdy co použít",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"TCP: spolehlivá data (web, pošta, soubory).",
|
||||
"UDP: rychlost a nízká režie (DNS, hry, real-time)."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz25_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "TCP je:",
|
||||
"options": [
|
||||
"Spolehlivý a spojovaný",
|
||||
"Nespolehlivý a nespojovaný",
|
||||
"Jen fyzická vrstva",
|
||||
"Jen ARP cache"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "TCP = spolehlivý a spojovaný"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "UDP je:",
|
||||
"options": [
|
||||
"Nespolehlivý a nespojovaný s malou režií",
|
||||
"Spolehlivý se třícestným handshake",
|
||||
"Jen store-and-forward",
|
||||
"Jen ICMP Time Exceeded"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "UDP = nespolehlivý, nespojovaný, malá režie"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Porty slouží k:",
|
||||
"options": [
|
||||
"Rozlišení aplikací na jednom uzlu",
|
||||
"Jen směrování IP",
|
||||
"Jen MAC učení",
|
||||
"Jen lámání vlákna"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Porty rozlišují aplikace na uzlu"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Navázání TCP spojení používá:",
|
||||
"options": [
|
||||
"Třícestné podání ruky (three-way handshake)",
|
||||
"Jen broadcast ARP",
|
||||
"Jen Source Quench",
|
||||
"Jen cut-through"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "TCP se navazuje three-way handshake"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Hlavička TCP má typicky:",
|
||||
"options": [
|
||||
"20 B (UDP 8 B)",
|
||||
"8 B (UDP 20 B)",
|
||||
"1500 B",
|
||||
"64 B min. rámec"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "TCP hlavička 20 B, UDP 8 B"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "TCP není vhodný pro real-time média hlavně proto, že:",
|
||||
"options": [
|
||||
"Ztráta segmentu zastaví proud a čeká se na znovudoručení",
|
||||
"Nemá žádné porty",
|
||||
"Nepodporuje unicast",
|
||||
"Nemá hlavičku"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "TCP čeká na znovudoručení – špatné pro real-time"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "SCTP oproti TCP nabízí:",
|
||||
"options": [
|
||||
"Multihoming a multistreaming",
|
||||
"Jen Ethernet II",
|
||||
"Jen RARP",
|
||||
"Jen hub flooding"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "SCTP: multihoming a multistreaming"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
185
content/lessons/networks/unit-26.json
Normal file
185
content/lessons/networks/unit-26.json
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
{
|
||||
"id": "unit-26",
|
||||
"subject": "networks",
|
||||
"number": 26,
|
||||
"titleKey": "net_unit26",
|
||||
"subtitleKey": "net_unit26_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 7 • Úvod",
|
||||
"title": "Aplikační protokoly",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Na uzlu běží více síťových aplikací současně.",
|
||||
"Běžící aplikace = v operační paměti (1+ procesů).",
|
||||
"Pracují podle pravidel – protokolů aplikační vrstvy.",
|
||||
"Data posílají po síti přes TCP nebo UDP."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 7 • Porty",
|
||||
"title": "Procesy a porty",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Port v TCP/UDP říká, které aplikaci data patří.",
|
||||
"Jeden proces může používat více portů.",
|
||||
"Dva procesy nesmí používat stejný port."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 7 • Rozsahy",
|
||||
"title": "Well-known, registrované, dynamické",
|
||||
"tip": "Tip: IANA",
|
||||
"lines": [
|
||||
"Well-known: 0–1023 – vyhrazené službám (norma IANA).",
|
||||
"Registrované: 1024–49151 – IANA jen registruje použití.",
|
||||
"Dynamické/privátní: 49152–65535 – volně k použití."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 7 • Spojení",
|
||||
"title": "Pětice hodnot",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Aplikační spojení určuje pětice:",
|
||||
"(transport, IP1, port1, IP2, port2).",
|
||||
"Klient osloví server na well-known portu."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 7 • Více spojení",
|
||||
"title": "Stejný server, různí klienti",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Na jeden uzel (např. web server) může být",
|
||||
"více aplikačních spojení z různých klientů.",
|
||||
"Rozlišují je IP a porty klientů (i transport)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "6 / 7 • Porty I",
|
||||
"title": "Časté well-known služby",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"20/21 FTP, 22 SSH, 23 Telnet, 25 SMTP, 53 DNS.",
|
||||
"67/68 DHCP, 80 HTTP, 110 POP3, 143 IMAP.",
|
||||
"123 NTP, 161 SNMP, 389 LDAP, 443 HTTPS."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "7 / 7 • Porty II",
|
||||
"title": "Zabezpečené a registrované",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"587 SMTPS, 636 LDAPS, 993 IMAPS, 995 POP3S.",
|
||||
"Registrované: 3306 MySQL, 3389 RDP, 5900 VNC.",
|
||||
"Dynamické porty: 49152–65535."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz26_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Aplikační protokoly typicky posílají data přes:",
|
||||
"options": [
|
||||
"TCP nebo UDP",
|
||||
"Jen fyzickou vrstvu",
|
||||
"Jen ARP",
|
||||
"Jen CAM tabulku"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Aplikace posílají data přes TCP/UDP"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Dva běžící procesy:",
|
||||
"options": [
|
||||
"Nesmí používat stejný port",
|
||||
"Musí sdílet jeden port",
|
||||
"Nesmí mít žádný port",
|
||||
"Používají jen MAC"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Dva procesy nesmí sdílet stejný port"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Well-known porty mají rozsah:",
|
||||
"options": [
|
||||
"0–1023",
|
||||
"1024–49151",
|
||||
"49152–65535",
|
||||
"Jen 80–443"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Well-known: 0–1023 (IANA)"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Dynamické (privátní) porty jsou:",
|
||||
"options": [
|
||||
"49152–65535",
|
||||
"0–1023",
|
||||
"Jen 20–21",
|
||||
"Jen 3306"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Dynamické porty: 49152–65535"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Aplikační spojení určuje:",
|
||||
"options": [
|
||||
"Pětice (transport, IP1, port1, IP2, port2)",
|
||||
"Jen MAC adresa",
|
||||
"Jen název souboru",
|
||||
"Jen VLAN ID"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Spojení = pětice hodnot"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "HTTP a HTTPS používají porty:",
|
||||
"options": [
|
||||
"80 a 443",
|
||||
"22 a 23",
|
||||
"25 a 110",
|
||||
"53 a 67"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "HTTP 80, HTTPS 443"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "SSH a DNS mají porty:",
|
||||
"options": [
|
||||
"22 a 53",
|
||||
"80 a 443",
|
||||
"3389 a 5900",
|
||||
"20 a 21"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "SSH 22, DNS 53"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "MySQL a RDP jsou typicky na portech:",
|
||||
"options": [
|
||||
"3306 a 3389",
|
||||
"80 a 443",
|
||||
"67 a 68",
|
||||
"161 a 162"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "MySQL 3306, RDP 3389"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
196
content/lessons/networks/unit-27.json
Normal file
196
content/lessons/networks/unit-27.json
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
{
|
||||
"id": "unit-27",
|
||||
"subject": "networks",
|
||||
"number": 27,
|
||||
"titleKey": "net_unit27",
|
||||
"subtitleKey": "net_unit27_sub",
|
||||
"slides": [
|
||||
{
|
||||
"kicker": "1 / 8 • Úvod",
|
||||
"title": "Bezdrátové sítě",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Patří sem hlavně: družicové, mikrovlnné a optické spoje.",
|
||||
"Dále Wi-Fi (LAN), WiMAX/LTE (MAN/WAN) a další."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "2 / 8 • Družice",
|
||||
"title": "Geostacionární spoje",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Geostacionární družice: ~36 000 km, stále nad stejným bodem.",
|
||||
"Pasivní odrážejí; aktivní mají transpondéry (převod + zisk).",
|
||||
"C-band ~6/4 GHz; KU-band 12–14 GHz (menší antény).",
|
||||
"Nevýhoda: zpoždění ~250–300 ms (RTT až ~600 ms)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "3 / 8 • VSAT",
|
||||
"title": "Přístup a topologie",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"Bod–bod, broadcast (TV) i multiple access.",
|
||||
"VSAT: terminály + centrální hub.",
|
||||
"Komunikace jen terminál ↔ hub (ne přímo mezi terminály)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "4 / 8 • Mikrovlny",
|
||||
"title": "Pozemní spoje",
|
||||
"tip": "Tip: přímá viditelnost",
|
||||
"lines": [
|
||||
"Frekvence 1–12 GHz, směrování parabolou, malý rozptyl.",
|
||||
"Dosah typicky do ~50 km; retranslační stanice.",
|
||||
"Troposférické spoje: odraz ve ~16 km, dosah až ~500 km."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "5 / 8 • Wi-Fi pojmy",
|
||||
"title": "AP, BSS, ESS",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"AP = přístupový bod; STA komunikují přes AP (ne přímo).",
|
||||
"BSS = stanice v buňce BSA; ESS = více buněk přes DS.",
|
||||
"Roaming při překrytí; ad-hoc = P2P bez AP; hotspot = Wi-Fi.",
|
||||
"ESSID identifikuje síť při přístupu k AP."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "6 / 8 • Standardy",
|
||||
"title": "802.11 a … ax",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"b: 11 Mb/s, 2,4 GHz; a/g: 54 Mb/s; n: MIMO, až 600 Mb/s.",
|
||||
"ac (Wi-Fi 5): 256-QAM, široké kanály; ax (Wi-Fi 6): OFDMA.",
|
||||
"OFDM: mnoho ortogonálních subnosných v kanálu."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "7 / 8 • Zabezpečení",
|
||||
"title": "WEP → WPA3",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"WEP: RC4, krátký IV – prolomeno (2001), nepoužívat.",
|
||||
"WPA: TKIP; WPA2: AES (doporučený základ).",
|
||||
"WPA3: SAE místo PSK, silnější ochrana hesla.",
|
||||
"802.1X/EAP: ověření klienta přes AP (RADIUS)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"kicker": "8 / 8 • Shrnutí",
|
||||
"title": "Dosahy a další",
|
||||
"tip": null,
|
||||
"lines": [
|
||||
"PAN <10 m (Bluetooth); LAN = Wi-Fi; MAN = WiMAX.",
|
||||
"WAN/mobilita: 802.16e, LTE; licencovaná pásma = méně rušení.",
|
||||
"Používej WPA2/WPA3 + AES, ne WEP."
|
||||
]
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Quiz",
|
||||
"subtitleKey": "net_quiz27_head",
|
||||
"items": [
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Geostacionární družice létají asi:",
|
||||
"options": [
|
||||
"36 000 km nad Zemí",
|
||||
"16 km v troposféře",
|
||||
"50 km nad povrchem",
|
||||
"Jen v nízké orbitě LEO"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Geostacionární družice ~36 000 km"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Hlavní nevýhoda družicového spoje je:",
|
||||
"options": [
|
||||
"Velké zpoždění (~250–300 ms)",
|
||||
"Žádný broadcast",
|
||||
"Jen kabelové médium",
|
||||
"Absence portů"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Družice: zpoždění stovky ms"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "V síti VSAT komunikace probíhá:",
|
||||
"options": [
|
||||
"Jen mezi terminálem a hubem",
|
||||
"Přímo mezi dvěma terminály",
|
||||
"Jen přes ARP",
|
||||
"Jen přes WEP"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "VSAT: jen terminál ↔ hub"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Mikrovlnný spoj typicky vyžaduje:",
|
||||
"options": [
|
||||
"Přímou viditelnost (dosah ~50 km)",
|
||||
"Jen metalický kabel",
|
||||
"Jen port 80",
|
||||
"Jen WEP klíč"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Mikrovlny: přímá viditelnost ~50 km"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "V infrastruktuře Wi-Fi stanice (STA):",
|
||||
"options": [
|
||||
"Komunikují přes AP, ne přímo mezi sebou",
|
||||
"Vždy jen ad-hoc bez AP",
|
||||
"Nepoužívají ESSID",
|
||||
"Běží jen na 36 000 km"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "STA komunikují přes AP"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "802.11ax (Wi-Fi 6) přináší zejména:",
|
||||
"options": [
|
||||
"OFDMA a vysokou kapacitu",
|
||||
"Jen WEP",
|
||||
"Jen C-band 4/6 GHz",
|
||||
"Jen troposférický odraz"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Wi-Fi 6 (ax): OFDMA"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "WEP je dnes:",
|
||||
"options": [
|
||||
"Prolomený a nevhodný",
|
||||
"Nejbezpečnější volba",
|
||||
"Totéž co WPA3-SAE",
|
||||
"Povinný od roku 2006"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "WEP prolomen – nepoužívat"
|
||||
},
|
||||
{
|
||||
"kind": "choice",
|
||||
"prompt": "Doporučené zabezpečení Wi-Fi je:",
|
||||
"options": [
|
||||
"WPA2/WPA3 s AES (ne WEP)",
|
||||
"Jen Open system WEP",
|
||||
"Jen Shared key WEP",
|
||||
"Bez hesla vždy"
|
||||
],
|
||||
"correct": 0,
|
||||
"hint": "Používej WPA2/WPA3 + AES"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
63
content/manifest.json
Normal file
63
content/manifest.json
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"subjects": [
|
||||
"deutsch",
|
||||
"networks",
|
||||
"hardware",
|
||||
"czech",
|
||||
"civics",
|
||||
"english",
|
||||
"math",
|
||||
"physics",
|
||||
"science",
|
||||
"tech-graphics",
|
||||
"presentation",
|
||||
"programming",
|
||||
"software"
|
||||
],
|
||||
"lessons": {
|
||||
"deutsch": [
|
||||
"unit-01",
|
||||
"unit-02",
|
||||
"unit-03"
|
||||
],
|
||||
"networks": [
|
||||
"unit-01",
|
||||
"unit-02",
|
||||
"unit-03",
|
||||
"unit-04",
|
||||
"unit-05",
|
||||
"unit-06",
|
||||
"unit-07",
|
||||
"unit-08",
|
||||
"unit-09",
|
||||
"unit-10",
|
||||
"unit-11",
|
||||
"unit-12",
|
||||
"unit-13",
|
||||
"unit-14",
|
||||
"unit-15",
|
||||
"unit-16",
|
||||
"unit-17",
|
||||
"unit-18",
|
||||
"unit-19",
|
||||
"unit-20",
|
||||
"unit-21",
|
||||
"unit-22",
|
||||
"unit-23",
|
||||
"unit-24",
|
||||
"unit-25",
|
||||
"unit-26",
|
||||
"unit-27"
|
||||
],
|
||||
"hardware": [
|
||||
"unit-01",
|
||||
"unit-02",
|
||||
"unit-03"
|
||||
],
|
||||
"czech": [
|
||||
"mluvnice",
|
||||
"orwell-1984",
|
||||
"fuks-spalovac-mrtvol"
|
||||
]
|
||||
}
|
||||
}
|
||||
389
content/paths.json
Normal file
389
content/paths.json
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
[
|
||||
{
|
||||
"subject": "deutsch",
|
||||
"units": [
|
||||
{
|
||||
"number": 1,
|
||||
"lesson": "unit-01",
|
||||
"title": {
|
||||
"cs": "Neue Freunde",
|
||||
"en": "Neue Freunde"
|
||||
},
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"lesson": "unit-02",
|
||||
"title": {
|
||||
"cs": "Aus aller Welt",
|
||||
"en": "Aus aller Welt"
|
||||
},
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"lesson": "unit-03",
|
||||
"title": {
|
||||
"cs": "Bei uns zu Hause",
|
||||
"en": "Bei uns zu Hause"
|
||||
},
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 4,
|
||||
"lesson": null,
|
||||
"title": {
|
||||
"cs": "Schule und Freizeit",
|
||||
"en": "Schule und Freizeit"
|
||||
},
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 5,
|
||||
"lesson": null,
|
||||
"title": {
|
||||
"cs": "Guten Appetit!",
|
||||
"en": "Guten Appetit!"
|
||||
},
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 6,
|
||||
"lesson": null,
|
||||
"title": {
|
||||
"cs": "Mein Tagesablauf",
|
||||
"en": "Mein Tagesablauf"
|
||||
},
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 7,
|
||||
"lesson": null,
|
||||
"title": {
|
||||
"cs": "Meine Freunde",
|
||||
"en": "Meine Freunde"
|
||||
},
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 8,
|
||||
"lesson": null,
|
||||
"title": {
|
||||
"cs": "Wir treffen uns in Salzburg",
|
||||
"en": "Wir treffen uns in Salzburg"
|
||||
},
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 9,
|
||||
"lesson": null,
|
||||
"title": {
|
||||
"cs": "Mein Haus ist meine Burg",
|
||||
"en": "Mein Haus ist meine Burg"
|
||||
},
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 10,
|
||||
"lesson": null,
|
||||
"title": {
|
||||
"cs": "Urlaub in Österreich",
|
||||
"en": "Urlaub in Österreich"
|
||||
},
|
||||
"unlocked": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"subject": "networks",
|
||||
"year": 1,
|
||||
"units": [
|
||||
{
|
||||
"number": 1,
|
||||
"lesson": "unit-01",
|
||||
"titleKey": "net_unit1",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"lesson": "unit-02",
|
||||
"titleKey": "net_unit2",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"lesson": "unit-03",
|
||||
"titleKey": "net_unit3",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 4,
|
||||
"lesson": "unit-04",
|
||||
"titleKey": "net_unit4",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 5,
|
||||
"lesson": "unit-05",
|
||||
"titleKey": "net_unit5",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 6,
|
||||
"lesson": "unit-06",
|
||||
"titleKey": "net_unit6",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 7,
|
||||
"lesson": "unit-07",
|
||||
"titleKey": "net_unit7",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 8,
|
||||
"lesson": "unit-08",
|
||||
"titleKey": "net_unit8",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 9,
|
||||
"lesson": "unit-09",
|
||||
"titleKey": "net_unit9",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 10,
|
||||
"lesson": "unit-10",
|
||||
"titleKey": "net_unit10",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 11,
|
||||
"lesson": "unit-11",
|
||||
"titleKey": "net_unit11",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 12,
|
||||
"lesson": "unit-12",
|
||||
"titleKey": "net_unit12",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 13,
|
||||
"lesson": "unit-13",
|
||||
"titleKey": "net_unit13",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 14,
|
||||
"lesson": "unit-14",
|
||||
"titleKey": "net_unit14",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 15,
|
||||
"lesson": "unit-15",
|
||||
"titleKey": "net_unit15",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 16,
|
||||
"lesson": "unit-16",
|
||||
"titleKey": "net_unit16",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 17,
|
||||
"lesson": "unit-17",
|
||||
"titleKey": "net_unit17",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 18,
|
||||
"lesson": "unit-18",
|
||||
"titleKey": "net_unit18",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 19,
|
||||
"lesson": "unit-19",
|
||||
"titleKey": "net_unit19",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 20,
|
||||
"lesson": "unit-20",
|
||||
"titleKey": "net_unit20",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 21,
|
||||
"lesson": "unit-21",
|
||||
"titleKey": "net_unit21",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 22,
|
||||
"lesson": "unit-22",
|
||||
"titleKey": "net_unit22",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 23,
|
||||
"lesson": "unit-23",
|
||||
"titleKey": "net_unit23",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 24,
|
||||
"lesson": "unit-24",
|
||||
"titleKey": "net_unit24",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 25,
|
||||
"lesson": "unit-25",
|
||||
"titleKey": "net_unit25",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 26,
|
||||
"lesson": "unit-26",
|
||||
"titleKey": "net_unit26",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 27,
|
||||
"lesson": "unit-27",
|
||||
"titleKey": "net_unit27",
|
||||
"unlocked": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"subject": "hardware",
|
||||
"units": [
|
||||
{
|
||||
"number": 1,
|
||||
"lesson": "unit-01",
|
||||
"titleKey": "hw_unit1",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"lesson": "unit-02",
|
||||
"titleKey": "hw_unit2",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"lesson": "unit-03",
|
||||
"titleKey": "hw_unit3",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 4,
|
||||
"lesson": null,
|
||||
"titleKey": "hw_unit4",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 5,
|
||||
"lesson": null,
|
||||
"titleKey": "hw_unit5",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 6,
|
||||
"lesson": null,
|
||||
"titleKey": "hw_unit6",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 7,
|
||||
"lesson": null,
|
||||
"titleKey": "hw_unit7",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 8,
|
||||
"lesson": null,
|
||||
"titleKey": "hw_unit8",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 9,
|
||||
"lesson": null,
|
||||
"titleKey": "hw_unit9",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 10,
|
||||
"lesson": null,
|
||||
"titleKey": "hw_unit10",
|
||||
"unlocked": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"subject": "czech",
|
||||
"units": [
|
||||
{
|
||||
"number": 1,
|
||||
"lesson": null,
|
||||
"route": null,
|
||||
"title": {
|
||||
"cs": "Literatura",
|
||||
"en": "Literatura"
|
||||
},
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"lesson": null,
|
||||
"route": "/czech/mluvnice",
|
||||
"title": {
|
||||
"cs": "Mluvnice",
|
||||
"en": "Mluvnice"
|
||||
},
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"lesson": null,
|
||||
"route": "/czech/reading",
|
||||
"title": {
|
||||
"cs": "Maturitní četba",
|
||||
"en": "Maturitní četba"
|
||||
},
|
||||
"unlocked": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"subject": "czech",
|
||||
"branch": "reading",
|
||||
"units": [
|
||||
{
|
||||
"number": 1,
|
||||
"lesson": "orwell-1984",
|
||||
"title": {
|
||||
"cs": "George Orwell – 1984",
|
||||
"en": "George Orwell – 1984"
|
||||
},
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"lesson": "fuks-spalovac-mrtvol",
|
||||
"title": {
|
||||
"cs": "Ladislav Fuks – Spalovač mrtvol",
|
||||
"en": "Ladislav Fuks – Spalovač mrtvol"
|
||||
},
|
||||
"unlocked": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
337
content/schema.ts
Normal file
337
content/schema.ts
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
/**
|
||||
* Shared lesson content schema.
|
||||
*
|
||||
* This is the single description of everything under `content/`. Both the
|
||||
* extractor in `tools/` and the Next.js app in `web/` type against it, so a
|
||||
* shape change here surfaces as a type error on both sides.
|
||||
*/
|
||||
|
||||
export type Lang = 'cs' | 'en';
|
||||
|
||||
/** A short piece of text that may differ per UI language. */
|
||||
export interface Localized {
|
||||
cs: string;
|
||||
en?: string;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- themes --- */
|
||||
|
||||
export interface ThemePalette {
|
||||
crust: string;
|
||||
base: string;
|
||||
mantle: string;
|
||||
surface0: string;
|
||||
surface1: string;
|
||||
surface2: string;
|
||||
overlay: string;
|
||||
text: string;
|
||||
subtext: string;
|
||||
accent: string;
|
||||
accent2: string;
|
||||
accent3: string;
|
||||
success: string;
|
||||
success2: string;
|
||||
warning: string;
|
||||
error: string;
|
||||
onAccent: string;
|
||||
node: string;
|
||||
lockedBg: string;
|
||||
lockedBorder: string;
|
||||
rail: string;
|
||||
finishDark: string;
|
||||
finishLight: string;
|
||||
finishStroke: string;
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
id: string;
|
||||
name: string;
|
||||
dark: ThemePalette;
|
||||
light: ThemePalette;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- subjects --- */
|
||||
|
||||
export interface Subject {
|
||||
/** Stable slug used in URLs, e.g. `deutsch`, `networks`. */
|
||||
id: string;
|
||||
/** i18n key holding the display name. */
|
||||
key: string;
|
||||
icon: 'flag-de' | 'wifi' | 'chip' | 'book' | null;
|
||||
unlocked: boolean;
|
||||
/** Route the subject node opens, or null when it is a locked placeholder. */
|
||||
route: string | null;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- units --- */
|
||||
|
||||
/**
|
||||
* One node on a serpentine learning path.
|
||||
*
|
||||
* A node carries either a literal `title` (German unit names are the same in
|
||||
* both languages) or a `titleKey` to look up in the translation tables.
|
||||
*/
|
||||
export interface UnitRef {
|
||||
/** 1-based position on the path. */
|
||||
number: number;
|
||||
/** Slug of the lesson file under `content/lessons/<subject>/`, if any. */
|
||||
lesson: string | null;
|
||||
title?: Localized;
|
||||
titleKey?: string;
|
||||
/** Destination for nodes that open another path rather than a lesson. */
|
||||
route?: string | null;
|
||||
unlocked: boolean;
|
||||
}
|
||||
|
||||
export interface Path {
|
||||
subject: string;
|
||||
/** Optional school-year grouping, as the networks subject uses. */
|
||||
year?: number;
|
||||
/** Set on secondary paths within a subject, e.g. the Czech reading list. */
|
||||
branch?: string;
|
||||
units: UnitRef[];
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------- theory slides - */
|
||||
|
||||
export interface Slide {
|
||||
/** Small label above the title, e.g. `1 / 4 • VLSM`. */
|
||||
kicker: string;
|
||||
title: string;
|
||||
/** Highlighted aside shown under the title. */
|
||||
tip: string | null;
|
||||
lines: string[];
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- exercises -- */
|
||||
|
||||
/** Multiple choice. `correct` indexes into `options`. */
|
||||
export interface ChoiceExercise {
|
||||
kind: 'choice';
|
||||
prompt: string;
|
||||
options: string[];
|
||||
correct: number;
|
||||
hint?: string;
|
||||
/** Czech gloss of the prompt, shown as a translation aid. */
|
||||
meaning?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A sentence split into literal segments with gaps between them.
|
||||
*
|
||||
* `segments.length === answers.length + 1`. Every gap variant in the desktop
|
||||
* app (verb insertion, number words, sequences, possessives, letters) collapses
|
||||
* into this one shape; `input` decides whether the gap is a dropdown fed from
|
||||
* `pool` or a free text field.
|
||||
*/
|
||||
export interface FillExercise {
|
||||
kind: 'fill';
|
||||
segments: string[];
|
||||
answers: string[];
|
||||
input: 'select' | 'text';
|
||||
/** Row label such as `1.` or `a)`. */
|
||||
label?: string;
|
||||
/** Group heading rendered above this row, e.g. a speaker's name. */
|
||||
heading?: string;
|
||||
/** Emoji or picture clue shown before the sentence. */
|
||||
emoji?: string;
|
||||
/** Extra clue such as the digits to spell out. */
|
||||
clue?: string;
|
||||
/** Czech translation revealed as a hint. */
|
||||
meaning?: string;
|
||||
/** Per-gap glosses, aligned with `answers`. */
|
||||
glosses?: (string | null)[];
|
||||
/**
|
||||
* Which of the task's word lists feeds each gap, aligned with `answers`.
|
||||
* Defaults to the task `pool`; `alt` selects `altPool`.
|
||||
*/
|
||||
gapPool?: ('main' | 'alt')[];
|
||||
/** Gaps hold single letters rather than whole words, so render them narrow. */
|
||||
letters?: boolean;
|
||||
}
|
||||
|
||||
/** Free-form answer checked against a model answer rather than exactly. */
|
||||
export interface FreeExercise {
|
||||
kind: 'free';
|
||||
question: string;
|
||||
questionCs?: string;
|
||||
sample: string;
|
||||
sampleCs?: string;
|
||||
}
|
||||
|
||||
/** Type the answer; `answers` lists all accepted spellings. */
|
||||
export interface TypedExercise {
|
||||
kind: 'typed';
|
||||
prompt: string;
|
||||
answers: string[];
|
||||
meaning?: string;
|
||||
/** Full model sentence revealed after checking, where one exists. */
|
||||
model?: string;
|
||||
/** Accepted answers are substrings of a longer sentence, not the whole input. */
|
||||
substring?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A prompt answered through several labelled fields, as the Czech grammar
|
||||
* exercises use ("determine gender, number and case").
|
||||
*/
|
||||
export interface FieldsExercise {
|
||||
kind: 'fields';
|
||||
prompt: string;
|
||||
fields: { label: string; answers: string[] }[];
|
||||
/** The full correct answer, revealed after checking. */
|
||||
solution?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-assessed exercise: think of the answer, then reveal the model solution.
|
||||
* Used where no automatic check is meaningful (morpheme analysis, punctuation,
|
||||
* rewriting direct speech).
|
||||
*/
|
||||
export interface RevealExercise {
|
||||
kind: 'reveal';
|
||||
prompt: string;
|
||||
solution: string;
|
||||
}
|
||||
|
||||
/** Drag or click words into order to build a sentence. */
|
||||
export interface AssemblyExercise {
|
||||
kind: 'assembly';
|
||||
prompt: string;
|
||||
words: string[];
|
||||
meaning?: string;
|
||||
}
|
||||
|
||||
/** Sort labelled items into named groups. */
|
||||
export interface AssignExercise {
|
||||
kind: 'assign';
|
||||
groups: string[];
|
||||
items: { emoji: string | null; label: string; group: number; meaning?: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A dialogue where some replies are missing.
|
||||
*
|
||||
* `answer` may list several accepted words separated by `|`, matching the
|
||||
* desktop app's answer comparison.
|
||||
*/
|
||||
export interface DialogueExercise {
|
||||
kind: 'dialogue';
|
||||
title: string;
|
||||
rows: {
|
||||
speaker: string;
|
||||
before: string;
|
||||
answer: string;
|
||||
after: string;
|
||||
meaning?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
/** Guess the word letter by letter. */
|
||||
export interface HangmanExercise {
|
||||
kind: 'hangman';
|
||||
word: string;
|
||||
hint: string;
|
||||
}
|
||||
|
||||
/** Open profile question with a model answer ("Steckbrief"). */
|
||||
export interface ProfileExercise {
|
||||
kind: 'profile';
|
||||
stem: string;
|
||||
sample: string;
|
||||
}
|
||||
|
||||
/** Editable grid, e.g. the possessive-pronoun table. */
|
||||
export interface TableExercise {
|
||||
kind: 'table';
|
||||
columns: string[];
|
||||
rows: string[];
|
||||
/** `answers[row][col]`; a prefilled cell has `given: true`. */
|
||||
answers: { value: string; given?: boolean }[][];
|
||||
}
|
||||
|
||||
/** VLSM subnetting: split a network into four blocks. */
|
||||
export interface SubnetExercise {
|
||||
kind: 'subnet';
|
||||
scenario: string;
|
||||
solution: string;
|
||||
blocks: {
|
||||
prefix: number;
|
||||
network: string;
|
||||
broadcast: string;
|
||||
firstHost: string;
|
||||
lastHost: string;
|
||||
}[] | null;
|
||||
}
|
||||
|
||||
export type Exercise =
|
||||
| ChoiceExercise
|
||||
| FillExercise
|
||||
| FreeExercise
|
||||
| TypedExercise
|
||||
| FieldsExercise
|
||||
| RevealExercise
|
||||
| AssemblyExercise
|
||||
| AssignExercise
|
||||
| DialogueExercise
|
||||
| HangmanExercise
|
||||
| ProfileExercise
|
||||
| TableExercise
|
||||
| SubnetExercise;
|
||||
|
||||
/* ---------------------------------------------------------------- lessons -- */
|
||||
|
||||
/** A group of exercises presented as one task on the unit map. */
|
||||
export interface Task {
|
||||
/** 1-based bubble number on the unit map. */
|
||||
number: number;
|
||||
/** Short label for the map bubble. */
|
||||
title: string;
|
||||
/** Longer heading for the exercise page, where it differs from `title`. */
|
||||
heading?: string;
|
||||
/** i18n key for the instruction line. */
|
||||
subtitleKey: string | null;
|
||||
/** Literal instruction, where the desktop app passes text instead of a key. */
|
||||
subtitle?: string;
|
||||
/** Worked example shown above the items. */
|
||||
example?: string;
|
||||
/** Extra note under the instruction. */
|
||||
note?: string;
|
||||
/** Words offered to every `select` gap in this task. */
|
||||
pool?: string[];
|
||||
/** Second word list, selected per gap via `FillExercise.gapPool`. */
|
||||
altPool?: string[];
|
||||
items: Exercise[];
|
||||
}
|
||||
|
||||
export interface VocabSection {
|
||||
header: string;
|
||||
items: { prompt: string; answers: string[]; meaning?: string }[];
|
||||
}
|
||||
|
||||
export interface Lesson {
|
||||
/** File slug, unique within the subject. */
|
||||
id: string;
|
||||
subject: string;
|
||||
/** Position on the subject path. */
|
||||
number: number;
|
||||
/** Literal title, or `titleKey` when the text comes from the i18n tables. */
|
||||
title?: Localized;
|
||||
titleKey?: string;
|
||||
subtitle?: Localized;
|
||||
subtitleKey?: string;
|
||||
/** Theory shown before the exercises. */
|
||||
slides: Slide[];
|
||||
tasks: Task[];
|
||||
/** Optional vocabulary trainer branch. */
|
||||
vocab?: VocabSection[];
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- manifest -- */
|
||||
|
||||
export interface ContentManifest {
|
||||
subjects: Subject[];
|
||||
paths: Path[];
|
||||
/** Lesson ids per subject, in path order. */
|
||||
lessons: Record<string, string[]>;
|
||||
}
|
||||
93
content/subjects.json
Normal file
93
content/subjects.json
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
[
|
||||
{
|
||||
"id": "deutsch",
|
||||
"icon": "flag-de",
|
||||
"route": "/deutsch",
|
||||
"key": "Deutsch",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"id": "networks",
|
||||
"icon": "wifi",
|
||||
"route": "/networks",
|
||||
"key": "Správa počítačových sítí",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"id": "hardware",
|
||||
"icon": "chip",
|
||||
"route": "/hardware",
|
||||
"key": "Technické vybavení",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"id": "czech",
|
||||
"icon": "book",
|
||||
"route": "/czech",
|
||||
"key": "Český jazyk a literatura",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"id": "civics",
|
||||
"icon": null,
|
||||
"route": null,
|
||||
"key": "Občanská nauka",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "english",
|
||||
"icon": null,
|
||||
"route": null,
|
||||
"key": "English",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "math",
|
||||
"icon": null,
|
||||
"route": null,
|
||||
"key": "Matematika",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "physics",
|
||||
"icon": null,
|
||||
"route": null,
|
||||
"key": "Fyzika",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "science",
|
||||
"icon": null,
|
||||
"route": null,
|
||||
"key": "Základy Přírodopisných věd",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "tech-graphics",
|
||||
"icon": null,
|
||||
"route": null,
|
||||
"key": "Technická grafika",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "presentation",
|
||||
"icon": null,
|
||||
"route": null,
|
||||
"key": "Prezentační grafika",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "programming",
|
||||
"icon": null,
|
||||
"route": null,
|
||||
"key": "Programování",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"id": "software",
|
||||
"icon": null,
|
||||
"route": null,
|
||||
"key": "Programové vybavení",
|
||||
"unlocked": false
|
||||
}
|
||||
]
|
||||
562
content/themes.json
Normal file
562
content/themes.json
Normal file
|
|
@ -0,0 +1,562 @@
|
|||
[
|
||||
{
|
||||
"id": "catppuccin",
|
||||
"name": "Catppuccin",
|
||||
"dark": {
|
||||
"crust": "#11111b",
|
||||
"base": "#1e1e2e",
|
||||
"mantle": "#181825",
|
||||
"surface0": "#313244",
|
||||
"surface1": "#45475a",
|
||||
"surface2": "#585b70",
|
||||
"overlay": "#6c7086",
|
||||
"text": "#cdd6f4",
|
||||
"subtext": "#a6adc8",
|
||||
"accent": "#cba6f7",
|
||||
"accent2": "#b4befe",
|
||||
"accent3": "#89b4fa",
|
||||
"success": "#a6e3a1",
|
||||
"success2": "#94e2d5",
|
||||
"warning": "#f9e2af",
|
||||
"error": "#f38ba8",
|
||||
"onAccent": "#1e1e2e",
|
||||
"node": "#2b2d40",
|
||||
"lockedBg": "#24263a",
|
||||
"lockedBorder": "#383b52",
|
||||
"rail": "#27293c",
|
||||
"finishDark": "#1f202e",
|
||||
"finishLight": "#3b3e58",
|
||||
"finishStroke": "#52556b"
|
||||
},
|
||||
"light": {
|
||||
"crust": "#dce0e8",
|
||||
"base": "#eff1f5",
|
||||
"mantle": "#e6e9ef",
|
||||
"surface0": "#ccd0da",
|
||||
"surface1": "#bcc0cc",
|
||||
"surface2": "#acb0be",
|
||||
"overlay": "#9ca0b0",
|
||||
"text": "#4c4f69",
|
||||
"subtext": "#6c6f85",
|
||||
"accent": "#8839ef",
|
||||
"accent2": "#7287fd",
|
||||
"accent3": "#1e66f5",
|
||||
"success": "#40a02b",
|
||||
"success2": "#179299",
|
||||
"warning": "#df8e1d",
|
||||
"error": "#d20f39",
|
||||
"onAccent": "#eff1f5",
|
||||
"node": "#e6e9ef",
|
||||
"lockedBg": "#dce0e8",
|
||||
"lockedBorder": "#bcc0cc",
|
||||
"rail": "#ccd0da",
|
||||
"finishDark": "#dce0e8",
|
||||
"finishLight": "#bcc0cc",
|
||||
"finishStroke": "#9ca0b0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "nord",
|
||||
"name": "Nord",
|
||||
"dark": {
|
||||
"crust": "#2e3440",
|
||||
"base": "#3b4252",
|
||||
"mantle": "#2e3440",
|
||||
"surface0": "#434c5e",
|
||||
"surface1": "#4c566a",
|
||||
"surface2": "#616e88",
|
||||
"overlay": "#7b88a1",
|
||||
"text": "#eceff4",
|
||||
"subtext": "#d8dee9",
|
||||
"accent": "#88c0d0",
|
||||
"accent2": "#81a1c1",
|
||||
"accent3": "#5e81ac",
|
||||
"success": "#a3be8c",
|
||||
"success2": "#8fbcbb",
|
||||
"warning": "#ebcb8b",
|
||||
"error": "#bf616a",
|
||||
"onAccent": "#2e3440",
|
||||
"node": "#434c5e",
|
||||
"lockedBg": "#3b4252",
|
||||
"lockedBorder": "#4c566a",
|
||||
"rail": "#2e3440",
|
||||
"finishDark": "#2e3440",
|
||||
"finishLight": "#434c5e",
|
||||
"finishStroke": "#4c566a"
|
||||
},
|
||||
"light": {
|
||||
"crust": "#d8dee9",
|
||||
"base": "#eceff4",
|
||||
"mantle": "#e5e9f0",
|
||||
"surface0": "#d8dee9",
|
||||
"surface1": "#c7ceda",
|
||||
"surface2": "#b0b8c8",
|
||||
"overlay": "#7b88a1",
|
||||
"text": "#2e3440",
|
||||
"subtext": "#4c566a",
|
||||
"accent": "#5e81ac",
|
||||
"accent2": "#81a1c1",
|
||||
"accent3": "#5e81ac",
|
||||
"success": "#a3be8c",
|
||||
"success2": "#8fbcbb",
|
||||
"warning": "#ebcb8b",
|
||||
"error": "#bf616a",
|
||||
"onAccent": "#eceff4",
|
||||
"node": "#e5e9f0",
|
||||
"lockedBg": "#d8dee9",
|
||||
"lockedBorder": "#c7ceda",
|
||||
"rail": "#d8dee9",
|
||||
"finishDark": "#d8dee9",
|
||||
"finishLight": "#c7ceda",
|
||||
"finishStroke": "#7b88a1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dracula",
|
||||
"name": "Dracula",
|
||||
"dark": {
|
||||
"crust": "#191a21",
|
||||
"base": "#282a36",
|
||||
"mantle": "#21222c",
|
||||
"surface0": "#44475a",
|
||||
"surface1": "#6272a4",
|
||||
"surface2": "#707eb0",
|
||||
"overlay": "#6272a4",
|
||||
"text": "#f8f8f2",
|
||||
"subtext": "#bfbfb2",
|
||||
"accent": "#bd93f9",
|
||||
"accent2": "#ff79c6",
|
||||
"accent3": "#8be9fd",
|
||||
"success": "#50fa7b",
|
||||
"success2": "#8be9fd",
|
||||
"warning": "#f1fa8c",
|
||||
"error": "#ff5555",
|
||||
"onAccent": "#282a36",
|
||||
"node": "#383a4a",
|
||||
"lockedBg": "#2f3240",
|
||||
"lockedBorder": "#44475a",
|
||||
"rail": "#21222c",
|
||||
"finishDark": "#191a21",
|
||||
"finishLight": "#383a4a",
|
||||
"finishStroke": "#6272a4"
|
||||
},
|
||||
"light": {
|
||||
"crust": "#d5d7e0",
|
||||
"base": "#f8f8f2",
|
||||
"mantle": "#efefea",
|
||||
"surface0": "#e2e2d8",
|
||||
"surface1": "#d0d0c4",
|
||||
"surface2": "#bfbfb2",
|
||||
"overlay": "#9a9a8c",
|
||||
"text": "#282a36",
|
||||
"subtext": "#44475a",
|
||||
"accent": "#7c3aed",
|
||||
"accent2": "#d946a6",
|
||||
"accent3": "#0891b2",
|
||||
"success": "#16a34a",
|
||||
"success2": "#0e7490",
|
||||
"warning": "#ca8a04",
|
||||
"error": "#dc2626",
|
||||
"onAccent": "#f8f8f2",
|
||||
"node": "#efefea",
|
||||
"lockedBg": "#e2e2d8",
|
||||
"lockedBorder": "#d0d0c4",
|
||||
"rail": "#e2e2d8",
|
||||
"finishDark": "#d5d7e0",
|
||||
"finishLight": "#d0d0c4",
|
||||
"finishStroke": "#9a9a8c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rose-pine",
|
||||
"name": "Rose Pine",
|
||||
"dark": {
|
||||
"crust": "#191724",
|
||||
"base": "#1f1d2e",
|
||||
"mantle": "#1f1d2e",
|
||||
"surface0": "#26233a",
|
||||
"surface1": "#403d52",
|
||||
"surface2": "#524f67",
|
||||
"overlay": "#6e6a86",
|
||||
"text": "#e0def4",
|
||||
"subtext": "#908caa",
|
||||
"accent": "#c4a7e7",
|
||||
"accent2": "#ebbcba",
|
||||
"accent3": "#9ccfd8",
|
||||
"success": "#31748f",
|
||||
"success2": "#9ccfd8",
|
||||
"warning": "#f6c177",
|
||||
"error": "#eb6f92",
|
||||
"onAccent": "#191724",
|
||||
"node": "#26233a",
|
||||
"lockedBg": "#21202e",
|
||||
"lockedBorder": "#403d52",
|
||||
"rail": "#191724",
|
||||
"finishDark": "#191724",
|
||||
"finishLight": "#26233a",
|
||||
"finishStroke": "#524f67"
|
||||
},
|
||||
"light": {
|
||||
"crust": "#f2e9e1",
|
||||
"base": "#faf4ed",
|
||||
"mantle": "#fffaf3",
|
||||
"surface0": "#f2e9e1",
|
||||
"surface1": "#dfdad9",
|
||||
"surface2": "#cecacd",
|
||||
"overlay": "#9893a5",
|
||||
"text": "#575279",
|
||||
"subtext": "#797593",
|
||||
"accent": "#907aa9",
|
||||
"accent2": "#d7827e",
|
||||
"accent3": "#56949f",
|
||||
"success": "#286983",
|
||||
"success2": "#56949f",
|
||||
"warning": "#ea9d34",
|
||||
"error": "#b4637a",
|
||||
"onAccent": "#faf4ed",
|
||||
"node": "#fffaf3",
|
||||
"lockedBg": "#f2e9e1",
|
||||
"lockedBorder": "#dfdad9",
|
||||
"rail": "#f2e9e1",
|
||||
"finishDark": "#f2e9e1",
|
||||
"finishLight": "#dfdad9",
|
||||
"finishStroke": "#9893a5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ocean",
|
||||
"name": "Ocean",
|
||||
"dark": {
|
||||
"crust": "#16161e",
|
||||
"base": "#1a1b26",
|
||||
"mantle": "#16161e",
|
||||
"surface0": "#24283b",
|
||||
"surface1": "#414868",
|
||||
"surface2": "#565f89",
|
||||
"overlay": "#565f89",
|
||||
"text": "#c0caf5",
|
||||
"subtext": "#a9b1d6",
|
||||
"accent": "#7aa2f7",
|
||||
"accent2": "#bb9af7",
|
||||
"accent3": "#7dcfff",
|
||||
"success": "#9ece6a",
|
||||
"success2": "#73daca",
|
||||
"warning": "#e0af68",
|
||||
"error": "#f7768e",
|
||||
"onAccent": "#1a1b26",
|
||||
"node": "#24283b",
|
||||
"lockedBg": "#1f2335",
|
||||
"lockedBorder": "#3b4261",
|
||||
"rail": "#16161e",
|
||||
"finishDark": "#16161e",
|
||||
"finishLight": "#24283b",
|
||||
"finishStroke": "#414868"
|
||||
},
|
||||
"light": {
|
||||
"crust": "#d6dae8",
|
||||
"base": "#e1e2ec",
|
||||
"mantle": "#d5d6e2",
|
||||
"surface0": "#c8c9d6",
|
||||
"surface1": "#b4b6c5",
|
||||
"surface2": "#9aa0b5",
|
||||
"overlay": "#7a8199",
|
||||
"text": "#2e3446",
|
||||
"subtext": "#4a5168",
|
||||
"accent": "#2e7de9",
|
||||
"accent2": "#7847bd",
|
||||
"accent3": "#007197",
|
||||
"success": "#387068",
|
||||
"success2": "#007197",
|
||||
"warning": "#8c6c3e",
|
||||
"error": "#c64343",
|
||||
"onAccent": "#e1e2ec",
|
||||
"node": "#d5d6e2",
|
||||
"lockedBg": "#c8c9d6",
|
||||
"lockedBorder": "#b4b6c5",
|
||||
"rail": "#c8c9d6",
|
||||
"finishDark": "#d6dae8",
|
||||
"finishLight": "#b4b6c5",
|
||||
"finishStroke": "#7a8199"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gruvbox",
|
||||
"name": "Gruvbox",
|
||||
"dark": {
|
||||
"crust": "#1d2021",
|
||||
"base": "#282828",
|
||||
"mantle": "#1d2021",
|
||||
"surface0": "#3c3836",
|
||||
"surface1": "#504945",
|
||||
"surface2": "#665c54",
|
||||
"overlay": "#928374",
|
||||
"text": "#ebdbb2",
|
||||
"subtext": "#a89984",
|
||||
"accent": "#fe8019",
|
||||
"accent2": "#d3869b",
|
||||
"accent3": "#83a598",
|
||||
"success": "#b8bb26",
|
||||
"success2": "#8ec07c",
|
||||
"warning": "#fabd2f",
|
||||
"error": "#fb4934",
|
||||
"onAccent": "#1d2021",
|
||||
"node": "#3c3836",
|
||||
"lockedBg": "#32302f",
|
||||
"lockedBorder": "#504945",
|
||||
"rail": "#1d2021",
|
||||
"finishDark": "#1d2021",
|
||||
"finishLight": "#3c3836",
|
||||
"finishStroke": "#504945"
|
||||
},
|
||||
"light": {
|
||||
"crust": "#ebdbb2",
|
||||
"base": "#fbf1c7",
|
||||
"mantle": "#f2e5bc",
|
||||
"surface0": "#ebdbb2",
|
||||
"surface1": "#d5c4a1",
|
||||
"surface2": "#bdae93",
|
||||
"overlay": "#928374",
|
||||
"text": "#3c3836",
|
||||
"subtext": "#504945",
|
||||
"accent": "#d65d0e",
|
||||
"accent2": "#b16286",
|
||||
"accent3": "#076678",
|
||||
"success": "#79740e",
|
||||
"success2": "#427b58",
|
||||
"warning": "#b57614",
|
||||
"error": "#9d0006",
|
||||
"onAccent": "#fbf1c7",
|
||||
"node": "#f2e5bc",
|
||||
"lockedBg": "#ebdbb2",
|
||||
"lockedBorder": "#d5c4a1",
|
||||
"rail": "#ebdbb2",
|
||||
"finishDark": "#ebdbb2",
|
||||
"finishLight": "#d5c4a1",
|
||||
"finishStroke": "#928374"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solarized",
|
||||
"name": "Solarized",
|
||||
"dark": {
|
||||
"crust": "#002b36",
|
||||
"base": "#073642",
|
||||
"mantle": "#002b36",
|
||||
"surface0": "#586e75",
|
||||
"surface1": "#657b83",
|
||||
"surface2": "#839496",
|
||||
"overlay": "#93a1a1",
|
||||
"text": "#fdf6e3",
|
||||
"subtext": "#93a1a1",
|
||||
"accent": "#268bd2",
|
||||
"accent2": "#6c71c4",
|
||||
"accent3": "#2aa198",
|
||||
"success": "#859900",
|
||||
"success2": "#2aa198",
|
||||
"warning": "#b58900",
|
||||
"error": "#dc322f",
|
||||
"onAccent": "#002b36",
|
||||
"node": "#073642",
|
||||
"lockedBg": "#002b36",
|
||||
"lockedBorder": "#586e75",
|
||||
"rail": "#002b36",
|
||||
"finishDark": "#002b36",
|
||||
"finishLight": "#073642",
|
||||
"finishStroke": "#586e75"
|
||||
},
|
||||
"light": {
|
||||
"crust": "#eee8d5",
|
||||
"base": "#fdf6e3",
|
||||
"mantle": "#eee8d5",
|
||||
"surface0": "#eee8d5",
|
||||
"surface1": "#e2dbc6",
|
||||
"surface2": "#c9c0a6",
|
||||
"overlay": "#657b83",
|
||||
"text": "#586e75",
|
||||
"subtext": "#657b83",
|
||||
"accent": "#268bd2",
|
||||
"accent2": "#6c71c4",
|
||||
"accent3": "#2aa198",
|
||||
"success": "#859900",
|
||||
"success2": "#2aa198",
|
||||
"warning": "#b58900",
|
||||
"error": "#dc322f",
|
||||
"onAccent": "#fdf6e3",
|
||||
"node": "#eee8d5",
|
||||
"lockedBg": "#e6dfc8",
|
||||
"lockedBorder": "#93a1a1",
|
||||
"rail": "#eee8d5",
|
||||
"finishDark": "#eee8d5",
|
||||
"finishLight": "#93a1a1",
|
||||
"finishStroke": "#586e75"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "everforest",
|
||||
"name": "Everforest",
|
||||
"dark": {
|
||||
"crust": "#1e2326",
|
||||
"base": "#272e33",
|
||||
"mantle": "#2d353b",
|
||||
"surface0": "#374145",
|
||||
"surface1": "#4f5b58",
|
||||
"surface2": "#7a8478",
|
||||
"overlay": "#859289",
|
||||
"text": "#d3c6aa",
|
||||
"subtext": "#a7c080",
|
||||
"accent": "#a7c080",
|
||||
"accent2": "#d699b6",
|
||||
"accent3": "#7fbbb3",
|
||||
"success": "#a7c080",
|
||||
"success2": "#83c092",
|
||||
"warning": "#dbbc7f",
|
||||
"error": "#e67e80",
|
||||
"onAccent": "#272e33",
|
||||
"node": "#374145",
|
||||
"lockedBg": "#2d353b",
|
||||
"lockedBorder": "#4f5b58",
|
||||
"rail": "#1e2326",
|
||||
"finishDark": "#1e2326",
|
||||
"finishLight": "#374145",
|
||||
"finishStroke": "#4f5b58"
|
||||
},
|
||||
"light": {
|
||||
"crust": "#e6e2cc",
|
||||
"base": "#fdf6e3",
|
||||
"mantle": "#f4f0d9",
|
||||
"surface0": "#efebd4",
|
||||
"surface1": "#e0dcc7",
|
||||
"surface2": "#a6b0a0",
|
||||
"overlay": "#939f91",
|
||||
"text": "#5c6a72",
|
||||
"subtext": "#829181",
|
||||
"accent": "#8da101",
|
||||
"accent2": "#df69ba",
|
||||
"accent3": "#35a77c",
|
||||
"success": "#8da101",
|
||||
"success2": "#35a77c",
|
||||
"warning": "#dfa000",
|
||||
"error": "#f85552",
|
||||
"onAccent": "#fdf6e3",
|
||||
"node": "#f4f0d9",
|
||||
"lockedBg": "#efebd4",
|
||||
"lockedBorder": "#e0dcc7",
|
||||
"rail": "#e6e2cc",
|
||||
"finishDark": "#e6e2cc",
|
||||
"finishLight": "#e0dcc7",
|
||||
"finishStroke": "#939f91"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "monokai",
|
||||
"name": "Monokai",
|
||||
"dark": {
|
||||
"crust": "#1d1e19",
|
||||
"base": "#272822",
|
||||
"mantle": "#1e1f1a",
|
||||
"surface0": "#3e3d32",
|
||||
"surface1": "#49483e",
|
||||
"surface2": "#75715e",
|
||||
"overlay": "#75715e",
|
||||
"text": "#f8f8f2",
|
||||
"subtext": "#a59f85",
|
||||
"accent": "#f92672",
|
||||
"accent2": "#ae81ff",
|
||||
"accent3": "#66d9ef",
|
||||
"success": "#a6e22e",
|
||||
"success2": "#a1efe4",
|
||||
"warning": "#e6db74",
|
||||
"error": "#f92672",
|
||||
"onAccent": "#272822",
|
||||
"node": "#3e3d32",
|
||||
"lockedBg": "#2e2f29",
|
||||
"lockedBorder": "#49483e",
|
||||
"rail": "#1d1e19",
|
||||
"finishDark": "#1d1e19",
|
||||
"finishLight": "#3e3d32",
|
||||
"finishStroke": "#49483e"
|
||||
},
|
||||
"light": {
|
||||
"crust": "#e6e3d4",
|
||||
"base": "#faf8f0",
|
||||
"mantle": "#f3f0e4",
|
||||
"surface0": "#e8e4d4",
|
||||
"surface1": "#d6d2c0",
|
||||
"surface2": "#b8b49e",
|
||||
"overlay": "#8a8674",
|
||||
"text": "#2d2a2e",
|
||||
"subtext": "#5b5854",
|
||||
"accent": "#e14775",
|
||||
"accent2": "#7c5cbf",
|
||||
"accent3": "#1c7d9b",
|
||||
"success": "#5b8c2a",
|
||||
"success2": "#1c7d9b",
|
||||
"warning": "#b08900",
|
||||
"error": "#e14775",
|
||||
"onAccent": "#faf8f0",
|
||||
"node": "#f3f0e4",
|
||||
"lockedBg": "#e8e4d4",
|
||||
"lockedBorder": "#d6d2c0",
|
||||
"rail": "#e6e3d4",
|
||||
"finishDark": "#e6e3d4",
|
||||
"finishLight": "#d6d2c0",
|
||||
"finishStroke": "#8a8674"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "one-dark",
|
||||
"name": "One Dark",
|
||||
"dark": {
|
||||
"crust": "#21252b",
|
||||
"base": "#282c34",
|
||||
"mantle": "#21252b",
|
||||
"surface0": "#3b4048",
|
||||
"surface1": "#4b5263",
|
||||
"surface2": "#5c6370",
|
||||
"overlay": "#5c6370",
|
||||
"text": "#abb2bf",
|
||||
"subtext": "#7f848e",
|
||||
"accent": "#c678dd",
|
||||
"accent2": "#61afef",
|
||||
"accent3": "#56b6c2",
|
||||
"success": "#98c379",
|
||||
"success2": "#56b6c2",
|
||||
"warning": "#e5c07b",
|
||||
"error": "#e06c75",
|
||||
"onAccent": "#282c34",
|
||||
"node": "#3b4048",
|
||||
"lockedBg": "#2c313a",
|
||||
"lockedBorder": "#4b5263",
|
||||
"rail": "#21252b",
|
||||
"finishDark": "#21252b",
|
||||
"finishLight": "#3b4048",
|
||||
"finishStroke": "#4b5263"
|
||||
},
|
||||
"light": {
|
||||
"crust": "#e5e5e6",
|
||||
"base": "#fafafa",
|
||||
"mantle": "#f0f0f0",
|
||||
"surface0": "#e5e5e6",
|
||||
"surface1": "#d0d0d2",
|
||||
"surface2": "#a0a1a7",
|
||||
"overlay": "#a0a1a7",
|
||||
"text": "#383a42",
|
||||
"subtext": "#696c77",
|
||||
"accent": "#a626a4",
|
||||
"accent2": "#4078f2",
|
||||
"accent3": "#0184bc",
|
||||
"success": "#50a14f",
|
||||
"success2": "#0184bc",
|
||||
"warning": "#c18401",
|
||||
"error": "#e45649",
|
||||
"onAccent": "#fafafa",
|
||||
"node": "#f0f0f0",
|
||||
"lockedBg": "#e5e5e6",
|
||||
"lockedBorder": "#d0d0d2",
|
||||
"rail": "#e5e5e6",
|
||||
"finishDark": "#e5e5e6",
|
||||
"finishLight": "#d0d0d2",
|
||||
"finishStroke": "#a0a1a7"
|
||||
}
|
||||
}
|
||||
]
|
||||
14
package.json
Normal file
14
package.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "maturita",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Educational maturita app: GTK 4 desktop client, Next.js web client, shared lesson content.",
|
||||
"scripts": {
|
||||
"content": "node tools/build-content.mjs",
|
||||
"web": "npm --prefix web run dev",
|
||||
"web:build": "npm --prefix web run build",
|
||||
"web:install": "npm --prefix web install",
|
||||
"desktop": "make -C desktop run",
|
||||
"desktop:build": "make -C desktop"
|
||||
}
|
||||
}
|
||||
239
tools/build-content.mjs
Normal file
239
tools/build-content.mjs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Regenerate `content/` from the desktop C sources.
|
||||
*
|
||||
* The C app remains the authoring surface for now, so this is a one-way
|
||||
* export: run it after changing lesson data in `desktop/` to refresh the JSON
|
||||
* the web app reads. It is deliberately strict — a shape it does not recognise
|
||||
* throws rather than emitting a half-empty lesson.
|
||||
*/
|
||||
|
||||
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { parseArray } from './lib/cparse.mjs';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { extractThemes } from './extract/themes.mjs';
|
||||
import { extractI18n } from './extract/i18n.mjs';
|
||||
import { extractNetworks } from './extract/networks.mjs';
|
||||
import { extractDeutschUnit1 } from './extract/deutsch.mjs';
|
||||
import { extractDeutschUnit2 } from './extract/deutsch-u2.mjs';
|
||||
import { extractDeutschUnit3 } from './extract/deutsch-u3.mjs';
|
||||
import { extractHardware } from './extract/hardware.mjs';
|
||||
import { extractMluvnice } from './extract/mluvnice.mjs';
|
||||
import { extractCetba } from './extract/cetba.mjs';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const DESKTOP = resolve(ROOT, 'desktop');
|
||||
const CONTENT = resolve(ROOT, 'content');
|
||||
|
||||
const write = (relPath, data) => {
|
||||
const file = resolve(CONTENT, relPath);
|
||||
mkdirSync(dirname(file), { recursive: true });
|
||||
writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
|
||||
return relPath;
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------- subjects --- */
|
||||
|
||||
/** Mirrors `sub_keys` in subjects.c; only four subjects have content today. */
|
||||
const SUBJECTS = [
|
||||
{ id: 'deutsch', icon: 'flag-de', route: '/deutsch' },
|
||||
{ id: 'networks', icon: 'wifi', route: '/networks' },
|
||||
{ id: 'hardware', icon: 'chip', route: '/hardware' },
|
||||
{ id: 'czech', icon: 'book', route: '/czech' },
|
||||
{ id: 'civics', icon: null, route: null },
|
||||
{ id: 'english', icon: null, route: null },
|
||||
{ id: 'math', icon: null, route: null },
|
||||
{ id: 'physics', icon: null, route: null },
|
||||
{ id: 'science', icon: null, route: null },
|
||||
{ id: 'tech-graphics', icon: null, route: null },
|
||||
{ id: 'presentation', icon: null, route: null },
|
||||
{ id: 'programming', icon: null, route: null },
|
||||
{ id: 'software', icon: null, route: null },
|
||||
];
|
||||
|
||||
function extractSubjects() {
|
||||
const keys = parseArray(readFileSync(`${DESKTOP}/subjects.c`, 'utf8'), 'sub_keys').map(String);
|
||||
if (keys.length !== SUBJECTS.length) {
|
||||
throw new Error(`subjects.c has ${keys.length} keys but the extractor knows ${SUBJECTS.length}`);
|
||||
}
|
||||
return SUBJECTS.map((s, i) => ({ ...s, key: keys[i], unlocked: s.route !== null }));
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- paths --- */
|
||||
|
||||
/** German unit titles from `unit_meta_init` in main.c. */
|
||||
const DEUTSCH_TITLES = [
|
||||
'Neue Freunde', 'Aus aller Welt', 'Bei uns zu Hause',
|
||||
'Schule und Freizeit', 'Guten Appetit!', 'Mein Tagesablauf',
|
||||
'Meine Freunde', 'Wir treffen uns in Salzburg',
|
||||
'Mein Haus ist meine Burg', 'Urlaub in Österreich',
|
||||
];
|
||||
|
||||
/**
|
||||
* `HW_UNITS` bubbles are drawn but only `HW_LESSONS` of them are playable, so
|
||||
* the trailing nodes stay locked placeholders as they do on the desktop map.
|
||||
*/
|
||||
const HW_UNITS = 10;
|
||||
|
||||
/** `cz_keys` in czech.c; "Literatura" has no content yet. */
|
||||
const CZECH_NODES = [
|
||||
{ number: 1, title: 'Literatura', route: null },
|
||||
{ number: 2, title: 'Mluvnice', route: '/czech/mluvnice' },
|
||||
{ number: 3, title: 'Maturitní četba', route: '/czech/reading' },
|
||||
];
|
||||
|
||||
function buildPaths({ networkLessons, deutschLessons, hardwareLessons, cetbaLessons }) {
|
||||
const done = new Set(deutschLessons.map((l) => l.number));
|
||||
|
||||
const deutsch = {
|
||||
subject: 'deutsch',
|
||||
units: DEUTSCH_TITLES.map((title, i) => ({
|
||||
number: i + 1,
|
||||
// Units 1-3 are playable in the desktop app; the rest are placeholders.
|
||||
lesson: done.has(i + 1) ? `unit-${String(i + 1).padStart(2, '0')}` : null,
|
||||
title: { cs: title, en: title },
|
||||
unlocked: i < 3 && done.has(i + 1),
|
||||
})),
|
||||
};
|
||||
|
||||
const networks = {
|
||||
subject: 'networks',
|
||||
year: 1,
|
||||
units: networkLessons.map((l) => ({
|
||||
number: l.number,
|
||||
lesson: l.id,
|
||||
titleKey: l.titleKey,
|
||||
unlocked: true,
|
||||
})),
|
||||
};
|
||||
|
||||
const byNumber = new Map(hardwareLessons.map((l) => [l.number, l]));
|
||||
const hardware = {
|
||||
subject: 'hardware',
|
||||
units: Array.from({ length: HW_UNITS }, (_, i) => {
|
||||
const lesson = byNumber.get(i + 1);
|
||||
return {
|
||||
number: i + 1,
|
||||
lesson: lesson?.id ?? null,
|
||||
titleKey: lesson?.titleKey ?? `hw_unit${i + 1}`,
|
||||
unlocked: Boolean(lesson),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const czech = {
|
||||
subject: 'czech',
|
||||
units: CZECH_NODES.map((node) => ({
|
||||
number: node.number,
|
||||
lesson: null,
|
||||
route: node.route,
|
||||
title: { cs: node.title, en: node.title },
|
||||
unlocked: node.route !== null,
|
||||
})),
|
||||
};
|
||||
|
||||
const reading = {
|
||||
subject: 'czech',
|
||||
branch: 'reading',
|
||||
units: cetbaLessons.map((l, i) => ({
|
||||
number: i + 1,
|
||||
lesson: l.id,
|
||||
title: l.title,
|
||||
unlocked: true,
|
||||
})),
|
||||
};
|
||||
|
||||
return [deutsch, networks, hardware, czech, reading];
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- main --- */
|
||||
|
||||
function main() {
|
||||
rmSync(resolve(CONTENT, 'lessons'), { recursive: true, force: true });
|
||||
|
||||
const written = [];
|
||||
|
||||
written.push(write('themes.json', extractThemes(DESKTOP)));
|
||||
|
||||
const i18n = extractI18n(DESKTOP);
|
||||
written.push(write('i18n/ui.json', i18n.ui));
|
||||
written.push(write('i18n/content.json', i18n.content));
|
||||
|
||||
const subjects = extractSubjects();
|
||||
written.push(write('subjects.json', subjects));
|
||||
|
||||
const networkLessons = extractNetworks(DESKTOP);
|
||||
for (const lesson of networkLessons) {
|
||||
written.push(write(`lessons/networks/${lesson.id}.json`, lesson));
|
||||
}
|
||||
|
||||
const deutschLessons = [
|
||||
extractDeutschUnit1(DESKTOP),
|
||||
extractDeutschUnit2(DESKTOP),
|
||||
extractDeutschUnit3(DESKTOP),
|
||||
];
|
||||
for (const lesson of deutschLessons) {
|
||||
written.push(write(`lessons/deutsch/${lesson.id}.json`, lesson));
|
||||
}
|
||||
|
||||
const hardwareLessons = extractHardware(DESKTOP);
|
||||
for (const lesson of hardwareLessons) {
|
||||
written.push(write(`lessons/hardware/${lesson.id}.json`, lesson));
|
||||
}
|
||||
|
||||
const czechLessons = [extractMluvnice(DESKTOP), ...extractCetba(DESKTOP)];
|
||||
for (const lesson of czechLessons) {
|
||||
written.push(write(`lessons/czech/${lesson.id}.json`, lesson));
|
||||
}
|
||||
|
||||
const cetbaLessons = czechLessons.filter((l) => l.id !== 'mluvnice');
|
||||
|
||||
const paths = buildPaths({
|
||||
networkLessons,
|
||||
deutschLessons,
|
||||
hardwareLessons,
|
||||
cetbaLessons,
|
||||
});
|
||||
written.push(write('paths.json', paths));
|
||||
|
||||
written.push(write('manifest.json', {
|
||||
subjects: subjects.map((s) => s.id),
|
||||
lessons: {
|
||||
deutsch: deutschLessons.map((l) => l.id),
|
||||
networks: networkLessons.map((l) => l.id),
|
||||
hardware: hardwareLessons.map((l) => l.id),
|
||||
czech: czechLessons.map((l) => l.id),
|
||||
},
|
||||
}));
|
||||
|
||||
const stats = {
|
||||
'network lessons': networkLessons.length,
|
||||
'network slides': networkLessons.reduce((a, l) => a + l.slides.length, 0),
|
||||
'network items': networkLessons.reduce(
|
||||
(a, l) => a + l.tasks.reduce((b, t) => b + t.items.length, 0), 0),
|
||||
'deutsch tasks': deutschLessons.reduce((a, l) => a + l.tasks.length, 0),
|
||||
'deutsch items': deutschLessons.reduce(
|
||||
(a, l) => a + l.tasks.reduce((b, t) => b + t.items.length, 0), 0),
|
||||
'vocab words': deutschLessons.reduce(
|
||||
(a, l) => a + (l.vocab ?? []).reduce((b, s) => b + s.items.length, 0), 0),
|
||||
'hardware lessons': hardwareLessons.length,
|
||||
'hardware slides': hardwareLessons.reduce((a, l) => a + l.slides.length, 0),
|
||||
'hardware items': hardwareLessons.reduce(
|
||||
(a, l) => a + l.tasks.reduce((b, t) => b + t.items.length, 0), 0),
|
||||
'czech lessons': czechLessons.length,
|
||||
'czech tasks': czechLessons.reduce((a, l) => a + l.tasks.length, 0),
|
||||
'czech items': czechLessons.reduce(
|
||||
(a, l) => a + l.tasks.reduce((b, t) => b + t.items.length, 0), 0),
|
||||
'ui strings': Object.keys(i18n.ui.cs).length,
|
||||
'content strings': Object.keys(i18n.content.cs).length,
|
||||
themes: 10,
|
||||
};
|
||||
|
||||
console.log(`wrote ${written.length} files to content/`);
|
||||
for (const [k, v] of Object.entries(stats)) console.log(` ${k}: ${v}`);
|
||||
}
|
||||
|
||||
main();
|
||||
114
tools/extract/cetba.mjs
Normal file
114
tools/extract/cetba.mjs
Normal 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] } : {}),
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
196
tools/extract/deutsch-helpers.mjs
Normal file
196
tools/extract/deutsch-helpers.mjs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import { parseArray, trimNulls } from '../lib/cparse.mjs';
|
||||
|
||||
/** Shared conversions for the three Deutsch units. */
|
||||
|
||||
export const opt = (v) => (v === null || v === undefined || v === '' ? undefined : String(v));
|
||||
|
||||
/** Split the pipe-separated accepted-answer lists used by TypedQ and friends. */
|
||||
export const alternatives = (s) =>
|
||||
String(s ?? '').split('|').map((a) => a.trim()).filter(Boolean);
|
||||
|
||||
/** `VerbQ`/`SeqQ` `{before, after, answer}` becomes a single-gap fill sentence. */
|
||||
export function verbFill(row, meaning, extra = {}) {
|
||||
const [before, after, answer] = row;
|
||||
return {
|
||||
kind: 'fill',
|
||||
segments: [before ?? '', after ?? ''],
|
||||
answers: [String(answer)],
|
||||
input: 'select',
|
||||
...(meaning ? { meaning: String(meaning) } : {}),
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `U3Fill` alternates literal segments and answers: s0, a0, s1, a1, s2, a2, s3.
|
||||
* Trailing NULLs mean the row uses fewer than three gaps.
|
||||
*/
|
||||
export function u3Fill(row, { gloss } = {}) {
|
||||
const [num, ...rest] = row;
|
||||
const mean = rest.pop();
|
||||
const parts = rest;
|
||||
|
||||
const segments = [];
|
||||
const answers = [];
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i % 2 === 0) segments.push(parts[i] ?? '');
|
||||
else if (parts[i] !== null && parts[i] !== undefined) answers.push(String(parts[i]));
|
||||
}
|
||||
|
||||
// Drop segments that only exist to pad unused gaps.
|
||||
while (segments.length > answers.length + 1) segments.pop();
|
||||
|
||||
return {
|
||||
kind: 'fill',
|
||||
segments,
|
||||
answers,
|
||||
input: 'select',
|
||||
...(opt(num) ? { label: String(num) } : {}),
|
||||
...(opt(mean) ? { meaning: String(mean) } : {}),
|
||||
...(gloss ? { glosses: [String(gloss)] } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** `U3Let`: s0, a0, s1, a1, s2, a2, s3, a3, s4 — gaps hold missing letters. */
|
||||
export function u3Letters(row) {
|
||||
const [num, s0, a0, s1, a1, s2, a2, s3, a3, s4, mean] = row;
|
||||
const rawSegments = [s0, s1, s2, s3, s4];
|
||||
const rawAnswers = [a0, a1, a2, a3];
|
||||
|
||||
const answers = rawAnswers.filter((a) => a !== null && a !== undefined).map(String);
|
||||
const segments = rawSegments.slice(0, answers.length + 1).map((s) => s ?? '');
|
||||
|
||||
return {
|
||||
kind: 'fill',
|
||||
segments,
|
||||
answers,
|
||||
input: 'text',
|
||||
letters: true,
|
||||
...(opt(num) ? { label: String(num) } : {}),
|
||||
...(opt(mean) ? { meaning: String(mean) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `Ex4Row`: seg[5], ans[4] (missing letters), czech, word[4], gmean[4], gaps.
|
||||
* Each gap's gloss pairs the completed German word with its Czech meaning.
|
||||
*/
|
||||
export function ex4Letters(row) {
|
||||
const [seg, ans, czech, word, gmean, gaps] = row;
|
||||
const answers = ans.slice(0, gaps).map(String);
|
||||
const segments = seg.slice(0, gaps + 1).map((s) => s ?? '');
|
||||
|
||||
const glosses = answers.map((_, i) => {
|
||||
const w = opt(word?.[i]);
|
||||
const m = opt(gmean?.[i]);
|
||||
if (w && m) return `${w} — ${m}`;
|
||||
return w ?? m ?? null;
|
||||
});
|
||||
|
||||
return {
|
||||
kind: 'fill',
|
||||
segments,
|
||||
answers,
|
||||
input: 'text',
|
||||
letters: true,
|
||||
...(opt(czech) ? { meaning: String(czech) } : {}),
|
||||
glosses,
|
||||
};
|
||||
}
|
||||
|
||||
/** `Ex2Row`: num, seg[4], ans[3], gaps — grouped under an `Ex2Item`'s translation. */
|
||||
export function ex2Row(row, czech) {
|
||||
const [num, seg, ans, gaps] = row;
|
||||
return {
|
||||
kind: 'fill',
|
||||
segments: seg.slice(0, gaps + 1).map((s) => s ?? ''),
|
||||
answers: ans.slice(0, gaps).map(String),
|
||||
input: 'select',
|
||||
...(opt(num) ? { label: String(num) } : {}),
|
||||
...(opt(czech) ? { meaning: String(czech) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** `ChoiceQ`: prompt, options[5], n_options, correct. */
|
||||
export function choiceItem(row, meaning) {
|
||||
const [prompt, options, n, correct] = row;
|
||||
const list = trimNulls(options ?? []).map(String);
|
||||
const count = typeof n === 'number' ? n : list.length;
|
||||
|
||||
if (typeof correct !== 'number' || correct < 0 || correct >= count) {
|
||||
throw new Error(`choice '${prompt}': correct index ${correct} out of range`);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'choice',
|
||||
prompt: String(prompt),
|
||||
options: list.slice(0, count),
|
||||
correct,
|
||||
...(opt(meaning) ? { meaning: String(meaning) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** `TypedQ`: prompt, pipe-separated answers, Czech meaning. */
|
||||
export function typedItem(row) {
|
||||
const [prompt, answers, meaning] = row;
|
||||
return {
|
||||
kind: 'typed',
|
||||
prompt: String(prompt),
|
||||
answers: alternatives(answers),
|
||||
...(opt(meaning) ? { meaning: String(meaning) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** `FreeQ`: question, Czech question, sample, Czech sample. */
|
||||
export function freeItem(row) {
|
||||
const [question, questionCs, sample, sampleCs] = row;
|
||||
return {
|
||||
kind: 'free',
|
||||
question: String(question),
|
||||
...(opt(questionCs) ? { questionCs: String(questionCs) } : {}),
|
||||
sample: String(sample),
|
||||
...(opt(sampleCs) ? { sampleCs: String(sampleCs) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** `AssemblyItem`: prompt, words[6], n. */
|
||||
export function assemblyItem(row, meaning) {
|
||||
const [prompt, words] = row;
|
||||
return {
|
||||
kind: 'assembly',
|
||||
prompt: opt(prompt) ?? '',
|
||||
words: trimNulls(words).map(String),
|
||||
...(opt(meaning) ? { meaning: String(meaning) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** One `assign` exercise built from `AssignItem[]`, group labels and meanings. */
|
||||
export function assignExercise(src, itemsName, groupsName, meaningName) {
|
||||
const meanings = meaningName ? parseArray(src, meaningName) : [];
|
||||
return {
|
||||
kind: 'assign',
|
||||
groups: parseArray(src, groupsName).map(String),
|
||||
items: parseArray(src, itemsName).map(([emoji, label, group], i) => ({
|
||||
emoji: emoji ?? null,
|
||||
label: String(label),
|
||||
group,
|
||||
...(opt(meanings[i]) ? { meaning: String(meanings[i]) } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/** Vocabulary trainer sections: `TransSection[]` referencing `TypedQ[]` arrays. */
|
||||
export function vocabSections(src, sectionsName) {
|
||||
return parseArray(src, sectionsName).map(([header, arrayRef]) => {
|
||||
const name = arrayRef?.__c;
|
||||
if (!name) throw new Error(`vocab section '${header}' has no data array`);
|
||||
return {
|
||||
header: String(header),
|
||||
items: parseArray(src, name).map(([prompt, answers, meaning]) => ({
|
||||
prompt: String(prompt),
|
||||
answers: alternatives(answers),
|
||||
...(opt(meaning) ? { meaning: String(meaning) } : {}),
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
159
tools/extract/deutsch-u2.mjs
Normal file
159
tools/extract/deutsch-u2.mjs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { parseArray, parseString } from '../lib/cparse.mjs';
|
||||
import {
|
||||
choiceItem,
|
||||
freeItem,
|
||||
opt,
|
||||
typedItem,
|
||||
verbFill,
|
||||
vocabSections,
|
||||
} from './deutsch-helpers.mjs';
|
||||
|
||||
/**
|
||||
* Deutsch Unit 2 ("Familie und Freunde").
|
||||
*
|
||||
* Task order, titles and instruction keys come from the `u2exN` builders at the
|
||||
* bottom of exercise_u2.c; each one names the `const` arrays holding its data.
|
||||
*/
|
||||
export function extractDeutschUnit2(desktopDir) {
|
||||
const src = readFileSync(`${desktopDir}/exercise_u2.c`, 'utf8');
|
||||
const tasks = [];
|
||||
|
||||
/** `build_verb_ex`: one pool feeding single-gap sentences. */
|
||||
const verbTask = (number, title, subtitleKey, prefix, { gloss = false } = {}) => {
|
||||
const meanings = parseArray(src, `${prefix}_mean`);
|
||||
const glosses = gloss ? parseArray(src, `${prefix}_gloss`) : null;
|
||||
return {
|
||||
number,
|
||||
title,
|
||||
subtitleKey,
|
||||
pool: parseArray(src, `${prefix}_pool`).map(String),
|
||||
items: parseArray(src, `${prefix}_rows`).map((row, i) =>
|
||||
verbFill(row, meanings[i], glosses && opt(glosses[i])
|
||||
? { glosses: [String(glosses[i])] }
|
||||
: {})),
|
||||
};
|
||||
};
|
||||
|
||||
const choiceTask = (number, title, subtitleKey, prefix) => {
|
||||
const meanings = parseArray(src, `${prefix}_mean`);
|
||||
return {
|
||||
number,
|
||||
title,
|
||||
subtitleKey,
|
||||
items: parseArray(src, `${prefix}_rows`).map((row, i) => choiceItem(row, meanings[i])),
|
||||
};
|
||||
};
|
||||
|
||||
const typedTask = (number, title, subtitleKey, rowsName, bankName) => ({
|
||||
number,
|
||||
title,
|
||||
subtitleKey,
|
||||
...(bankName ? { note: parseString(src, bankName) } : {}),
|
||||
items: parseArray(src, rowsName).map(typedItem),
|
||||
});
|
||||
|
||||
/* 1 — Verben konjugieren: two people, each a headed group over one pool. */
|
||||
{
|
||||
const pool = parseArray(src, 'g01_pool').map(String);
|
||||
const groups = [
|
||||
['Peter Fritsch:', 'g01_peter', 'g01_peter_mean'],
|
||||
['Jana Nová und Pavol Korčák:', 'g01_jana', 'g01_jana_mean'],
|
||||
];
|
||||
|
||||
const items = groups.flatMap(([heading, rowsName, meanName]) => {
|
||||
const meanings = parseArray(src, meanName);
|
||||
return parseArray(src, rowsName).map((row, i) =>
|
||||
verbFill(row, meanings[i], i === 0 ? { heading } : {}));
|
||||
});
|
||||
|
||||
tasks.push({
|
||||
number: 1,
|
||||
title: 'Verben konjugieren',
|
||||
subtitleKey: 'sub_verben',
|
||||
pool,
|
||||
items,
|
||||
});
|
||||
}
|
||||
|
||||
tasks.push(choiceTask(2, 'aus oder in', 'sub_ausin', 'g02'));
|
||||
tasks.push(choiceTask(3, 'Fragewörter', 'sub_wer', 'g03'));
|
||||
tasks.push(verbTask(4, 'sprechen', 'sub_sprich', 'g04'));
|
||||
tasks.push(typedTask(5, 'Nationalitäten', 'sub_nation', 'g05_rows', 'g05_bank'));
|
||||
tasks.push(typedTask(6, 'Woher?', 'sub_woher', 'g06_rows'));
|
||||
tasks.push(typedTask(7, 'Länder schreiben', 'sub_laender', 'g07_rows'));
|
||||
tasks.push(verbTask(8, 'Verben einsetzen', 'sub_verb2', 'g08'));
|
||||
|
||||
/* 9 — Freie Antwort */
|
||||
tasks.push({
|
||||
number: 9,
|
||||
title: 'Freie Antwort',
|
||||
subtitleKey: 'sub_free',
|
||||
items: parseArray(src, 'ex9_free_qs').map(freeItem),
|
||||
});
|
||||
|
||||
tasks.push(verbTask(10, 'Euro', 'sub_euro', 'g10'));
|
||||
tasks.push(typedTask(11, 'Wörter suchen', 'sub_wortsuchen', 'g11_rows'));
|
||||
tasks.push(choiceTask(12, 'Was ist richtig?', 'sub_verb2', 'g12'));
|
||||
|
||||
/* 13 — Ordne zu: a Fragewort dropdown and an answer dropdown per row. */
|
||||
{
|
||||
const meanings = parseArray(src, 'g13_mean');
|
||||
tasks.push({
|
||||
number: 13,
|
||||
title: 'Ordne zu',
|
||||
subtitleKey: 'sub_ordne',
|
||||
pool: parseArray(src, 'g13_fw_pool').map(String),
|
||||
altPool: parseArray(src, 'g13_ans_pool').map(String),
|
||||
items: parseArray(src, 'g13_rows').map(([mid, fw, ans], i) => ({
|
||||
kind: 'fill',
|
||||
segments: ['', ` ${mid} → `, ''],
|
||||
answers: [String(fw), String(ans)],
|
||||
input: 'select',
|
||||
gapPool: ['main', 'alt'],
|
||||
label: `${i + 1}.`,
|
||||
...(opt(meanings[i]) ? { meaning: String(meanings[i]) } : {}),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
tasks.push(verbTask(14, 'Lückentext', 'sub_luecke', 'g14', { gloss: true }));
|
||||
tasks.push(verbTask(15, 'Verbinde', 'sub_verbinde', 'g15', { gloss: true }));
|
||||
tasks.push(verbTask(16, 'Zahlen', 'sub_zahlpaar', 'g16'));
|
||||
|
||||
/* 17 — Steckbrief */
|
||||
tasks.push({
|
||||
number: 17,
|
||||
title: 'Steckbrief',
|
||||
subtitleKey: 'sub_steckbrief',
|
||||
items: parseArray(src, 's01_rows').map(([stem, sample]) => ({
|
||||
kind: 'profile',
|
||||
stem: String(stem),
|
||||
sample: String(sample),
|
||||
})),
|
||||
});
|
||||
|
||||
/* 18 — Berufe: hangman, one round per profession. */
|
||||
{
|
||||
const words = parseArray(src, 'hm_words').map(String);
|
||||
const tips = parseArray(src, 'hm_tips').map(String);
|
||||
tasks.push({
|
||||
number: 18,
|
||||
title: 'Berufe',
|
||||
subtitleKey: 'sub_berufe',
|
||||
items: words.map((word, i) => ({ kind: 'hangman', word, hint: tips[i] })),
|
||||
});
|
||||
}
|
||||
|
||||
tasks.push(typedTask(19, 'Nationalität', 'sub_bistdu', 's03_rows'));
|
||||
|
||||
return {
|
||||
id: 'unit-02',
|
||||
subject: 'deutsch',
|
||||
number: 2,
|
||||
title: { cs: 'Aus aller Welt', en: 'Aus aller Welt' },
|
||||
slides: [],
|
||||
tasks,
|
||||
vocab: vocabSections(src, 'u2_trans_sections'),
|
||||
};
|
||||
}
|
||||
174
tools/extract/deutsch-u3.mjs
Normal file
174
tools/extract/deutsch-u3.mjs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { parseArray, parseString } from '../lib/cparse.mjs';
|
||||
import {
|
||||
alternatives,
|
||||
assemblyItem,
|
||||
assignExercise,
|
||||
ex2Row,
|
||||
ex4Letters,
|
||||
opt,
|
||||
typedItem,
|
||||
u3Fill,
|
||||
u3Letters,
|
||||
vocabSections,
|
||||
} from './deutsch-helpers.mjs';
|
||||
|
||||
/**
|
||||
* Deutsch Unit 3 ("Bei uns zu Hause").
|
||||
*
|
||||
* Most tasks go through `u3_build_drop` (dropdown gaps over a shared pool);
|
||||
* the rest are hand-built pages whose data arrays are read here directly.
|
||||
*/
|
||||
export function extractDeutschUnit3(desktopDir) {
|
||||
const src = readFileSync(`${desktopDir}/exercise_u3.c`, 'utf8');
|
||||
const tasks = [];
|
||||
|
||||
/** `u3_build_drop`: `U3Fill` rows sharing one dropdown pool. */
|
||||
const dropTask = (number, title, subtitleKey, prefix, { example, gloss } = {}) => {
|
||||
const glosses = gloss ? parseArray(src, `${prefix}_gloss`) : null;
|
||||
return {
|
||||
number,
|
||||
title,
|
||||
subtitleKey,
|
||||
...(example ? { example } : {}),
|
||||
pool: parseArray(src, `${prefix}_pool`).map(String),
|
||||
items: parseArray(src, `${prefix}_rows`).map((row, i) =>
|
||||
u3Fill(row, { gloss: glosses ? opt(glosses[i]) : undefined })),
|
||||
};
|
||||
};
|
||||
|
||||
tasks.push(dropTask(1, 'Familienpaare', 'sub3_pair', 'ex1_u3', {
|
||||
example: 'Beispiel: der Vater ↔ die Mutter',
|
||||
}));
|
||||
|
||||
/* 2 — mein oder dein: rows grouped per item, each item has one translation. */
|
||||
tasks.push({
|
||||
number: 2,
|
||||
title: 'mein oder dein',
|
||||
subtitleKey: 'sub3_poss',
|
||||
example:
|
||||
'Beispiel: Ist das dein Vater? – Nein, das ist nicht mein Vater. ' +
|
||||
'Das ist sein Onkel.',
|
||||
pool: parseArray(src, 'ex2_u3_pool').map(String),
|
||||
items: parseArray(src, 'ex2_u3_items').flatMap(([czech, rowsRef]) => {
|
||||
const name = rowsRef?.__c;
|
||||
if (!name) throw new Error(`ex2_u3_items row group for '${czech}' has no data array`);
|
||||
return parseArray(src, name).map((row) => ex2Row(row, czech));
|
||||
}),
|
||||
});
|
||||
|
||||
tasks.push(dropTask(3, 'ein / kein', 'sub3_haustier', 'ex3_u3', {
|
||||
example:
|
||||
'Beispiel: Hast du eine Katze? – Nein, ich habe keine Katze. ' +
|
||||
'Ich habe einen Hund.',
|
||||
}));
|
||||
|
||||
/* 4 — Lückentext: missing letters inside words. */
|
||||
tasks.push({
|
||||
number: 4,
|
||||
title: 'Lückentext',
|
||||
subtitleKey: 'sub3_buchst',
|
||||
items: parseArray(src, 'ex4_u3_rows').map(ex4Letters),
|
||||
});
|
||||
|
||||
/* 5 — Marcos Familie */
|
||||
tasks.push({
|
||||
number: 5,
|
||||
title: 'Marcos Familie',
|
||||
subtitleKey: 'sub3_marco',
|
||||
note: parseString(src, 'ex5_u3_bank'),
|
||||
items: parseArray(src, 'ex5_u3_rows').map(typedItem),
|
||||
});
|
||||
|
||||
/* 6 — Sortieren */
|
||||
tasks.push({
|
||||
number: 6,
|
||||
title: 'Sortieren',
|
||||
subtitleKey: 'sub3_sort',
|
||||
items: [assignExercise(src, 'ex6_u3_items', 'ex6_u3_groups', 'ex6_u3_meaning')],
|
||||
});
|
||||
|
||||
/* 7 — Possessivtabelle: nouns down the side, persons across the top. */
|
||||
{
|
||||
const persons = ['ich', 'du', 'er', 'sie', 'es', 'wir', 'ihr', 'sie / Sie'];
|
||||
const nouns = ['der Garten', 'die Idee', 'das Fest', 'die Geschwister'];
|
||||
const forms = [
|
||||
['mein', 'dein', 'sein', 'ihr', 'sein', 'unser', 'euer', 'ihr'],
|
||||
['meine', 'deine', 'seine', 'ihre', 'seine', 'unsere', 'eure', 'ihre'],
|
||||
['mein', 'dein', 'sein', 'ihr', 'sein', 'unser', 'euer', 'ihr'],
|
||||
['meine', 'deine', 'seine', 'ihre', 'seine', 'unsere', 'eure', 'ihre'],
|
||||
];
|
||||
|
||||
tasks.push({
|
||||
number: 7,
|
||||
title: 'Possessivtabelle',
|
||||
subtitleKey: 'sub3_tabelle',
|
||||
items: [{
|
||||
kind: 'table',
|
||||
columns: persons,
|
||||
rows: nouns,
|
||||
answers: forms.map((row) => row.map((value) => ({ value }))),
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
tasks.push(dropTask(8, 'Akkusativ', 'sub3_akk', 'ex8_u3', {
|
||||
example: 'Beispiel: die Inliner (ich) → Ich brauche meine Inliner.',
|
||||
gloss: true,
|
||||
}));
|
||||
tasks.push(dropTask(9, 'kein / nicht', 'sub3_nicht', 'ex9_u3'));
|
||||
|
||||
/* 10 — Sätze bauen */
|
||||
{
|
||||
const meanings = parseArray(src, 'ex10_u3_meaning');
|
||||
tasks.push({
|
||||
number: 10,
|
||||
title: 'Sätze bauen',
|
||||
subtitleKey: 'sub3_satz',
|
||||
items: parseArray(src, 'ex10_u3_items').map((row, i) => assemblyItem(row, meanings[i])),
|
||||
});
|
||||
}
|
||||
|
||||
/* 11 + 14 — `u3_build_kw`: free sentence checked for the required keywords. */
|
||||
const keywordTask = (number, title, subtitleKey, qsName, example) => ({
|
||||
number,
|
||||
title,
|
||||
subtitleKey,
|
||||
example,
|
||||
items: parseArray(src, qsName).map(([prompt, answers, mean, german]) => ({
|
||||
kind: 'typed',
|
||||
prompt: String(prompt),
|
||||
answers: alternatives(answers),
|
||||
substring: true,
|
||||
...(opt(mean) ? { meaning: String(mean) } : {}),
|
||||
...(opt(german) ? { model: String(german) } : {}),
|
||||
})),
|
||||
});
|
||||
|
||||
tasks.push(keywordTask(11, 'Was siehst du?', 'sub3_sehen', 'ex11_u3_qs',
|
||||
'Beispiel: Ich sehe eine Burg. / Ich sehe ein Schloss.'));
|
||||
|
||||
tasks.push(dropTask(12, 'Wem gehört das?', 'sub3_wem', 'ex12_u3'));
|
||||
tasks.push(dropTask(13, 'Es gibt …', 'sub3_gibt', 'ex13_u3'));
|
||||
|
||||
tasks.push(keywordTask(14, 'Beschreiben', 'sub3_saetze', 'ex14_u3_qs',
|
||||
'Beispiel: Ich sehe einen Jungen. Er liest ein Buch.'));
|
||||
|
||||
/* 15 — Wochenende: missing letters again, but with no per-gap glosses. */
|
||||
tasks.push({
|
||||
number: 15,
|
||||
title: 'Wochenende',
|
||||
subtitleKey: 'sub3_wochen',
|
||||
items: parseArray(src, 'ex15_u3_rows').map(u3Letters),
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'unit-03',
|
||||
subject: 'deutsch',
|
||||
number: 3,
|
||||
title: { cs: 'Bei uns zu Hause', en: 'Bei uns zu Hause' },
|
||||
slides: [],
|
||||
tasks,
|
||||
vocab: vocabSections(src, 'u3_trans_sections'),
|
||||
};
|
||||
}
|
||||
249
tools/extract/deutsch.mjs
Normal file
249
tools/extract/deutsch.mjs
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { parseArray, trimNulls } from '../lib/cparse.mjs';
|
||||
|
||||
/**
|
||||
* Deutsch Unit 1 ("Neue Freunde").
|
||||
*
|
||||
* The desktop app defines each exercise's data as a `const` array next to its
|
||||
* `build_*` function; the task order and titles come from
|
||||
* `build_exercise_page` in main.c and `u1_ex_names` in globals.c.
|
||||
*/
|
||||
|
||||
const opt = (v) => (v === null || v === undefined || v === '' ? undefined : String(v));
|
||||
|
||||
/** Split the pipe-separated accepted-answer lists used by TypedQ. */
|
||||
const alternatives = (s) => String(s).split('|').map((a) => a.trim()).filter(Boolean);
|
||||
|
||||
/** `{before, after, answer}` becomes a single-gap fill sentence. */
|
||||
function verbFill(row, meaning, extra = {}) {
|
||||
const [before, after, answer] = row;
|
||||
return {
|
||||
kind: 'fill',
|
||||
segments: [before ?? '', after ?? ''],
|
||||
answers: [String(answer)],
|
||||
input: 'select',
|
||||
...(meaning ? { meaning: String(meaning) } : {}),
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
export function extractDeutschUnit1(desktopDir) {
|
||||
const u1 = readFileSync(`${desktopDir}/exercise_u1.c`, 'utf8');
|
||||
const common = readFileSync(`${desktopDir}/exercise_common.c`, 'utf8');
|
||||
|
||||
const tasks = [];
|
||||
|
||||
/* 1 — Dialog: three dialogues sharing one word pool, one gap per line. */
|
||||
{
|
||||
const meanings = parseArray(common, 'ex1_meaning');
|
||||
const pool = parseArray(common, 'ex1_pool');
|
||||
const dialogues = [
|
||||
['Sich vorstellen', parseArray(common, 'ex1_d1')],
|
||||
['Begrüßung', parseArray(common, 'ex1_d2')],
|
||||
['Verabschiedung', parseArray(common, 'ex1_d3')],
|
||||
];
|
||||
|
||||
let i = 0;
|
||||
const items = dialogues.map(([title, rows]) => ({
|
||||
kind: 'dialogue',
|
||||
title,
|
||||
rows: rows.map(([speaker, before, answer, after]) => ({
|
||||
speaker: speaker ?? '',
|
||||
before: before ?? '',
|
||||
answer: String(answer),
|
||||
after: after ?? '',
|
||||
meaning: opt(meanings[i++]),
|
||||
})),
|
||||
}));
|
||||
|
||||
tasks.push({ number: 1, title: 'Dialog', subtitleKey: 'sub_dialog', pool, items });
|
||||
}
|
||||
|
||||
/* 2 — Sätze bilden */
|
||||
{
|
||||
const meanings = parseArray(common, 'ex2_meaning');
|
||||
tasks.push({
|
||||
number: 2,
|
||||
title: 'Sätze bilden',
|
||||
subtitleKey: 'sub_assembly',
|
||||
items: parseArray(common, 'ex2_items').map(([prompt, words], i) => ({
|
||||
kind: 'assembly',
|
||||
prompt: opt(prompt) ?? '',
|
||||
words: trimNulls(words).map(String),
|
||||
meaning: opt(meanings[i]),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
/* 3 + 9 — multiple choice */
|
||||
const choiceTask = (number, title, subtitleKey, qsName, meanName) => {
|
||||
const meanings = parseArray(u1, meanName);
|
||||
return {
|
||||
number,
|
||||
title,
|
||||
subtitleKey,
|
||||
items: parseArray(u1, qsName).map(([prompt, options, n, correct], i) => ({
|
||||
kind: 'choice',
|
||||
prompt: String(prompt),
|
||||
options: trimNulls(options).map(String).slice(0, n),
|
||||
correct,
|
||||
meaning: opt(meanings[i]),
|
||||
})),
|
||||
};
|
||||
};
|
||||
tasks.push(choiceTask(3, 'Was ist richtig?', 'sub_choice_num', 'ex3_questions', 'ex3_meaning'));
|
||||
|
||||
/* 4 — Freie Antwort */
|
||||
tasks.push({
|
||||
number: 4,
|
||||
title: 'Freie Antwort',
|
||||
subtitleKey: 'sub_free',
|
||||
items: parseArray(u1, 'ex4_questions').map(([question, questionCs, sample, sampleCs]) => ({
|
||||
kind: 'free',
|
||||
question: String(question),
|
||||
questionCs: opt(questionCs),
|
||||
sample: String(sample),
|
||||
sampleCs: opt(sampleCs),
|
||||
})),
|
||||
});
|
||||
|
||||
/* 5 — Zahlen: digits shown as the clue, pick the German word. */
|
||||
{
|
||||
const meanings = parseArray(u1, 'ex5_meaning');
|
||||
tasks.push({
|
||||
number: 5,
|
||||
title: 'Zahlen',
|
||||
subtitleKey: 'sub_zahlen',
|
||||
pool: parseArray(u1, 'ex5_pool'),
|
||||
items: parseArray(u1, 'ex5_data').map(([digits, answer], i) => ({
|
||||
kind: 'fill',
|
||||
segments: ['', ''],
|
||||
answers: [String(answer)],
|
||||
input: 'select',
|
||||
clue: String(digits),
|
||||
meaning: opt(meanings[i]),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
/* 6 — Wie viel?: the emoji is repeated `count` times as the clue. */
|
||||
{
|
||||
const meanings = parseArray(u1, 'ex6_meaning');
|
||||
tasks.push({
|
||||
number: 6,
|
||||
title: 'Wie viel?',
|
||||
subtitleKey: 'sub_wieviel',
|
||||
pool: parseArray(u1, 'ex6_pool'),
|
||||
items: parseArray(u1, 'ex6_data').map(([emoji, count, noun, answer], i) => ({
|
||||
kind: 'fill',
|
||||
segments: ['', ` ${noun}`],
|
||||
answers: [String(answer)],
|
||||
input: 'select',
|
||||
emoji: Array.from({ length: count }, () => emoji).join(' '),
|
||||
meaning: opt(meanings[i]),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
/* 7 — Zahlenreihe */
|
||||
{
|
||||
const meanings = parseArray(u1, 'ex7_meaning');
|
||||
tasks.push({
|
||||
number: 7,
|
||||
title: 'Zahlenreihe',
|
||||
subtitleKey: 'sub_reihe',
|
||||
pool: parseArray(u1, 'ex7_pool'),
|
||||
items: parseArray(u1, 'ex7_data').map((row, i) => verbFill(row, meanings[i])),
|
||||
});
|
||||
}
|
||||
|
||||
/* 8 — Verb einsetzen */
|
||||
{
|
||||
const meanings = parseArray(u1, 'ex8_meaning');
|
||||
tasks.push({
|
||||
number: 8,
|
||||
title: 'Verb einsetzen',
|
||||
subtitleKey: 'sub_verb',
|
||||
pool: parseArray(u1, 'ex8_pool'),
|
||||
items: parseArray(u1, 'ex8_data').map((row, i) => verbFill(row, meanings[i])),
|
||||
});
|
||||
}
|
||||
|
||||
tasks.push(choiceTask(9, 'Wer? Wie? Wo?', 'sub_wer', 'ex9_questions', 'ex9_meaning'));
|
||||
|
||||
/* 10 — Wörter trennen: split a run-together string into words. */
|
||||
{
|
||||
const meanings = parseArray(u1, 'ex10_meaning');
|
||||
tasks.push({
|
||||
number: 10,
|
||||
title: 'Wörter trennen',
|
||||
subtitleKey: 'sub_assembly',
|
||||
items: parseArray(u1, 'ex10_items').map(([prompt, words], i) => ({
|
||||
kind: 'assembly',
|
||||
prompt: opt(prompt) ?? '',
|
||||
words: trimNulls(words).map(String),
|
||||
meaning: opt(meanings[i]),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
/* 11 + 13 — sort items into groups */
|
||||
const assignTask = (number, title, subtitleKey, itemsName, groupsName, meanName) => {
|
||||
const meanings = parseArray(u1, meanName);
|
||||
return {
|
||||
number,
|
||||
title,
|
||||
subtitleKey,
|
||||
items: [{
|
||||
kind: 'assign',
|
||||
groups: parseArray(u1, groupsName).map(String),
|
||||
items: parseArray(u1, itemsName).map(([emoji, label, group], i) => ({
|
||||
emoji: emoji ?? null,
|
||||
label: String(label),
|
||||
group,
|
||||
meaning: opt(meanings[i]),
|
||||
})),
|
||||
}],
|
||||
};
|
||||
};
|
||||
tasks.push(assignTask(11, 'Grußformen', 'sub_gruss', 'ex11_items', 'ex11_groups', 'ex11_meaning'));
|
||||
|
||||
/* 12 — Was macht er/sie? */
|
||||
{
|
||||
const meanings = parseArray(u1, 'ex12_meaning');
|
||||
tasks.push({
|
||||
number: 12,
|
||||
title: 'Was macht er/sie?',
|
||||
subtitleKey: 'sub_bild',
|
||||
pool: parseArray(u1, 'ex12_pool'),
|
||||
items: parseArray(u1, 'ex12_data').map(([emoji, before, after, answer], i) =>
|
||||
verbFill([before, after, answer], meanings[i], { emoji: String(emoji) })),
|
||||
});
|
||||
}
|
||||
|
||||
tasks.push(assignTask(13, 'Länder', 'sub_land', 'ex13_items', 'ex13_groups', 'ex13_meaning'));
|
||||
|
||||
/* Vocabulary trainer branch: one section per textbook page. */
|
||||
const vocab = parseArray(u1, 'u1_trans_sections').map(([header, arrayRef]) => {
|
||||
const name = arrayRef?.__c;
|
||||
if (!name) throw new Error(`vocab section '${header}' has no data array`);
|
||||
return {
|
||||
header: String(header),
|
||||
items: parseArray(u1, name).map(([prompt, answers, meaning]) => ({
|
||||
prompt: String(prompt),
|
||||
answers: alternatives(answers),
|
||||
meaning: opt(meaning),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'unit-01',
|
||||
subject: 'deutsch',
|
||||
number: 1,
|
||||
title: { cs: 'Neue Freunde', en: 'Neue Freunde' },
|
||||
slides: [],
|
||||
tasks,
|
||||
vocab,
|
||||
};
|
||||
}
|
||||
73
tools/extract/hardware.mjs
Normal file
73
tools/extract/hardware.mjs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { parseArrayInFunction, trimNulls } from '../lib/cparse.mjs';
|
||||
|
||||
/**
|
||||
* Hardware ("Technické vybavení").
|
||||
*
|
||||
* Structurally identical to the network lessons: each `build_hw_unitN_page`
|
||||
* holds a local `NetSlide slides[]` and each `build_hw_unitN_exercise_page`
|
||||
* holds a local `ChoiceQ qs[]` with a parallel `hints[]`.
|
||||
*/
|
||||
|
||||
const HW_LESSONS = 3;
|
||||
|
||||
function toSlide(row, label) {
|
||||
if (!Array.isArray(row)) throw new Error(`${label}: slide is not a braced list`);
|
||||
const [kicker, title, tip, lines] = row;
|
||||
return {
|
||||
kicker: kicker ?? '',
|
||||
title: title ?? '',
|
||||
tip: tip ?? null,
|
||||
lines: trimNulls(lines ?? []).map(String),
|
||||
};
|
||||
}
|
||||
|
||||
function toChoice(row, hint, label) {
|
||||
if (!Array.isArray(row)) throw new Error(`${label}: question is not a braced list`);
|
||||
const [prompt, options, nOptions, correct] = row;
|
||||
const list = trimNulls(options ?? []).map(String);
|
||||
const n = typeof nOptions === 'number' ? nOptions : list.length;
|
||||
|
||||
if (typeof correct !== 'number' || correct < 0 || correct >= n) {
|
||||
throw new Error(`${label}: correct index ${correct} out of range for ${n} options`);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'choice',
|
||||
prompt: String(prompt),
|
||||
options: list.slice(0, n),
|
||||
correct,
|
||||
...(hint ? { hint: String(hint) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function extractHardware(desktopDir) {
|
||||
const src = readFileSync(`${desktopDir}/hw.c`, 'utf8');
|
||||
const lessons = [];
|
||||
|
||||
for (let n = 1; n <= HW_LESSONS; n++) {
|
||||
const slides = parseArrayInFunction(src, `build_hw_unit${n}_page`, 'slides')
|
||||
.map((row, i) => toSlide(row, `hw unit ${n} slide ${i + 1}`));
|
||||
|
||||
const exFn = `build_hw_unit${n}_exercise_page`;
|
||||
const qs = parseArrayInFunction(src, exFn, 'qs');
|
||||
const hints = parseArrayInFunction(src, exFn, 'hints');
|
||||
|
||||
lessons.push({
|
||||
id: `unit-${String(n).padStart(2, '0')}`,
|
||||
subject: 'hardware',
|
||||
number: n,
|
||||
titleKey: `hw_unit${n}`,
|
||||
subtitleKey: `hw_unit${n}_sub`,
|
||||
slides,
|
||||
tasks: [{
|
||||
number: 1,
|
||||
title: 'Quiz',
|
||||
subtitleKey: `hw_quiz${n}_head`,
|
||||
items: qs.map((row, i) => toChoice(row, hints[i], `hw unit ${n} q${i + 1}`)),
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
return lessons;
|
||||
}
|
||||
35
tools/extract/i18n.mjs
Normal file
35
tools/extract/i18n.mjs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { parseArray } from '../lib/cparse.mjs';
|
||||
|
||||
/**
|
||||
* `tr_ui` holds UI chrome keyed by short identifiers; `tr_content` holds
|
||||
* lesson text keyed by the Czech string itself (its `cs` column is usually
|
||||
* NULL, meaning "the key already is the Czech text").
|
||||
*/
|
||||
function toTable(entries, { keyIsCzech }) {
|
||||
const cs = {};
|
||||
const en = {};
|
||||
|
||||
for (const row of entries) {
|
||||
if (!Array.isArray(row)) continue;
|
||||
const [key, csText, enText] = row;
|
||||
if (key === null || key === undefined) continue; // terminator
|
||||
|
||||
cs[key] = csText ?? (keyIsCzech ? key : null);
|
||||
if (enText !== null && enText !== undefined) en[key] = enText;
|
||||
}
|
||||
|
||||
// A null Czech value in tr_ui would render as an empty label; drop it.
|
||||
for (const k of Object.keys(cs)) if (cs[k] === null) delete cs[k];
|
||||
|
||||
return { cs, en };
|
||||
}
|
||||
|
||||
export function extractI18n(desktopDir) {
|
||||
const src = readFileSync(`${desktopDir}/i18n.c`, 'utf8');
|
||||
|
||||
return {
|
||||
ui: toTable(parseArray(src, 'tr_ui'), { keyIsCzech: false }),
|
||||
content: toTable(parseArray(src, 'tr_content'), { keyIsCzech: true }),
|
||||
};
|
||||
}
|
||||
145
tools/extract/mluvnice.mjs
Normal file
145
tools/extract/mluvnice.mjs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { parseArray, trimNulls } from '../lib/cparse.mjs';
|
||||
|
||||
/**
|
||||
* Czech grammar ("Mluvnice"), 20 exercises in one lesson.
|
||||
*
|
||||
* Unlike the other subjects, mluvnice.c passes its instructions as literal
|
||||
* Czech strings rather than i18n keys, so they are copied verbatim from the
|
||||
* `build_mluvnice_exercise_page` switch below.
|
||||
*/
|
||||
|
||||
/** One row per `case` in `build_mluvnice_exercise_page`, in the same order. */
|
||||
const EXERCISES = [
|
||||
['typed', 'Pravopis – i/y po obojetných souhláskách', 'Doplňte i/í nebo y/ý.', null],
|
||||
['choice', 'Pravopis – velká/malá písmena', 'Vyberte správnou variantu.', null],
|
||||
['typed', 'Slovní druhy', 'Určete slovní druh podtržených slov.',
|
||||
'Do pole napište název slovního druhu.'],
|
||||
['typed', 'Mluvnické kategorie – podstatná jména',
|
||||
'Určete rod, číslo a pád podtržených slov.',
|
||||
'Vyplňte tři pole: rod, číslo a pád.'],
|
||||
['typed', 'Mluvnické kategorie – slovesa',
|
||||
'Určete osobu, číslo, čas a způsob u těchto sloves.',
|
||||
'U rozkazovacího a podmiňovacího způsobu napište do pole čas „neurčitý“.'],
|
||||
['reveal', 'Slovotvorba', 'Rozeberte slova na morfémy.',
|
||||
'Určete předponu, kořen, příponu a koncovku.'],
|
||||
['typed', 'Větné členy', 'Určete větný člen podtržených výrazů.', null],
|
||||
['typed', 'Druhy vedlejších vět', 'Určete druh vedlejší věty.', null],
|
||||
['typed', 'Synonyma', 'Ke každému slovu napište synonymum.', null],
|
||||
['typed', 'Antonyma', 'Napište opak.', null],
|
||||
['typed', 'Pravopis – s/z na začátku slova', 'Vyberte správnou předponu s-/z-.', null],
|
||||
['reveal', 'Skladba – najděte chybu',
|
||||
'Každá věta obsahuje jednu chybu. Najděte ji a opravte.', null],
|
||||
['typed', 'Tvarosloví – správný tvar', 'Dejte slovo do správného tvaru.', null],
|
||||
['choice', 'Výběr ze čtyř možností', 'Vyberte správnou odpověď.', null],
|
||||
['reveal', 'Přímá a nepřímá řeč', 'Přepište přímou řeč na nepřímou.', null],
|
||||
['reveal', 'Slovní zásoba – rozdíly', 'Vysvětlete rozdíl mezi těmito dvojicemi slov.', null],
|
||||
['reveal', 'Interpunkce', 'Doplňte čárky tam, kde patří.', null],
|
||||
['typed', 'Obrazná pojmenování',
|
||||
'Určete, o jaký druh obrazného pojmenování jde.', null],
|
||||
['typed', 'Stylistika – slohové útvary',
|
||||
'Přiřaďte ukázku ke správnému slohovému útvaru.', null],
|
||||
['choice', 'Souhrnné opakování', 'Vyberte správnou odpověď.', null],
|
||||
];
|
||||
|
||||
/** Only exercise 14 ships per-question explanations. */
|
||||
const CHOICE_EXPLANATIONS = { 14: 'e14_expl' };
|
||||
|
||||
/** `MluvItem`: prompt, shown, nblank, hint[5], answers[5]. */
|
||||
function fieldsItem(row, label) {
|
||||
const [prompt, shown, nblank, hints, answers] = row;
|
||||
if (typeof nblank !== 'number' || nblank < 1) {
|
||||
throw new Error(`${label}: expected at least one blank, got ${nblank}`);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'fields',
|
||||
prompt: String(prompt),
|
||||
fields: Array.from({ length: nblank }, (_, i) => ({
|
||||
label: String(hints[i] ?? ''),
|
||||
answers: String(answers[i] ?? '').split('|').map((a) => a.trim()).filter(Boolean),
|
||||
})),
|
||||
...(shown ? { solution: String(shown) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function choiceItem(row, expl, label) {
|
||||
const [prompt, options, nOptions, correct] = row;
|
||||
const list = trimNulls(options ?? []).map(String);
|
||||
const n = typeof nOptions === 'number' ? nOptions : list.length;
|
||||
|
||||
if (typeof correct !== 'number' || correct < 0 || correct >= n) {
|
||||
throw new Error(`${label}: correct index ${correct} out of range for ${n} options`);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'choice',
|
||||
prompt: String(prompt),
|
||||
options: list.slice(0, n),
|
||||
correct,
|
||||
...(expl ? { hint: String(expl) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function extractMluvnice(desktopDir) {
|
||||
const src = readFileSync(`${desktopDir}/mluvnice.c`, 'utf8');
|
||||
const names = parseArray(src, 'mluv_names').map(String);
|
||||
|
||||
if (names.length !== EXERCISES.length) {
|
||||
throw new Error(`mluv_names has ${names.length} entries but the extractor knows ${EXERCISES.length}`);
|
||||
}
|
||||
|
||||
const tasks = EXERCISES.map(([type, heading, subtitle, note], index) => {
|
||||
const number = index + 1;
|
||||
const label = `mluvnice ex${number}`;
|
||||
const base = {
|
||||
number,
|
||||
// Short label for the map bubble; the page shows the full heading.
|
||||
title: names[index],
|
||||
heading,
|
||||
subtitleKey: null,
|
||||
subtitle,
|
||||
...(note ? { note } : {}),
|
||||
};
|
||||
|
||||
if (type === 'choice') {
|
||||
const explName = CHOICE_EXPLANATIONS[number];
|
||||
const expls = explName ? parseArray(src, explName).map(String) : [];
|
||||
return {
|
||||
...base,
|
||||
items: parseArray(src, `e${number}_qs`)
|
||||
.map((row, i) => choiceItem(row, expls[i], `${label} q${i + 1}`)),
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'reveal') {
|
||||
return {
|
||||
...base,
|
||||
items: parseArray(src, `e${number}_items`).map(([prompt, solution]) => ({
|
||||
kind: 'reveal',
|
||||
prompt: String(prompt),
|
||||
solution: String(solution),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
items: parseArray(src, `e${number}_items`)
|
||||
.map((row, i) => fieldsItem(row, `${label} item ${i + 1}`)),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'mluvnice',
|
||||
subject: 'czech',
|
||||
number: 1,
|
||||
title: { cs: 'Mluvnice', en: 'Czech grammar' },
|
||||
subtitle: {
|
||||
cs: 'Vyberte cvičení a dokončete je.',
|
||||
en: 'Pick an exercise and complete it.',
|
||||
},
|
||||
slides: [],
|
||||
tasks,
|
||||
};
|
||||
}
|
||||
97
tools/extract/networks.mjs
Normal file
97
tools/extract/networks.mjs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { parseArray, parseArrayInFunction, trimNulls } from '../lib/cparse.mjs';
|
||||
|
||||
const NET_LESSONS = 27;
|
||||
|
||||
/** `NetSlide` is positional: kicker, title, tip, line[8]. */
|
||||
function toSlide(row, label) {
|
||||
if (!Array.isArray(row)) throw new Error(`${label}: slide is not a braced list`);
|
||||
const [kicker, title, tip, lines] = row;
|
||||
return {
|
||||
kicker: kicker ?? '',
|
||||
title: title ?? '',
|
||||
tip: tip ?? null,
|
||||
lines: trimNulls(lines ?? []).map((l) => String(l)),
|
||||
};
|
||||
}
|
||||
|
||||
/** `ChoiceQ` is positional: prompt, options[5], n_options, correct. */
|
||||
function toChoice(row, hint, label) {
|
||||
if (!Array.isArray(row)) throw new Error(`${label}: question is not a braced list`);
|
||||
const [prompt, options, nOptions, correct] = row;
|
||||
const list = trimNulls(options ?? []).map((o) => String(o));
|
||||
const n = typeof nOptions === 'number' ? nOptions : list.length;
|
||||
|
||||
if (typeof correct !== 'number' || correct < 0 || correct >= n) {
|
||||
throw new Error(`${label}: correct index ${correct} out of range for ${n} options`);
|
||||
}
|
||||
|
||||
const out = { kind: 'choice', prompt: String(prompt), options: list.slice(0, n), correct };
|
||||
if (hint) out.hint = String(hint);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Unit 1 is the VLSM walkthrough: four scenarios plus a per-block answer key. */
|
||||
function subnetTasks(src) {
|
||||
const tasks = parseArray(src, 'net_tasks');
|
||||
const answers = parseArray(src, 'net_ans');
|
||||
|
||||
return tasks.map(([scenario, solution], i) => {
|
||||
const rows = answers[i] ?? [];
|
||||
const blocks = rows
|
||||
.map(([prefix, network, broadcast, firstHost, lastHost]) => ({
|
||||
prefix, network, broadcast, firstHost, lastHost,
|
||||
}))
|
||||
// A prefix of -1 marks a scenario that has no valid allocation.
|
||||
.filter((b) => typeof b.prefix === 'number' && b.prefix > 0);
|
||||
|
||||
return {
|
||||
kind: 'subnet',
|
||||
scenario: String(scenario),
|
||||
solution: String(solution),
|
||||
blocks: blocks.length ? blocks : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function extractNetworks(desktopDir) {
|
||||
const src = readFileSync(`${desktopDir}/net.c`, 'utf8');
|
||||
const lessons = [];
|
||||
|
||||
for (let n = 1; n <= NET_LESSONS; n++) {
|
||||
const slides = parseArrayInFunction(src, `build_net_unit${n}_page`, 'slides')
|
||||
.map((row, i) => toSlide(row, `net unit ${n} slide ${i + 1}`));
|
||||
|
||||
let tasks;
|
||||
if (n === 1) {
|
||||
tasks = [{
|
||||
number: 1,
|
||||
title: 'VLSM',
|
||||
subtitleKey: 'net_ex1_title',
|
||||
items: subnetTasks(src),
|
||||
}];
|
||||
} else {
|
||||
const qs = parseArray(src, `net${n}_qs`);
|
||||
const hints = parseArray(src, `net${n}_hints`);
|
||||
tasks = [{
|
||||
number: 1,
|
||||
title: 'Quiz',
|
||||
subtitleKey: `net_quiz${n}_head`,
|
||||
items: qs.map((row, i) => toChoice(row, hints[i], `net unit ${n} q${i + 1}`)),
|
||||
}];
|
||||
}
|
||||
|
||||
lessons.push({
|
||||
id: `unit-${String(n).padStart(2, '0')}`,
|
||||
subject: 'networks',
|
||||
number: n,
|
||||
// Titles live in the i18n tables; the web app resolves these keys.
|
||||
titleKey: `net_unit${n}`,
|
||||
subtitleKey: `net_unit${n}_sub`,
|
||||
slides,
|
||||
tasks,
|
||||
});
|
||||
}
|
||||
|
||||
return lessons;
|
||||
}
|
||||
48
tools/extract/themes.mjs
Normal file
48
tools/extract/themes.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { parseStruct } from '../lib/cparse.mjs';
|
||||
|
||||
/** Field order of `ThemePalette` in maturita.h, as the C initializers are positional. */
|
||||
const FIELDS = [
|
||||
'crust', 'base', 'mantle', 'surface0', 'surface1', 'surface2',
|
||||
'overlay', 'text', 'subtext', 'accent', 'accent2', 'accent3',
|
||||
'success', 'success2', 'warning', 'error', 'onAccent',
|
||||
'node', 'lockedBg', 'lockedBorder', 'rail',
|
||||
'finishDark', 'finishLight', 'finishStroke',
|
||||
];
|
||||
|
||||
/** slug, display name, and the C variable prefix used in theme.c. */
|
||||
const THEMES = [
|
||||
['catppuccin', 'Catppuccin', 'cat'],
|
||||
['nord', 'Nord', 'nord'],
|
||||
['dracula', 'Dracula', 'dra'],
|
||||
['rose-pine', 'Rose Pine', 'rose'],
|
||||
['ocean', 'Ocean', 'ocean'],
|
||||
['gruvbox', 'Gruvbox', 'gruv'],
|
||||
['solarized', 'Solarized', 'sol'],
|
||||
['everforest', 'Everforest', 'ever'],
|
||||
['monokai', 'Monokai', 'mono'],
|
||||
['one-dark', 'One Dark', 'one'],
|
||||
];
|
||||
|
||||
function toPalette(values, label) {
|
||||
if (values.length !== FIELDS.length) {
|
||||
throw new Error(`${label}: expected ${FIELDS.length} colors, got ${values.length}`);
|
||||
}
|
||||
const out = {};
|
||||
values.forEach((v, i) => {
|
||||
if (typeof v !== 'number') throw new Error(`${label}: field ${FIELDS[i]} is not a hex literal`);
|
||||
out[FIELDS[i]] = '#' + v.toString(16).padStart(6, '0');
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
export function extractThemes(desktopDir) {
|
||||
const src = readFileSync(`${desktopDir}/theme.c`, 'utf8');
|
||||
|
||||
return THEMES.map(([id, name, prefix]) => ({
|
||||
id,
|
||||
name,
|
||||
dark: toPalette(parseStruct(src, `${prefix}_dark`), `${id} dark`),
|
||||
light: toPalette(parseStruct(src, `${prefix}_light`), `${id} light`),
|
||||
}));
|
||||
}
|
||||
354
tools/lib/cparse.mjs
Normal file
354
tools/lib/cparse.mjs
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
/**
|
||||
* Minimal reader for the C initializer syntax used by the desktop sources.
|
||||
*
|
||||
* It understands only what the lesson data actually uses: string literals with
|
||||
* adjacent concatenation, char literals, integers, `NULL`, identifiers,
|
||||
* designated initializers and nested braces. Anything else throws so a bad
|
||||
* extraction fails loudly instead of silently producing empty lessons.
|
||||
*/
|
||||
|
||||
const IDENT_START = /[A-Za-z_]/;
|
||||
const IDENT_PART = /[A-Za-z0-9_]/;
|
||||
|
||||
/** Strip comments while preserving string/char literals verbatim. */
|
||||
export function stripComments(src) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
|
||||
while (i < src.length) {
|
||||
const c = src[i];
|
||||
|
||||
if (c === '"' || c === "'") {
|
||||
const quote = c;
|
||||
out += c;
|
||||
i++;
|
||||
while (i < src.length) {
|
||||
if (src[i] === '\\') {
|
||||
out += src[i] + (src[i + 1] ?? '');
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
out += src[i];
|
||||
if (src[i] === quote) {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c === '/' && src[i + 1] === '*') {
|
||||
const end = src.indexOf('*/', i + 2);
|
||||
i = end === -1 ? src.length : end + 2;
|
||||
out += ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c === '/' && src[i + 1] === '/') {
|
||||
const end = src.indexOf('\n', i);
|
||||
i = end === -1 ? src.length : end;
|
||||
out += ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
out += c;
|
||||
i++;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
const ESCAPES = {
|
||||
n: '\n',
|
||||
t: '\t',
|
||||
r: '\r',
|
||||
'0': '\0',
|
||||
'\\': '\\',
|
||||
'"': '"',
|
||||
"'": "'",
|
||||
a: '\x07',
|
||||
b: '\b',
|
||||
f: '\f',
|
||||
v: '\v',
|
||||
};
|
||||
|
||||
class Reader {
|
||||
constructor(src, label) {
|
||||
this.src = src;
|
||||
this.label = label;
|
||||
this.i = 0;
|
||||
}
|
||||
|
||||
fail(msg) {
|
||||
const at = this.src.slice(Math.max(0, this.i - 60), this.i + 60);
|
||||
throw new Error(`${this.label}: ${msg} near\n...${at}...`);
|
||||
}
|
||||
|
||||
ws() {
|
||||
while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
|
||||
}
|
||||
|
||||
peek() {
|
||||
this.ws();
|
||||
return this.src[this.i];
|
||||
}
|
||||
|
||||
eat(ch) {
|
||||
this.ws();
|
||||
if (this.src[this.i] !== ch) this.fail(`expected '${ch}', got '${this.src[this.i]}'`);
|
||||
this.i++;
|
||||
}
|
||||
|
||||
tryEat(ch) {
|
||||
this.ws();
|
||||
if (this.src[this.i] === ch) {
|
||||
this.i++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** One string literal, honouring C's adjacent-literal concatenation. */
|
||||
readString() {
|
||||
let value = '';
|
||||
let first = true;
|
||||
|
||||
for (;;) {
|
||||
this.ws();
|
||||
if (this.src[this.i] !== '"') {
|
||||
if (first) this.fail('expected string literal');
|
||||
return value;
|
||||
}
|
||||
first = false;
|
||||
this.i++;
|
||||
while (this.i < this.src.length && this.src[this.i] !== '"') {
|
||||
if (this.src[this.i] === '\\') {
|
||||
const esc = this.src[this.i + 1];
|
||||
if (esc in ESCAPES) {
|
||||
value += ESCAPES[esc];
|
||||
this.i += 2;
|
||||
continue;
|
||||
}
|
||||
if (esc === 'u' || esc === 'x') {
|
||||
const m = /^\\(u[0-9a-fA-F]{4}|x[0-9a-fA-F]+)/.exec(this.src.slice(this.i));
|
||||
if (!m) this.fail('bad escape');
|
||||
const hex = m[1].slice(1);
|
||||
value += String.fromCodePoint(parseInt(hex, 16));
|
||||
this.i += m[0].length;
|
||||
continue;
|
||||
}
|
||||
value += esc;
|
||||
this.i += 2;
|
||||
continue;
|
||||
}
|
||||
value += this.src[this.i];
|
||||
this.i++;
|
||||
}
|
||||
if (this.src[this.i] !== '"') this.fail('unterminated string');
|
||||
this.i++;
|
||||
}
|
||||
}
|
||||
|
||||
readIdent() {
|
||||
this.ws();
|
||||
if (!IDENT_START.test(this.src[this.i] ?? '')) this.fail('expected identifier');
|
||||
let start = this.i;
|
||||
while (this.i < this.src.length && IDENT_PART.test(this.src[this.i])) this.i++;
|
||||
return this.src.slice(start, this.i);
|
||||
}
|
||||
|
||||
/**
|
||||
* A single initializer value. Braces become arrays, or plain objects when the
|
||||
* body uses designated initializers.
|
||||
*/
|
||||
readValue() {
|
||||
this.ws();
|
||||
const c = this.src[this.i];
|
||||
|
||||
if (c === undefined) this.fail('unexpected end of input');
|
||||
if (c === '"') return this.readString();
|
||||
if (c === '{') return this.readBraced();
|
||||
|
||||
// Cast prefix such as `(int)` or `(const char *)`: skip it and read on.
|
||||
if (c === '(') {
|
||||
const cast = /^\(\s*(?:const\s+|unsigned\s+|signed\s+)*[A-Za-z_]\w*\s*\**\s*\)/.exec(
|
||||
this.src.slice(this.i)
|
||||
);
|
||||
if (cast) {
|
||||
this.i += cast[0].length;
|
||||
return this.readValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (c === "'") {
|
||||
this.i++;
|
||||
let ch;
|
||||
if (this.src[this.i] === '\\') {
|
||||
const esc = this.src[this.i + 1];
|
||||
ch = esc in ESCAPES ? ESCAPES[esc] : esc;
|
||||
this.i += 2;
|
||||
} else {
|
||||
ch = this.src[this.i];
|
||||
this.i++;
|
||||
}
|
||||
this.eat("'");
|
||||
return ch;
|
||||
}
|
||||
|
||||
// Numeric or simple arithmetic/negation, e.g. `-1`, `4`, `G_N_ELEMENTS(x)`.
|
||||
const numMatch = /^[+-]?(?:0[xX][0-9a-fA-F]+|\d+)(?![\w.])/.exec(this.src.slice(this.i));
|
||||
if (numMatch) {
|
||||
this.i += numMatch[0].length;
|
||||
return Number(numMatch[0]);
|
||||
}
|
||||
|
||||
if (IDENT_START.test(c)) {
|
||||
const name = this.readIdent();
|
||||
if (name === 'NULL') return null;
|
||||
if (name === 'TRUE') return true;
|
||||
if (name === 'FALSE') return false;
|
||||
// Macro call or bare constant: keep the source text for the caller.
|
||||
if (this.peek() === '(') {
|
||||
let depth = 0;
|
||||
const start = this.i;
|
||||
while (this.i < this.src.length) {
|
||||
if (this.src[this.i] === '(') depth++;
|
||||
else if (this.src[this.i] === ')') {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
this.i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.i++;
|
||||
}
|
||||
return { __c: name + this.src.slice(start, this.i) };
|
||||
}
|
||||
return { __c: name };
|
||||
}
|
||||
|
||||
this.fail(`unexpected character '${c}'`);
|
||||
}
|
||||
|
||||
readBraced() {
|
||||
this.eat('{');
|
||||
const list = [];
|
||||
const obj = {};
|
||||
let designated = false;
|
||||
|
||||
for (;;) {
|
||||
if (this.tryEat('}')) break;
|
||||
|
||||
if (this.peek() === '.') {
|
||||
this.i++;
|
||||
const field = this.readIdent();
|
||||
this.eat('=');
|
||||
obj[field] = this.readValue();
|
||||
designated = true;
|
||||
} else {
|
||||
list.push(this.readValue());
|
||||
}
|
||||
|
||||
if (this.tryEat(',')) continue;
|
||||
this.eat('}');
|
||||
break;
|
||||
}
|
||||
|
||||
return designated ? obj : list;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an array/struct definition by name and parse its initializer.
|
||||
* Matches e.g. `static const ChoiceQ net2_qs[] = { ... };`
|
||||
*/
|
||||
export function parseArray(src, name, { label = name } = {}) {
|
||||
const clean = stripComments(src);
|
||||
const re = new RegExp(
|
||||
`\\b${name}\\s*(?:\\[[^\\]]*\\]\\s*)+=\\s*(?=\\{)`,
|
||||
'g'
|
||||
);
|
||||
const m = re.exec(clean);
|
||||
if (!m) throw new Error(`${label}: array '${name}' not found`);
|
||||
|
||||
const reader = new Reader(clean, label);
|
||||
reader.i = m.index + m[0].length;
|
||||
const value = reader.readValue();
|
||||
if (!Array.isArray(value)) throw new Error(`${label}: '${name}' is not a braced list`);
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Parse a scalar struct definition, e.g. `static const ThemePalette nord_dark = {...};` */
|
||||
export function parseStruct(src, name, { label = name } = {}) {
|
||||
const clean = stripComments(src);
|
||||
const re = new RegExp(`\\b${name}\\s*=\\s*(?=\\{)`, 'g');
|
||||
const m = re.exec(clean);
|
||||
if (!m) throw new Error(`${label}: struct '${name}' not found`);
|
||||
|
||||
const reader = new Reader(clean, label);
|
||||
reader.i = m.index + m[0].length;
|
||||
return reader.readValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as parseArray but scoped to the body of a single function, so the many
|
||||
* identically-named `static const NetSlide slides[]` locals stay distinct.
|
||||
*/
|
||||
export function parseArrayInFunction(src, fnName, arrayName) {
|
||||
const clean = stripComments(src);
|
||||
const fnRe = new RegExp(`\\b${fnName}\\s*\\([^)]*\\)\\s*\\{`, 'g');
|
||||
const m = fnRe.exec(clean);
|
||||
if (!m) throw new Error(`function '${fnName}' not found`);
|
||||
|
||||
// Walk to the matching closing brace so we only search this body.
|
||||
let depth = 0;
|
||||
let i = m.index + m[0].length - 1;
|
||||
const start = i;
|
||||
for (; i < clean.length; i++) {
|
||||
if (clean[i] === '"' || clean[i] === "'") {
|
||||
const q = clean[i++];
|
||||
while (i < clean.length && clean[i] !== q) i += clean[i] === '\\' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (clean[i] === '{') depth++;
|
||||
else if (clean[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
const body = clean.slice(start, i + 1);
|
||||
return parseArray(body, arrayName, { label: `${fnName}/${arrayName}` });
|
||||
}
|
||||
|
||||
/** Parse a scalar string definition, e.g. `const char *g05_bank = "a, b, c";` */
|
||||
export function parseString(src, name, { label = name } = {}) {
|
||||
const clean = stripComments(src);
|
||||
const re = new RegExp(`\\b${name}\\s*=\\s*(?=")`, 'g');
|
||||
const m = re.exec(clean);
|
||||
if (!m) throw new Error(`${label}: string '${name}' not found`);
|
||||
|
||||
const reader = new Reader(clean, label);
|
||||
reader.i = m.index + m[0].length;
|
||||
return reader.readString();
|
||||
}
|
||||
|
||||
/** Resolve `#define NAME value` integer macros. */
|
||||
export function parseDefines(src) {
|
||||
const out = {};
|
||||
const re = /^[ \t]*#[ \t]*define[ \t]+([A-Za-z_]\w*)[ \t]+([^\n/]+)/gm;
|
||||
let m;
|
||||
while ((m = re.exec(src))) {
|
||||
const raw = m[2].trim();
|
||||
if (/^-?\d+$/.test(raw)) out[m[1]] = Number(raw);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Trailing NULLs are array terminators in the C data, not content. */
|
||||
export function trimNulls(list) {
|
||||
const out = [...list];
|
||||
while (out.length && (out.at(-1) === null || out.at(-1) === undefined)) out.pop();
|
||||
return out;
|
||||
}
|
||||
7
web/next-env.d.ts
vendored
Normal file
7
web/next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/types/root-params.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
10
web/next.config.ts
Normal file
10
web/next.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { NextConfig } from 'next';
|
||||
|
||||
const config: NextConfig = {
|
||||
// `content/` lives outside web/, so allow tracing files from the repo root.
|
||||
outputFileTracingRoot: __dirname + '/..',
|
||||
// Routes are built from content ids at runtime, so they cannot be statically typed.
|
||||
typedRoutes: false,
|
||||
};
|
||||
|
||||
export default config;
|
||||
1076
web/package-lock.json
generated
Normal file
1076
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
24
web/package.json
Normal file
24
web/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "maturita-web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"content": "node ../tools/build-content.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^16.3.5",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
38
web/src/app/ThemeScript.tsx
Normal file
38
web/src/app/ThemeScript.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { Theme } from '../../../content/schema';
|
||||
|
||||
/**
|
||||
* Applies the stored palette before the first paint.
|
||||
*
|
||||
* Without this the page renders with the dark fallback from `globals.css` and
|
||||
* only recolours once <AppProvider>'s effect runs, which shows as a dark flash
|
||||
* for anyone using a light theme.
|
||||
*/
|
||||
export function ThemeScript({ themes }: { themes: Theme[] }) {
|
||||
const palettes = Object.fromEntries(
|
||||
themes.map((theme) => [theme.id, { dark: theme.dark, light: theme.light }])
|
||||
);
|
||||
|
||||
const source = `
|
||||
(function () {
|
||||
try {
|
||||
var palettes = ${JSON.stringify(palettes)};
|
||||
var stored = JSON.parse(localStorage.getItem('maturita.settings') || '{}');
|
||||
var theme = palettes[stored.themeId] || palettes['catppuccin'];
|
||||
var mode = stored.mode === 'light' ? 'light' : 'dark';
|
||||
var palette = theme[mode];
|
||||
var root = document.documentElement;
|
||||
|
||||
for (var key in palette) {
|
||||
root.style.setProperty('--' + key.replace(/[A-Z]/g, function (c) {
|
||||
return '-' + c.toLowerCase();
|
||||
}), palette[key]);
|
||||
}
|
||||
|
||||
root.dataset.mode = mode;
|
||||
root.style.colorScheme = mode;
|
||||
if (stored.lang === 'en' || stored.lang === 'cs') root.lang = stored.lang;
|
||||
} catch (e) {}
|
||||
})();`;
|
||||
|
||||
return <script dangerouslySetInnerHTML={{ __html: source }} />;
|
||||
}
|
||||
12
web/src/app/czech/mluvnice/page.tsx
Normal file
12
web/src/app/czech/mluvnice/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getLesson } from '@/content';
|
||||
import { LessonScreen } from '@/components/LessonScreen';
|
||||
|
||||
export default async function MluvnicePage() {
|
||||
try {
|
||||
return <LessonScreen lesson={await getLesson('czech', 'mluvnice')} backHref="/czech" />;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
28
web/src/app/czech/page.tsx
Normal file
28
web/src/app/czech/page.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getLessons, getPath } from '@/content';
|
||||
import { PathScreen } from '@/components/PathScreen';
|
||||
|
||||
/**
|
||||
* The three Czech areas from `build_czech_page`: Literatura (still empty),
|
||||
* Mluvnice and Maturitní četba. Each node links on rather than opening a
|
||||
* lesson, so `hrefBase` is unused here.
|
||||
*/
|
||||
export default async function CzechPage() {
|
||||
const path = await getPath('czech');
|
||||
if (!path) notFound();
|
||||
|
||||
const lessons = await getLessons('czech');
|
||||
const taskCounts = Object.fromEntries(lessons.map((l) => [l.id, l.tasks.length]));
|
||||
|
||||
return (
|
||||
<PathScreen
|
||||
path={path}
|
||||
titleKey="Český jazyk a literatura"
|
||||
subtitleKey="czech_sub"
|
||||
backHref="/subjects"
|
||||
hrefBase="/czech"
|
||||
taskCounts={taskCounts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
21
web/src/app/czech/reading/[book]/page.tsx
Normal file
21
web/src/app/czech/reading/[book]/page.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getLesson, getPath } from '@/content';
|
||||
import { LessonScreen } from '@/components/LessonScreen';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const path = await getPath('czech', 'reading');
|
||||
return (path?.units ?? [])
|
||||
.filter((unit) => unit.lesson !== null)
|
||||
.map((unit) => ({ book: unit.lesson as string }));
|
||||
}
|
||||
|
||||
export default async function BookPage({ params }: { params: Promise<{ book: string }> }) {
|
||||
const { book } = await params;
|
||||
|
||||
try {
|
||||
return <LessonScreen lesson={await getLesson('czech', book)} backHref="/czech/reading" />;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
24
web/src/app/czech/reading/page.tsx
Normal file
24
web/src/app/czech/reading/page.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getLessons, getPath } from '@/content';
|
||||
import { PathScreen } from '@/components/PathScreen';
|
||||
|
||||
/** The reading list: one node per book, both unlocked. */
|
||||
export default async function ReadingListPage() {
|
||||
const path = await getPath('czech', 'reading');
|
||||
if (!path) notFound();
|
||||
|
||||
const lessons = await getLessons('czech');
|
||||
const taskCounts = Object.fromEntries(lessons.map((l) => [l.id, l.tasks.length]));
|
||||
|
||||
return (
|
||||
<PathScreen
|
||||
path={path}
|
||||
title="Maturitní četba"
|
||||
subtitle="Vyberte knihu a projděte si přehled, kvíz a řazení děje."
|
||||
backHref="/czech"
|
||||
hrefBase="/czech/reading"
|
||||
taskCounts={taskCounts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
30
web/src/app/deutsch/[lesson]/page.tsx
Normal file
30
web/src/app/deutsch/[lesson]/page.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getLesson, getLessonIds } from '@/content';
|
||||
import { LessonScreen } from '@/components/LessonScreen';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const ids = await getLessonIds('deutsch');
|
||||
return ids.map((lesson) => ({ lesson }));
|
||||
}
|
||||
|
||||
export default async function DeutschLessonPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ lesson: string }>;
|
||||
}) {
|
||||
const { lesson: id } = await params;
|
||||
|
||||
try {
|
||||
const lesson = await getLesson('deutsch', id);
|
||||
return (
|
||||
<LessonScreen
|
||||
lesson={lesson}
|
||||
backHref="/deutsch"
|
||||
vocabHref={lesson.vocab?.length ? `/deutsch/${id}/vocab` : undefined}
|
||||
/>
|
||||
);
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
142
web/src/app/deutsch/[lesson]/vocab/VocabTrainer.tsx
Normal file
142
web/src/app/deutsch/[lesson]/vocab/VocabTrainer.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type { VocabSection } from '@/content';
|
||||
import { useApp } from '@/app/providers';
|
||||
import { TopBar } from '@/components/TopBar';
|
||||
import { isCorrect } from '@/lib/answers';
|
||||
|
||||
interface Card {
|
||||
section: string;
|
||||
prompt: string;
|
||||
answers: string[];
|
||||
meaning?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vocabulary drill.
|
||||
*
|
||||
* Follows the desktop rule from `sub_translate`: a wrong answer goes back to the
|
||||
* end of the queue, so the round only finishes once every word was translated
|
||||
* correctly at least once.
|
||||
*/
|
||||
export function VocabTrainer({
|
||||
sections,
|
||||
backHref,
|
||||
}: {
|
||||
sections: VocabSection[];
|
||||
backHref: string;
|
||||
}) {
|
||||
const { t } = useApp();
|
||||
|
||||
const cards = useMemo<Card[]>(
|
||||
() =>
|
||||
sections.flatMap((section) =>
|
||||
section.items.map((item) => ({ section: section.header, ...item }))
|
||||
),
|
||||
[sections]
|
||||
);
|
||||
|
||||
const [queue, setQueue] = useState<number[]>(() => cards.map((_, i) => i));
|
||||
const [value, setValue] = useState('');
|
||||
const [verdict, setVerdict] = useState<'idle' | 'ok' | 'wrong' | 'done'>('idle');
|
||||
const [solvedCount, setSolvedCount] = useState(0);
|
||||
/** Answer of the card just missed, kept because the queue has already rotated. */
|
||||
const [missed, setMissed] = useState('');
|
||||
|
||||
const current = queue.length > 0 ? cards[queue[0]] : null;
|
||||
const total = cards.length;
|
||||
|
||||
const submit = () => {
|
||||
if (!current || verdict === 'done') return;
|
||||
|
||||
if (current.answers.some((a) => isCorrect(value, a))) {
|
||||
const rest = queue.slice(1);
|
||||
setSolvedCount((n) => n + 1);
|
||||
setQueue(rest);
|
||||
setValue('');
|
||||
setVerdict(rest.length === 0 ? 'done' : 'ok');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wrong: show the expected answer and requeue the card at the back.
|
||||
setMissed(current.answers[0] ?? '');
|
||||
setVerdict('wrong');
|
||||
setQueue((q) => [...q.slice(1), q[0]]);
|
||||
setValue('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<TopBar title="Vokabeltraining" backHref={backHref} />
|
||||
|
||||
<main className="page">
|
||||
<div className="page__head">
|
||||
<h1 className="page__title">Vokabeltraining</h1>
|
||||
<p className="page__sub">{t('sub_translate')}</p>
|
||||
</div>
|
||||
|
||||
<div className="stack">
|
||||
<div className="bar" aria-hidden>
|
||||
<div
|
||||
className="bar__fill"
|
||||
style={{ width: `${total ? (solvedCount / total) * 100 : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="muted tabular">
|
||||
{t('trans_progress').replace('%d', String(solvedCount)).replace('%d', String(total))}
|
||||
</p>
|
||||
|
||||
{verdict === 'done' || !current ? (
|
||||
<p className="feedback feedback--ok" role="status">
|
||||
{t('trans_done')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="ex__item">
|
||||
<span className="card__kicker">{current.section}</span>
|
||||
<p className="ex__prompt" style={{ fontSize: '1.3rem' }}>
|
||||
{current.prompt}
|
||||
</p>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submit();
|
||||
}}
|
||||
>
|
||||
<div className="row">
|
||||
<input
|
||||
className="gap"
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder={t('your_answer')}
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
spellCheck={false}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<button className="btn" type="submit">
|
||||
{t('check')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{verdict === 'ok' && (
|
||||
<p className="feedback feedback--ok" role="status">
|
||||
{t('feedback_ok')}
|
||||
</p>
|
||||
)}
|
||||
{verdict === 'wrong' && (
|
||||
<p className="feedback feedback--err" role="status">
|
||||
{t('trans_wrong').replace('%s', missed)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
web/src/app/deutsch/[lesson]/vocab/page.tsx
Normal file
21
web/src/app/deutsch/[lesson]/vocab/page.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getLesson, getLessonIds } from '@/content';
|
||||
import { VocabTrainer } from './VocabTrainer';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const ids = await getLessonIds('deutsch');
|
||||
return ids.map((lesson) => ({ lesson }));
|
||||
}
|
||||
|
||||
export default async function VocabPage({ params }: { params: Promise<{ lesson: string }> }) {
|
||||
const { lesson: id } = await params;
|
||||
|
||||
try {
|
||||
const lesson = await getLesson('deutsch', id);
|
||||
if (!lesson.vocab?.length) notFound();
|
||||
return <VocabTrainer sections={lesson.vocab} backHref={`/deutsch/${id}`} />;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
24
web/src/app/deutsch/page.tsx
Normal file
24
web/src/app/deutsch/page.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getLessons, getPath } from '@/content';
|
||||
import { PathScreen } from '@/components/PathScreen';
|
||||
|
||||
export default async function DeutschPathPage() {
|
||||
const path = await getPath('deutsch');
|
||||
if (!path) notFound();
|
||||
|
||||
const lessons = await getLessons('deutsch');
|
||||
const taskCounts = Object.fromEntries(lessons.map((l) => [l.id, l.tasks.length]));
|
||||
|
||||
return (
|
||||
<PathScreen
|
||||
path={path}
|
||||
titleKey="roadmap_title"
|
||||
subtitleKey="roadmap_sub"
|
||||
backHref="/subjects"
|
||||
hrefBase="/deutsch"
|
||||
taskCounts={taskCounts}
|
||||
finish
|
||||
/>
|
||||
);
|
||||
}
|
||||
1005
web/src/app/globals.css
Normal file
1005
web/src/app/globals.css
Normal file
File diff suppressed because it is too large
Load diff
23
web/src/app/hardware/[lesson]/page.tsx
Normal file
23
web/src/app/hardware/[lesson]/page.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getLesson, getLessonIds } from '@/content';
|
||||
import { LessonScreen } from '@/components/LessonScreen';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const ids = await getLessonIds('hardware');
|
||||
return ids.map((lesson) => ({ lesson }));
|
||||
}
|
||||
|
||||
export default async function HardwareLessonPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ lesson: string }>;
|
||||
}) {
|
||||
const { lesson: id } = await params;
|
||||
|
||||
try {
|
||||
return <LessonScreen lesson={await getLesson('hardware', id)} backHref="/hardware" />;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
24
web/src/app/hardware/page.tsx
Normal file
24
web/src/app/hardware/page.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getLessons, getPath } from '@/content';
|
||||
import { PathScreen } from '@/components/PathScreen';
|
||||
|
||||
export default async function HardwarePage() {
|
||||
const path = await getPath('hardware');
|
||||
if (!path) notFound();
|
||||
|
||||
const lessons = await getLessons('hardware');
|
||||
const taskCounts = Object.fromEntries(lessons.map((l) => [l.id, l.tasks.length]));
|
||||
|
||||
return (
|
||||
<PathScreen
|
||||
path={path}
|
||||
// The desktop app keys subject titles by their Czech text.
|
||||
titleKey="Technické vybavení"
|
||||
subtitleKey="hw_sub"
|
||||
backHref="/subjects"
|
||||
hrefBase="/hardware"
|
||||
taskCounts={taskCounts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
36
web/src/app/layout.tsx
Normal file
36
web/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { Metadata, Viewport } from 'next';
|
||||
|
||||
import { getThemes, getTranslations } from '@/content';
|
||||
import { AppProvider } from './providers';
|
||||
import { ThemeScript } from './ThemeScript';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'maturita.c',
|
||||
description:
|
||||
'Vzdělávací aplikace pro přípravu k maturitě — němčina, počítačové sítě, ' +
|
||||
'technické vybavení a český jazyk.',
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
themeColor: '#1e1e2e',
|
||||
};
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const [themes, translations] = await Promise.all([getThemes(), getTranslations()]);
|
||||
|
||||
return (
|
||||
<html lang="cs" data-mode="dark" suppressHydrationWarning>
|
||||
<head>
|
||||
<ThemeScript themes={themes} />
|
||||
</head>
|
||||
<body>
|
||||
<AppProvider themes={themes} translations={translations}>
|
||||
{children}
|
||||
</AppProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
53
web/src/app/networks/NetworkYearsScreen.tsx
Normal file
53
web/src/app/networks/NetworkYearsScreen.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { useApp } from '@/app/providers';
|
||||
import { TopBar } from '@/components/TopBar';
|
||||
import { LockIcon } from '@/components/icons';
|
||||
|
||||
/** Only year 1 has content, matching `build_net_years_page` in the desktop app. */
|
||||
const YEARS = [1, 2, 3, 4];
|
||||
|
||||
export function NetworkYearsScreen() {
|
||||
const { t } = useApp();
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<TopBar title={t('net_years_title')} backHref="/subjects" />
|
||||
|
||||
<main className="page">
|
||||
<div className="page__head">
|
||||
<h1 className="page__title">{t('net_years_title')}</h1>
|
||||
<p className="page__sub">{t('net_years_sub')}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid">
|
||||
{YEARS.map((year) => {
|
||||
const label = t(`net_year${year}`);
|
||||
const locked = year !== 1;
|
||||
|
||||
if (locked) {
|
||||
return (
|
||||
<div className="card card--locked" key={year} aria-disabled>
|
||||
<span className="row" style={{ color: 'var(--overlay)' }}>
|
||||
<LockIcon size={22} />
|
||||
</span>
|
||||
<h2 className="card__title">{label}</h2>
|
||||
<p className="card__body">{t('net_year_locked_sub')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className="card card--interactive" href={`/networks/year-${year}`} key={year}>
|
||||
<span className="card__kicker">{year}</span>
|
||||
<h2 className="card__title">{label}</h2>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
6
web/src/app/networks/page.tsx
Normal file
6
web/src/app/networks/page.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { NetworkYearsScreen } from './NetworkYearsScreen';
|
||||
|
||||
/** Year picker: year 1 has all 27 lessons, years 2-4 are placeholders. */
|
||||
export default function NetworkYearsPage() {
|
||||
return <NetworkYearsScreen />;
|
||||
}
|
||||
23
web/src/app/networks/year-1/[lesson]/page.tsx
Normal file
23
web/src/app/networks/year-1/[lesson]/page.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getLesson, getLessonIds } from '@/content';
|
||||
import { LessonScreen } from '@/components/LessonScreen';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const ids = await getLessonIds('networks');
|
||||
return ids.map((lesson) => ({ lesson }));
|
||||
}
|
||||
|
||||
export default async function NetworkLessonPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ lesson: string }>;
|
||||
}) {
|
||||
const { lesson: id } = await params;
|
||||
|
||||
try {
|
||||
return <LessonScreen lesson={await getLesson('networks', id)} backHref="/networks/year-1" />;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
23
web/src/app/networks/year-1/page.tsx
Normal file
23
web/src/app/networks/year-1/page.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getLessons, getPath } from '@/content';
|
||||
import { PathScreen } from '@/components/PathScreen';
|
||||
|
||||
export default async function NetworkYear1Page() {
|
||||
const path = await getPath('networks');
|
||||
if (!path) notFound();
|
||||
|
||||
const lessons = await getLessons('networks');
|
||||
const taskCounts = Object.fromEntries(lessons.map((l) => [l.id, l.tasks.length]));
|
||||
|
||||
return (
|
||||
<PathScreen
|
||||
path={path}
|
||||
titleKey="net_years_title"
|
||||
subtitleKey="net_sub"
|
||||
backHref="/networks"
|
||||
hrefBase="/networks/year-1"
|
||||
taskCounts={taskCounts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
20
web/src/app/page.tsx
Normal file
20
web/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { useApp } from './providers';
|
||||
|
||||
/** Welcome screen: branded greeting plus a continue button, as on the desktop. */
|
||||
export default function WelcomePage() {
|
||||
const { t } = useApp();
|
||||
|
||||
return (
|
||||
<main className="welcome">
|
||||
<h1 className="welcome__title">maturita.c</h1>
|
||||
<p className="welcome__body">{t('welcome_body')}</p>
|
||||
<Link className="btn btn--lg" href="/subjects">
|
||||
{t('continue')}
|
||||
</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
150
web/src/app/providers.tsx
Normal file
150
web/src/app/providers.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
'use client';
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import type { Lang, Theme, ThemePalette } from '../../../content/schema';
|
||||
import type { TranslationTable } from '@/content';
|
||||
|
||||
/**
|
||||
* Client-side app settings: palette, dark/light mode and UI language.
|
||||
*
|
||||
* These mirror `progress/settings.conf` in the desktop app and persist to
|
||||
* localStorage so a reload keeps the student's choices.
|
||||
*/
|
||||
|
||||
const STORAGE_KEY = 'maturita.settings';
|
||||
|
||||
interface Settings {
|
||||
themeId: string;
|
||||
mode: 'dark' | 'light';
|
||||
lang: Lang;
|
||||
}
|
||||
|
||||
const DEFAULTS: Settings = { themeId: 'catppuccin', mode: 'dark', lang: 'cs' };
|
||||
|
||||
interface AppContextValue extends Settings {
|
||||
themes: Theme[];
|
||||
palette: ThemePalette;
|
||||
setThemeId: (id: string) => void;
|
||||
setMode: (mode: 'dark' | 'light') => void;
|
||||
setLang: (lang: Lang) => void;
|
||||
/** Translate a key, falling back to Czech and then to the key itself. */
|
||||
t: (key: string) => string;
|
||||
/** Like `t`, but returns undefined for unknown keys instead of the key. */
|
||||
tOpt: (key: string | null | undefined) => string | undefined;
|
||||
}
|
||||
|
||||
const AppContext = createContext<AppContextValue | null>(null);
|
||||
|
||||
function readStored(): Settings {
|
||||
if (typeof window === 'undefined') return DEFAULTS;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return DEFAULTS;
|
||||
return { ...DEFAULTS, ...(JSON.parse(raw) as Partial<Settings>) };
|
||||
} catch {
|
||||
return DEFAULTS;
|
||||
}
|
||||
}
|
||||
|
||||
/** Turn a palette into the CSS custom properties the stylesheet consumes. */
|
||||
function paletteVars(palette: ThemePalette): Record<string, string> {
|
||||
const vars: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(palette)) {
|
||||
// camelCase -> kebab-case, e.g. lockedBg -> --locked-bg
|
||||
const name = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
||||
vars[`--${name}`] = value;
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
export function AppProvider({
|
||||
themes,
|
||||
translations,
|
||||
children,
|
||||
}: {
|
||||
themes: Theme[];
|
||||
translations: TranslationTable;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [settings, setSettings] = useState<Settings>(DEFAULTS);
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
|
||||
// Read persisted settings after mount so server and client markup agree.
|
||||
useEffect(() => {
|
||||
setSettings(readStored());
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated) return;
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
}, [settings, hydrated]);
|
||||
|
||||
const theme = useMemo(
|
||||
() => themes.find((x) => x.id === settings.themeId) ?? themes[0],
|
||||
[themes, settings.themeId]
|
||||
);
|
||||
const palette = settings.mode === 'light' ? theme.light : theme.dark;
|
||||
|
||||
// Apply the palette to <html> so it also covers the page background.
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
for (const [name, value] of Object.entries(paletteVars(palette))) {
|
||||
root.style.setProperty(name, value);
|
||||
}
|
||||
root.dataset.mode = settings.mode;
|
||||
root.style.colorScheme = settings.mode;
|
||||
}, [palette, settings.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = settings.lang;
|
||||
}, [settings.lang]);
|
||||
|
||||
const t = useCallback(
|
||||
(key: string) => {
|
||||
if (!key) return '';
|
||||
if (settings.lang === 'en') {
|
||||
return translations.en[key] ?? translations.cs[key] ?? key;
|
||||
}
|
||||
return translations.cs[key] ?? key;
|
||||
},
|
||||
[translations, settings.lang]
|
||||
);
|
||||
|
||||
const tOpt = useCallback(
|
||||
(key: string | null | undefined) => {
|
||||
if (!key) return undefined;
|
||||
const table = settings.lang === 'en' ? translations.en : translations.cs;
|
||||
return table[key] ?? (settings.lang === 'en' ? translations.cs[key] : undefined);
|
||||
},
|
||||
[translations, settings.lang]
|
||||
);
|
||||
|
||||
const value: AppContextValue = {
|
||||
...settings,
|
||||
themes,
|
||||
palette,
|
||||
setThemeId: (themeId) => setSettings((s) => ({ ...s, themeId })),
|
||||
setMode: (mode) => setSettings((s) => ({ ...s, mode })),
|
||||
setLang: (lang) => setSettings((s) => ({ ...s, lang })),
|
||||
t,
|
||||
tOpt,
|
||||
};
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
}
|
||||
|
||||
export function useApp(): AppContextValue {
|
||||
const ctx = useContext(AppContext);
|
||||
if (!ctx) throw new Error('useApp must be used inside <AppProvider>');
|
||||
return ctx;
|
||||
}
|
||||
74
web/src/app/settings/page.tsx
Normal file
74
web/src/app/settings/page.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
'use client';
|
||||
|
||||
import { useApp } from '@/app/providers';
|
||||
import { TopBar } from '@/components/TopBar';
|
||||
|
||||
/** Mode, palette and language, mirroring the desktop settings panel. */
|
||||
export default function SettingsPage() {
|
||||
const { t, themes, themeId, mode, lang, setThemeId, setMode, setLang } = useApp();
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<TopBar title={t('settings_title')} backHref="/subjects" showTools={false} />
|
||||
|
||||
<main className="page">
|
||||
<div className="page__head">
|
||||
<h1 className="page__title">{t('settings_title')}</h1>
|
||||
</div>
|
||||
|
||||
<div className="settings">
|
||||
<section className="settings__group">
|
||||
<span className="settings__label">{t('mode')}</span>
|
||||
<div className="segmented" role="group" aria-label={t('mode')}>
|
||||
<button aria-pressed={mode === 'dark'} onClick={() => setMode('dark')}>
|
||||
{t('mode_dark')}
|
||||
</button>
|
||||
<button aria-pressed={mode === 'light'} onClick={() => setMode('light')}>
|
||||
{t('mode_light')}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="settings__group">
|
||||
<span className="settings__label">{t('theme')}</span>
|
||||
<div className="swatches">
|
||||
{themes.map((theme) => {
|
||||
const palette = mode === 'light' ? theme.light : theme.dark;
|
||||
return (
|
||||
<button
|
||||
className="swatch"
|
||||
key={theme.id}
|
||||
aria-pressed={theme.id === themeId}
|
||||
onClick={() => setThemeId(theme.id)}
|
||||
>
|
||||
<span className="swatch__dots">
|
||||
{/* Keyed by position: some palettes reuse the same hex twice. */}
|
||||
{[palette.accent, palette.accent3, palette.success, palette.warning].map(
|
||||
(color, i) => (
|
||||
<span className="swatch__dot" key={i} style={{ background: color }} />
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
<span className="swatch__name">{theme.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="settings__group">
|
||||
<span className="settings__label">{t('language')}</span>
|
||||
<div className="segmented" role="group" aria-label={t('language')}>
|
||||
<button aria-pressed={lang === 'cs'} onClick={() => setLang('cs')}>
|
||||
Čeština
|
||||
</button>
|
||||
<button aria-pressed={lang === 'en'} onClick={() => setLang('en')}>
|
||||
English
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
155
web/src/app/stats/StatsScreen.tsx
Normal file
155
web/src/app/stats/StatsScreen.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
'use client';
|
||||
|
||||
import { useApp } from '@/app/providers';
|
||||
import { TopBar } from '@/components/TopBar';
|
||||
import { useProgress } from '@/lib/progress';
|
||||
|
||||
export interface SubjectStats {
|
||||
id: string;
|
||||
key: string;
|
||||
lessons: {
|
||||
id: string;
|
||||
number: number;
|
||||
titleKey: string | null;
|
||||
title: string | null;
|
||||
taskCount: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress overview: totals at the top, then a bar per subject and a per-lesson
|
||||
* breakdown, following the layout of the desktop statistics page.
|
||||
*/
|
||||
export function StatsScreen({
|
||||
tracked,
|
||||
locked,
|
||||
}: {
|
||||
tracked: SubjectStats[];
|
||||
locked: { id: string; key: string }[];
|
||||
}) {
|
||||
const { t, tOpt } = useApp();
|
||||
const { doneCount, ready } = useProgress();
|
||||
|
||||
const totals = tracked.reduce(
|
||||
(acc, subject) => {
|
||||
const total = subject.lessons.reduce((a, l) => a + l.taskCount, 0);
|
||||
const done = subject.lessons.reduce((a, l) => a + doneCount(subject.id, l.id), 0);
|
||||
const finished = subject.lessons.filter(
|
||||
(l) => l.taskCount > 0 && doneCount(subject.id, l.id) >= l.taskCount
|
||||
).length;
|
||||
|
||||
return {
|
||||
done: acc.done + done,
|
||||
total: acc.total + total,
|
||||
finishedUnits: acc.finishedUnits + finished,
|
||||
openUnits: acc.openUnits + subject.lessons.length,
|
||||
};
|
||||
},
|
||||
{ done: 0, total: 0, finishedUnits: 0, openUnits: 0 }
|
||||
);
|
||||
|
||||
const percent = totals.total ? Math.round((totals.done / totals.total) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<TopBar title={t('stats_title')} backHref="/subjects" showTools={false} />
|
||||
|
||||
<main className="page">
|
||||
<div className="page__head">
|
||||
<h1 className="page__title">{t('stats_title')}</h1>
|
||||
<p className="page__sub">{t('stats_sub')}</p>
|
||||
</div>
|
||||
|
||||
<div className="summary">
|
||||
<Cell label={t('stats_ex_label')} value={ready ? `${totals.done} / ${totals.total}` : '—'} />
|
||||
<Cell label={t('stats_pct_label')} value={ready ? `${percent} %` : '—'} />
|
||||
<Cell
|
||||
label={t('stats_units_label')}
|
||||
value={ready ? `${totals.finishedUnits} / ${totals.openUnits}` : '—'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2 className="settings__label">{t('stats_section')}</h2>
|
||||
|
||||
{tracked.map((subject) => {
|
||||
const total = subject.lessons.reduce((a, l) => a + l.taskCount, 0);
|
||||
const done = subject.lessons.reduce((a, l) => a + doneCount(subject.id, l.id), 0);
|
||||
|
||||
return (
|
||||
<div key={subject.id}>
|
||||
<Row
|
||||
name={t(subject.key)}
|
||||
count={total ? `${done} / ${total}` : t('stats_locked')}
|
||||
ratio={total ? done / total : 0}
|
||||
/>
|
||||
|
||||
{/* Per-lesson detail, only where a lesson has tasks. */}
|
||||
<div style={{ paddingLeft: 16 }}>
|
||||
{subject.lessons
|
||||
.filter((lesson) => lesson.taskCount > 0)
|
||||
.map((lesson) => {
|
||||
const lessonDone = doneCount(subject.id, lesson.id);
|
||||
return (
|
||||
<Row
|
||||
key={lesson.id}
|
||||
name={`${lesson.number}. ${
|
||||
tOpt(lesson.titleKey) ?? lesson.title ?? lesson.id
|
||||
}`}
|
||||
count={`${lessonDone} / ${lesson.taskCount}`}
|
||||
ratio={lessonDone / lesson.taskCount}
|
||||
small
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{locked.map((subject) => (
|
||||
<Row key={subject.id} name={t(subject.key)} count={t('stats_locked')} ratio={0} dim />
|
||||
))}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Cell = ({ label, value }: { label: string; value: string }) => (
|
||||
<div className="summary__cell">
|
||||
<div className="summary__value tabular">{value}</div>
|
||||
<div className="summary__label">{label}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function Row({
|
||||
name,
|
||||
count,
|
||||
ratio,
|
||||
small,
|
||||
dim,
|
||||
}: {
|
||||
name: string;
|
||||
count: string;
|
||||
ratio: number;
|
||||
small?: boolean;
|
||||
dim?: boolean;
|
||||
}) {
|
||||
const complete = ratio >= 1;
|
||||
|
||||
return (
|
||||
<div className="stat-row" style={{ opacity: dim ? 0.5 : 1 }}>
|
||||
<div className="stat-row__head">
|
||||
<span className="stat-row__name" style={small ? { fontSize: '0.9rem' } : undefined}>
|
||||
{name}
|
||||
</span>
|
||||
<span className="stat-row__count">{count}</span>
|
||||
</div>
|
||||
<div className="bar">
|
||||
<div
|
||||
className={`bar__fill${complete ? ' bar__fill--done' : ''}`}
|
||||
style={{ width: `${Math.min(100, ratio * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
web/src/app/stats/page.tsx
Normal file
29
web/src/app/stats/page.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { getLessons, getSubjects } from '@/content';
|
||||
import { StatsScreen, type SubjectStats } from './StatsScreen';
|
||||
|
||||
export default async function StatsPage() {
|
||||
const subjects = await getSubjects();
|
||||
|
||||
const tracked: SubjectStats[] = await Promise.all(
|
||||
subjects
|
||||
.filter((s) => s.unlocked)
|
||||
.map(async (subject) => {
|
||||
const lessons = await getLessons(subject.id);
|
||||
return {
|
||||
id: subject.id,
|
||||
key: subject.key,
|
||||
lessons: lessons.map((l) => ({
|
||||
id: l.id,
|
||||
number: l.number,
|
||||
titleKey: l.titleKey ?? null,
|
||||
title: l.title?.cs ?? null,
|
||||
taskCount: l.tasks.length,
|
||||
})),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const locked = subjects.filter((s) => !s.unlocked).map((s) => ({ id: s.id, key: s.key }));
|
||||
|
||||
return <StatsScreen tracked={tracked} locked={locked} />;
|
||||
}
|
||||
58
web/src/app/subjects/SubjectsScreen.tsx
Normal file
58
web/src/app/subjects/SubjectsScreen.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { Subject } from '@/content';
|
||||
import { useApp } from '@/app/providers';
|
||||
import { TopBar } from '@/components/TopBar';
|
||||
import { LockIcon, SUBJECT_ICONS } from '@/components/icons';
|
||||
|
||||
/** Subject picker. Locked subjects are dimmed placeholders, as on the desktop. */
|
||||
export function SubjectsScreen({ subjects }: { subjects: Subject[] }) {
|
||||
const { t } = useApp();
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<TopBar title={t('subjects_title')} backHref="/" />
|
||||
|
||||
<main className="page">
|
||||
<div className="page__head">
|
||||
<h1 className="page__title">{t('subjects_title')}</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid">
|
||||
{subjects.map((subject) => (
|
||||
<SubjectCard key={subject.id} subject={subject} label={t(subject.key)} />
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SubjectCard({ subject, label }: { subject: Subject; label: string }) {
|
||||
const Icon = subject.icon ? SUBJECT_ICONS[subject.icon] : null;
|
||||
|
||||
const inner = (
|
||||
<>
|
||||
<span className="row" style={{ color: 'var(--accent2)' }}>
|
||||
{Icon ? <Icon /> : <LockIcon size={24} />}
|
||||
</span>
|
||||
<h2 className="card__title">{label}</h2>
|
||||
</>
|
||||
);
|
||||
|
||||
if (!subject.unlocked || !subject.route) {
|
||||
return (
|
||||
<div className="card card--locked" aria-disabled>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className="card card--interactive" href={subject.route}>
|
||||
{inner}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
6
web/src/app/subjects/page.tsx
Normal file
6
web/src/app/subjects/page.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { getSubjects } from '@/content';
|
||||
import { SubjectsScreen } from './SubjectsScreen';
|
||||
|
||||
export default async function SubjectsPage() {
|
||||
return <SubjectsScreen subjects={await getSubjects()} />;
|
||||
}
|
||||
127
web/src/components/LessonScreen.tsx
Normal file
127
web/src/components/LessonScreen.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { Lesson } from '@/content';
|
||||
import { useApp } from '@/app/providers';
|
||||
import { useProgress } from '@/lib/progress';
|
||||
import { PathMap, type PathNode } from './PathMap';
|
||||
import { Slides } from './Slides';
|
||||
import { TopBar } from './TopBar';
|
||||
import { TaskRenderer } from './exercises/TaskRenderer';
|
||||
|
||||
/**
|
||||
* A lesson: optional theory slides, a map of exercise bubbles and the exercise
|
||||
* itself. All three live on one route and swap in place, which keeps the
|
||||
* student's progress state without a round trip.
|
||||
*/
|
||||
export function LessonScreen({
|
||||
lesson,
|
||||
backHref,
|
||||
vocabHref,
|
||||
}: {
|
||||
lesson: Lesson;
|
||||
backHref: string;
|
||||
vocabHref?: string;
|
||||
}) {
|
||||
const { t, tOpt } = useApp();
|
||||
const { isDone, markDone, ready } = useProgress();
|
||||
|
||||
const hasSlides = lesson.slides.length > 0;
|
||||
const [view, setView] = useState<'slides' | 'map'>(hasSlides ? 'slides' : 'map');
|
||||
const [taskNumber, setTaskNumber] = useState<number | null>(null);
|
||||
|
||||
const title = tOpt(lesson.titleKey) ?? lesson.title?.cs ?? lesson.id;
|
||||
const subtitle = tOpt(lesson.subtitleKey) ?? lesson.subtitle?.cs;
|
||||
|
||||
const task = taskNumber === null ? null : lesson.tasks.find((x) => x.number === taskNumber);
|
||||
|
||||
/* --- exercise view --- */
|
||||
if (task) {
|
||||
const heading = task.heading ?? task.title;
|
||||
return (
|
||||
<div className="shell">
|
||||
<TopBar title={heading} />
|
||||
<main className="page">
|
||||
<div className="page__head">
|
||||
<h1 className="page__title">{heading}</h1>
|
||||
</div>
|
||||
|
||||
<TaskRenderer
|
||||
key={task.number}
|
||||
task={task}
|
||||
subtitle={tOpt(task.subtitleKey) ?? task.subtitle}
|
||||
onSolved={() => markDone(lesson.subject, lesson.id, task.number)}
|
||||
/>
|
||||
|
||||
<div className="actions" style={{ marginTop: 28 }}>
|
||||
<button className="btn btn--ghost" onClick={() => setTaskNumber(null)}>
|
||||
{t('back')}
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* --- theory view --- */
|
||||
if (view === 'slides') {
|
||||
return (
|
||||
<div className="shell">
|
||||
<TopBar title={title} backHref={backHref} />
|
||||
<main className="page">
|
||||
<div className="page__head">
|
||||
<h1 className="page__title">{title}</h1>
|
||||
{subtitle && <p className="page__sub">{subtitle}</p>}
|
||||
</div>
|
||||
|
||||
<Slides slides={lesson.slides} onFinish={() => setView('map')} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* --- exercise map --- */
|
||||
const nodes: PathNode[] = lesson.tasks.map((x) => ({
|
||||
number: x.number,
|
||||
label: x.title,
|
||||
locked: false,
|
||||
done: ready && isDone(lesson.subject, lesson.id, x.number),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<TopBar title={title} backHref={backHref} />
|
||||
<main className="page">
|
||||
<div className="page__head">
|
||||
<h1 className="page__title">{title}</h1>
|
||||
{subtitle && <p className="page__sub">{subtitle}</p>}
|
||||
</div>
|
||||
|
||||
{/* A single-task lesson (every network quiz) needs no bubble map. */}
|
||||
{lesson.tasks.length === 1 ? (
|
||||
<div className="stack">
|
||||
<button className="btn" onClick={() => setTaskNumber(lesson.tasks[0].number)}>
|
||||
{lesson.tasks[0].title}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<PathMap nodes={nodes} onSelect={(node) => setTaskNumber(node.number)} />
|
||||
)}
|
||||
|
||||
<div className="actions" style={{ marginTop: 32 }}>
|
||||
{hasSlides && (
|
||||
<button className="btn btn--ghost" onClick={() => setView('slides')}>
|
||||
{t('net_slide_prev')}
|
||||
</button>
|
||||
)}
|
||||
{vocabHref && (
|
||||
<a className="btn btn--ghost" href={vocabHref}>
|
||||
Vokabeltraining
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
151
web/src/components/PathMap.tsx
Normal file
151
web/src/components/PathMap.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { CheckIcon, LockIcon } from './icons';
|
||||
|
||||
export interface PathNode {
|
||||
number: number;
|
||||
label: string;
|
||||
/** Destination for nodes that navigate; omit when using `onSelect`. */
|
||||
href?: string | null;
|
||||
locked: boolean;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serpentine learning path.
|
||||
*
|
||||
* The desktop version measures the window and folds the rail into rows; here
|
||||
* the node count per row is derived from the viewport width and rows alternate
|
||||
* direction in CSS, so the shape survives a resize without measuring anything.
|
||||
*/
|
||||
export function PathMap({
|
||||
nodes,
|
||||
finish,
|
||||
onSelect,
|
||||
}: {
|
||||
nodes: PathNode[];
|
||||
finish?: boolean;
|
||||
/** Used instead of `href` when a node selects something on the same page. */
|
||||
onSelect?: (node: PathNode) => void;
|
||||
}) {
|
||||
const perRow = useNodesPerRow();
|
||||
const rows: PathNode[][] = [];
|
||||
|
||||
for (let i = 0; i < nodes.length; i += perRow) {
|
||||
rows.push(nodes.slice(i, i + perRow));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="path">
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div className="path__row" key={rowIndex}>
|
||||
{row.map((node, i) => (
|
||||
<Fragmentish key={node.number}>
|
||||
{i > 0 && <Rail done={row[i - 1].done} />}
|
||||
<PathNodeButton node={node} onSelect={onSelect} />
|
||||
</Fragmentish>
|
||||
))}
|
||||
|
||||
{/* Keep the last row left-aligned instead of stretching its nodes. */}
|
||||
{row.length < perRow && <span className="spacer" />}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{finish && (
|
||||
<div className="path__row">
|
||||
<span className="node node--finish" aria-label="Finish">
|
||||
<span className="node__number">★</span>
|
||||
</span>
|
||||
<span className="spacer" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Rail = ({ done }: { done: boolean }) => (
|
||||
<span className={`path__rail${done ? ' path__rail--done' : ''}`} aria-hidden />
|
||||
);
|
||||
|
||||
/** Avoids importing Fragment just to group a rail with its node. */
|
||||
function Fragmentish({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
function PathNodeButton({
|
||||
node,
|
||||
onSelect,
|
||||
}: {
|
||||
node: PathNode;
|
||||
onSelect?: (node: PathNode) => void;
|
||||
}) {
|
||||
const state = node.locked ? 'locked' : node.done ? 'done' : 'current';
|
||||
const className = `node node--${state}`;
|
||||
|
||||
const inner = (
|
||||
<>
|
||||
{node.done ? (
|
||||
<CheckIcon />
|
||||
) : node.locked ? (
|
||||
<LockIcon />
|
||||
) : (
|
||||
<span className="node__number">{node.number}</span>
|
||||
)}
|
||||
<span className="node__label">{node.label}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
if (node.locked) {
|
||||
return (
|
||||
<span className={className} aria-disabled title={node.label}>
|
||||
{inner}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (onSelect) {
|
||||
return (
|
||||
<button className={className} title={node.label} onClick={() => onSelect(node)}>
|
||||
{inner}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (!node.href) {
|
||||
return (
|
||||
<span className={className} aria-disabled title={node.label}>
|
||||
{inner}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className={className} href={node.href} title={node.label}>
|
||||
{inner}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
/** Node count per serpentine row, chosen to keep labels legible on phones. */
|
||||
function useNodesPerRow(): number {
|
||||
const [perRow, setPerRow] = useState(4);
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
const w = window.innerWidth;
|
||||
if (w < 420) setPerRow(2);
|
||||
else if (w < 620) setPerRow(3);
|
||||
else if (w < 860) setPerRow(4);
|
||||
else setPerRow(5);
|
||||
};
|
||||
|
||||
update();
|
||||
window.addEventListener('resize', update);
|
||||
return () => window.removeEventListener('resize', update);
|
||||
}, []);
|
||||
|
||||
return perRow;
|
||||
}
|
||||
71
web/src/components/PathScreen.tsx
Normal file
71
web/src/components/PathScreen.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
'use client';
|
||||
|
||||
import type { Path } from '@/content';
|
||||
import { useApp } from '@/app/providers';
|
||||
import { useProgress } from '@/lib/progress';
|
||||
import { PathMap, type PathNode } from './PathMap';
|
||||
import { TopBar } from './TopBar';
|
||||
|
||||
export interface PathScreenProps {
|
||||
path: Path;
|
||||
/** i18n key for the heading; falls back to `title` when it has no key. */
|
||||
titleKey?: string;
|
||||
title?: string;
|
||||
subtitleKey?: string;
|
||||
subtitle?: string;
|
||||
backHref: string;
|
||||
/** Base URL that each unit slug is appended to. */
|
||||
hrefBase: string;
|
||||
/** Tasks per lesson, used to decide whether a unit counts as finished. */
|
||||
taskCounts: Record<string, number>;
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
/** Renders a subject's serpentine unit path with live completion state. */
|
||||
export function PathScreen({
|
||||
path,
|
||||
titleKey,
|
||||
title,
|
||||
subtitleKey,
|
||||
subtitle,
|
||||
backHref,
|
||||
hrefBase,
|
||||
taskCounts,
|
||||
finish,
|
||||
}: PathScreenProps) {
|
||||
const { tOpt } = useApp();
|
||||
const { doneCount, ready } = useProgress();
|
||||
|
||||
const nodes: PathNode[] = path.units.map((unit) => {
|
||||
const total = unit.lesson ? taskCounts[unit.lesson] ?? 0 : 0;
|
||||
const done = unit.lesson ? doneCount(path.subject, unit.lesson) : 0;
|
||||
|
||||
return {
|
||||
number: unit.number,
|
||||
label: tOpt(unit.titleKey) ?? unit.title?.cs ?? String(unit.number),
|
||||
// A node either opens a lesson under `hrefBase` or links on to its own path.
|
||||
href: unit.lesson ? `${hrefBase}/${unit.lesson}` : unit.route ?? null,
|
||||
locked: !unit.unlocked,
|
||||
// Only claim completion once progress has loaded, to avoid a flash of green.
|
||||
done: ready && total > 0 && done >= total,
|
||||
};
|
||||
});
|
||||
|
||||
const heading = tOpt(titleKey) ?? title ?? '';
|
||||
const sub = tOpt(subtitleKey) ?? subtitle;
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<TopBar title={heading} backHref={backHref} />
|
||||
|
||||
<main className="page">
|
||||
<div className="page__head">
|
||||
<h1 className="page__title">{heading}</h1>
|
||||
{sub && <p className="page__sub">{sub}</p>}
|
||||
</div>
|
||||
|
||||
<PathMap nodes={nodes} finish={finish} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
61
web/src/components/Slides.tsx
Normal file
61
web/src/components/Slides.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { Slide } from '@/content';
|
||||
import { useApp } from '@/app/providers';
|
||||
|
||||
/** Theory note-cards with prev/next navigation, as in `build_net_unit_page`. */
|
||||
export function Slides({ slides, onFinish }: { slides: Slide[]; onFinish: () => void }) {
|
||||
const { t } = useApp();
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
if (slides.length === 0) return null;
|
||||
|
||||
const slide = slides[index];
|
||||
const last = index === slides.length - 1;
|
||||
|
||||
return (
|
||||
<div className="stack">
|
||||
<article className="slide">
|
||||
<span className="slide__kicker">{slide.kicker}</span>
|
||||
<h2 className="slide__title">{slide.title}</h2>
|
||||
{slide.tip && <p className="slide__tip">{slide.tip}</p>}
|
||||
|
||||
<ul className="slide__lines">
|
||||
{slide.lines.map((line, i) => (
|
||||
<li key={i}>{line}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<div className="slides__nav">
|
||||
<button
|
||||
className="btn btn--ghost"
|
||||
disabled={index === 0}
|
||||
onClick={() => setIndex((i) => i - 1)}
|
||||
>
|
||||
{t('net_slide_prev')}
|
||||
</button>
|
||||
|
||||
<div className="dots" role="tablist" aria-label={slide.title}>
|
||||
{slides.map((s, i) => (
|
||||
<button
|
||||
key={i}
|
||||
role="tab"
|
||||
aria-selected={i === index}
|
||||
// Only the active dot carries aria-current; `false` still reads as current.
|
||||
aria-current={i === index ? 'true' : undefined}
|
||||
aria-label={s.title}
|
||||
onClick={() => setIndex(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button className="btn" onClick={() => (last ? onFinish() : setIndex((i) => i + 1))}>
|
||||
{last ? t('net_slide_start') : t('net_slide_next')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
web/src/components/TopBar.tsx
Normal file
52
web/src/components/TopBar.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { useApp } from '@/app/providers';
|
||||
import { BackIcon, ChartIcon, GearIcon } from './icons';
|
||||
|
||||
/**
|
||||
* Header shared by every screen: a circular back button, the page title and the
|
||||
* statistics / settings shortcuts, mirroring the desktop app's top bar.
|
||||
*/
|
||||
export function TopBar({
|
||||
title,
|
||||
backHref,
|
||||
showTools = true,
|
||||
}: {
|
||||
title: string;
|
||||
/** Explicit destination; falls back to browser history when omitted. */
|
||||
backHref?: string;
|
||||
showTools?: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { t } = useApp();
|
||||
|
||||
return (
|
||||
<header className="topbar">
|
||||
{backHref ? (
|
||||
<Link className="icon-btn" href={backHref} aria-label={t('back')}>
|
||||
<BackIcon />
|
||||
</Link>
|
||||
) : (
|
||||
<button className="icon-btn" onClick={() => router.back()} aria-label={t('back')}>
|
||||
<BackIcon />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<span className="topbar__title">{title}</span>
|
||||
|
||||
{showTools && (
|
||||
<>
|
||||
<Link className="icon-btn" href="/stats" aria-label={t('stats_title')}>
|
||||
<ChartIcon />
|
||||
</Link>
|
||||
<Link className="icon-btn" href="/settings" aria-label={t('settings_title')}>
|
||||
<GearIcon />
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
117
web/src/components/exercises/AssemblyTask.tsx
Normal file
117
web/src/components/exercises/AssemblyTask.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type { AssemblyExercise } from '@/content';
|
||||
import { TaskShell, type CheckState, type TaskFrame } from './TaskShell';
|
||||
|
||||
/**
|
||||
* Build a sentence by clicking scrambled words in the right order.
|
||||
*
|
||||
* The desktop app uses drag and drop; clicking is used here so the exercise
|
||||
* works the same on a phone. Clicking a placed word sends it back to the bank.
|
||||
*/
|
||||
export function AssemblyTask({
|
||||
items,
|
||||
onSolved,
|
||||
...frame
|
||||
}: TaskFrame & {
|
||||
items: AssemblyExercise[];
|
||||
onSolved: () => void;
|
||||
}) {
|
||||
// Scramble once per mount so re-renders do not reshuffle mid-exercise.
|
||||
const banks = useMemo(() => items.map((item) => shuffle(item.words)), [items]);
|
||||
|
||||
const [placed, setPlaced] = useState<number[][]>(() => items.map(() => []));
|
||||
const [state, setState] = useState<CheckState>('idle');
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
const rowCorrect = (i: number) => {
|
||||
const built = placed[i].map((bankIndex) => banks[i][bankIndex]);
|
||||
return built.length === items[i].words.length && built.every((w, j) => w === items[i].words[j]);
|
||||
};
|
||||
|
||||
const check = () => {
|
||||
setChecked(true);
|
||||
const allCorrect = items.every((_, i) => rowCorrect(i));
|
||||
setState(allCorrect ? 'ok' : 'wrong');
|
||||
if (allCorrect) onSolved();
|
||||
};
|
||||
|
||||
const place = (item: number, bankIndex: number) => {
|
||||
setPlaced((prev) => prev.map((row, i) => (i === item ? [...row, bankIndex] : row)));
|
||||
setChecked(false);
|
||||
setState('idle');
|
||||
};
|
||||
|
||||
const remove = (item: number, position: number) => {
|
||||
setPlaced((prev) =>
|
||||
prev.map((row, i) => (i === item ? row.filter((_, p) => p !== position) : row))
|
||||
);
|
||||
setChecked(false);
|
||||
setState('idle');
|
||||
};
|
||||
|
||||
return (
|
||||
<TaskShell
|
||||
{...frame}
|
||||
state={state}
|
||||
onCheck={check}
|
||||
onReveal={() => setRevealed(true)}
|
||||
revealed={revealed}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
const status = checked ? (rowCorrect(i) ? ' slot--ok' : ' slot--wrong') : '';
|
||||
|
||||
return (
|
||||
<div className="ex__item" key={i}>
|
||||
{item.prompt && (
|
||||
<p className="ex__prompt">
|
||||
<span className="ex__num">{i + 1}</span>
|
||||
{item.prompt}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className={`slot${status}`}>
|
||||
{placed[i].length === 0 && (
|
||||
<span className="slot__placeholder">…</span>
|
||||
)}
|
||||
{placed[i].map((bankIndex, position) => (
|
||||
<button className="chip" key={position} onClick={() => remove(i, position)}>
|
||||
{banks[i][bankIndex]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bank">
|
||||
{banks[i].map((word, bankIndex) => (
|
||||
<button
|
||||
className={`chip${placed[i].includes(bankIndex) ? ' chip--used' : ''}`}
|
||||
key={bankIndex}
|
||||
disabled={placed[i].includes(bankIndex)}
|
||||
onClick={() => place(i, bankIndex)}
|
||||
>
|
||||
{word}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{item.meaning && <p className="ex__meaning">{item.meaning}</p>}
|
||||
{revealed && <p className="ex__model">{item.words.join(' ')}</p>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TaskShell>
|
||||
);
|
||||
}
|
||||
|
||||
/** Fisher-Yates, matching `shuffle_indices` in the desktop app. */
|
||||
function shuffle<T>(input: readonly T[]): T[] {
|
||||
const out = [...input];
|
||||
for (let i = out.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[out[i], out[j]] = [out[j], out[i]];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
117
web/src/components/exercises/AssignTask.tsx
Normal file
117
web/src/components/exercises/AssignTask.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type { AssignExercise } from '@/content';
|
||||
import { useApp } from '@/app/providers';
|
||||
import { TaskShell, type CheckState, type TaskFrame } from './TaskShell';
|
||||
|
||||
/**
|
||||
* Sort cards into named groups.
|
||||
*
|
||||
* Mirrors the desktop interaction: select a group, then click cards to move
|
||||
* them in; clicking a card inside a group returns it to the pool.
|
||||
*/
|
||||
export function AssignTask({
|
||||
exercise,
|
||||
onSolved,
|
||||
...frame
|
||||
}: TaskFrame & {
|
||||
exercise: AssignExercise;
|
||||
onSolved: () => void;
|
||||
}) {
|
||||
const { t } = useApp();
|
||||
const order = useMemo(() => shuffle(exercise.items.map((_, i) => i)), [exercise]);
|
||||
|
||||
/** Group index per card, or null while the card is still in the pool. */
|
||||
const [assigned, setAssigned] = useState<(number | null)[]>(() => exercise.items.map(() => null));
|
||||
const [activeGroup, setActiveGroup] = useState(0);
|
||||
const [state, setState] = useState<CheckState>('idle');
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const check = () => {
|
||||
setChecked(true);
|
||||
const allCorrect = exercise.items.every((item, i) => assigned[i] === item.group);
|
||||
setState(allCorrect ? 'ok' : 'wrong');
|
||||
if (allCorrect) onSolved();
|
||||
};
|
||||
|
||||
const move = (card: number, group: number | null) => {
|
||||
setAssigned((prev) => prev.map((g, i) => (i === card ? group : g)));
|
||||
setChecked(false);
|
||||
setState('idle');
|
||||
};
|
||||
|
||||
const cardLabel = (i: number) => {
|
||||
const item = exercise.items[i];
|
||||
return `${item.emoji ? `${item.emoji} ` : ''}${item.label}`;
|
||||
};
|
||||
|
||||
const cardStatus = (i: number) => {
|
||||
if (!checked || assigned[i] === null) return '';
|
||||
return assigned[i] === exercise.items[i].group ? ' gap--ok' : ' gap--wrong';
|
||||
};
|
||||
|
||||
return (
|
||||
<TaskShell {...frame} state={state} onCheck={check}>
|
||||
<div className="ex__item">
|
||||
<p className="ex__prompt">{t('assign_hint')}</p>
|
||||
|
||||
<div className="segmented" role="group">
|
||||
{exercise.groups.map((group, gi) => (
|
||||
<button key={gi} aria-pressed={activeGroup === gi} onClick={() => setActiveGroup(gi)}>
|
||||
{group}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bank">
|
||||
{order
|
||||
.filter((i) => assigned[i] === null)
|
||||
.map((i) => (
|
||||
<button className="chip" key={i} onClick={() => move(i, activeGroup)}>
|
||||
{cardLabel(i)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="groups">
|
||||
{exercise.groups.map((group, gi) => (
|
||||
<div className="group" key={gi}>
|
||||
<span className="group__title">{group}</span>
|
||||
{order
|
||||
.filter((i) => assigned[i] === gi)
|
||||
.map((i) => (
|
||||
<button className={`chip${cardStatus(i)}`} key={i} onClick={() => move(i, null)}>
|
||||
{cardLabel(i)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{checked && (
|
||||
<ul className="ex__meaning" style={{ listStyle: 'none', padding: 0 }}>
|
||||
{exercise.items.map(
|
||||
(item, i) =>
|
||||
item.meaning && (
|
||||
<li key={i}>
|
||||
{item.label} — {item.meaning}
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</TaskShell>
|
||||
);
|
||||
}
|
||||
|
||||
function shuffle<T>(input: readonly T[]): T[] {
|
||||
const out = [...input];
|
||||
for (let i = out.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[out[i], out[j]] = [out[j], out[i]];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
73
web/src/components/exercises/ChoiceTask.tsx
Normal file
73
web/src/components/exercises/ChoiceTask.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { ChoiceExercise } from '@/content';
|
||||
import { TaskShell, type CheckState, type TaskFrame } from './TaskShell';
|
||||
|
||||
/** Multiple choice, one correct option per question. */
|
||||
export function ChoiceTask({
|
||||
items,
|
||||
onSolved,
|
||||
...frame
|
||||
}: TaskFrame & {
|
||||
items: ChoiceExercise[];
|
||||
onSolved: () => void;
|
||||
}) {
|
||||
const [picked, setPicked] = useState<(number | null)[]>(() => items.map(() => null));
|
||||
const [state, setState] = useState<CheckState>('idle');
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const check = () => {
|
||||
setChecked(true);
|
||||
const allCorrect = items.every((q, i) => picked[i] === q.correct);
|
||||
setState(allCorrect ? 'ok' : 'wrong');
|
||||
if (allCorrect) onSolved();
|
||||
};
|
||||
|
||||
const pick = (question: number, option: number) => {
|
||||
setPicked((prev) => prev.map((v, i) => (i === question ? option : v)));
|
||||
// Editing an answer clears the previous verdict so the colours stay honest.
|
||||
setChecked(false);
|
||||
setState('idle');
|
||||
};
|
||||
|
||||
return (
|
||||
<TaskShell {...frame} state={state} onCheck={check}>
|
||||
{items.map((q, qi) => (
|
||||
<div className="ex__item" key={qi}>
|
||||
<p className="ex__prompt">
|
||||
<span className="ex__num">{qi + 1}</span>
|
||||
{q.prompt}
|
||||
</p>
|
||||
|
||||
<div className="opts">
|
||||
{q.options.map((option, oi) => {
|
||||
const selected = picked[qi] === oi;
|
||||
let modifier = '';
|
||||
if (checked && selected) modifier = oi === q.correct ? ' opt--ok' : ' opt--wrong';
|
||||
// After a wrong guess, also point at the right answer.
|
||||
if (checked && !selected && oi === q.correct && picked[qi] !== null) {
|
||||
modifier = ' opt--ok';
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`opt${modifier}`}
|
||||
key={oi}
|
||||
aria-pressed={selected}
|
||||
onClick={() => pick(qi, oi)}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{q.hint && checked && <p className="ex__meaning">{q.hint}</p>}
|
||||
{q.meaning && checked && <p className="ex__meaning">{q.meaning}</p>}
|
||||
</div>
|
||||
))}
|
||||
</TaskShell>
|
||||
);
|
||||
}
|
||||
96
web/src/components/exercises/DialogueTask.tsx
Normal file
96
web/src/components/exercises/DialogueTask.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { DialogueExercise } from '@/content';
|
||||
import { isCorrect, primaryAnswer } from '@/lib/answers';
|
||||
import { TaskShell, type CheckState, type TaskFrame } from './TaskShell';
|
||||
|
||||
/** Short dialogues with one missing word per line, picked from a shared pool. */
|
||||
export function DialogueTask({
|
||||
items,
|
||||
pool,
|
||||
onSolved,
|
||||
...frame
|
||||
}: TaskFrame & {
|
||||
items: DialogueExercise[];
|
||||
pool?: string[];
|
||||
onSolved: () => void;
|
||||
}) {
|
||||
const [values, setValues] = useState<string[][]>(() => items.map((d) => d.rows.map(() => '')));
|
||||
const [state, setState] = useState<CheckState>('idle');
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
const check = () => {
|
||||
setChecked(true);
|
||||
const allCorrect = items.every((dialogue, d) =>
|
||||
dialogue.rows.every((row, r) => isCorrect(values[d][r] ?? '', row.answer))
|
||||
);
|
||||
setState(allCorrect ? 'ok' : 'wrong');
|
||||
if (allCorrect) onSolved();
|
||||
};
|
||||
|
||||
const set = (dialogue: number, row: number, value: string) => {
|
||||
setValues((prev) =>
|
||||
prev.map((rows, d) => (d === dialogue ? rows.map((v, r) => (r === row ? value : v)) : rows))
|
||||
);
|
||||
setChecked(false);
|
||||
setState('idle');
|
||||
};
|
||||
|
||||
return (
|
||||
<TaskShell
|
||||
{...frame}
|
||||
state={state}
|
||||
onCheck={check}
|
||||
onReveal={() => setRevealed(true)}
|
||||
revealed={revealed}
|
||||
>
|
||||
{items.map((dialogue, d) => (
|
||||
<div className="ex__item" key={d}>
|
||||
<p className="ex__prompt">{dialogue.title}</p>
|
||||
|
||||
{dialogue.rows.map((row, r) => {
|
||||
const status = checked
|
||||
? isCorrect(values[d][r] ?? '', row.answer)
|
||||
? 'ok'
|
||||
: 'wrong'
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div key={r} style={{ marginBottom: 10 }}>
|
||||
<div className="sentence">
|
||||
{row.speaker && <span className="speaker">{row.speaker}</span>}
|
||||
{row.before && <span>{row.before}</span>}
|
||||
|
||||
<select
|
||||
className={`gap${status ? ` gap--${status}` : ''}`}
|
||||
value={values[d][r] ?? ''}
|
||||
onChange={(e) => set(d, r, e.target.value)}
|
||||
>
|
||||
<option value="">—</option>
|
||||
{(pool ?? []).map((word) => (
|
||||
<option key={word} value={word}>
|
||||
{word}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{row.after && <span>{row.after}</span>}
|
||||
</div>
|
||||
|
||||
{row.meaning && <p className="ex__meaning">{row.meaning}</p>}
|
||||
{revealed && (
|
||||
<p className="ex__model">
|
||||
{`${row.before}${primaryAnswer(row.answer)}${row.after}`.trim()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</TaskShell>
|
||||
);
|
||||
}
|
||||
84
web/src/components/exercises/FieldsTask.tsx
Normal file
84
web/src/components/exercises/FieldsTask.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { FieldsExercise } from '@/content';
|
||||
import { isCorrect } from '@/lib/answers';
|
||||
import { TaskShell, type CheckState, type TaskFrame } from './TaskShell';
|
||||
|
||||
/**
|
||||
* A prompt answered through several labelled fields.
|
||||
*
|
||||
* The Czech grammar exercises use this for questions with more than one part —
|
||||
* "determine the gender, number and case" fills three fields from one sentence.
|
||||
*/
|
||||
export function FieldsTask({
|
||||
items,
|
||||
onSolved,
|
||||
...frame
|
||||
}: TaskFrame & {
|
||||
items: FieldsExercise[];
|
||||
onSolved: () => void;
|
||||
}) {
|
||||
const [values, setValues] = useState<string[][]>(() =>
|
||||
items.map((item) => item.fields.map(() => ''))
|
||||
);
|
||||
const [state, setState] = useState<CheckState>('idle');
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
const ok = (i: number, f: number) =>
|
||||
items[i].fields[f].answers.some((a) => isCorrect(values[i][f] ?? '', a));
|
||||
|
||||
const check = () => {
|
||||
setChecked(true);
|
||||
const allCorrect = items.every((item, i) => item.fields.every((_, f) => ok(i, f)));
|
||||
setState(allCorrect ? 'ok' : 'wrong');
|
||||
if (allCorrect) onSolved();
|
||||
};
|
||||
|
||||
const set = (item: number, field: number, value: string) => {
|
||||
setValues((prev) =>
|
||||
prev.map((row, i) => (i === item ? row.map((v, f) => (f === field ? value : v)) : row))
|
||||
);
|
||||
setChecked(false);
|
||||
setState('idle');
|
||||
};
|
||||
|
||||
return (
|
||||
<TaskShell
|
||||
{...frame}
|
||||
state={state}
|
||||
onCheck={check}
|
||||
onReveal={() => setRevealed(true)}
|
||||
revealed={revealed}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<div className="ex__item" key={i}>
|
||||
<p className="ex__prompt">
|
||||
<span className="ex__num">{i + 1}</span>
|
||||
{item.prompt}
|
||||
</p>
|
||||
|
||||
<div className="fields">
|
||||
{item.fields.map((field, f) => (
|
||||
<label className="fields__field" key={f}>
|
||||
<span className="fields__label">{field.label}</span>
|
||||
<input
|
||||
className={`gap${checked ? (ok(i, f) ? ' gap--ok' : ' gap--wrong') : ''}`}
|
||||
type="text"
|
||||
value={values[i][f] ?? ''}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onChange={(e) => set(i, f, e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{revealed && item.solution && <p className="ex__model">{item.solution}</p>}
|
||||
</div>
|
||||
))}
|
||||
</TaskShell>
|
||||
);
|
||||
}
|
||||
212
web/src/components/exercises/FillTask.tsx
Normal file
212
web/src/components/exercises/FillTask.tsx
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { FillExercise } from '@/content';
|
||||
import { useApp } from '@/app/providers';
|
||||
import { isCorrect, primaryAnswer } from '@/lib/answers';
|
||||
import { TaskShell, type CheckState, type TaskFrame } from './TaskShell';
|
||||
|
||||
/**
|
||||
* Sentences with gaps.
|
||||
*
|
||||
* Every gap variant in the desktop app collapses to this component: a dropdown
|
||||
* fed from the task's word pool, or a free text field when there is no pool.
|
||||
*/
|
||||
export function FillTask({
|
||||
items,
|
||||
pool,
|
||||
altPool,
|
||||
onSolved,
|
||||
...frame
|
||||
}: TaskFrame & {
|
||||
items: FillExercise[];
|
||||
pool?: string[];
|
||||
altPool?: string[];
|
||||
onSolved: () => void;
|
||||
}) {
|
||||
const { t } = useApp();
|
||||
const [values, setValues] = useState<string[][]>(() => items.map((it) => it.answers.map(() => '')));
|
||||
const [state, setState] = useState<CheckState>('idle');
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
const check = () => {
|
||||
setChecked(true);
|
||||
const allCorrect = items.every((item, i) =>
|
||||
item.answers.every((answer, g) => isCorrect(values[i][g] ?? '', answer))
|
||||
);
|
||||
setState(allCorrect ? 'ok' : 'wrong');
|
||||
if (allCorrect) onSolved();
|
||||
};
|
||||
|
||||
const set = (item: number, gap: number, value: string) => {
|
||||
setValues((prev) =>
|
||||
prev.map((row, i) => (i === item ? row.map((v, g) => (g === gap ? value : v)) : row))
|
||||
);
|
||||
setChecked(false);
|
||||
setState('idle');
|
||||
};
|
||||
|
||||
return (
|
||||
<TaskShell
|
||||
{...frame}
|
||||
state={state}
|
||||
onCheck={check}
|
||||
onReveal={() => setRevealed(true)}
|
||||
revealed={revealed}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<div className="ex__item" key={i}>
|
||||
{item.heading && <h2 className="ex__group">{item.heading}</h2>}
|
||||
|
||||
<div className={`sentence${item.letters ? ' sentence--letters' : ''}`}>
|
||||
{item.label && <span className="ex__num">{item.label}</span>}
|
||||
{item.emoji && <span className="sentence__emoji">{item.emoji}</span>}
|
||||
{item.clue && <span className="sentence__clue">{item.clue}</span>}
|
||||
|
||||
{chunk(item).map((group, g) =>
|
||||
group === SPACE ? (
|
||||
<span key={g}> </span>
|
||||
) : (
|
||||
<span className="sentence__chunk" key={g}>
|
||||
{group.map((node, n) =>
|
||||
typeof node === 'string' ? (
|
||||
<span key={n}>{node}</span>
|
||||
) : (
|
||||
<Gap
|
||||
key={n}
|
||||
value={values[i][node.gap] ?? ''}
|
||||
onChange={(v) => set(i, node.gap, v)}
|
||||
pool={
|
||||
item.input === 'select'
|
||||
? item.gapPool?.[node.gap] === 'alt'
|
||||
? altPool
|
||||
: pool
|
||||
: undefined
|
||||
}
|
||||
placeholder={item.letters ? '' : t('your_answer')}
|
||||
letters={item.letters}
|
||||
status={
|
||||
checked
|
||||
? isCorrect(values[i][node.gap] ?? '', item.answers[node.gap])
|
||||
? 'ok'
|
||||
: 'wrong'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{item.meaning && <p className="ex__meaning">{item.meaning}</p>}
|
||||
|
||||
{item.glosses?.some(Boolean) && (
|
||||
<p className="ex__gloss">{item.glosses.filter(Boolean).join(' · ')}</p>
|
||||
)}
|
||||
|
||||
{revealed && (
|
||||
<p className="ex__model">
|
||||
{buildSolution(item)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</TaskShell>
|
||||
);
|
||||
}
|
||||
|
||||
const SPACE = Symbol('space');
|
||||
type GapRef = { gap: number };
|
||||
type Chunk = (string | GapRef)[] | typeof SPACE;
|
||||
|
||||
/**
|
||||
* Split a sentence into groups that must not be broken across lines.
|
||||
*
|
||||
* A letter gap sits inside a word, so wrapping between the gap and the letters
|
||||
* around it would tear the word in half ("Öster[re]" / "ich."). Splitting only
|
||||
* at real spaces keeps each word — gaps included — on one line.
|
||||
*/
|
||||
function chunk(item: FillExercise): Chunk[] {
|
||||
const chunks: Chunk[] = [];
|
||||
let current: (string | GapRef)[] = [];
|
||||
|
||||
const flush = () => {
|
||||
if (current.length) chunks.push(current);
|
||||
current = [];
|
||||
};
|
||||
|
||||
item.segments.forEach((segment, s) => {
|
||||
for (const part of segment.split(/(\s+)/)) {
|
||||
if (!part) continue;
|
||||
if (/^\s+$/.test(part)) {
|
||||
flush();
|
||||
chunks.push(SPACE);
|
||||
} else {
|
||||
current.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
// One gap follows every segment except the last.
|
||||
if (s < item.answers.length) current.push({ gap: s });
|
||||
});
|
||||
|
||||
flush();
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function Gap({
|
||||
value,
|
||||
onChange,
|
||||
pool,
|
||||
placeholder,
|
||||
letters,
|
||||
status,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
pool?: string[];
|
||||
placeholder: string;
|
||||
letters?: boolean;
|
||||
status?: 'ok' | 'wrong';
|
||||
}) {
|
||||
const className =
|
||||
`gap${letters ? ' gap--letters' : ''}${status ? ` gap--${status}` : ''}`;
|
||||
|
||||
if (pool?.length) {
|
||||
return (
|
||||
<select className={className} value={value} onChange={(e) => onChange(e.target.value)}>
|
||||
<option value="">—</option>
|
||||
{pool.map((word) => (
|
||||
<option key={word} value={word}>
|
||||
{word}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
className={className}
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Reassemble the sentence with its correct answers filled in. */
|
||||
function buildSolution(item: FillExercise): string {
|
||||
return item.segments
|
||||
.map((segment, s) => segment + (s < item.answers.length ? primaryAnswer(item.answers[s]) : ''))
|
||||
.join('')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
81
web/src/components/exercises/FreeTask.tsx
Normal file
81
web/src/components/exercises/FreeTask.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { FreeExercise } from '@/content';
|
||||
import { useApp } from '@/app/providers';
|
||||
import { TaskShell, type CheckState, type TaskFrame } from './TaskShell';
|
||||
|
||||
/**
|
||||
* Open questions with a model answer.
|
||||
*
|
||||
* There is no single right string, so writing something for every question
|
||||
* counts as done and reveals the samples to compare against — the same rule the
|
||||
* desktop app applies.
|
||||
*/
|
||||
export function FreeTask({
|
||||
items,
|
||||
onSolved,
|
||||
...frame
|
||||
}: TaskFrame & {
|
||||
items: FreeExercise[];
|
||||
onSolved: () => void;
|
||||
}) {
|
||||
const { t, lang } = useApp();
|
||||
const [values, setValues] = useState<string[]>(() => items.map(() => ''));
|
||||
const [state, setState] = useState<CheckState>('idle');
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
const check = () => {
|
||||
const answered = values.every((v) => v.trim().length > 0);
|
||||
setState(answered ? 'ok' : 'wrong');
|
||||
if (answered) {
|
||||
setRevealed(true);
|
||||
onSolved();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TaskShell {...frame} state={state} onCheck={check}>
|
||||
{items.map((item, i) => (
|
||||
<div className="ex__item" key={i}>
|
||||
<p className="ex__prompt">
|
||||
<span className="ex__num">{i + 1}</span>
|
||||
{item.question}
|
||||
</p>
|
||||
|
||||
{item.questionCs && <p className="ex__meaning">{t(item.questionCs)}</p>}
|
||||
|
||||
<input
|
||||
className="gap"
|
||||
style={{ width: '100%', minWidth: 0 }}
|
||||
type="text"
|
||||
value={values[i]}
|
||||
placeholder={t('your_answer')}
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setValues((prev) => prev.map((v, j) => (j === i ? value : v)));
|
||||
setState('idle');
|
||||
}}
|
||||
/>
|
||||
|
||||
{revealed && (
|
||||
<p className="ex__model">
|
||||
{item.sample}
|
||||
{item.sampleCs && (
|
||||
<>
|
||||
{' — '}
|
||||
<span className="muted">{t(item.sampleCs)}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* The umlaut tip only matters while typing German. */}
|
||||
{lang === 'cs' && <p className="ex__meaning">{t('hint_umlauts')}</p>}
|
||||
</TaskShell>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue