Files
lernbar/src/app/(angemeldet)/handouts/[id]/page.tsx
T
Florian WildeandClaude Opus 5 2babef9293 Baustein 10a: PWA, Offlinebetrieb, Druckansichten
Installierbar: Manifest, Symbole aus dem eigenen Zeichen erzeugt
(scripts/symbole.ts, kein Bilddienst), Verknuepfungen zu Lernmodus,
Berichtsheft und Handouts.

Offline gilt eine Regel, und die steht im Dienstarbeiter: Nur das
Lernmaterial liegt im Browserspeicher - Handouts, Merkkarten, Glossar,
die eigene Woche. Lernzielkontrollen, Berichtsheft, Bewertungen,
Feedbackgespraeche, Anhaenge und jedes PDF niemals. Das ist keine
Sparsamkeit: Was im Browsercache liegt, ist an der serverseitigen
Rechtepruefung vorbei.

Bewertungen ohne Netz gehen in eine Warteschlange und werden
nachgereicht. Die Schlange haengt an der Benutzerkennung, nicht am
Geraet - auf einem geteilten Werkstattgeraet bekaeme sonst der Zweite
die offenen Bewertungen des Ersten aufs Konto. Der Tag der Bewertung
kommt vom Geraet und wird auf heute gedeckelt (tagDerBewertung, 4
Tests): Eine vorgestellte Uhr darf das Wiederholen nicht wegstellen.

Druck: Wer die dunkle Fassung eingestellt hatte, druckte bisher schwarze
Flaechen. Im Druck wird jetzt die komplette helle Palette gesetzt, dazu
A4-Raender, keine Umbrueche mitten in Karten und Zeilen, aufgeklappte
details. Druckknoepfe bei Handout, Glossar und Auswertung.

Durchstich in scripts/pruefungen/baustein-10.mjs mit echtem Abschalten
des Netzes: 33 Pruefungen, darunter der Nachweis, dass nichts
Bewertungsrelevantes im Speicher landet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 15:47:06 +02:00

