Baustein 4: Merkkarten mit Wiederholung nach Zeitabstand
- Abgespecktes SM-2 mit drei Bewertungsstufen statt sechs - Abstaende 1, 3, dann Abstand mal Leichtigkeit; gedeckelt bei 180 Tagen - "nochmal" bringt die Karte in derselben Sitzung wieder - Taeglicher Stapel, gedeckelt auf 20 Karten, ueberfaellige zuerst - Fuenf Faecher zur Anzeige, abgeleitet aus dem Abstand - Filter nach Themenblock fuer gezieltes Ueben - Lernmodus fuers Handy: grosse Flaechen, Antippen, Wischen, kein Hover; am Rechner zusaetzlich Leertaste und Tasten 1/2/3 - Tabellen karte und karten_verlauf, jede Bewertung wird mitgeschrieben - Merkkarten zaehlen im Wochenfortschritt mit, gelernt ab Fach 4 - Kopfzeile am Handy einzeilig, Tipp-Hervorhebung abgeschaltet - 29 neue Tests, insgesamt 133 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
103a603eb4
commit
bb592ad2bd
@@ -0,0 +1,56 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { inhalte } from '@/lib/inhalte';
|
||||
import { bewerte } from '@/lib/repos/karten';
|
||||
import { bewertungen, fachNamen, type Bewertung } from '@/lib/lernen/sm2';
|
||||
import { alsDatumText, heute } from '@/lib/zeit';
|
||||
|
||||
export type Bewertungsergebnis = {
|
||||
fehler?: string;
|
||||
/** Wann die Karte wieder drankommt, in Worten. */
|
||||
wiederAm?: string;
|
||||
fach?: number;
|
||||
fachName?: string;
|
||||
/** Kommt die Karte noch in dieser Sitzung wieder? */
|
||||
nochHeute?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Eine Selbstbewertung entgegennehmen.
|
||||
*
|
||||
* Nur der Azubi lernt – der Ausbilder sieht die Karten, bewertet sie aber nicht
|
||||
* für ihn. Sonst stünde im Lernstand etwas, das der Azubi nie gekonnt hat.
|
||||
*/
|
||||
export async function karteBewerten(karteId: string, bewertung: string): Promise<Bewertungsergebnis> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
if (benutzer.rolle !== 'azubi') {
|
||||
return { fehler: 'Nur der Azubi führt seinen eigenen Lernstand.' };
|
||||
}
|
||||
|
||||
if (!inhalte().karteNach.has(karteId)) {
|
||||
return { fehler: 'Diese Karte gibt es nicht.' };
|
||||
}
|
||||
if (!bewertungen.includes(bewertung as Bewertung)) {
|
||||
return { fehler: 'Unbekannte Bewertung.' };
|
||||
}
|
||||
|
||||
const heuteDatum = heute();
|
||||
const ergebnis = bewerte(benutzer.id, karteId, bewertung as Bewertung, heuteDatum);
|
||||
|
||||
revalidatePath('/karten');
|
||||
revalidatePath('/');
|
||||
|
||||
return {
|
||||
wiederAm:
|
||||
ergebnis.faelligAm <= heuteDatum
|
||||
? 'heute noch einmal'
|
||||
: ergebnis.stand.abstandTage === 1
|
||||
? 'morgen'
|
||||
: `in ${ergebnis.stand.abstandTage} Tagen (${alsDatumText(ergebnis.faelligAm)})`,
|
||||
fach: ergebnis.fach,
|
||||
fachName: fachNamen[ergebnis.fach],
|
||||
nochHeute: ergebnis.faelligAm <= heuteDatum,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { karteBewerten, type Bewertungsergebnis } from '../aktionen';
|
||||
|
||||
export type Lernkarte = {
|
||||
id: string;
|
||||
frage: string;
|
||||
antwort: string;
|
||||
handoutId: string;
|
||||
handoutTitel: string;
|
||||
blockTitel: string;
|
||||
fach: number;
|
||||
fachName: string;
|
||||
};
|
||||
|
||||
type Bewertung = 'sitzt' | 'unsicher' | 'nochmal';
|
||||
|
||||
const knoepfe: { bewertung: Bewertung; text: string; taste: string; klasse: string; erklaerung: string }[] = [
|
||||
{ bewertung: 'nochmal', text: 'Nochmal', taste: '1', klasse: 'bewerten-nochmal', erklaerung: 'kommt heute wieder' },
|
||||
{ bewertung: 'unsicher', text: 'Unsicher', taste: '2', klasse: 'bewerten-unsicher', erklaerung: 'kommt morgen' },
|
||||
{ bewertung: 'sitzt', text: 'Sitzt', taste: '3', klasse: 'bewerten-sitzt', erklaerung: 'kommt später' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Der Lernmodus.
|
||||
*
|
||||
* Gebaut für das Handy: große Flächen, Antippen zum Umdrehen, Wischen als
|
||||
* Abkürzung, kein Überfahren mit der Maus. Am Rechner tun es zusätzlich die
|
||||
* Tasten 1, 2, 3 und die Leertaste.
|
||||
*/
|
||||
export function Lernmodus({ karten, block }: { karten: Lernkarte[]; block: string | null }) {
|
||||
const [stapel, setzeStapel] = useState<Lernkarte[]>(karten);
|
||||
const [stelle, setzeStelle] = useState(0);
|
||||
const [umgedreht, setzeUmgedreht] = useState(false);
|
||||
const [laeuft, setzeLaeuft] = useState(false);
|
||||
const [rueckmeldung, setzeRueckmeldung] = useState<Bewertungsergebnis | null>(null);
|
||||
const [erledigt, setzeErledigt] = useState(0);
|
||||
const [zaehler, setzeZaehler] = useState<Record<Bewertung, number>>({ sitzt: 0, unsicher: 0, nochmal: 0 });
|
||||
|
||||
const beruehrungStart = useRef<{ x: number; y: number } | null>(null);
|
||||
const karte = stapel[stelle];
|
||||
const fertig = !karte;
|
||||
|
||||
const bewerten = useCallback(
|
||||
async (bewertung: Bewertung) => {
|
||||
if (!karte || laeuft) return;
|
||||
setzeLaeuft(true);
|
||||
|
||||
const ergebnis = await karteBewerten(karte.id, bewertung);
|
||||
setzeRueckmeldung(ergebnis);
|
||||
setzeZaehler((z) => ({ ...z, [bewertung]: z[bewertung] + 1 }));
|
||||
setzeErledigt((e) => e + 1);
|
||||
|
||||
// „Nochmal" heißt: heute wieder. Also hinten anhängen, statt sie
|
||||
// erst am nächsten Tag zu zeigen – sonst ist die Bewertung folgenlos.
|
||||
if (bewertung === 'nochmal') {
|
||||
setzeStapel((s) => [...s, karte]);
|
||||
}
|
||||
|
||||
setzeUmgedreht(false);
|
||||
setzeStelle((s) => s + 1);
|
||||
setzeLaeuft(false);
|
||||
},
|
||||
[karte, laeuft],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function beiTaste(ereignis: KeyboardEvent) {
|
||||
if (ereignis.target instanceof HTMLElement && ['INPUT', 'TEXTAREA'].includes(ereignis.target.tagName)) return;
|
||||
|
||||
if (ereignis.key === ' ' || ereignis.key === 'Enter') {
|
||||
ereignis.preventDefault();
|
||||
if (!umgedreht) setzeUmgedreht(true);
|
||||
return;
|
||||
}
|
||||
if (!umgedreht) return;
|
||||
|
||||
const treffer = knoepfe.find((k) => k.taste === ereignis.key);
|
||||
if (treffer) {
|
||||
ereignis.preventDefault();
|
||||
void bewerten(treffer.bewertung);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', beiTaste);
|
||||
return () => window.removeEventListener('keydown', beiTaste);
|
||||
}, [umgedreht, bewerten]);
|
||||
|
||||
function beiBeruehrungStart(ereignis: React.TouchEvent) {
|
||||
const punkt = ereignis.touches[0];
|
||||
if (punkt) beruehrungStart.current = { x: punkt.clientX, y: punkt.clientY };
|
||||
}
|
||||
|
||||
function beiBeruehrungEnde(ereignis: React.TouchEvent) {
|
||||
const start = beruehrungStart.current;
|
||||
const punkt = ereignis.changedTouches[0];
|
||||
beruehrungStart.current = null;
|
||||
if (!start || !punkt) return;
|
||||
|
||||
const dx = punkt.clientX - start.x;
|
||||
const dy = punkt.clientY - start.y;
|
||||
if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy)) return; // kein eindeutiges Wischen
|
||||
|
||||
if (!umgedreht) {
|
||||
setzeUmgedreht(true);
|
||||
return;
|
||||
}
|
||||
void bewerten(dx > 0 ? 'sitzt' : 'nochmal');
|
||||
}
|
||||
|
||||
if (fertig) {
|
||||
return (
|
||||
<div className="stapel">
|
||||
<div className="karte-grund stapel" style={{ textAlign: 'center', padding: '2rem 1.25rem' }}>
|
||||
<p style={{ fontSize: '2.5rem', margin: 0 }} aria-hidden="true">
|
||||
✓
|
||||
</p>
|
||||
<h1 style={{ marginBottom: '0.2rem' }}>Stapel durch</h1>
|
||||
<p className="leise">
|
||||
{erledigt} {erledigt === 1 ? 'Karte' : 'Karten'} bewertet.
|
||||
</p>
|
||||
|
||||
<ul className="bewertung-bilanz">
|
||||
<li>
|
||||
<strong>{zaehler.sitzt}</strong> <span>sitzt</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{zaehler.unsicher}</strong> <span>unsicher</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{zaehler.nochmal}</strong> <span>nochmal</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
Morgen wartet der nächste Stapel. Wer täglich zehn Minuten macht, hat vor der Prüfung nichts
|
||||
nachzuholen.
|
||||
</p>
|
||||
|
||||
<div className="reihe" style={{ justifyContent: 'center' }}>
|
||||
<Link href="/karten" className="knopf">
|
||||
Zurück zur Übersicht
|
||||
</Link>
|
||||
<Link href="/" className="knopf knopf-zweit">
|
||||
Zur Woche
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const uebrig = stapel.length - stelle;
|
||||
|
||||
return (
|
||||
<div className="lernmodus">
|
||||
<div className="lernmodus-kopf nicht-drucken">
|
||||
<Link href="/karten" className="knopf knopf-still">
|
||||
← Abbrechen
|
||||
</Link>
|
||||
<span className="leise">
|
||||
{block ? `${karte.blockTitel} · ` : ''}
|
||||
noch {uebrig} {uebrig === 1 ? 'Karte' : 'Karten'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="balken"
|
||||
role="progressbar"
|
||||
aria-valuenow={erledigt}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={erledigt + uebrig}
|
||||
aria-label={`${erledigt} von ${erledigt + uebrig} Karten bewertet`}
|
||||
>
|
||||
<span style={{ width: `${Math.round((erledigt / (erledigt + uebrig)) * 100)}%` }} />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`lernkarte${umgedreht ? ' lernkarte-offen' : ''}`}
|
||||
onClick={() => !umgedreht && setzeUmgedreht(true)}
|
||||
onTouchStart={beiBeruehrungStart}
|
||||
onTouchEnd={beiBeruehrungEnde}
|
||||
aria-label={umgedreht ? 'Antwort' : 'Frage – zum Umdrehen antippen'}
|
||||
aria-expanded={umgedreht}
|
||||
>
|
||||
<span className="lernkarte-herkunft">
|
||||
<span className="handout-kuerzel">{karte.handoutId}</span>
|
||||
<span className="leise">{karte.handoutTitel}</span>
|
||||
</span>
|
||||
|
||||
<span className="lernkarte-frage">{karte.frage}</span>
|
||||
|
||||
{umgedreht ? (
|
||||
<>
|
||||
<span className="lernkarte-trenner" aria-hidden="true" />
|
||||
<span className="lernkarte-antwort">{karte.antwort}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="lernkarte-hinweis">
|
||||
Antworte erst laut oder im Kopf.
|
||||
<br />
|
||||
<strong>Antippen zum Umdrehen</strong>
|
||||
<span className="nur-am-rechner"> · oder Leertaste</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{umgedreht ? (
|
||||
<div className="bewerten">
|
||||
{knoepfe.map((knopf) => (
|
||||
<button
|
||||
key={knopf.bewertung}
|
||||
type="button"
|
||||
className={`bewerten-knopf ${knopf.klasse}`}
|
||||
onClick={() => void bewerten(knopf.bewertung)}
|
||||
disabled={laeuft}
|
||||
>
|
||||
<span className="bewerten-text">{knopf.text}</span>
|
||||
<span className="bewerten-erklaerung">{knopf.erklaerung}</span>
|
||||
<span className="bewerten-taste nur-am-rechner" aria-hidden="true">
|
||||
{knopf.taste}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="leise lernmodus-fuss">
|
||||
Wischen: nach rechts für „sitzt“, nach links für „nochmal“.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p aria-live="polite" className="lernmodus-rueckmeldung">
|
||||
{rueckmeldung?.wiederAm ? `Zuletzt: kommt ${rueckmeldung.wiederAm}.` : ''}
|
||||
{rueckmeldung?.fehler ? rueckmeldung.fehler : ''}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { inhalte } from '@/lib/inhalte';
|
||||
import { alsKartenstand, staendeVon } from '@/lib/repos/karten';
|
||||
import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2';
|
||||
import { heute } from '@/lib/zeit';
|
||||
import { Lernmodus, type Lernkarte } from './Lernmodus';
|
||||
|
||||
export const metadata: Metadata = { title: 'Lernmodus' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const STAPELGROESSE = 20;
|
||||
|
||||
export default async function LernSeite({ searchParams }: { searchParams: Promise<{ block?: string }> }) {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
if (benutzer.rolle !== 'azubi') redirect('/karten');
|
||||
|
||||
const { block } = await searchParams;
|
||||
const alle = inhalte();
|
||||
const gewaehlterBlock = block && alle.blockNach.has(block) ? block : undefined;
|
||||
|
||||
const staende = staendeVon(benutzer.id);
|
||||
const heuteDatum = heute();
|
||||
|
||||
const faellige = heutigerStapel(
|
||||
alle.karten.map((karte) => ({
|
||||
karteId: karte.id,
|
||||
faelligAm: staende.get(karte.id)?.faelligAm ?? null,
|
||||
block: karte.block,
|
||||
})),
|
||||
heuteDatum,
|
||||
STAPELGROESSE,
|
||||
gewaehlterBlock,
|
||||
);
|
||||
|
||||
const karten: Lernkarte[] = faellige.map((eintrag) => {
|
||||
const karte = alle.karteNach.get(eintrag.karteId)!;
|
||||
const stand = alsKartenstand(staende.get(karte.id));
|
||||
return {
|
||||
id: karte.id,
|
||||
frage: karte.frage,
|
||||
antwort: karte.antwort,
|
||||
handoutId: karte.handoutId,
|
||||
handoutTitel: karte.handoutTitel,
|
||||
blockTitel: alle.blockNach.get(karte.block)?.titel ?? karte.block,
|
||||
fach: fach(stand),
|
||||
fachName: fachNamen[fach(stand)] ?? '',
|
||||
};
|
||||
});
|
||||
|
||||
if (karten.length === 0) {
|
||||
return (
|
||||
<div className="stapel">
|
||||
<div className="karte-grund stapel" style={{ textAlign: 'center', padding: '2rem 1.25rem' }}>
|
||||
<p style={{ fontSize: '2.5rem', margin: 0 }} aria-hidden="true">
|
||||
✓
|
||||
</p>
|
||||
<h1 style={{ marginBottom: '0.2rem' }}>Für heute nichts fällig</h1>
|
||||
<p className="leise" style={{ marginBottom: 0 }}>
|
||||
{gewaehlterBlock
|
||||
? 'In diesem Themenblock ist heute keine Karte dran.'
|
||||
: 'Alle Karten sitzen gerade fest genug. Morgen kommt der nächste Stapel.'}
|
||||
</p>
|
||||
<div className="reihe" style={{ justifyContent: 'center' }}>
|
||||
<Link href="/karten" className="knopf">
|
||||
Zurück zur Übersicht
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Lernmodus karten={karten} block={gewaehlterBlock ?? null} />;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { erfordereBenutzer } from '@/lib/auth/rechte';
|
||||
import { inhalte } from '@/lib/inhalte';
|
||||
import { alsKartenstand, staendeVon, uebersicht } from '@/lib/repos/karten';
|
||||
import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2';
|
||||
import { alsDatumText, heute } from '@/lib/zeit';
|
||||
|
||||
export const metadata: Metadata = { title: 'Merkkarten' };
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const STAPELGROESSE = 20;
|
||||
|
||||
export default async function KartenSeite() {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
const alle = inhalte();
|
||||
const heuteDatum = heute();
|
||||
|
||||
const istAzubi = benutzer.rolle === 'azubi';
|
||||
const staende = istAzubi ? staendeVon(benutzer.id) : new Map();
|
||||
const stand = istAzubi
|
||||
? uebersicht(benutzer.id, alle.karten.map((k) => k.id), heuteDatum)
|
||||
: { gesamt: alle.karten.length, gelernt: 0, faellig: 0, nieGelernt: alle.karten.length, jeFach: { 1: alle.karten.length, 2: 0, 3: 0, 4: 0, 5: 0 } };
|
||||
|
||||
const faelligeAlle = heutigerStapel(
|
||||
alle.karten.map((karte) => ({
|
||||
karteId: karte.id,
|
||||
faelligAm: staende.get(karte.id)?.faelligAm ?? null,
|
||||
block: karte.block,
|
||||
})),
|
||||
heuteDatum,
|
||||
9999,
|
||||
);
|
||||
|
||||
const jeBlock = alle.bloecke.map((block) => {
|
||||
const desBlocks = alle.karten.filter((k) => k.block === block.id);
|
||||
const faellig = faelligeAlle.filter((f) => f.block === block.id).length;
|
||||
const gelernt = desBlocks.filter((k) => {
|
||||
const zeile = staende.get(k.id);
|
||||
return zeile ? fach(alsKartenstand(zeile)) >= 4 : false;
|
||||
}).length;
|
||||
return { block, gesamt: desBlocks.length, faellig, gelernt };
|
||||
});
|
||||
|
||||
if (!istAzubi) {
|
||||
return (
|
||||
<div className="stapel">
|
||||
<div>
|
||||
<h1>Merkkarten</h1>
|
||||
<p className="leise">
|
||||
{alle.karten.length} Karten aus {alle.handouts.length} Handouts. Gelernt wird nur vom Azubi – du
|
||||
siehst hier, was es gibt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="raster">
|
||||
{jeBlock.map(({ block, gesamt }) => (
|
||||
<section key={block.id} className="karte">
|
||||
<h2 style={{ fontSize: '1rem', marginBottom: '0.2rem' }}>{block.titel}</h2>
|
||||
<p className="leise" style={{ marginBottom: 0 }}>
|
||||
{gesamt} {gesamt === 1 ? 'Karte' : 'Karten'}
|
||||
</p>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const heuteStapel = Math.min(stand.faellig, STAPELGROESSE);
|
||||
|
||||
return (
|
||||
<div className="stapel">
|
||||
<div>
|
||||
<h1>Merkkarten</h1>
|
||||
<p className="leise">
|
||||
{stand.gesamt} Karten insgesamt · {stand.gelernt} sitzen · {stand.nieGelernt} noch nie gelernt
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="karte-grund stapel" aria-labelledby="heute">
|
||||
<h2 id="heute" style={{ marginBottom: 0 }}>
|
||||
Heute fällig
|
||||
</h2>
|
||||
|
||||
{stand.faellig === 0 ? (
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
Nichts. Alle Karten liegen weiter hinten – morgen ist wieder etwas dran.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
<strong style={{ fontSize: '2rem' }}>{heuteStapel}</strong>{' '}
|
||||
{heuteStapel === 1 ? 'Karte' : 'Karten'}
|
||||
{stand.faellig > STAPELGROESSE ? (
|
||||
<span className="leise"> von {stand.faellig} fälligen – der Rest kommt morgen</span>
|
||||
) : null}
|
||||
</p>
|
||||
<div>
|
||||
<Link href="/karten/lernen" className="knopf knopf-akzent">
|
||||
Lernen starten
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="stapel" aria-labelledby="bloecke">
|
||||
<div>
|
||||
<h2 id="bloecke" style={{ marginBottom: '0.2rem' }}>
|
||||
Nach Themenblock
|
||||
</h2>
|
||||
<p className="leise" style={{ margin: 0 }}>
|
||||
Vor einer Lernzielkontrolle gezielt üben – hier sind auch Karten dabei, die eigentlich erst später
|
||||
dran wären.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="raster">
|
||||
{jeBlock.map(({ block, gesamt, faellig, gelernt }) => (
|
||||
<section key={block.id} className="karte stapel-eng">
|
||||
<h3 style={{ fontSize: '1rem', marginBottom: '0.2rem' }}>{block.titel}</h3>
|
||||
<p className="leise" style={{ margin: 0 }}>
|
||||
{gesamt} Karten · {gelernt} sitzen · {faellig} heute fällig
|
||||
</p>
|
||||
<div className="balken" aria-hidden="true">
|
||||
<span style={{ width: `${gesamt === 0 ? 0 : Math.round((gelernt / gesamt) * 100)}%` }} />
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
href={`/karten/lernen?block=${block.id}`}
|
||||
className={`knopf ${faellig > 0 ? '' : 'knopf-zweit'}`}
|
||||
>
|
||||
{faellig > 0 ? `${Math.min(faellig, STAPELGROESSE)} lernen` : 'Trotzdem üben'}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="karte stapel-eng" aria-labelledby="faecher">
|
||||
<h2 id="faecher" style={{ fontSize: '1rem', marginBottom: '0.2rem' }}>
|
||||
Wie fest die Karten sitzen
|
||||
</h2>
|
||||
<ul className="faecher">
|
||||
{[1, 2, 3, 4, 5].map((nummer) => {
|
||||
const anzahl = stand.jeFach[nummer] ?? 0;
|
||||
const anteil = stand.gesamt === 0 ? 0 : Math.round((anzahl / stand.gesamt) * 100);
|
||||
return (
|
||||
<li key={nummer}>
|
||||
<span className="fach-nummer">{nummer}</span>
|
||||
<span className="fach-name">{fachNamen[nummer]}</span>
|
||||
<span className="fach-balken" aria-hidden="true">
|
||||
<span style={{ width: `${anteil}%` }} />
|
||||
</span>
|
||||
<span className="fach-zahl">{anzahl}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{stand.gesamt > stand.nieGelernt ? (
|
||||
<section className="karte" aria-labelledby="naechste">
|
||||
<h2 id="naechste" style={{ fontSize: '1rem', marginBottom: '0.4rem' }}>
|
||||
Als Nächstes fällig
|
||||
</h2>
|
||||
<ul className="stapel-eng" style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
||||
{[...staende.values()]
|
||||
.filter((z) => z.faelligAm > heuteDatum)
|
||||
.sort((a, b) => a.faelligAm.localeCompare(b.faelligAm))
|
||||
.slice(0, 5)
|
||||
.map((z) => {
|
||||
const karte = alle.karteNach.get(z.karteId);
|
||||
if (!karte) return null;
|
||||
return (
|
||||
<li key={z.karteId} className="reihe" style={{ gap: '0.5rem', flexWrap: 'nowrap' }}>
|
||||
<span className="handout-kuerzel">{karte.handoutId}</span>
|
||||
<span style={{ flex: 1 }}>{karte.frage}</span>
|
||||
<span className="leise">{alsDatumText(z.faelligAm)}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user