Baustein 2: Inhalte einlesen und pruefen, Handouts, Suche, Glossar

Die fachlichen Inhalte liegen als Dateien unter content/ und werden beim
Start eingelesen, geprueft und in den Speicher geladen.

- Inhaltslader mit Zod-Schemata und Querverweispruefung
- Fehlermeldungen nennen Datei und Zeile, gesammelt statt beim ersten
  Fehler geworfen; die Anwendung startet bei Fehlern nicht
- npm run inhalte:pruefen prueft ohne Start
- Handout-Ansicht mit festem Aufbau, Uebersicht nach Themenbloecken
- Volltextsuche mit Gewichtung und Trefferausschnitt
- Gelesen-Markierung je Handout
- Glossar mit Suche und automatischer Verlinkung in den Handouts
- Beispielinhalte: 4 Wochen, 12 Handouts, 24 Merkkarten, 4 Aufgaben,
  4 Checklisten, 2 Lernzielkontrollen, 24 Glossareintraege
- rahmenplan.yaml aus FIAusbV 2020 und KMK-Rahmenlehrplan, mit Fundstellen
- gray-matter entfernt, YAML-Kopf wird selbst herausgeloest
- npm run screenshots nimmt die Bilder fuer die Anleitung auf
- 39 neue Tests, insgesamt 68

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-10 07:56:38 +02:00
co-authored by Claude Opus 5
parent e856349cd4
commit 1af5613e9b
70 changed files with 4779 additions and 124 deletions
@@ -0,0 +1,51 @@
'use client';
import { useRouter } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
/**
* Suchfeld für die Handouts. Sucht beim Tippen mit kurzer Verzögerung, damit
* nicht bei jedem Buchstaben eine Anfrage losgeht, und funktioniert auch ohne
* JavaScript – dann eben erst beim Absenden.
*/
export function Suchfeld({ anfangswert }: { anfangswert: string }) {
const [wert, setzeWert] = useState(anfangswert);
const router = useRouter();
const erstesMal = useRef(true);
useEffect(() => {
if (erstesMal.current) {
erstesMal.current = false;
return;
}
const zeitgeber = setTimeout(() => {
const ziel = wert.trim() ? `/handouts?suche=${encodeURIComponent(wert.trim())}` : '/handouts';
router.replace(ziel, { scroll: false });
}, 250);
return () => clearTimeout(zeitgeber);
}, [wert, router]);
return (
<form action="/handouts" method="get" role="search" className="nicht-drucken">
<label htmlFor="suche" className="nur-lesehilfe">
Handouts durchsuchen
</label>
<div className="reihe" style={{ gap: '0.5rem', flexWrap: 'nowrap' }}>
<input
id="suche"
name="suche"
type="search"
placeholder="Suchen – z. B. Subnetting, SMART, Vererbung"
value={wert}
onChange={(e) => setzeWert(e.target.value)}
autoComplete="off"
/>
<button type="submit" className="knopf knopf-zweit" style={{ flexShrink: 0 }}>
Suchen
</button>
</div>
</form>
);
}