190 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Metadata } from 'next';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { erfordereBenutzer } from '@/lib/auth/rechte';
import { inhalte } from '@/lib/inhalte';
import { verlinkeGlossar } from '@/lib/inhalte/glossarverweise';
import { gelesenAm } from '@/lib/repos/fortschritt';
import { alsZeitpunktText } from '@/lib/zeit';
import { gelesenUmschalten } from './aktionen';
import { Druckknopf } from '@/komponenten/Druckknopf';
export const dynamic = 'force-dynamic';
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params;
const handout = inhalte().handoutNach.get(id.toUpperCase());
return { title: handout ? `${handout.id} – ${handout.titel}` : 'Handout' };
}
export default async function HandoutSeite({ params }: { params: Promise<{ id: string }> }) {
const benutzer = await erfordereBenutzer();
const { id } = await params;
const alle = inhalte();
const handout = alle.handoutNach.get(id.toUpperCase());
if (!handout) notFound();
const block = alle.blockNach.get(handout.block);
const lernfeld = alle.lernfeldNach.get(handout.lernfeld);
const positionen = handout.rahmenplan.map((p) => alle.positionNach.get(p)).filter((p) => p !== undefined);
const woche = alle.wocheNach.get(handout.woche);
const istAzubi = benutzer.rolle === 'azubi';
const gelesen = istAzubi ? gelesenAm(benutzer.id, handout.id) : null;
const desBlocks = alle.handoutsNachBlock.get(handout.block) ?? [];
const stelle = desBlocks.findIndex((h) => h.id === handout.id);
const vorher = stelle > 0 ? desBlocks[stelle - 1] : undefined;
const nachher = stelle < desBlocks.length - 1 ? desBlocks[stelle + 1] : undefined;
const wesentlichHtml = handout.wesentlich ? verlinkeGlossar(handout.wesentlich.html, alle.glossar) : '';
return (
<article className="stapel handout">
<nav className="leise nicht-drucken" aria-label="Wo du bist">
<Link href="/handouts">Handouts</Link>
{block ? (
<>
{' › '}
<Link href={`/handouts?block=${block.id}`}>{block.titel}</Link>
</>
) : null}
</nav>
<header className="stapel-eng">
<div className="reihe" style={{ gap: '0.6rem' }}>
<span className="handout-kuerzel handout-kuerzel-gross">{handout.id}</span>
{gelesen ? (
<span className="marke-status" data-ton="gut">
✓ gelesen am {alsZeitpunktText(gelesen)}
</span>
) : null}
</div>
<h1 style={{ marginBottom: '0.2rem' }}>{handout.titel}</h1>
<p className="handout-worum">{handout.worum}</p>
</header>
<div className="handout-zuordnung karte">
<dl>
<div>
<dt>Ausbildungsrahmenplan</dt>
<dd>
{positionen.length === 0
? '–'
: positionen.map((p) => (
<span key={p.id} title={`${p.titel} (${p.verordnung})`} className="zuordnung-marke">
<strong>{p.id}</strong> {p.kurz}
</span>
))}
</dd>
</div>
<div>
<dt>Lernfeld</dt>
<dd>
{lernfeld ? (
<span className="zuordnung-marke" title={`${lernfeld.stunden} Stunden im ${lernfeld.jahr}. Jahr`}>
<strong>{lernfeld.id}</strong> {lernfeld.titel}
</span>
) : (
'–'
)}
</dd>
</div>
<div>
<dt>Woche</dt>
<dd>
{woche ? (
<span className="zuordnung-marke">
<strong>{woche.nummer}</strong> {woche.titel}
</span>
) : (
'–'
)}
</dd>
</div>
</dl>
</div>
{handout.wesentlich ? (
<section aria-labelledby="wesentlich">
<h2 id="wesentlich">Das Wesentliche</h2>
<div className="fliesstext" dangerouslySetInnerHTML={{ __html: wesentlichHtml }} />
</section>
) : null}
{handout.uebersicht ? (
<section aria-labelledby="uebersicht">
<h2 id="uebersicht">Übersicht</h2>
<div className="tabelle-huelle fliesstext" dangerouslySetInnerHTML={{ __html: handout.uebersicht.html }} />
</section>
) : null}
<aside className="merksatz" aria-label="Merksatz">
<span className="merksatz-kennzeichnung">Merksatz</span>
<p>{handout.merksatz}</p>
</aside>
{handout.pruefungsfrage ? (
<details className="pruefungsfrage">
<summary>Prüfungsfrage anzeigen</summary>
<div className="fliesstext" dangerouslySetInnerHTML={{ __html: handout.pruefungsfrage.html }} />
</details>
) : null}
<section aria-labelledby="karten" className="karte stapel-eng">
<h2 id="karten" style={{ marginBottom: '0.3rem' }}>
Merkkarten zu diesem Handout
</h2>
<p className="leise" style={{ margin: 0 }}>
{handout.karten.length === 1 ? 'Eine Karte' : `${handout.karten.length} Karten`} – im Lernmodus tauchen sie
nach Fälligkeit auf.
</p>
<ol className="stapel-eng" style={{ margin: '0.5rem 0 0', paddingLeft: '1.2rem' }}>
{handout.karten.map((karte, index) => (
<li key={index}>
<strong>{karte.frage}</strong>
<br />
<span className="leise">{karte.antwort}</span>
</li>
))}
</ol>
</section>
{istAzubi ? (
<form action={gelesenUmschalten} className="nicht-drucken">
<input type="hidden" name="handoutId" value={handout.id} />
<input type="hidden" name="gelesen" value={gelesen ? '0' : '1'} />
<button type="submit" className={`knopf ${gelesen ? 'knopf-zweit' : 'knopf-akzent'}`}>
{gelesen ? 'Doch noch nicht gelesen' : 'Als gelesen markieren'}
</button>
</form>
) : null}
<div className="reihe nicht-drucken">
<Druckknopf beschriftung="Handout drucken" />
</div>
<nav className="reihe nicht-drucken" style={{ justifyContent: 'space-between' }} aria-label="Weitere Handouts">
{vorher ? (
<Link href={`/handouts/${vorher.id}`} className="knopf knopf-still">
← {vorher.id} {vorher.titel}
</Link>
) : (
<span />
)}
{nachher ? (
<Link href={`/handouts/${nachher.id}`} className="knopf knopf-still">
{nachher.id} {nachher.titel} →
</Link>
) : (
<span />
)}
</nav>
<p className="leise nicht-drucken" style={{ marginBottom: 0 }}>
Quelle: <code>{handout.datei}</code>
</p>
</article>
);
}