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
+72
View File
@@ -0,0 +1,72 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { erfordereBenutzer } from '@/lib/auth/rechte';
import { inhalte } from '@/lib/inhalte';
import { glossarAnker } from '@/lib/inhalte/glossarverweise';
import { normalisiere } from '@/lib/inhalte/laden';
import { GlossarSuche } from './GlossarSuche';
export const metadata: Metadata = { title: 'Glossar' };
export const dynamic = 'force-dynamic';
export default async function GlossarSeite({ searchParams }: { searchParams: Promise<{ suche?: string }> }) {
await erfordereBenutzer();
const parameter = await searchParams;
const alle = inhalte();
const suchbegriff = (parameter.suche ?? '').trim();
const gesucht = normalisiere(suchbegriff);
const eintraege = [...alle.glossar].sort((a, b) => a.begriff.localeCompare(b.begriff, 'de-DE'));
const gefiltert = gesucht
? eintraege.filter(
(e) =>
normalisiere(e.begriff).includes(gesucht) ||
normalisiere(e.steht_fuer ?? '').includes(gesucht) ||
normalisiere(e.erklaerung).includes(gesucht) ||
e.formen.some((f) => normalisiere(f).includes(gesucht)),
)
: eintraege;
return (
<div className="stapel">
<div>
<h1>Glossar</h1>
<p className="leise">
{alle.glossar.length} Begriffe. In den Handouts sind sie verlinkt – ein Klick führt hierher.
</p>
</div>
<GlossarSuche anfangswert={suchbegriff} />
{gefiltert.length === 0 ? (
<p className="karte">Kein Begriff passt zu &bdquo;{suchbegriff}&ldquo;.</p>
) : (
<dl className="glossar">
{gefiltert.map((eintrag) => (
<div key={eintrag.begriff} id={glossarAnker(eintrag.begriff)} className="glossar-eintrag karte">
<dt>
{eintrag.begriff}
{eintrag.steht_fuer ? <span className="glossar-langform">{eintrag.steht_fuer}</span> : null}
</dt>
<dd>
<p style={{ marginBottom: eintrag.siehe_auch.length > 0 ? '0.5rem' : 0 }}>{eintrag.erklaerung}</p>
{eintrag.siehe_auch.length > 0 ? (
<p className="leise" style={{ margin: 0 }}>
Siehe auch:{' '}
{eintrag.siehe_auch.map((verweis, index) => (
<span key={verweis}>
{index > 0 ? ', ' : ''}
<Link href={`#${glossarAnker(verweis)}`}>{verweis}</Link>
</span>
))}
</p>
) : null}
</dd>
</div>
))}
</dl>
)}
</div>
);
}