feat(cards): recovery mode, undo, FSRS slider, streak header, stats charts, blog
Some checks are pending
CI / validate (push) Waiting to run
Some checks are pending
CI / validate (push) Waiting to run
Study-View:
- Graceful Backlog Recovery: Banner bei >30 fälligen Karten, Recovery-Queue
sortiert nach Stability aufsteigend (25er-Batch, ?recovery=true)
- Undo letzte Bewertung: 5s-Toast mit RAF-Fortschrittsbalken, Ctrl/Cmd+Z,
prevSnapshot-Spalte in reviews (Migration 0001, Prod deployed)
- FSRS-Tooltip nach Reveal: State / Stability / Difficulty als Popover
Deck-Edit:
- Neuer Abschnitt „Lern-Algorithmus" mit request_retention-Slider (50–99 %)
Header:
- Streak-Pill (🔥 N) + fällige-Karten-Pill via GET /api/v1/me/summary
Stats-Page:
- Difficulty-Distribution (5 Buckets, Farb-Bars)
- Deck-Fortschritt (Mastery % = stability>21, max 6 Decks)
API:
- GET /me/summary: streak_days + due_now (leichtgewichtiger Header-Endpoint)
- GET /reviews/due: ?recovery=true → stability-sort, Limit 25
- POST /reviews/:cardId/:subIndex/undo: prevSnapshot-Restore, 409 wenn leer
- /me/stats: difficulty_distribution + deck_mastery
Landing:
- 5 Blog-Artikel (Quizlet-Paywall, FSRS, Datenschutz, Anki, Lernkarten-Tipps)
- BlogTeaser-Komponente auf Startseite, Footer-Spalte „Artikel"
i18n: 11 neue Schlüssel in DE/EN/FR/IT/ES
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
21ec535173
commit
abf493aeec
27 changed files with 2667 additions and 29 deletions
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "cards"."reviews" ADD COLUMN "prev_snapshot" jsonb;
|
||||
|
|
@ -8,6 +8,13 @@
|
|||
"when": 1778604624860,
|
||||
"tag": "0000_baseline",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1747180800000,
|
||||
"tag": "0001_reviews_prev_snapshot",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { index, integer, primaryKey, real, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
import { index, integer, jsonb, primaryKey, real, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
|
||||
import { cardsSchema } from './_schema.ts';
|
||||
import { cards } from './cards.ts';
|
||||
|
|
@ -37,6 +37,7 @@ export const reviews = cardsSchema.table(
|
|||
.notNull()
|
||||
.default('new'),
|
||||
lastReview: timestamp('last_review', { withTimezone: true, mode: 'date' }),
|
||||
prevSnapshot: jsonb('prev_snapshot').$type<Record<string, unknown> | null>().default(null),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({ columns: [t.cardId, t.subIndex] }),
|
||||
|
|
|
|||
|
|
@ -68,6 +68,47 @@ export function meRouter(deps: MeDeps = {}): Hono<{ Variables: AuthVars }> {
|
|||
return c.json(payload);
|
||||
});
|
||||
|
||||
/**
|
||||
* Schlanker Kurzschnitt: nur Streak und fällige Karten. Ideal für
|
||||
* Home-Screen-Widgets oder schnelle Polling-Clients.
|
||||
*/
|
||||
r.get('/summary', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const db = dbOf();
|
||||
const now = new Date();
|
||||
const ninetyAgo = new Date(Date.now() - 91 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const [dueCountRow] = await db
|
||||
.select({ n: sql<number>`count(*)::int` })
|
||||
.from(reviews)
|
||||
.where(and(eq(reviews.userId, userId), lte(reviews.due, now)));
|
||||
|
||||
const dayRows = await db
|
||||
.select({
|
||||
day: sql<string>`to_char(${reviews.lastReview}, 'YYYY-MM-DD')`,
|
||||
n: sql<number>`count(*)::int`,
|
||||
})
|
||||
.from(reviews)
|
||||
.where(
|
||||
and(eq(reviews.userId, userId), isNotNull(reviews.lastReview), gte(reviews.lastReview, ninetyAgo))
|
||||
)
|
||||
.groupBy(sql`to_char(${reviews.lastReview}, 'YYYY-MM-DD')`);
|
||||
|
||||
const byDay = new Map(dayRows.map((r) => [r.day, r.n]));
|
||||
let streak = 0;
|
||||
for (let i = 0; i < 91; i++) {
|
||||
const d = new Date(Date.now() - i * 24 * 60 * 60 * 1000);
|
||||
const key = d.toISOString().slice(0, 10);
|
||||
if ((byDay.get(key) ?? 0) > 0) streak++;
|
||||
else break;
|
||||
}
|
||||
|
||||
return c.json({
|
||||
streak_days: streak,
|
||||
due_now: dueCountRow?.n ?? 0,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Statistik-Snapshot für die Account-/Stats-Page. Nicht-cached, alle
|
||||
* Aggregate per Query. Ein einzelner Aufruf reicht für die /stats-UI.
|
||||
|
|
@ -221,6 +262,67 @@ export function meRouter(deps: MeDeps = {}): Hono<{ Variables: AuthVars }> {
|
|||
last_review: r.lastReview ? r.lastReview.toISOString() : null,
|
||||
}));
|
||||
|
||||
// Difficulty-Distribution: 5 Buckets über alle non-new Reviews.
|
||||
const diffRows = await db
|
||||
.select({
|
||||
bucket: sql<string>`CASE
|
||||
WHEN ${reviews.difficulty} < 2 THEN 'very_easy'
|
||||
WHEN ${reviews.difficulty} < 4 THEN 'easy'
|
||||
WHEN ${reviews.difficulty} < 6 THEN 'medium'
|
||||
WHEN ${reviews.difficulty} < 8 THEN 'hard'
|
||||
ELSE 'very_hard'
|
||||
END`,
|
||||
n: sql<number>`count(*)::int`,
|
||||
})
|
||||
.from(reviews)
|
||||
.where(and(eq(reviews.userId, userId), sql`${reviews.state} != 'new'`))
|
||||
.groupBy(
|
||||
sql`CASE
|
||||
WHEN ${reviews.difficulty} < 2 THEN 'very_easy'
|
||||
WHEN ${reviews.difficulty} < 4 THEN 'easy'
|
||||
WHEN ${reviews.difficulty} < 6 THEN 'medium'
|
||||
WHEN ${reviews.difficulty} < 8 THEN 'hard'
|
||||
ELSE 'very_hard'
|
||||
END`
|
||||
);
|
||||
|
||||
const BUCKETS = ['very_easy', 'easy', 'medium', 'hard', 'very_hard'] as const;
|
||||
const diffMap = new Map(diffRows.map((r) => [r.bucket, r.n]));
|
||||
const difficultyDistribution = BUCKETS.map((b) => ({ bucket: b, n: diffMap.get(b) ?? 0 }));
|
||||
|
||||
// Deck-Mastery: pro Deck total reviews + mastered (stability > 21).
|
||||
const deckMasteryRows = await db
|
||||
.select({
|
||||
deck_id: decks.id,
|
||||
deck_name: decks.name,
|
||||
total: sql<number>`count(${reviews.cardId})::int`,
|
||||
mastered: sql<number>`count(${reviews.cardId}) FILTER (WHERE ${reviews.stability} > 21)::int`,
|
||||
})
|
||||
.from(decks)
|
||||
.leftJoin(cards, eq(cards.deckId, decks.id))
|
||||
.leftJoin(
|
||||
reviews,
|
||||
and(
|
||||
eq(reviews.cardId, cards.id),
|
||||
eq(reviews.userId, userId),
|
||||
sql`${reviews.state} != 'new'`
|
||||
)
|
||||
)
|
||||
.where(eq(decks.userId, userId))
|
||||
.groupBy(decks.id, decks.name)
|
||||
.orderBy(
|
||||
sql`count(${reviews.cardId}) FILTER (WHERE ${reviews.stability} > 21)::float
|
||||
/ NULLIF(count(${reviews.cardId}), 0) DESC NULLS LAST`
|
||||
);
|
||||
|
||||
const deckMastery = deckMasteryRows.map((r) => ({
|
||||
deck_id: r.deck_id,
|
||||
deck_name: r.deck_name,
|
||||
total: r.total,
|
||||
mastered: r.mastered,
|
||||
pct: r.total > 0 ? r.mastered / r.total : 0,
|
||||
}));
|
||||
|
||||
return c.json({
|
||||
user_id: userId,
|
||||
generated_at: now.toISOString(),
|
||||
|
|
@ -238,6 +340,8 @@ export function meRouter(deps: MeDeps = {}): Hono<{ Variables: AuthVars }> {
|
|||
due_forecast: dueForecast,
|
||||
leech_threshold: LEECH_THRESHOLD,
|
||||
leech_cards: leechCards,
|
||||
difficulty_distribution: difficultyDistribution,
|
||||
deck_mastery: deckMastery,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -28,10 +28,12 @@ export function reviewsRouter(deps: ReviewsDeps = {}): Hono<{ Variables: AuthVar
|
|||
r.get('/due', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const deckId = c.req.query('deck_id');
|
||||
const limit = Math.min(Number(c.req.query('limit') ?? 100), 500);
|
||||
const recovery = c.req.query('recovery') === 'true';
|
||||
const limit = recovery ? 25 : Math.min(Number(c.req.query('limit') ?? 100), 500);
|
||||
const now = new Date();
|
||||
|
||||
const conditions = [eq(reviews.userId, userId), lte(reviews.due, now)];
|
||||
const orderBy = recovery ? asc(reviews.stability) : asc(reviews.due);
|
||||
|
||||
if (deckId) {
|
||||
// Wenn deck_id angegeben, joinen wir auf cards.deck_id.
|
||||
|
|
@ -43,7 +45,7 @@ export function reviewsRouter(deps: ReviewsDeps = {}): Hono<{ Variables: AuthVar
|
|||
.from(reviews)
|
||||
.innerJoin(cards, eq(cards.id, reviews.cardId))
|
||||
.where(and(...conditions, eq(cards.deckId, deckId)))
|
||||
.orderBy(asc(reviews.due))
|
||||
.orderBy(orderBy)
|
||||
.limit(limit);
|
||||
return c.json({
|
||||
reviews: rows.map((r) => ({ ...toReviewDto(r.review), card: r.card })),
|
||||
|
|
@ -55,7 +57,7 @@ export function reviewsRouter(deps: ReviewsDeps = {}): Hono<{ Variables: AuthVar
|
|||
.select()
|
||||
.from(reviews)
|
||||
.where(and(...conditions))
|
||||
.orderBy(asc(reviews.due))
|
||||
.orderBy(orderBy)
|
||||
.limit(limit);
|
||||
return c.json({ reviews: rows.map(toReviewDto), total: rows.length });
|
||||
});
|
||||
|
|
@ -114,6 +116,9 @@ export function reviewsRouter(deps: ReviewsDeps = {}): Hono<{ Variables: AuthVar
|
|||
(hit.deck.fsrsSettings as object) ?? {}
|
||||
);
|
||||
|
||||
// Snapshot des alten Zustands vor dem Überschreiben — ermöglicht Undo.
|
||||
const snapshot = toReviewDto(hit.review) as Record<string, unknown>;
|
||||
|
||||
const [updated] = await dbOf()
|
||||
.update(reviews)
|
||||
.set({
|
||||
|
|
@ -127,6 +132,7 @@ export function reviewsRouter(deps: ReviewsDeps = {}): Hono<{ Variables: AuthVar
|
|||
lapses: next.lapses,
|
||||
state: next.state,
|
||||
lastReview: next.last_review ? new Date(next.last_review) : null,
|
||||
prevSnapshot: snapshot,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
|
|
@ -140,6 +146,51 @@ export function reviewsRouter(deps: ReviewsDeps = {}): Hono<{ Variables: AuthVar
|
|||
return c.json(toReviewDto(updated));
|
||||
});
|
||||
|
||||
/**
|
||||
* Macht die letzte Bewertung rückgängig — stellt prevSnapshot wieder her
|
||||
* und löscht den Snapshot danach. Nur einmal pro Bewertung möglich.
|
||||
*/
|
||||
r.post('/:cardId/:subIndex/undo', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const cardId = c.req.param('cardId');
|
||||
const subIndex = Number(c.req.param('subIndex'));
|
||||
|
||||
if (!Number.isInteger(subIndex) || subIndex < 0) {
|
||||
return c.json({ error: 'invalid_sub_index' }, 422);
|
||||
}
|
||||
|
||||
const [hit] = await dbOf()
|
||||
.select()
|
||||
.from(reviews)
|
||||
.where(and(eq(reviews.cardId, cardId), eq(reviews.subIndex, subIndex), eq(reviews.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (!hit) return c.json({ error: 'not_found' }, 404);
|
||||
if (!hit.prevSnapshot) return c.json({ error: 'no_snapshot' }, 409);
|
||||
|
||||
const snap = hit.prevSnapshot as Record<string, unknown>;
|
||||
|
||||
const [restored] = await dbOf()
|
||||
.update(reviews)
|
||||
.set({
|
||||
due: new Date(snap['due'] as string),
|
||||
stability: snap['stability'] as number,
|
||||
difficulty: snap['difficulty'] as number,
|
||||
elapsedDays: snap['elapsed_days'] as number,
|
||||
scheduledDays: snap['scheduled_days'] as number,
|
||||
learningSteps: snap['learning_steps'] as number,
|
||||
reps: snap['reps'] as number,
|
||||
lapses: snap['lapses'] as number,
|
||||
state: snap['state'] as typeof reviews.$inferSelect['state'],
|
||||
lastReview: snap['last_review'] ? new Date(snap['last_review'] as string) : null,
|
||||
prevSnapshot: null,
|
||||
})
|
||||
.where(and(eq(reviews.cardId, cardId), eq(reviews.subIndex, subIndex), eq(reviews.userId, userId)))
|
||||
.returning();
|
||||
|
||||
return c.json(toReviewDto(restored));
|
||||
});
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
|
|
|
|||
75
apps/landing/src/components/BlogTeaser.astro
Normal file
75
apps/landing/src/components/BlogTeaser.astro
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
const posts = [
|
||||
{
|
||||
href: '/blog/quizlet-paywall',
|
||||
tag: 'Migration',
|
||||
title: 'Quizlet zieht die Paywall hoch — was jetzt?',
|
||||
summary:
|
||||
'Fünf Millionen Nutzer:innen haben Quizlet in den letzten zwei Jahren verlassen. Was hinter der Bezahlschranke steckt und wie der Wechsel zu Cardecky in wenigen Minuten klappt.',
|
||||
},
|
||||
{
|
||||
href: '/blog/fsrs-algorithmus',
|
||||
tag: 'Algorithmus',
|
||||
title: 'Weniger lernen, mehr behalten: Was FSRS bedeutet',
|
||||
summary:
|
||||
'FSRS reduziert die nötigen Wiederholungen um 20–30 % bei gleicher Retention. Wie der Algorithmus funktioniert — und warum er bei anderen Apps noch nicht der Standard ist.',
|
||||
},
|
||||
{
|
||||
href: '/blog/deine-daten',
|
||||
tag: 'Datenschutz',
|
||||
title: 'Deine Karten, deine Daten',
|
||||
summary:
|
||||
'Lerndaten verraten mehr als die meisten ahnen. Was Cardecky damit macht — und was nicht — lässt sich im öffentlichen Quellcode nachprüfen.',
|
||||
},
|
||||
{
|
||||
href: '/blog/anki-zu-kompliziert',
|
||||
tag: 'Vergleich',
|
||||
title: 'Anki ist mächtig — und trotzdem schwer empfehlbar',
|
||||
summary:
|
||||
'Anki ist technisch überlegen. Trotzdem hören die meisten Nutzer:innen auf — nicht wegen des Algorithmus, sondern wegen Interface und Lernkurve.',
|
||||
},
|
||||
{
|
||||
href: '/blog/gute-lernkarten',
|
||||
tag: 'Lernen',
|
||||
title: 'Wie man Lernkarten schreibt, die wirklich funktionieren',
|
||||
summary:
|
||||
'Der beste Algorithmus nützt nichts, wenn die Karten schlecht gebaut sind. Fünf Prinzipien — mit Gegenbeispielen.',
|
||||
},
|
||||
] as const;
|
||||
---
|
||||
|
||||
<section class="border-b border-rule py-20 sm:py-28">
|
||||
<div class="mx-auto max-w-content px-6">
|
||||
<p class="section-label">Artikel</p>
|
||||
<h2 class="mt-3 font-serif text-display text-ink">Hintergrund & Methodik.</h2>
|
||||
<p class="mt-4 max-w-prose text-muted">
|
||||
Warum Spaced Repetition funktioniert, was die Konkurrenz falsch macht — und wie man
|
||||
Karten baut, die das Gehirn tatsächlich behält.
|
||||
</p>
|
||||
|
||||
<ul class="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3" role="list">
|
||||
{posts.map((post) => (
|
||||
<li>
|
||||
<a
|
||||
href={post.href}
|
||||
class="group flex h-full flex-col rounded-xl border border-rule bg-paper p-6 transition-shadow hover:shadow-md"
|
||||
>
|
||||
<span class="section-label mb-3 inline-block">{post.tag}</span>
|
||||
<p class="font-serif text-lg font-semibold leading-snug text-ink group-hover:text-leaf transition-colors">
|
||||
{post.title}
|
||||
</p>
|
||||
<p class="mt-3 flex-1 text-sm leading-relaxed text-muted">
|
||||
{post.summary}
|
||||
</p>
|
||||
<span class="mt-5 inline-flex items-center gap-1 text-sm font-medium text-leaf">
|
||||
Lesen
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -3,7 +3,7 @@ const year = new Date().getFullYear();
|
|||
---
|
||||
<footer class="bg-ink py-12 text-white/60">
|
||||
<div class="mx-auto max-w-content px-6">
|
||||
<div class="grid gap-8 sm:grid-cols-3">
|
||||
<div class="grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<p class="font-serif text-base font-semibold text-white">Cardecky</p>
|
||||
<p class="mt-2 text-sm leading-relaxed">
|
||||
|
|
@ -21,6 +21,17 @@ const year = new Date().getFullYear();
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-white/40">Artikel</p>
|
||||
<ul class="mt-3 space-y-2 text-sm">
|
||||
<li><a href="/blog/quizlet-paywall" class="hover:text-white transition-colors">Quizlet-Alternative</a></li>
|
||||
<li><a href="/blog/fsrs-algorithmus" class="hover:text-white transition-colors">Was ist FSRS?</a></li>
|
||||
<li><a href="/blog/deine-daten" class="hover:text-white transition-colors">Deine Daten</a></li>
|
||||
<li><a href="/blog/anki-zu-kompliziert" class="hover:text-white transition-colors">Anki vs. Cardecky</a></li>
|
||||
<li><a href="/blog/gute-lernkarten" class="hover:text-white transition-colors">Gute Lernkarten</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-white/40">Verein</p>
|
||||
<ul class="mt-3 space-y-2 text-sm">
|
||||
|
|
|
|||
321
apps/landing/src/pages/blog/anki-zu-kompliziert.astro
Normal file
321
apps/landing/src/pages/blog/anki-zu-kompliziert.astro
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import Nav from '../../components/Nav.astro';
|
||||
import Footer from '../../components/Footer.astro';
|
||||
import '../../styles/base.css';
|
||||
|
||||
const APP_URL = 'https://cardecky.mana.how';
|
||||
const ANKI_IMPORT_URL = 'https://cardecky.mana.how/import';
|
||||
|
||||
const title = 'Anki ist mächtig — und trotzdem schwer empfehlbar | Cardecky';
|
||||
const description =
|
||||
'Anki ist der Goldstandard für Spaced Repetition. Aber die Lernkurve ist brutal, das' +
|
||||
' Interface stammt aus 2006, und einen Freund damit anzufangen ist fast unmöglich. Was' +
|
||||
' Cardecky anders macht.';
|
||||
---
|
||||
|
||||
<Layout {title} {description}>
|
||||
<Nav />
|
||||
|
||||
<main class="mx-auto max-w-prose px-6 py-16 sm:py-24">
|
||||
|
||||
<!-- Artikel-Header -->
|
||||
<header class="mb-12">
|
||||
<p class="section-label mb-4">Vergleich · Anki</p>
|
||||
<h1 class="font-serif text-4xl leading-tight text-ink sm:text-5xl">
|
||||
Anki ist mächtig — und trotzdem schwer empfehlbar
|
||||
</h1>
|
||||
<p class="mt-5 text-lg leading-relaxed text-muted">
|
||||
Anki ist der technisch überlegene Lernkarten-Standard. Die meisten Nutzer:innen scheitern
|
||||
trotzdem — nicht am Algorithmus, sondern am Interface. Was das konkret bedeutet und wo
|
||||
Cardecky ansetzt.
|
||||
</p>
|
||||
<p class="mt-4 text-xs text-muted">Stand: Mai 2026</p>
|
||||
</header>
|
||||
|
||||
<article class="prose-cardecky">
|
||||
|
||||
<!-- 1 -->
|
||||
<h2>Das Paradox: alle wissen, dass es funktioniert</h2>
|
||||
|
||||
<p>
|
||||
Wenn jemand über Lernkarten redet, fällt Anki. Wenn jemand über Spaced Repetition redet,
|
||||
fällt Anki. Medizinstudenten schwören seit Jahren darauf, Sprachenlerner haben ganze
|
||||
Karrieren darauf aufgebaut, und wer tief in der Lernforschung sitzt, kennt die Studie, die
|
||||
die Studie belegt. Anki funktioniert. Das ist nicht strittig.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Und trotzdem kennt fast jeder mindestens eine Person, die Anki angefangen und aufgehört hat.
|
||||
Nicht weil das Lernen nicht funktioniert hat — sondern weil die App selbst im Weg stand. Das
|
||||
ist das Paradox: ein Werkzeug, das technisch unbestritten ist und dessen Nutzer:innen
|
||||
massenweise aussteigen, bevor sie je richtig angefangen haben.
|
||||
</p>
|
||||
|
||||
<!-- 2 -->
|
||||
<h2>Was Anki wirklich kann</h2>
|
||||
|
||||
<p>
|
||||
Es wäre unfair, Anki nur durch den Blick auf seine Schwächen zu beschreiben. Anki ist seit
|
||||
2006 im Aufbau und hat in dieser Zeit eine Tiefe entwickelt, die kein neueres Tool auch nur
|
||||
annähernd erreicht.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Das Add-on-Ökosystem allein ist bemerkenswert: hunderte von Community-Erweiterungen, von
|
||||
automatischer Bildsuche über LaTeX-Rendering bis zu spezialisierten Scheduling-Varianten.
|
||||
Die Community-Deck-Bibliothek im Medizin-Bereich — Zanki, Lightyear und ihre Abkömmlinge
|
||||
— ist jahrelange kollektive Arbeit, die sich schlicht nicht replizieren lässt. Wer Medizin
|
||||
studiert und in dieses Ökosystem einsteigt, bekommt kuratierte, geprüfte, von Tausenden
|
||||
mitgepflegte Decks. Das ist ein realer Wert.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Seit 2024 ist zudem FSRS — Free Spaced Repetition Scheduler — als opt-in in Anki enthalten.
|
||||
FSRS modelliert zwei Gedächtnisparameter pro Karte (Stabilität und Abrufbarkeit) und passt
|
||||
Wiederholungsintervalle präziser an als der ältere SM-2-Algorithmus, auf dem Anki lange
|
||||
fuhr. Wer FSRS in Anki aktiviert, hat Stand 2026 einen der genauesten Scheduling-Algorithmen,
|
||||
die öffentlich verfügbar sind.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Anki ist außerdem Open Source, kostenlos auf Desktop und Android, und wird von einer
|
||||
kleinen Kern-Entwicklergruppe mit bemerkenswerter Kontinuität gepflegt. Das ist keine
|
||||
Selbstverständlichkeit.
|
||||
</p>
|
||||
|
||||
<!-- 3 -->
|
||||
<h2>Warum es trotzdem nicht funktioniert — für die meisten</h2>
|
||||
|
||||
<p>
|
||||
Das Problem ist nicht der Algorithmus. Das Problem ist alles darum herum.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Wer Anki zum ersten Mal öffnet, trifft auf ein Interface, das seit 2006 im Wesentlichen
|
||||
gleich geblieben ist. Das ist kein Vorwurf an die Entwickler:innen — es ist eine
|
||||
Beschreibung der Realität. Decks, Tags, Note-Types, Card-Types, Fields, Templates,
|
||||
Scheduling-Optionen, Add-on-Management: all das erwartet eine Einarbeitungszeit, die in
|
||||
keinem Verhältnis zum eigentlichen Lernziel steht. Wer Vokabeln für die nächste
|
||||
Prüfung lernen will, muss zuerst verstehen, was der Unterschied zwischen einer Note und
|
||||
einer Card ist.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Das „Notes vs. Cards"-Konzept verdient einen eigenen Absatz: Anki speichert Inhalte als
|
||||
„Notes" (das Quell-Datenpaar) und generiert daraus „Cards" (die tatsächlich wiederholten
|
||||
Einheiten). Das ist konzeptionell sauber und erlaubt Dinge wie eine Note, die
|
||||
vorwärts und rückwärts abgefragt wird. Es ist aber auch ein Konzept, das nirgendwo erklärt
|
||||
wird — man stolpert irgendwann hinein. Anfänger:innen glauben oft, sie hätten hundert
|
||||
Karten erstellt, und wundern sich dann, warum die Queue zweihundert Einträge zeigt.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Hinzu kommt die Card-Creation-Tax: Anki hat keine Abkürzungen für das Erstellen von Karten.
|
||||
Alles ist manuell, alles erfordert Entscheidungen über Felder und Vorlagen, bevor
|
||||
überhaupt der erste Inhalt steht. Für jemanden, der gerade mit Lernen anfangen will, ist
|
||||
das eine hohe Hürde. Nicht unüberwindbar, aber hoch genug, dass viele vorher aufhören.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Und dann sind da noch die Einstellungen. Anki bietet eine Einstellung namens „Maximum
|
||||
interval" mit einem Standardwert von „36500" — ohne sichtbare Einheit. 36500 was? Tage,
|
||||
wie sich herausstellt. Das ist charakteristisch für das Interface insgesamt: viel Kontrolle,
|
||||
wenig Kontext.
|
||||
</p>
|
||||
|
||||
<!-- 4 -->
|
||||
<h2>Der iOS-Preis als Zugangsbarriere</h2>
|
||||
|
||||
<p>
|
||||
Anki ist die einzige App im Vergleichsfeld, die auf einer der großen Plattformen Geld
|
||||
kostet: AnkiMobile für iOS ist $24.99. Desktop und Android sind kostenlos. Das ist kein
|
||||
Fehler, sondern eine bewusste Entscheidung des Entwicklers, der die iOS-App als primäre
|
||||
Finanzierungsquelle für das Gesamtprojekt nutzt — eine legitime und transparente Wahl.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Trotzdem ist es faktisch eine Barriere. Wer Anki auf dem Telefon nutzen will — und das
|
||||
ist die realistische Nutzungssituation für tägliche Wiederholungen —, zahlt einmalig
|
||||
$24.99. Für viele ist das in Ordnung. Für andere, gerade für Schüler:innen oder
|
||||
Studierende mit knappem Budget, ist es der Moment, an dem sie aufhören zu schauen.
|
||||
</p>
|
||||
|
||||
<!-- 5 -->
|
||||
<h2>Der Backlog-Zyklus: wie man in den Burn-out kommt</h2>
|
||||
|
||||
<p>
|
||||
Spaced Repetition funktioniert nur, wenn man es regelmäßig macht. Was passiert, wenn man
|
||||
eine Woche aussetzt — Prüfungsphase, Urlaub, einfach keine Zeit —, ist bei Anki besonders
|
||||
schmerzhaft: die übersprungenen Wiederholungen stapeln sich nicht auf morgen, sie stapeln
|
||||
sich auf heute. Wer nach zehn Tagen Pause zurückkommt, trifft auf einen Rückstand von
|
||||
mehreren hundert Karten. Nicht als Richtwert, sondern als reale Queue, die abgearbeitet
|
||||
werden will.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Das ist kein Bug von Anki, sondern eine direkte Konsequenz des Algorithmus: Karten, die
|
||||
hätten wiederholt werden sollen, werden fällig. Aber die psychologische Wirkung ist
|
||||
destruktiv. Die meisten Menschen, die nach einer Pause auf 500+ fällige Karten schauen,
|
||||
schließen die App wieder. Manche löschen das Deck. Der Backlog wird zur Mauer, die das
|
||||
Weiterlernen blockiert.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Es gibt Add-ons und manuelle Workarounds, die den Rückstand kappen oder verteilen. Aber
|
||||
auch das setzt voraus, dass man weiß, dass es sie gibt — und wie man sie konfiguriert.
|
||||
</p>
|
||||
|
||||
<!-- 6 -->
|
||||
<h2>Was Cardecky daraus gelernt hat — und was Cardecky nicht hat</h2>
|
||||
|
||||
<p>
|
||||
Cardecky ist aus der Beobachtung entstanden, dass die meisten Menschen Anki nicht wegen
|
||||
des Algorithmus verlassen, sondern wegen des Aufwands davor, danach und drumherum. Das
|
||||
ist die Zielgruppe, die Cardecky bedienen will: Nutzer:innen, die Anki wollten und
|
||||
gescheitert sind — nicht Power-User, die Anki lieben.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Was das konkret bedeutet: FSRS ist in Cardecky der Standard, kein opt-in. Card-Erstellung
|
||||
ohne Vorlage-Entscheidungen, direkt starten. Backlog-Management mit weicheren
|
||||
Auffang-Mechanismen nach Pausen. Kein Notes-vs-Cards-Konzept, das erklärt werden muss.
|
||||
Kein Interface aus 2006.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Gleichzeitig soll hier nichts beschönigt werden. Cardecky hat nicht, was Anki in zwanzig
|
||||
Jahren aufgebaut hat:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Keine medizinischen Community-Decks.</strong> Zanki, Lightyear und ihre Varianten
|
||||
sind in Anki, nicht in Cardecky. Wer Medizin studiert und von diesen Decks abhängt,
|
||||
wechselt nicht zu Cardecky.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Kein Add-on-Ökosystem.</strong> Die Tiefe, die Anki durch hunderte
|
||||
Community-Erweiterungen hat, gibt es bei Cardecky nicht. Was ausgeliefert wird, ist das,
|
||||
was da ist.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Keine zwanzigjährige Community.</strong> Foren, Reddit-Threads, YouTube-Tutorials,
|
||||
Stack-Overflow-Fragen — all das existiert für Anki in einem Umfang, den Cardecky nicht
|
||||
hat und in absehbarer Zeit nicht haben wird.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Kein Desktop-Client.</strong> Cardecky ist eine Web-App, installierbar als PWA.
|
||||
Eine native App für iOS und Android ist in Planung, aber noch nicht verfügbar.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Das sind reale Einschränkungen, keine Randnotizen.
|
||||
</p>
|
||||
|
||||
<!-- 7 -->
|
||||
<h2>Wer schon Anki-Decks hat: Import mitgebracht</h2>
|
||||
|
||||
<p>
|
||||
Für alle, die bereits in Anki investiert haben und den Wechsel trotzdem ausprobieren
|
||||
wollen: Cardecky liest .apkg-Dateien direkt ein. Decks und Karten werden übernommen,
|
||||
Formatierung soweit das Format es trägt. Der FSRS-Lernstand beginnt in Cardecky frisch —
|
||||
das ist eine technische Einschränkung, keine Absicht: FSRS-Parameter sind algorithmisch
|
||||
nicht direkt zwischen verschiedenen Implementierungen übertragbar.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Wer seine Anki-Decks nach <a href={ANKI_IMPORT_URL} target="_blank" rel="noopener">cardecky.mana.how/import</a>
|
||||
hochlädt, kann sofort weiterlernen. Die Queue fängt bei null an — was je nach Perspektive
|
||||
ein Nachteil oder ein Neubeginn ist.
|
||||
</p>
|
||||
|
||||
<!-- CTA -->
|
||||
<hr class="my-12 border-rule" />
|
||||
|
||||
<div class="rounded-xl border border-rule bg-paper p-8">
|
||||
<p class="font-serif text-2xl text-ink">Anki wollte, aber nicht durchgehalten?</p>
|
||||
<p class="mt-3 text-muted leading-relaxed">
|
||||
Cardecky ist für genau diesen Fall gebaut. Bestehende Anki-Decks können direkt
|
||||
importiert werden. Kein Abo, kein Kreditkartenfeld, keine Konfiguration vor dem ersten
|
||||
Lernen.
|
||||
</p>
|
||||
<div class="mt-6 flex flex-wrap gap-4">
|
||||
<a href={APP_URL} class="btn-primary">
|
||||
Cardecky öffnen
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href={ANKI_IMPORT_URL} class="btn-ghost">Anki-Decks importieren</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/* Prose-Styles für den Artikel — ohne externes @tailwindcss/typography */
|
||||
.prose-cardecky h2 {
|
||||
font-family: var(--font-serif, Georgia, serif);
|
||||
font-size: 1.375rem;
|
||||
font-weight: 600;
|
||||
color: theme('colors.ink');
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.prose-cardecky p {
|
||||
color: theme('colors.ink');
|
||||
line-height: 1.75;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.prose-cardecky ul,
|
||||
.prose-cardecky ol {
|
||||
margin-bottom: 1.25rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.prose-cardecky ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.prose-cardecky ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.prose-cardecky li {
|
||||
color: theme('colors.ink');
|
||||
line-height: 1.75;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose-cardecky li + li {
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.prose-cardecky strong {
|
||||
font-weight: 600;
|
||||
color: theme('colors.ink');
|
||||
}
|
||||
|
||||
.prose-cardecky a {
|
||||
color: theme('colors.leaf');
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.prose-cardecky a:hover {
|
||||
color: theme('colors.ink');
|
||||
}
|
||||
|
||||
.prose-cardecky hr {
|
||||
border-color: theme('colors.rule');
|
||||
}
|
||||
</style>
|
||||
289
apps/landing/src/pages/blog/deine-daten.astro
Normal file
289
apps/landing/src/pages/blog/deine-daten.astro
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import Nav from '../../components/Nav.astro';
|
||||
import Footer from '../../components/Footer.astro';
|
||||
import '../../styles/base.css';
|
||||
|
||||
const APP_URL = 'https://cardecky.mana.how';
|
||||
const GIT_URL = 'https://git.mana.how/mana/cards';
|
||||
|
||||
const title = 'Deine Karten, deine Daten — warum Data Ownership beim Lernen wichtig ist | Cardecky';
|
||||
const description =
|
||||
'Lerndaten verraten mehr als die meisten Nutzer:innen ahnen. Was Cardecky' +
|
||||
' stattdessen tut: 0 Tracker, DSGVO-Self-Service, vollständiger Export — und' +
|
||||
' warum die Vereinsform das strukturell absichert.';
|
||||
---
|
||||
|
||||
<Layout {title} {description}>
|
||||
<Nav />
|
||||
|
||||
<main class="mx-auto max-w-prose px-6 py-16 sm:py-24">
|
||||
|
||||
<header class="mb-12">
|
||||
<p class="section-label mb-4">Datenschutz · Data Ownership</p>
|
||||
<h1 class="font-serif text-4xl leading-tight text-ink sm:text-5xl">
|
||||
Deine Karten, deine Daten
|
||||
</h1>
|
||||
<p class="mt-5 text-lg leading-relaxed text-muted">
|
||||
Lerndaten sind eine der intimsten Datenkategorien, die eine App sammeln kann.
|
||||
Was Cardecky damit macht — und was nicht —, lässt sich im Code nachprüfen.
|
||||
</p>
|
||||
<p class="mt-4 text-xs text-muted">Stand: Mai 2026</p>
|
||||
</header>
|
||||
|
||||
<article class="prose-cardecky">
|
||||
|
||||
<h2>Was Lerndaten eigentlich verraten</h2>
|
||||
|
||||
<p>
|
||||
Stell dir vor, du lernst seit drei Jahren Japanisch. Jeden Morgen zehn Minuten,
|
||||
tausende Wiederholungen, ein Lernstand, der dich kennt: welche Zeichen du immer
|
||||
wieder vergisst, wie schnell du abrufst, wann du einbrichst. Diese Muster sind
|
||||
nicht banal. Sie sind ein Protokoll deines Gedächtnisses.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ein Lernprofil über 18 Monate ist ein Fingerabdruck deiner Denkweise — präziser
|
||||
als die meisten Profile, die Werbetreibende kaufen. Was lässt sich daraus
|
||||
ableiten? Lerngeschwindigkeit und Konzentrationsmuster. Schwächen und Stärken
|
||||
nach Fachgebiet. Lernzeiten — und damit Lebensrhythmus. Bei Kindern und
|
||||
Jugendlichen kommen Schul- und Entwicklungsprofile hinzu, die besonders
|
||||
schutzbedürftig sind.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Das ist kein Paranoia-Szenario. Es ist eine nüchterne Beschreibung dessen, was
|
||||
eine App sammelt, die Spaced Repetition betreibt. Die Frage ist, wer diese Daten
|
||||
kontrolliert.
|
||||
</p>
|
||||
|
||||
<h2>Was andere Apps damit machen</h2>
|
||||
|
||||
<p>
|
||||
Quizlet hat 2022–2023 seine Kernfunktionen hinter eine Bezahlschranke gesperrt.
|
||||
Wer wechseln wollte, stand vor einem weiteren Problem: Quizlet bietet keinen
|
||||
sauberen Export. Sets lassen sich nicht als standardisiertes Dateiformat
|
||||
herunterladen. Wer geht, verliert seinen Lernstand oder muss ihn mühsam manuell
|
||||
übertragen. Das ist kein Zufall — das ist Geschäftsmodell. Lock-in hält Churn
|
||||
unten, und Churn unten zu halten ist für eine VC-finanzierte Plattform ein
|
||||
wichtigerer Wert als Portabilität.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Das zweite Problem ist weniger sichtbar: Quizlets Datenschutzerklärung erlaubt
|
||||
die Nutzung von Lerndaten für Produktverbesserungen, Forschung und
|
||||
„personalisierte Erfahrungen" — Formulierungen, die weit genug sind, um viel
|
||||
zu erlauben. Das gilt strukturell für viele ad-finanzierte oder
|
||||
VC-finanzierte Bildungsplattformen: Das Geschäftsmodell enthält einen
|
||||
Anreiz, Nutzerdaten zu verwerten. Dieser Anreiz verschwindet nicht, weil
|
||||
die App ein Lern-Interface hat.
|
||||
</p>
|
||||
|
||||
<h2>Was „0 Tracker" konkret bedeutet — und wie man es nachprüft</h2>
|
||||
|
||||
<p>
|
||||
Cardecky enthält keine Tracking-Bibliotheken. Kein PostHog, kein Plausible,
|
||||
kein Google Analytics, kein Mixpanel, kein Segment. Das ist nicht eine
|
||||
Marketing-Aussage — es ist im Quellcode nachprüfbar.
|
||||
</p>
|
||||
|
||||
<pre><code>git grep -i "posthog\|plausible\|mixpanel\|segment\|analytics" apps/web/src/</code></pre>
|
||||
|
||||
<p>
|
||||
Das Ergebnis ist leer. Wer das nicht glaubt, kann es selbst ausführen —
|
||||
der Code liegt öffentlich auf{' '}
|
||||
<a href={GIT_URL} target="_blank" rel="noopener">git.mana.how/till/cards-web</a>.
|
||||
Das ist der Unterschied zwischen einem Versprechen und einem Nachweis.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Was stattdessen passiert: Serverseitig werden funktionale Logs für Betrieb
|
||||
und Fehlerdiagnose geschrieben — Request-Zeiten, HTTP-Status-Codes, keine
|
||||
Nutzerprofile. Es gibt keine Cross-Session-Verknüpfung, keinen
|
||||
Behaviour-Graph, keine Retargeting-Pixel. Die Server stehen auf dem eigenen
|
||||
Mac Mini von mana e.V. in der Schweiz, hinter einem Cloudflare Tunnel als
|
||||
reine TLS-Terminierung — kein US-Cloud-Provider verarbeitet deine Lerndaten.
|
||||
</p>
|
||||
|
||||
<h2>DSGVO-Export und Anti-Lock-In — als aktive Features</h2>
|
||||
|
||||
<p>
|
||||
Daten-Souveränität ist in Cardecky kein Compliance-Text im Footer. Sie ist in
|
||||
konkreten Features sichtbar.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Self-Service-Export.</strong> Unter Einstellungen kannst du deine Daten
|
||||
jederzeit exportieren, ohne Support-Ticket, ohne Wartezeit. Das geht in drei
|
||||
Formaten:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>.apkg</strong> (Anki-Format) — der de-facto-Standard für Lernkarten,
|
||||
importierbar in Anki, AnkiDroid, AnkiMobile und jede andere
|
||||
FSRS-kompatible App
|
||||
</li>
|
||||
<li>
|
||||
<strong>.csv</strong> — lesbar in jeder Tabellenkalkulation, importierbar
|
||||
in andere Systeme
|
||||
</li>
|
||||
<li>
|
||||
<strong>FSRS-State-JSON</strong> — enthält deinen vollständigen Lernstand:
|
||||
Wiederholungshistorie, Stabilitätswerte, Fälligkeitsdaten. Damit nimmst du
|
||||
nicht nur deine Karten mit, sondern auch das Gedächtnis, das die App über
|
||||
sie aufgebaut hat
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Das ist der Unterschied zwischen Export-als-Rückgabe und Export-als-Portabilität.
|
||||
Du kannst mit vollem Lernstand zu Anki wechseln. Du verlierst nichts.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>DSGVO-Löschung ohne Ticket.</strong> Wer sein Konto löschen will, tut
|
||||
das in der App — vollständig, ohne eine E-Mail schreiben zu müssen. Das ist
|
||||
kein regulatorischer Reflex. Es folgt aus Wert 1 von mana e.V.: Daten gehören
|
||||
den Nutzer:innen, nicht uns. Wer geht, geht mit allem.
|
||||
</p>
|
||||
|
||||
<h2>Warum die Vereinsform hier einen strukturellen Unterschied macht</h2>
|
||||
|
||||
<p>
|
||||
mana e.V. ist ein Schweizer Verein nach ZGB Art. 60 ff. Kein Investor hat
|
||||
Anteile. Es gibt keine Wachstumsziele, keine Exit-Strategie, keine Runde, die
|
||||
zurückgezahlt werden muss. Der Verein finanziert sich aus Mitgliedsbeiträgen,
|
||||
Spenden und fairen Credit-Käufen — nicht aus Werbeeinnahmen oder dem Verkauf
|
||||
von Nutzerdaten.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
VC-finanzierte Apps stehen unter dem Druck, einen Return zu liefern. Das formt
|
||||
Produktentscheidungen, oft unsichtbar. Lock-in ist wertvoll, weil er Churn
|
||||
verhindert. Daten sind wertvoll, weil sie Targeting ermöglichen.
|
||||
Engagement-Maximierung ist wertvoll, weil sie Nutzungszeit erhöht. Keiner
|
||||
dieser Anreize trifft auf mana e.V. zu — nicht weil die Menschen dahinter
|
||||
besonders tugendhaft sind, sondern weil die Rechtsform diese Anreize
|
||||
strukturell nicht enthält.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Die Schweizer Rechtsform ist dabei kein Steuertrick. Sie gibt dem Verein
|
||||
politische Unabhängigkeit außerhalb der großen Blöcke EU und USA — und sie
|
||||
bedeutet: Es gibt keine Unternehmensanteile, die eine andere Firma kaufen
|
||||
könnte, um die Datenschutzversprechen anschließend wegzuverhandeln.
|
||||
</p>
|
||||
|
||||
<h2>Offene Punkte</h2>
|
||||
|
||||
<p>
|
||||
Ehrlichkeit darüber, was noch fehlt:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Native Mobile-App.</strong> Eine SwiftUI-App für iOS und iPadOS ist in
|
||||
Entwicklung — die Kern-Lernschleife ist fertig, der App-Store-Release steht noch
|
||||
aus. Wer heute auf dem iPhone lernen will, nutzt die Web-App über den Browser.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Offline-Support.</strong> Die PWA ist online-first. Vollständiger
|
||||
Offline-Betrieb — Karten laden und bewerten ohne Verbindung, Sync beim
|
||||
nächsten Online-Gang — ist geplant, aber noch nicht live.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Das sind keine versteckten Einschränkungen. Sie stehen hier, weil eine App,
|
||||
die Transparenz als Wert nennt, das auch auf sich selbst anwenden muss.
|
||||
</p>
|
||||
|
||||
<hr class="my-12 border-rule" />
|
||||
|
||||
<div class="rounded-xl border border-rule bg-paper p-8">
|
||||
<p class="font-serif text-2xl text-ink">Lernen ohne Tracking.</p>
|
||||
<p class="mt-3 text-muted leading-relaxed">
|
||||
Konto erstellen, loslegen. Keine Werbung, kein Analytics-Code,
|
||||
jederzeit exportierbar.
|
||||
</p>
|
||||
<div class="mt-6 flex flex-wrap gap-4">
|
||||
<a href={APP_URL} class="btn-primary">
|
||||
Cardecky öffnen
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href={GIT_URL} target="_blank" rel="noopener" class="btn-ghost">
|
||||
Code ansehen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.prose-cardecky h2 {
|
||||
font-family: var(--font-serif, Georgia, serif);
|
||||
font-size: 1.375rem;
|
||||
font-weight: 600;
|
||||
color: theme('colors.ink');
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.prose-cardecky p {
|
||||
color: theme('colors.ink');
|
||||
line-height: 1.75;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.prose-cardecky ul,
|
||||
.prose-cardecky ol {
|
||||
margin-bottom: 1.25rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.prose-cardecky ul { list-style-type: disc; }
|
||||
.prose-cardecky ol { list-style-type: decimal; }
|
||||
|
||||
.prose-cardecky li {
|
||||
color: theme('colors.ink');
|
||||
line-height: 1.75;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose-cardecky strong {
|
||||
font-weight: 600;
|
||||
color: theme('colors.ink');
|
||||
}
|
||||
|
||||
.prose-cardecky a {
|
||||
color: theme('colors.leaf');
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.prose-cardecky a:hover { color: theme('colors.ink'); }
|
||||
|
||||
.prose-cardecky hr { border-color: theme('colors.rule'); }
|
||||
|
||||
.prose-cardecky pre {
|
||||
background: theme('colors.paper');
|
||||
border: 1px solid theme('colors.rule');
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.prose-cardecky code {
|
||||
font-family: ui-monospace, 'Cascadia Code', monospace;
|
||||
font-size: 0.875rem;
|
||||
color: theme('colors.ink');
|
||||
}
|
||||
</style>
|
||||
246
apps/landing/src/pages/blog/fsrs-algorithmus.astro
Normal file
246
apps/landing/src/pages/blog/fsrs-algorithmus.astro
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import Nav from '../../components/Nav.astro';
|
||||
import Footer from '../../components/Footer.astro';
|
||||
import '../../styles/base.css';
|
||||
|
||||
const APP_URL = 'https://cardecky.mana.how';
|
||||
|
||||
const title = 'Weniger lernen, mehr behalten: Was FSRS bedeutet | Cardecky';
|
||||
const description =
|
||||
'FSRS ist der modernste Spaced-Repetition-Algorithmus — und reduziert die nötigen' +
|
||||
' Wiederholungen um 20–30 % bei gleicher Lernretention. Wie er funktioniert,' +
|
||||
' warum er noch nicht überall Standard ist, und wie Cardecky ihn einsetzt.';
|
||||
---
|
||||
|
||||
<Layout {title} {description}>
|
||||
<Nav />
|
||||
|
||||
<main class="mx-auto max-w-prose px-6 py-16 sm:py-24">
|
||||
|
||||
<header class="mb-12">
|
||||
<p class="section-label mb-4">Algorithmus · Spaced Repetition</p>
|
||||
<h1 class="font-serif text-4xl leading-tight text-ink sm:text-5xl">
|
||||
Weniger lernen, mehr behalten
|
||||
</h1>
|
||||
<p class="mt-5 text-lg leading-relaxed text-muted">
|
||||
FSRS ist der aktuell präziseste Algorithmus für Lernkarten — und er ist noch
|
||||
nicht der Standard. Was sich hinter dem Kürzel verbirgt, warum es einen
|
||||
Unterschied macht, und was er nicht lösen kann.
|
||||
</p>
|
||||
<p class="mt-4 text-xs text-muted">Stand: Mai 2026</p>
|
||||
</header>
|
||||
|
||||
<article class="prose-cardecky">
|
||||
|
||||
<h2>Das Paradox des Lernens</h2>
|
||||
|
||||
<p>
|
||||
Wer viel lernt, behält oft wenig. Das liegt nicht an mangelnder Disziplin,
|
||||
sondern an einer einfachen physiologischen Tatsache: Das Gehirn vergisst nach
|
||||
einer vorhersagbaren Kurve. Hermann Ebbinghaus hat das im 19. Jahrhundert
|
||||
empirisch beschrieben. Seitdem hat sich an der Grundkurve nichts geändert —
|
||||
aber daran, wie gut wir darauf reagieren können, hat sich einiges getan.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Die meisten Lernmethoden ignorieren die Vergessenskurve. Sie lernen in Blöcken,
|
||||
mit Wiederholungen, die zu früh kommen (egal, was man schon weiß) oder zu spät
|
||||
(wenn es bereits vergessen ist). Das Ergebnis: mehr Zeitaufwand, weniger Retention.
|
||||
Spaced Repetition ist der direkte Angriff auf dieses Problem.
|
||||
</p>
|
||||
|
||||
<h2>Was Spaced Repetition löst</h2>
|
||||
|
||||
<p>
|
||||
Das Prinzip ist einfach: Eine Karte wird genau dann wieder gezeigt, wenn man sie
|
||||
vergessen hat — oder kurz davor. Nicht jeden Tag. Nicht nach festem Zeitplan.
|
||||
Sondern genau dann, wenn das Wiederholen etwas bringt.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Eine Karte, die man immer sofort richtig beantwortet, wird seltener gezeigt.
|
||||
Eine, die man regelmäßig vergisst, kommt häufiger. Das klingt selbstverständlich.
|
||||
Die entscheidende Frage ist: Wie genau berechnet ein Algorithmus diesen Zeitpunkt?
|
||||
</p>
|
||||
|
||||
<h2>SM-2 und FSRS: Was sich geändert hat</h2>
|
||||
|
||||
<p>
|
||||
Der Algorithmus, der die letzten Jahrzehnte dominiert hat, heißt <strong>SM-2</strong>
|
||||
— entwickelt von Piotr Woźniak in den späten 1980ern und bis heute das Fundament
|
||||
der meisten Lern-Apps, einschließlich des größten Teils der Anki-Nutzerbasis.
|
||||
SM-2 funktioniert. Aber es hat strukturelle Schwächen.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
SM-2 schätzt, wie lange eine Erinnerung hält, auf Basis eines festen
|
||||
Intervall-Multiplikators und einer einfachen Bewertungsskala. Es kennt keine
|
||||
separaten Konzepte für <strong>Stability</strong> (wie lange eine Erinnerung nach
|
||||
einer Wiederholung hält) und <strong>Difficulty</strong> (wie schwer eine Karte
|
||||
grundsätzlich ist). Es behandelt beide als einen kombinierten Faktor — was
|
||||
bedeutet, dass eine Karte, die man einmal vergessen hat, dauerhaft mit schlechterer
|
||||
Scheduling-Qualität bestraft wird, auch wenn man sie danach zehnmal in Folge
|
||||
richtig beantwortet.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>FSRS</strong> (Free Spaced Repetition Scheduler) arbeitet anders. Der
|
||||
Algorithmus, hauptsächlich entwickelt von Jarrett Ye und seit 2022 quelloffen
|
||||
verfügbar, modelliert Stability und Difficulty als getrennte Parameter. Er nutzt
|
||||
ein Modell, das mit realen Review-Daten trainiert wurde — nicht mit theoretischen
|
||||
Annahmen über das Gedächtnis.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Das Ergebnis: <strong>20–30 % weniger Reviews bei gleicher Retention.</strong>
|
||||
Das ist keine Marketing-Angabe, sondern das Ergebnis von Benchmarks auf realen
|
||||
Datensätzen, die beim Entwickeln von FSRS veröffentlicht wurden. Wer täglich
|
||||
200 Karten reviewt, kommt mit FSRS auf 140–160 — bei gleichem Lernergebnis.
|
||||
Über Monate und Jahre summiert sich das auf Dutzende Stunden.
|
||||
</p>
|
||||
|
||||
<h2>Warum FSRS bei anderen nicht der Standard ist</h2>
|
||||
|
||||
<p>
|
||||
Anki hat FSRS erst 2024 in den Kern integriert — und dann nur als opt-in. Wer
|
||||
Anki installiert und keine Einstellungen ändert, nutzt heute noch SM-2. Die meisten
|
||||
Nutzer:innen wissen nicht, dass FSRS existiert, geschweige denn, wie sie es
|
||||
aktivieren.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Das ist keine Kritik an Anki. Es ist eine Open-Source-App mit einem langen
|
||||
Kompatibilitäts-Vertrag gegenüber Millionen Nutzer:innen und deren bestehenden
|
||||
Decks. Defaults zu ändern ist dort riskant — technisch und in der Community.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Quizlet ist der andere relevante Name in diesem Bereich. Dort hat Spaced
|
||||
Repetition nicht den Weg ins opt-in genommen, sondern vollständig raus.
|
||||
Algorithmen, die Nutzer:innen davon abhalten, dieselbe Karte immer wieder
|
||||
zu sehen, erzeugen weniger Session-Zeit. Weniger Session-Zeit ist ein
|
||||
schlechter Wert für eine werbefinanzierte Plattform.
|
||||
</p>
|
||||
|
||||
<h2>Wie Cardecky das umsetzt</h2>
|
||||
|
||||
<p>
|
||||
Cardecky nutzt FSRS als Standard — kein opt-in, kein Versteck in den
|
||||
Einstellungen, keine Legacy-Defaults. Jede neue Karte, jede neue Wiederholung
|
||||
wird von FSRS geplant. Das ist der Ausgangspunkt, nicht ein Premium-Feature.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Der nächste Schritt ist die <strong>FSRS-Parameter-Optimierung pro Nutzer:in</strong>.
|
||||
FSRS enthält 17 Parameter, die beschreiben, wie ein individuelles Gedächtnis auf
|
||||
Wiederholungen reagiert. Die Standard-Parameter wurden auf einem großen,
|
||||
heterogenen Datensatz trainiert — sie sind gut, aber sie passen nicht perfekt
|
||||
auf jede Person und jedes Lerngebiet. Wer Vokabeln einer neuen Sprache lernt,
|
||||
vergisst anders als jemand, der medizinische Fakten büffelt. Aus der persönlichen
|
||||
Review-History lassen sich diese Parameter re-optimieren. Das ist in Cardecky
|
||||
in Arbeit.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ebenfalls geplant: ein <strong>Transparenz-Tooltip pro Karte</strong> — „Warum
|
||||
werde ich heute gefragt?" Nicht als technische Dokumentation, sondern als
|
||||
konkreter Einblick: Stability, Difficulty, letztes Rating, berechnetes nächstes
|
||||
Intervall. Wer verstehen will, warum eine Karte jetzt auftaucht und nicht in
|
||||
drei Wochen, bekommt die Antwort direkt.
|
||||
</p>
|
||||
|
||||
<h2>Was FSRS nicht löst</h2>
|
||||
|
||||
<p>
|
||||
Ein guter Algorithmus ersetzt keine guten Karten.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
FSRS kann eine Karte optimal planen — aber wenn sie schlecht formuliert ist,
|
||||
zu viel Information auf einmal trägt, oder eine Verbindung zum eigenen Kontext
|
||||
fehlt, hilft das beste Scheduling wenig. Das Gehirn verankert Informationen,
|
||||
wenn sie bedeutungsvoll sind. Dafür ist kein Algorithmus zuständig.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Das andere offene Problem: Wer eine längere Pause macht — Urlaub, Prüfungsstress,
|
||||
Krankheit — baut sich eine Backlog-Queue auf. Anki-Nutzer:innen kennen das Gefühl,
|
||||
nach zehn Tagen Pause auf 500+ fällige Karten zu schauen. Mit einer besseren
|
||||
Scheduling-Basis ist das Problem kleiner, weil FSRS Intervalle realistischer setzt
|
||||
und weniger Überplanungen produziert — aber wer lange pausiert, kommt um den
|
||||
Backlog nicht ganz herum. Strategien dafür (selektive Queue-Verkleinerung,
|
||||
Deck-Priorisierung nach Prüfungsdatum) sind ein separates Problem vom Algorithmus.
|
||||
</p>
|
||||
|
||||
<hr class="my-12 border-rule" />
|
||||
|
||||
<div class="rounded-xl border border-rule bg-paper p-8">
|
||||
<p class="font-serif text-2xl text-ink">Mit FSRS starten — kostenlos.</p>
|
||||
<p class="mt-3 text-muted leading-relaxed">
|
||||
Cardecky nutzt FSRS by default. Kein Opt-in, kein Abo, kein Konfigurieren.
|
||||
Einfach loslegen.
|
||||
</p>
|
||||
<div class="mt-6 flex flex-wrap gap-4">
|
||||
<a href={APP_URL} class="btn-primary">
|
||||
Cardecky öffnen
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.prose-cardecky h2 {
|
||||
font-family: var(--font-serif, Georgia, serif);
|
||||
font-size: 1.375rem;
|
||||
font-weight: 600;
|
||||
color: theme('colors.ink');
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.prose-cardecky p {
|
||||
color: theme('colors.ink');
|
||||
line-height: 1.75;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.prose-cardecky ul,
|
||||
.prose-cardecky ol {
|
||||
margin-bottom: 1.25rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.prose-cardecky ul { list-style-type: disc; }
|
||||
.prose-cardecky ol { list-style-type: decimal; }
|
||||
|
||||
.prose-cardecky li {
|
||||
color: theme('colors.ink');
|
||||
line-height: 1.75;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose-cardecky strong {
|
||||
font-weight: 600;
|
||||
color: theme('colors.ink');
|
||||
}
|
||||
|
||||
.prose-cardecky a {
|
||||
color: theme('colors.leaf');
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.prose-cardecky a:hover { color: theme('colors.ink'); }
|
||||
|
||||
.prose-cardecky hr { border-color: theme('colors.rule'); }
|
||||
</style>
|
||||
408
apps/landing/src/pages/blog/gute-lernkarten.astro
Normal file
408
apps/landing/src/pages/blog/gute-lernkarten.astro
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import Nav from '../../components/Nav.astro';
|
||||
import Footer from '../../components/Footer.astro';
|
||||
import '../../styles/base.css';
|
||||
|
||||
const APP_URL = 'https://cardecky.mana.how';
|
||||
|
||||
const title = 'Wie man Lernkarten schreibt, die wirklich funktionieren | Cardecky';
|
||||
const description =
|
||||
'Die meisten Lernkarten sind zu lang, zu abstrakt oder zu kontextfrei. Fünf konkrete' +
|
||||
' Prinzipien für Karten, die das Gehirn tatsächlich verankert — mit Gegenbeispielen.';
|
||||
---
|
||||
|
||||
<Layout {title} {description}>
|
||||
<Nav />
|
||||
|
||||
<main class="mx-auto max-w-prose px-6 py-16 sm:py-24">
|
||||
|
||||
<!-- Artikel-Header -->
|
||||
<header class="mb-12">
|
||||
<p class="section-label mb-4">Lernen · Methodik</p>
|
||||
<h1 class="font-serif text-4xl leading-tight text-ink sm:text-5xl">
|
||||
Wie man Lernkarten schreibt, die wirklich funktionieren
|
||||
</h1>
|
||||
<p class="mt-5 text-lg leading-relaxed text-muted">
|
||||
Der beste Algorithmus nützt nichts, wenn die Karten schlecht gebaut sind. Fünf
|
||||
Prinzipien für Karten, die das Gehirn tatsächlich behält.
|
||||
</p>
|
||||
<p class="mt-4 text-xs text-muted">Stand: Mai 2026</p>
|
||||
</header>
|
||||
|
||||
<article class="prose-cardecky">
|
||||
|
||||
<!-- Einstieg -->
|
||||
<p>
|
||||
Wer mit Spaced Repetition anfängt und nach drei Wochen merkt, dass es nicht
|
||||
funktioniert, sucht den Fehler meistens an der falschen Stelle: beim Algorithmus, bei
|
||||
der App, bei der Wiederholungsfrequenz. Die eigentliche Ursache sitzt fast immer
|
||||
früher — in den Karten selbst.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Schlechte Karten machen gutes Scheduling wertlos. Eine Karte, die drei Fakten
|
||||
gleichzeitig abfragt, hat kein definiertes Lernergebnis. Eine Karte ohne Kontext
|
||||
wird isoliert abgespeichert — und isoliert Gespeichertes verblasst, egal wie oft
|
||||
es wiederholt wird. FSRS kann berechnen, wann du eine Karte wieder sehen sollst;
|
||||
es kann nicht reparieren, was die Karte von vornherein falsch fragt.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Die folgenden fünf Prinzipien kommen aus der Lernforschung, aus Piotr Woźniaks
|
||||
„Twenty Rules of Formulating Knowledge" und aus der Praxis der Anki- und
|
||||
FSRS-Community. Sie sind nicht kompliziert — aber sie erfordern beim Schreiben
|
||||
Disziplin.
|
||||
</p>
|
||||
|
||||
<!-- Prinzip 1 -->
|
||||
<h2>1. Eine Frage, eine Antwort</h2>
|
||||
|
||||
<p>
|
||||
Das ist der häufigste Anfängerfehler, und er fühlt sich beim Schreiben nicht wie
|
||||
ein Fehler an. Man kennt ein Thema, will es „effizient" auf Karten bringen, und
|
||||
packt fünf zusammengehörige Fakten auf eine Karte. Das Ergebnis ist eine Karte, die
|
||||
du dir nie wirklich merken kannst — weil du nie weißt, ob du sie gewusst hast.
|
||||
</p>
|
||||
|
||||
<p>Beispiel einer schlechten Karte:</p>
|
||||
|
||||
<pre>F: Was sind die Eigenschaften von Mitochondrien?
|
||||
A: Doppelmembran, eigene DNA, ATP-Produktion durch
|
||||
oxidative Phosphorylierung, Apoptose-Steuerung,
|
||||
Ursprung aus endosymbiotischer Bakterien.</pre>
|
||||
|
||||
<p>Besser: fünf Karten.</p>
|
||||
|
||||
<pre>F: Welche Membran-Struktur haben Mitochondrien?
|
||||
A: Doppelmembran (Außen- und Innenmembran).
|
||||
|
||||
F: Wodurch unterscheidet sich die Mitochondrien-DNA
|
||||
von der Zellkern-DNA?
|
||||
A: Sie ist zirkulär und erinnert an Bakterien-DNA
|
||||
— Hinweis auf den endosymbiontischen Ursprung.</pre>
|
||||
|
||||
<p>
|
||||
Jede Karte hat jetzt ein klares Richtig oder Falsch. Wenn du sie beim Scheduling
|
||||
als „schwierig" markierst, weiß der Algorithmus, was du nicht weißt — und kann
|
||||
das gezielt wiederholen. Mit der Sammelkarte kann er das nicht.
|
||||
</p>
|
||||
|
||||
<!-- Prinzip 2 -->
|
||||
<h2>2. Kontext ist kein Luxus</h2>
|
||||
|
||||
<p>
|
||||
Isolierte Fakten vergisst das Gehirn schneller als eingebettete. Das ist keine
|
||||
Faustregel, sondern ein gut belegter Effekt: Bedeutungsträgern gegenüber
|
||||
bedeutungsfreien Stimuli fällt das Behalten leichter, weil das Gehirn neue
|
||||
Information an bestehende Netzwerke knüpfen kann.
|
||||
</p>
|
||||
|
||||
<p>Eine typische kontextfreie Karte:</p>
|
||||
|
||||
<pre>F: Was ist Apoptose?
|
||||
A: Programmierter Zelltod.</pre>
|
||||
|
||||
<p>Eine Karte mit Kontext:</p>
|
||||
|
||||
<pre>F: Warum ist Apoptose für die Embryonalentwicklung
|
||||
wichtig?
|
||||
A: Finger entstehen, weil die Zellen zwischen ihnen
|
||||
gezielt in Apoptose gehen — nicht, weil Finger
|
||||
wachsen, sondern weil das Material dazwischen
|
||||
verschwindet.</pre>
|
||||
|
||||
<p>
|
||||
Die zweite Karte fragt dasselbe Konzept ab — aber in einem Zusammenhang, der sich
|
||||
bildlich einprägt. „Programmierter Zelltod" bleibt ein Begriff. Finger, die sich
|
||||
aus einem Gewebeklumpen herausschälen, bleibt ein Bild.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Das bedeutet nicht, dass jede Karte einen langen Erklärungstext braucht. Ein
|
||||
einziger konkreter Anwendungsfall, ein Beispiel, ein Gegenbeispiel — das reicht
|
||||
meistens. Die Frage „in welchem Zusammenhang?" vor dem Schreiben stellen.
|
||||
</p>
|
||||
|
||||
<!-- Prinzip 3 -->
|
||||
<h2>3. Minimales, aber vollständiges Wissen</h2>
|
||||
|
||||
<p>
|
||||
Piotr Woźniak nennt das „minimum information principle": eine Karte so kurz wie
|
||||
möglich, aber nicht kürzer. Die Spannung liegt im „aber nicht kürzer" — eine Karte,
|
||||
die so gekürzt wurde, dass sie nur noch mit Vorwissen richtig beantwortet werden
|
||||
kann, das du beim Wiederholen vielleicht nicht mehr parat hast, testet nichts
|
||||
Sinnvolles.
|
||||
</p>
|
||||
|
||||
<p>Zu lang:</p>
|
||||
|
||||
<pre>F: Erkläre den Unterschied zwischen prokaryotischen
|
||||
und eukaryotischen Zellen, inklusive Beispielen,
|
||||
evolutionärem Ursprung und Zellorganellen.
|
||||
A: [langer Absatz]</pre>
|
||||
|
||||
<p>Zu kurz (und zu vage):</p>
|
||||
|
||||
<pre>F: Prokaryoten?
|
||||
A: Keine Zellkerne.</pre>
|
||||
|
||||
<p>Genau richtig:</p>
|
||||
|
||||
<pre>F: Was fehlt Prokaryoten, das Eukaryoten haben?
|
||||
A: Einen membranumhüllten Zellkern.</pre>
|
||||
|
||||
<p>
|
||||
Ein gutes Testkriterium: Wenn du eine Karte nach einem Monat Pause siehst und die
|
||||
Frage vollständig verstehst, ist sie gut formuliert. Wenn du erst rekonstruieren
|
||||
musst, was du damals gemeint hast, ist sie zu kurz oder zu vage.
|
||||
</p>
|
||||
|
||||
<!-- Prinzip 4 -->
|
||||
<h2>4. Cloze richtig einsetzen</h2>
|
||||
|
||||
<p>
|
||||
Lückentexte — in Anki-Notation <code>{{c1::Begriff}}</code> — sind nicht
|
||||
einfach eine andere Formatierung derselben Karte. Sie eignen sich besonders gut für
|
||||
Prozesse, Sequenzen und Zusammenhänge, weil der umgebende Satz als Kontext direkt
|
||||
sichtbar bleibt.
|
||||
</p>
|
||||
|
||||
<p>Klassische Frage-Antwort-Karte für einen Begriff im Kontext:</p>
|
||||
|
||||
<pre>F: Was ist das Prinzip hinter dem Krebs-Zyklus?
|
||||
A: Acetyl-CoA wird oxidiert, ATP gewonnen,
|
||||
CO₂ abgegeben.</pre>
|
||||
|
||||
<p>Als Cloze — stärker, weil der Satz als Ganzes gelernt wird:</p>
|
||||
|
||||
<pre>Der Krebs-Zyklus oxidiert {{c1::Acetyl-CoA}},
|
||||
gewinnt dabei {{c2::ATP}} und gibt
|
||||
{{c3::CO₂}} ab.</pre>
|
||||
|
||||
<p>
|
||||
Cloze ist schwächer, wenn der Lückentext rein zufällig wirkt — also wenn aus dem
|
||||
Satz nicht klar wird, was gesucht ist, und jedes beliebige Wort passen könnte.
|
||||
Dann ist eine direkte Frage ehrlicher.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Cloze ist auch kein Allheilmittel gegen das Ein-Fakt-Problem: ein Satz mit fünf
|
||||
Lücken ist immer noch eine Karte mit fünf Fakten. Besser: pro Satz eine Lücke,
|
||||
mehrere Karten.
|
||||
</p>
|
||||
|
||||
<!-- Prinzip 5 -->
|
||||
<h2>5. Eigene Worte, eigene Bilder</h2>
|
||||
|
||||
<p>
|
||||
Auswendig gelernte Definitionen verblassen schneller als selbst formulierte
|
||||
Erklärungen. Das liegt daran, dass bei der Eigenformulierung das Verstehen
|
||||
vorausgeht — wer etwas in eigene Worte übersetzt, hat es bereits verarbeitet,
|
||||
nicht nur kopiert.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Lehrbuchdefinition direkt übernommen — typische Anfänger-Karte:
|
||||
</p>
|
||||
|
||||
<pre>F: Was ist osmotischer Druck?
|
||||
A: Der Druck, der aufgewendet werden muss, um den
|
||||
osmotischen Fluss einer Lösung durch eine
|
||||
semipermeable Membran zu verhindern.</pre>
|
||||
|
||||
<p>Eigenformulierung:</p>
|
||||
|
||||
<pre>F: Was ist osmotischer Druck — in einem Satz,
|
||||
ohne Fachsprache?
|
||||
A: Das Wasser will auf die Seite mit mehr
|
||||
gelöstem Zeug; der osmotische Druck ist der
|
||||
Gegendruck, der das verhindert.</pre>
|
||||
|
||||
<p>
|
||||
Die zweite Version verliert Präzision — das ist ein echter Trade-off. Für
|
||||
klausurrelevante exakte Definitionen braucht man beide: die präzise Formulierung
|
||||
und die eigene Erklärung. Dann zwei Karten. Nicht eine Karte, die beides
|
||||
gleichzeitig will.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Bilder funktionieren nach demselben Prinzip: visuell verankerte Information hat
|
||||
mehr Anknüpfungspunkte im Gedächtnis als rein verbale. Ein Diagramm, ein
|
||||
selbst gezeichneter Sketch, ein Screenshot aus einem Lehrvideo — das schlägt
|
||||
eine nochmals kopierte Textdefinition fast immer.
|
||||
</p>
|
||||
|
||||
<!-- AI-Generierung -->
|
||||
<h2>Was AI-Generierung kann — und was nicht</h2>
|
||||
|
||||
<p>
|
||||
AI-Tools können Karten aus einem Text generieren, und das kann die Arbeit
|
||||
reduzieren: statt zwanzig Karten von Hand zu schreiben, bekommt man einen
|
||||
Entwurf, der überarbeitet werden kann. Das ist ein nützliches Werkzeug.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Es ersetzt aber nicht das Nachdenken über die eigene Formulierung. AI-generierte
|
||||
Karten folgen oft dem Muster der Lehrbuchdefinition — sie übernehmen Sprache
|
||||
aus dem Quelltext, statt sie zu übersetzen. Sie splitten zu selten von alleine
|
||||
auf ein Fakt pro Karte. Und sie können nicht wissen, welcher Kontext für dich
|
||||
der einprägsame ist.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Der sinnvolle Workflow: AI generiert einen Rohsatz von Karten, du überarbeitest
|
||||
ihn nach den fünf Prinzipien. Das ist weniger Arbeit als von null, aber es ist
|
||||
immer noch Arbeit.
|
||||
</p>
|
||||
|
||||
<!-- Cardecky-Einbindung -->
|
||||
<h2>Welche Kartentypen welches Prinzip unterstützen</h2>
|
||||
|
||||
<p>
|
||||
Cardecky hat vier native Kartentypen, die direkt auf diese Prinzipien einzahlen:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Vorderseite/Rückseite</strong> — der Standard für Prinzip 1 und 3. Eine
|
||||
Frage, eine Antwort. Keine Extras.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Lückentext (Cloze)</strong> — für Prinzip 4: Prozesse und Zusammenhänge,
|
||||
bei denen der umgebende Satz als Kontext stehen bleibt.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Bild-Karte</strong> — für Prinzip 5: Diagramme, Sketchnotes, annotierte
|
||||
Screenshots als eigenständiger Karteninhalt.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Typing-Karte</strong> — erzwingt freies Erinnern statt
|
||||
Wiedererkennen; sinnvoll für Formeln, Fachbegriffe und exakte
|
||||
Definitionen, bei denen das Erkennen zu wenig ist.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Multiple Choice ist ebenfalls verfügbar — nützlich für erste Annäherungen an neues
|
||||
Material, wo freies Erinnern noch nicht möglich ist. Für konsolidiertes Wissen ist
|
||||
Wiedererkennen aus Optionen aber schwächer als freies Abrufen.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In Arbeit: ein AI-Cloze-Generator, der aus einem eigenen Text direkt
|
||||
Lückentextkarten erstellt, und ein Card-Split-Vorschlag, der Karten mit
|
||||
mehreren Fakten automatisch zur Teilung vorschlägt. Beide Features sind noch nicht
|
||||
live — sie werden ergänzt, wenn sie auf einem Qualitätsniveau sind, das die
|
||||
oben beschriebenen Prinzipien nicht untergräbt.
|
||||
</p>
|
||||
|
||||
<!-- CTA -->
|
||||
<hr class="my-12 border-rule" />
|
||||
|
||||
<div class="rounded-xl border border-rule bg-paper p-8">
|
||||
<p class="font-serif text-2xl text-ink">Karten schreiben — in Cardecky</p>
|
||||
<p class="mt-3 text-muted leading-relaxed">
|
||||
Alle fünf Kartentypen verfügbar, FSRS-Scheduling ab der ersten Karte,
|
||||
kein Abo für das eigentliche Lernen.
|
||||
</p>
|
||||
<div class="mt-6 flex flex-wrap gap-4">
|
||||
<a href={APP_URL} class="btn-primary">
|
||||
Cardecky öffnen
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/* Prose-Styles für den Artikel — ohne externes @tailwindcss/typography */
|
||||
.prose-cardecky h2 {
|
||||
font-family: var(--font-serif, Georgia, serif);
|
||||
font-size: 1.375rem;
|
||||
font-weight: 600;
|
||||
color: theme('colors.ink');
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.prose-cardecky p {
|
||||
color: theme('colors.ink');
|
||||
line-height: 1.75;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.prose-cardecky ul,
|
||||
.prose-cardecky ol {
|
||||
margin-bottom: 1.25rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.prose-cardecky ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.prose-cardecky ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.prose-cardecky li {
|
||||
color: theme('colors.ink');
|
||||
line-height: 1.75;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose-cardecky li + li {
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.prose-cardecky strong {
|
||||
font-weight: 600;
|
||||
color: theme('colors.ink');
|
||||
}
|
||||
|
||||
.prose-cardecky a {
|
||||
color: theme('colors.leaf');
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.prose-cardecky a:hover {
|
||||
color: theme('colors.ink');
|
||||
}
|
||||
|
||||
.prose-cardecky hr {
|
||||
border-color: theme('colors.rule');
|
||||
}
|
||||
|
||||
.prose-cardecky pre {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
background-color: theme('colors.paper');
|
||||
border: 1px solid theme('colors.rule');
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
overflow-x: auto;
|
||||
color: theme('colors.ink');
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.prose-cardecky code {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.875em;
|
||||
background-color: theme('colors.paper');
|
||||
border: 1px solid theme('colors.rule');
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.1em 0.35em;
|
||||
}
|
||||
</style>
|
||||
97
apps/landing/src/pages/blog/index.astro
Normal file
97
apps/landing/src/pages/blog/index.astro
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import Nav from '../../components/Nav.astro';
|
||||
import Footer from '../../components/Footer.astro';
|
||||
import '../../styles/base.css';
|
||||
|
||||
const posts = [
|
||||
{
|
||||
href: '/blog/quizlet-paywall',
|
||||
tag: 'Migration',
|
||||
title: 'Quizlet zieht die Paywall hoch — was jetzt?',
|
||||
summary:
|
||||
'Fünf Millionen Nutzer:innen haben Quizlet in den letzten zwei Jahren verlassen. Was hinter der Bezahlschranke steckt und wie der Wechsel in wenigen Minuten klappt.',
|
||||
date: 'Mai 2026',
|
||||
},
|
||||
{
|
||||
href: '/blog/fsrs-algorithmus',
|
||||
tag: 'Algorithmus',
|
||||
title: 'Weniger lernen, mehr behalten: Was FSRS bedeutet',
|
||||
summary:
|
||||
'FSRS reduziert die nötigen Wiederholungen um 20–30 % bei gleicher Retention. Wie der Algorithmus funktioniert — und warum er bei anderen Apps noch nicht der Standard ist.',
|
||||
date: 'Mai 2026',
|
||||
},
|
||||
{
|
||||
href: '/blog/deine-daten',
|
||||
tag: 'Datenschutz',
|
||||
title: 'Deine Karten, deine Daten',
|
||||
summary:
|
||||
'Lerndaten verraten mehr als die meisten ahnen. Was Cardecky damit macht — und was nicht — lässt sich im öffentlichen Quellcode nachprüfen.',
|
||||
date: 'Mai 2026',
|
||||
},
|
||||
{
|
||||
href: '/blog/anki-zu-kompliziert',
|
||||
tag: 'Vergleich',
|
||||
title: 'Anki ist mächtig — und trotzdem schwer empfehlbar',
|
||||
summary:
|
||||
'Anki ist technisch überlegen. Trotzdem hören die meisten auf — nicht wegen des Algorithmus, sondern wegen Interface und Lernkurve.',
|
||||
date: 'Mai 2026',
|
||||
},
|
||||
{
|
||||
href: '/blog/gute-lernkarten',
|
||||
tag: 'Lernen',
|
||||
title: 'Wie man Lernkarten schreibt, die wirklich funktionieren',
|
||||
summary:
|
||||
'Der beste Algorithmus nützt nichts, wenn die Karten schlecht gebaut sind. Fünf Prinzipien — mit Gegenbeispielen.',
|
||||
date: 'Mai 2026',
|
||||
},
|
||||
] as const;
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Artikel — Cardecky"
|
||||
description="Hintergrund und Methodik rund um Spaced Repetition, Lernkarten und die Cardecky-App."
|
||||
>
|
||||
<Nav />
|
||||
|
||||
<main class="mx-auto max-w-content px-6 py-16 sm:py-24">
|
||||
<p class="section-label">Blog</p>
|
||||
<h1 class="mt-3 font-serif text-display text-ink">Artikel.</h1>
|
||||
<p class="mt-4 max-w-prose text-muted">
|
||||
Hintergrund zu Spaced Repetition, Vergleiche mit anderen Apps und Methodik
|
||||
für besseres Lernen.
|
||||
</p>
|
||||
|
||||
<ul class="mt-12 space-y-4" role="list">
|
||||
{posts.map((post) => (
|
||||
<li>
|
||||
<a
|
||||
href={post.href}
|
||||
class="group flex flex-col gap-2 rounded-xl border border-rule bg-paper p-6 transition-shadow hover:shadow-md sm:flex-row sm:items-start sm:gap-8"
|
||||
>
|
||||
<div class="flex shrink-0 items-center gap-3 sm:w-40 sm:flex-col sm:items-start sm:gap-1">
|
||||
<span class="section-label">{post.tag}</span>
|
||||
<span class="text-xs text-muted">{post.date}</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-serif text-xl font-semibold leading-snug text-ink group-hover:text-leaf transition-colors">
|
||||
{post.title}
|
||||
</p>
|
||||
<p class="mt-2 text-sm leading-relaxed text-muted">
|
||||
{post.summary}
|
||||
</p>
|
||||
</div>
|
||||
<span class="hidden shrink-0 items-center gap-1 text-sm font-medium text-leaf sm:flex">
|
||||
Lesen
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
||||
309
apps/landing/src/pages/blog/quizlet-paywall.astro
Normal file
309
apps/landing/src/pages/blog/quizlet-paywall.astro
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import Nav from '../../components/Nav.astro';
|
||||
import Footer from '../../components/Footer.astro';
|
||||
import '../../styles/base.css';
|
||||
|
||||
const APP_URL = 'https://cardecky.mana.how';
|
||||
const IMPORT_URL = 'https://cardecky.mana.how/import';
|
||||
|
||||
const title = 'Quizlet zieht die Paywall hoch — was jetzt? | Cardecky';
|
||||
const description =
|
||||
'Quizlet hat 2022–2023 Learn Mode, Test Mode und echtes Spaced Repetition hinter eine' +
|
||||
' Bezahlschranke gesperrt. Was das konkret bedeutet — und wie der Wechsel zu Cardecky in' +
|
||||
' wenigen Minuten klappt.';
|
||||
---
|
||||
|
||||
<Layout {title} {description}>
|
||||
<Nav />
|
||||
|
||||
<main class="mx-auto max-w-prose px-6 py-16 sm:py-24">
|
||||
|
||||
<!-- Artikel-Header -->
|
||||
<header class="mb-12">
|
||||
<p class="section-label mb-4">Lernkarten · Migration</p>
|
||||
<h1 class="font-serif text-4xl leading-tight text-ink sm:text-5xl">
|
||||
Quizlet zieht die Paywall hoch — was jetzt?
|
||||
</h1>
|
||||
<p class="mt-5 text-lg leading-relaxed text-muted">
|
||||
Millionen von Nutzer:innen haben jahrelang auf Quizlet gelernt. Dann kam die
|
||||
Bezahlschranke. Was dahintersteckt, was du verloren hast — und wie du in wenigen Minuten
|
||||
auf Cardecky umziehst.
|
||||
</p>
|
||||
<p class="mt-4 text-xs text-muted">Stand: Mai 2026</p>
|
||||
</header>
|
||||
|
||||
<article class="prose-cardecky">
|
||||
|
||||
<!-- 1 -->
|
||||
<h2>Was Quizlet getan hat</h2>
|
||||
|
||||
<p>
|
||||
Quizlet startete 2005 als Karteikarten-Tool ohne Geschäftsmodell. Das hat sich geändert.
|
||||
2022 und 2023 hat das Unternehmen schrittweise die Kern-Lernmodi — Learn Mode, Test Mode
|
||||
und den zugehörigen Scheduling-Algorithmus — hinter ein Abo (Quizlet Plus,
|
||||
$35.99/Jahr) gesperrt. Wer nicht zahlt, kann Karten ansehen und in simplen
|
||||
Karteikarten-Ansichten blättern. Der eigentliche Lernmechanismus, der aus dem
|
||||
Blättern ein gezieltes Üben macht, ist nur noch für Zahler zugänglich.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Quizlet hat das nicht als Einschnitt kommuniziert, sondern als
|
||||
„Premium-Erweiterung". Die Realität: wer sich über Monate eine Karten-Sammlung
|
||||
aufgebaut hat, kann diese ohne Abo kaum noch sinnvoll nutzen.
|
||||
</p>
|
||||
|
||||
<!-- 2 -->
|
||||
<h2>Was konkret hinter der Paywall verschwunden ist</h2>
|
||||
|
||||
<p>
|
||||
Das sind keine Randfeatures. Learn Mode ist der Modus, der Quizlet überhaupt vom
|
||||
einfachen Karteikarten-Blättern unterscheidet: er verfolgt deinen Fortschritt,
|
||||
wiederholt Karten, die du falsch beantwortest hast, und passt die Reihenfolge
|
||||
deiner Lernkarten dynamisch an. Test Mode erstellt automatisierte Prüfungs-
|
||||
Simulationen aus deinen Sets. Ohne diese Modi ist Quizlet ein Karten-Viewer —
|
||||
nützlich zum Nachschlagen, nicht zum Lernen.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Hinzu kommt: Quizlet sammelt Nutzungsdaten und schaltet Werbung für Nicht-Zahler.
|
||||
Der Code ist proprietär — du kannst nicht nachprüfen, welche Daten wohin gehen.
|
||||
Und Daten-Export gibt es nur in begrenztem Umfang; wer zu einem anderen Dienst
|
||||
wechseln will, hat keine einfache Brücke.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Rund fünf Millionen Nutzer:innen haben in den vergangenen zwei Jahren aktiv nach
|
||||
Alternativen gesucht. Das ist kein Protest, sondern eine vernünftige Reaktion
|
||||
darauf, dass ein Werkzeug, auf das man sich verlassen hatte, unter den Händen
|
||||
wegbricht.
|
||||
</p>
|
||||
|
||||
<!-- 3 -->
|
||||
<h2>Was „echtes Spaced Repetition" bedeutet</h2>
|
||||
|
||||
<p>
|
||||
Spaced Repetition ist ein lernpsychologisches Prinzip: Karten, die du sicher
|
||||
weißt, siehst du seltener; Karten, bei denen du schwankst oder scheitern,
|
||||
kommen früher wieder. Der Effekt ist gut belegt — das Vergessen-Intervall passt
|
||||
sich an dein tatsächliches Erinnerungsvermögen an, statt nach fester Reihenfolge
|
||||
zu laufen.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Die Qualität hängt am Algorithmus. Quizlets hauseigene Implementierung war eine
|
||||
vereinfachte Variante. Der heute wissenschaftlich bevorzugte Standard ist
|
||||
<strong>FSRS</strong> (Free Spaced Repetition Scheduler) — entwickelt 2022 von
|
||||
Jarrett Ye, inzwischen in mehreren Studien gegen ältere Algorithmen getestet,
|
||||
standardmäßig in Anki 23.10+ enthalten. FSRS modelliert zwei Gedächtnisparameter
|
||||
pro Karte (Stabilität und Abrufbarkeit) und passt die Wiederholungsabstände
|
||||
präziser an als frühere Verfahren.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Cardecky implementiert FSRS direkt — nicht als nachträgliches Feature, sondern
|
||||
als Kern des Scheduling-Systems ab Tag 1.
|
||||
</p>
|
||||
|
||||
<!-- 4 -->
|
||||
<h2>Cardecky als Alternative — was wir bieten und was noch fehlt</h2>
|
||||
|
||||
<p>
|
||||
Cardecky ist die Spaced-Repetition-App des mana e.V., eines Schweizer Vereins.
|
||||
Das ist keine Marketing-Formel, sondern eine Aussage über das Geschäftsmodell:
|
||||
kein Investor, kein Renditedruck, keine Exit-Strategie. Die App läuft auf
|
||||
selbst-betriebener Infrastruktur in Deutschland.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Was heute verfügbar ist:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>FSRS-Algorithmus</strong> — aktueller Stand der Forschung, serverside
|
||||
berechnet, keine Näherungslösung.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Sechs Kartentypen</strong> — Vorderseite/Rückseite, Lückentext, Multiple
|
||||
Choice, Bild, Tipp-Karte, Rich Text.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Quizlet-Import</strong> — direkt eingebaut. Deine Sets landen in Minuten
|
||||
in Cardecky, mit Formatierung.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Anki-Import</strong> — .apkg-Dateien werden geparst, Decks und Karten
|
||||
übernommen.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Kein Tracking, keine Werbung</strong> — kein Analytics-Code, nachweisbar
|
||||
im öffentlichen Quellcode unter
|
||||
<a href="https://git.mana.how/mana/cards" target="_blank" rel="noopener">
|
||||
git.mana.how/mana/cards</a>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Anti-Lock-in per Design</strong> — Export jederzeit als .apkg, CSV
|
||||
oder DSGVO-Vollexport. Wir bauen keine Karte, die du nicht mitnehmen kannst.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Was noch fehlt, und das soll hier nicht versteckt werden: Cardecky ist jung.
|
||||
Eine native App (iOS/Android) ist in Planung, aber noch nicht verfügbar — heute
|
||||
ist Cardecky eine Web-App, installierbar als PWA. Die kuratierte Deck-Bibliothek
|
||||
wächst; wer spezifische Fachbereiche sucht, findet dort heute noch Lücken.
|
||||
Kollaborative Decks im Team sind auf der Roadmap, noch nicht live. Wer eine
|
||||
ausgereifte Desktop-App mit jahrelanger Community erwartet, sollte auch Anki
|
||||
in Betracht ziehen — das ist keine Schwäche, sondern ein Hinweis auf den
|
||||
unterschiedlichen Stand.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Die Kernfunktionen — Karten anlegen, lernen, importieren, exportieren — sind
|
||||
vollständig und ohne Bezahlschranke zugänglich. Tiers gibt es nur für
|
||||
KI-gestützte Features (automatische Kartengenerierung, Erklärungen via mana LLM)
|
||||
und erweiterten Storage. Das eigentliche Lernen ist und bleibt kostenlos.
|
||||
</p>
|
||||
|
||||
<!-- 5 -->
|
||||
<h2>Migration in drei Schritten</h2>
|
||||
|
||||
<p>
|
||||
Der Wechsel von Quizlet zu Cardecky dauert je nach Set-Größe fünf bis fünfzehn
|
||||
Minuten.
|
||||
</p>
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
<strong>Quizlet-Export vorbereiten.</strong> Öffne in Quizlet das Set, das du
|
||||
mitnehmen willst. Unter „Mehr" → „Exportieren" kannst du das Set als
|
||||
Tab-getrennte Textdatei herunterladen. Quizlet bietet diesen Export auch ohne
|
||||
Plus-Abo an.
|
||||
</li>
|
||||
<li>
|
||||
<strong>In Cardecky importieren.</strong> Gehe auf
|
||||
<a href={IMPORT_URL} target="_blank" rel="noopener">cardecky.mana.how/import</a>
|
||||
und wähle „Quizlet-Import". Lade die Textdatei hoch oder füge den Text direkt
|
||||
ein — Cardecky erkennt das Format automatisch und zeigt dir vor dem Import eine
|
||||
Vorschau der Karten.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Deck einrichten und starten.</strong> Nach dem Import landet das Deck in
|
||||
deiner Bibliothek. FSRS-Scheduling startet automatisch beim ersten Lernen. Du
|
||||
musst nichts konfigurieren — das System passt sich anhand deiner Antworten an.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<p>
|
||||
Wer bereits Anki-Decks hat, kann alternativ .apkg-Dateien direkt hochladen — der
|
||||
Import-Flow ist derselbe.
|
||||
</p>
|
||||
|
||||
<!-- 6 -->
|
||||
<h2>Datenschutz und Unabhängigkeit</h2>
|
||||
|
||||
<p>
|
||||
Cardecky wird von mana e.V. betrieben — einem Schweizer Verein ohne Investoren,
|
||||
ohne Werbepartner, ohne Renditeziel. Server stehen in Deutschland, Datenverarbeitung
|
||||
läuft ausschließlich auf Vereinsinfrastruktur. Es gibt keinen Analytics-Code,
|
||||
keinen Tracking-Pixel, keine Drittanbieter-SDK für Nutzungsauswertung. Das lässt
|
||||
sich im öffentlichen Quellcode nachprüfen.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Daten-Souveränität ist für mana kein Marketingversprechen, sondern Wert 1 in der
|
||||
Werte-Hierarchie des Vereins: Daten gehören den Nutzer:innen, nicht uns. Jederzeit
|
||||
Export, jederzeit Löschung, DSGVO-Auskunft auf Knopfdruck. Diese Versprechen
|
||||
stehen in den Vereinsstatuten und können nicht durch eine Unternehmens-Übernahme
|
||||
wegverhandelt werden — weil es keine Unternehmensanteile gibt, die man kaufen
|
||||
könnte.
|
||||
</p>
|
||||
|
||||
<!-- CTA -->
|
||||
<hr class="my-12 border-rule" />
|
||||
|
||||
<div class="rounded-xl border border-rule bg-paper p-8">
|
||||
<p class="font-serif text-2xl text-ink">Bereit für den Wechsel?</p>
|
||||
<p class="mt-3 text-muted leading-relaxed">
|
||||
Import deiner Quizlet-Sets dauert wenige Minuten. Kein Abo, kein
|
||||
Kreditkartenfeld, keine Fallstricke.
|
||||
</p>
|
||||
<div class="mt-6 flex flex-wrap gap-4">
|
||||
<a href={APP_URL} class="btn-primary">
|
||||
Cardecky öffnen
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href={IMPORT_URL} class="btn-ghost">Direkt zum Import</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/* Prose-Styles für den Artikel — ohne externes @tailwindcss/typography */
|
||||
.prose-cardecky h2 {
|
||||
font-family: var(--font-serif, Georgia, serif);
|
||||
font-size: 1.375rem;
|
||||
font-weight: 600;
|
||||
color: theme('colors.ink');
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.prose-cardecky p {
|
||||
color: theme('colors.ink');
|
||||
line-height: 1.75;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.prose-cardecky ul,
|
||||
.prose-cardecky ol {
|
||||
margin-bottom: 1.25rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.prose-cardecky ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.prose-cardecky ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.prose-cardecky li {
|
||||
color: theme('colors.ink');
|
||||
line-height: 1.75;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose-cardecky li + li {
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.prose-cardecky strong {
|
||||
font-weight: 600;
|
||||
color: theme('colors.ink');
|
||||
}
|
||||
|
||||
.prose-cardecky a {
|
||||
color: theme('colors.leaf');
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.prose-cardecky a:hover {
|
||||
color: theme('colors.ink');
|
||||
}
|
||||
|
||||
.prose-cardecky hr {
|
||||
border-color: theme('colors.rule');
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,6 +6,7 @@ import CardTypes from '../components/CardTypes.astro';
|
|||
import HowItWorks from '../components/HowItWorks.astro';
|
||||
import Features from '../components/Features.astro';
|
||||
import ManaSection from '../components/ManaSection.astro';
|
||||
import BlogTeaser from '../components/BlogTeaser.astro';
|
||||
import CTASection from '../components/CTASection.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import '../styles/base.css';
|
||||
|
|
@ -21,6 +22,7 @@ import '../styles/base.css';
|
|||
<HowItWorks />
|
||||
<Features />
|
||||
<ManaSection />
|
||||
<BlogTeaser />
|
||||
<CTASection />
|
||||
</main>
|
||||
<Footer />
|
||||
|
|
|
|||
|
|
@ -68,8 +68,19 @@ export interface UserStats {
|
|||
due_forecast: { day: string; n: number }[];
|
||||
leech_threshold: number;
|
||||
leech_cards: LeechCard[];
|
||||
difficulty_distribution: { bucket: 'very_easy' | 'easy' | 'medium' | 'hard' | 'very_hard'; n: number }[];
|
||||
deck_mastery: { deck_id: string; deck_name: string; total: number; mastered: number; pct: number }[];
|
||||
}
|
||||
|
||||
export function loadStats() {
|
||||
return api<UserStats>('/api/v1/me/stats');
|
||||
}
|
||||
|
||||
export interface UserSummary {
|
||||
streak_days: number;
|
||||
due_now: number;
|
||||
}
|
||||
|
||||
export function loadSummary() {
|
||||
return api<UserSummary>('/api/v1/me/summary');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ export type DueReview = Review & {
|
|||
card?: Pick<Card, 'id' | 'deck_id' | 'type' | 'fields'>;
|
||||
};
|
||||
|
||||
export function listDueReviews(opts: { deckId?: string; limit?: number } = {}) {
|
||||
export function listDueReviews(opts: { deckId?: string; limit?: number; recovery?: boolean } = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (opts.deckId) params.set('deck_id', opts.deckId);
|
||||
if (opts.limit) params.set('limit', String(opts.limit));
|
||||
if (opts.recovery) params.set('recovery', 'true');
|
||||
const qs = params.toString();
|
||||
return api<{ reviews: DueReview[]; total: number }>(
|
||||
`/api/v1/reviews/due${qs ? `?${qs}` : ''}`
|
||||
|
|
@ -21,3 +22,7 @@ export function gradeReview(cardId: string, subIndex: number, rating: Rating) {
|
|||
body: { rating },
|
||||
});
|
||||
}
|
||||
|
||||
export function undoReview(cardId: string, subIndex: number) {
|
||||
return api<Review>(`/api/v1/reviews/${cardId}/${subIndex}/undo`, { method: 'POST' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||
import { t } from '$lib/i18n/index.svelte.ts';
|
||||
import { loadSummary } from '$lib/api/me.ts';
|
||||
|
||||
const navItems = $derived([
|
||||
{ id: 'decks', label: t('nav.decks') },
|
||||
|
|
@ -30,6 +32,20 @@
|
|||
devUser.user?.email?.charAt(0).toUpperCase() ??
|
||||
'?'
|
||||
);
|
||||
|
||||
let streakDays = $state(0);
|
||||
let dueNow = $state(0);
|
||||
|
||||
onMount(async () => {
|
||||
if (!devUser.id) return;
|
||||
try {
|
||||
const s = await loadSummary();
|
||||
streakDays = s.streak_days;
|
||||
dueNow = s.due_now;
|
||||
} catch {
|
||||
// ignorieren — Header ist non-critical
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="bottom-bar" role="navigation" aria-label={t('common.main_nav')}>
|
||||
|
|
@ -38,6 +54,22 @@
|
|||
|
||||
<div class="divider" aria-hidden="true"></div>
|
||||
|
||||
{#if devUser.id && (streakDays > 0 || dueNow > 0)}
|
||||
<div class="header-meta" aria-label="Lernstatus">
|
||||
{#if streakDays > 0}
|
||||
<span class="streak-pill" title="{streakDays} Tage Streak">
|
||||
🔥 {streakDays}
|
||||
</span>
|
||||
{/if}
|
||||
{#if dueNow > 0}
|
||||
<span class="due-pill" title="{dueNow} Karten fällig">
|
||||
{dueNow}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="divider" aria-hidden="true"></div>
|
||||
{/if}
|
||||
|
||||
<!-- Hauptnavigation -->
|
||||
{#each navItems as item (item.id)}
|
||||
<button
|
||||
|
|
@ -187,4 +219,32 @@
|
|||
text-decoration: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.streak-pill {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 9999px;
|
||||
background: hsl(30 100% 55% / 0.12);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.due-pill {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
background: hsl(var(--color-primary));
|
||||
padding: 0.125rem 0.4rem;
|
||||
border-radius: 9999px;
|
||||
min-width: 1.25rem;
|
||||
text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -315,6 +315,16 @@ export const de: TranslationNode = {
|
|||
leeches_none: 'Keine zähen Karten — sauber.',
|
||||
leeches_lapses: '{n} Ausrutscher',
|
||||
leeches_lapses_one: '{n} Ausrutscher',
|
||||
difficulty_title: 'Schwierigkeitsverteilung',
|
||||
difficulty_desc: 'Wie schwer sind deine Karten laut FSRS.',
|
||||
difficulty_very_easy: 'Sehr leicht',
|
||||
difficulty_easy: 'Leicht',
|
||||
difficulty_medium: 'Mittel',
|
||||
difficulty_hard: 'Schwer',
|
||||
difficulty_very_hard: 'Sehr schwer',
|
||||
mastery_title: 'Deck-Fortschritt',
|
||||
mastery_desc: 'Anteil gemeisterter Karten (Stabilität > 21 Tage).',
|
||||
mastery_empty: 'Noch keine Review-Daten.',
|
||||
loading: 'Lade…',
|
||||
error: 'Fehler: {msg}',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -310,6 +310,16 @@ export const en: TranslationNode = {
|
|||
leeches_none: 'No leeches — clean slate.',
|
||||
leeches_lapses: '{n} lapses',
|
||||
leeches_lapses_one: '{n} lapse',
|
||||
difficulty_title: 'Difficulty distribution',
|
||||
difficulty_desc: 'How difficult are your cards according to FSRS.',
|
||||
difficulty_very_easy: 'Very easy',
|
||||
difficulty_easy: 'Easy',
|
||||
difficulty_medium: 'Medium',
|
||||
difficulty_hard: 'Hard',
|
||||
difficulty_very_hard: 'Very hard',
|
||||
mastery_title: 'Deck progress',
|
||||
mastery_desc: 'Share of mastered cards (stability > 21 days).',
|
||||
mastery_empty: 'No review data yet.',
|
||||
loading: 'Loading…',
|
||||
error: 'Error: {msg}',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -310,6 +310,16 @@ export const es: TranslationNode = {
|
|||
leeches_none: 'Ninguna carta difícil — todo bien.',
|
||||
leeches_lapses: '{n} olvidos',
|
||||
leeches_lapses_one: '{n} olvido',
|
||||
difficulty_title: 'Difficulty distribution',
|
||||
difficulty_desc: 'How difficult are your cards according to FSRS.',
|
||||
difficulty_very_easy: 'Very easy',
|
||||
difficulty_easy: 'Easy',
|
||||
difficulty_medium: 'Medium',
|
||||
difficulty_hard: 'Hard',
|
||||
difficulty_very_hard: 'Very hard',
|
||||
mastery_title: 'Deck progress',
|
||||
mastery_desc: 'Share of mastered cards (stability > 21 days).',
|
||||
mastery_empty: 'No review data yet.',
|
||||
loading: 'Cargando…',
|
||||
error: 'Error: {msg}',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -310,6 +310,16 @@ export const fr: TranslationNode = {
|
|||
leeches_none: 'Aucune carte difficile — tout va bien.',
|
||||
leeches_lapses: '{n} oublis',
|
||||
leeches_lapses_one: '{n} oubli',
|
||||
difficulty_title: 'Difficulty distribution',
|
||||
difficulty_desc: 'How difficult are your cards according to FSRS.',
|
||||
difficulty_very_easy: 'Very easy',
|
||||
difficulty_easy: 'Easy',
|
||||
difficulty_medium: 'Medium',
|
||||
difficulty_hard: 'Hard',
|
||||
difficulty_very_hard: 'Very hard',
|
||||
mastery_title: 'Deck progress',
|
||||
mastery_desc: 'Share of mastered cards (stability > 21 days).',
|
||||
mastery_empty: 'No review data yet.',
|
||||
loading: 'Chargement…',
|
||||
error: 'Erreur : {msg}',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -310,6 +310,16 @@ export const it: TranslationNode = {
|
|||
leeches_none: 'Nessuna carta difficile — pulito.',
|
||||
leeches_lapses: '{n} ricadute',
|
||||
leeches_lapses_one: '{n} ricaduta',
|
||||
difficulty_title: 'Difficulty distribution',
|
||||
difficulty_desc: 'How difficult are your cards according to FSRS.',
|
||||
difficulty_very_easy: 'Very easy',
|
||||
difficulty_easy: 'Easy',
|
||||
difficulty_medium: 'Medium',
|
||||
difficulty_hard: 'Hard',
|
||||
difficulty_very_hard: 'Very hard',
|
||||
mastery_title: 'Deck progress',
|
||||
mastery_desc: 'Share of mastered cards (stability > 21 days).',
|
||||
mastery_empty: 'No review data yet.',
|
||||
loading: 'Caricamento…',
|
||||
error: 'Errore: {msg}',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@
|
|||
let category = $state<DeckCategoryId | null>(null);
|
||||
let visibility = $state<'private' | 'space' | 'public'>('private');
|
||||
|
||||
let fsrsRetention = $state(0.9);
|
||||
|
||||
let loading = $state(true);
|
||||
let saving = $state(false);
|
||||
let deleting = $state(false);
|
||||
|
|
@ -54,6 +56,7 @@
|
|||
color = d.color ?? '#6366f1';
|
||||
category = (d.category as DeckCategoryId | null) ?? null;
|
||||
visibility = (d.visibility as 'private' | 'space' | 'public') ?? 'private';
|
||||
fsrsRetention = (d.fsrs_settings as { request_retention?: number } | null)?.request_retention ?? 0.9;
|
||||
if (d.forked_from_marketplace_deck_id) {
|
||||
void loadMarketplaceInfo(d);
|
||||
}
|
||||
|
|
@ -98,6 +101,7 @@
|
|||
color,
|
||||
category: category ?? undefined,
|
||||
visibility,
|
||||
fsrs_settings: { request_retention: fsrsRetention },
|
||||
});
|
||||
toasts.success('Gespeichert.');
|
||||
goto(`/decks/${deckId}`);
|
||||
|
|
@ -259,7 +263,45 @@
|
|||
</form>
|
||||
</section>
|
||||
|
||||
<!-- ── 2. Marketplace (nur für Forks) ─────────────────────────── -->
|
||||
<!-- ── 2. Lern-Algorithmus ───────────────────────────────────────── -->
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">Lern-Algorithmus</h2>
|
||||
<div class="section-body">
|
||||
<div class="field">
|
||||
<div class="retention-header">
|
||||
<span class="field-label">Ziel-Retention</span>
|
||||
<span class="retention-value">{Math.round(fsrsRetention * 100)} %</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max="99"
|
||||
step="1"
|
||||
value={Math.round(fsrsRetention * 100)}
|
||||
oninput={(e) => (fsrsRetention = Number((e.target as HTMLInputElement).value) / 100)}
|
||||
class="retention-slider"
|
||||
/>
|
||||
<div class="retention-ticks" aria-hidden="true">
|
||||
<span>50 %</span>
|
||||
<span>70 %</span>
|
||||
<span>90 %</span>
|
||||
<span>99 %</span>
|
||||
</div>
|
||||
<p class="field-hint">
|
||||
Wie viele Karten du beim Abfragen richtig beantwortest (im Durchschnitt).
|
||||
Höher = mehr Wiederholungen, aber bessere Erinnerung.
|
||||
<strong>90 % ist ein guter Ausgangswert.</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" disabled={!canSave} class="btn-primary" onclick={onSave}>
|
||||
{saving ? 'Speichern…' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── 3. Marketplace (nur für Forks) ─────────────────────────── -->
|
||||
{#if isForked}
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">Marketplace</h2>
|
||||
|
|
@ -295,7 +337,7 @@
|
|||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- ── 3. Gefahrenzone ────────────────────────────────────────── -->
|
||||
<!-- ── 4. Gefahrenzone ────────────────────────────────────────── -->
|
||||
<section class="settings-section danger-section">
|
||||
<h2 class="section-title">Weitere Aktionen</h2>
|
||||
<div class="section-body danger-body">
|
||||
|
|
@ -725,4 +767,41 @@
|
|||
.btn-ghost:hover {
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
/* ── Retention slider ──────────────────────────────────────────── */
|
||||
|
||||
.retention-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.retention-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--color-primary));
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.retention-slider {
|
||||
width: 100%;
|
||||
accent-color: hsl(var(--color-primary));
|
||||
cursor: pointer;
|
||||
margin: 0.25rem 0 0.125rem;
|
||||
}
|
||||
|
||||
.retention-ticks {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
padding: 0 0.125rem;
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
line-height: 1.5;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
const retentionLayers = stackLayers('stats-retention', 3);
|
||||
const forecastLayers = stackLayers('stats-forecast', 3);
|
||||
const leechesLayers = stackLayers('stats-leeches', 3);
|
||||
const diffLayers = stackLayers('stats-diff', 3);
|
||||
const masteryLayers = stackLayers('stats-mastery', 3);
|
||||
|
||||
const peakDay = $derived.by(() => {
|
||||
if (!stats) return 1;
|
||||
|
|
@ -57,6 +59,11 @@
|
|||
return Math.max(1, ...stats.due_forecast.map((d) => d.n));
|
||||
});
|
||||
|
||||
const maxDiffN = $derived.by(() => {
|
||||
if (!stats) return 1;
|
||||
return Math.max(1, ...stats.difficulty_distribution.map(d => d.n));
|
||||
});
|
||||
|
||||
function cellLevel(n: number): number {
|
||||
if (n === 0) return 0;
|
||||
if (n <= 2) return 1;
|
||||
|
|
@ -347,6 +354,79 @@
|
|||
</CardSurface>
|
||||
</li>
|
||||
|
||||
<!-- Schwierigkeitsverteilung -->
|
||||
<li class="stack-wrap">
|
||||
{#each diffLayers as layer, i (i)}
|
||||
<div class="layer" style:transform="translate({layer.dx}px,{layer.dy}px) rotate({layer.tilt}deg)" aria-hidden="true"></div>
|
||||
{/each}
|
||||
<CardSurface size="md" colorAccent="#EC4899">
|
||||
<div class="card-inner">
|
||||
<div class="card-corner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"><path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/></svg>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-title">{t('stats.difficulty_title')}</p>
|
||||
<p class="card-desc">{t('stats.difficulty_desc')}</p>
|
||||
<div class="diff-list">
|
||||
{#each stats!.difficulty_distribution as item (item.bucket)}
|
||||
{@const colors = { very_easy: '#10B981', easy: '#84CC16', medium: '#F59E0B', hard: '#F97316', very_hard: '#EF4444' }}
|
||||
{@const labels = { very_easy: t('stats.difficulty_very_easy'), easy: t('stats.difficulty_easy'), medium: t('stats.difficulty_medium'), hard: t('stats.difficulty_hard'), very_hard: t('stats.difficulty_very_hard') }}
|
||||
<div class="diff-row">
|
||||
<span class="diff-label">{labels[item.bucket]}</span>
|
||||
<div class="diff-bar-wrap">
|
||||
<div
|
||||
class="diff-bar-fill"
|
||||
style:width="{(item.n / maxDiffN) * 100}%"
|
||||
style:background={colors[item.bucket]}
|
||||
></div>
|
||||
</div>
|
||||
<span class="diff-count">{item.n}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardSurface>
|
||||
</li>
|
||||
|
||||
<!-- Deck-Fortschritt -->
|
||||
<li class="stack-wrap">
|
||||
{#each masteryLayers as layer, i (i)}
|
||||
<div class="layer" style:transform="translate({layer.dx}px,{layer.dy}px) rotate({layer.tilt}deg)" aria-hidden="true"></div>
|
||||
{/each}
|
||||
<CardSurface size="md" colorAccent="#7C3AED">
|
||||
<div class="card-inner">
|
||||
<div class="card-corner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-title">{t('stats.mastery_title')}</p>
|
||||
<p class="card-desc">{t('stats.mastery_desc')}</p>
|
||||
{#if stats!.deck_mastery.length === 0}
|
||||
<p class="retention-none">{t('stats.mastery_empty')}</p>
|
||||
{:else}
|
||||
<div class="mastery-list">
|
||||
{#each stats!.deck_mastery.slice(0, 6) as deck (deck.deck_id)}
|
||||
<div class="mastery-row">
|
||||
<div class="mastery-head">
|
||||
<span class="mastery-name">{deck.deck_name}</span>
|
||||
<span class="mastery-count">{deck.mastered} / {deck.total}</span>
|
||||
</div>
|
||||
<div class="mastery-bar-wrap">
|
||||
<div
|
||||
class="mastery-bar-fill"
|
||||
style:width="{deck.pct * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</CardSurface>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
|
|
@ -627,4 +707,99 @@
|
|||
|
||||
.leech-sep { opacity: 0.5; }
|
||||
.leech-lapses { color: #DC2626; font-weight: 500; }
|
||||
|
||||
/* Difficulty Distribution */
|
||||
.diff-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.diff-row {
|
||||
display: grid;
|
||||
grid-template-columns: 4.5rem 1fr 1.5rem;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.diff-label {
|
||||
font-size: 0.5625rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.diff-bar-wrap {
|
||||
height: 0.375rem;
|
||||
background: hsl(var(--color-border));
|
||||
border-radius: 9999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diff-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 9999px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.diff-count {
|
||||
font-size: 0.5625rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Deck Mastery */
|
||||
.mastery-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
margin-top: auto;
|
||||
max-height: 11rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mastery-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.mastery-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.mastery-name {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 8rem;
|
||||
}
|
||||
|
||||
.mastery-count {
|
||||
font-size: 0.5625rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mastery-bar-wrap {
|
||||
height: 0.3125rem;
|
||||
background: hsl(var(--color-border));
|
||||
border-radius: 9999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mastery-bar-fill {
|
||||
height: 100%;
|
||||
background: #7C3AED;
|
||||
border-radius: 9999px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
} from '@cards/domain';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
import { getDeck } from '$lib/api/decks.ts';
|
||||
import { listDueReviews, gradeReview, type DueReview } from '$lib/api/reviews.ts';
|
||||
import { listDueReviews, gradeReview, undoReview, type DueReview } from '$lib/api/reviews.ts';
|
||||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||
import { renderMarkdown } from '$lib/markdown.ts';
|
||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
|
|
@ -32,6 +32,15 @@
|
|||
let loading = $state(true);
|
||||
let busy = $state(false);
|
||||
let stats = $state({ reviewed: 0, again: 0 });
|
||||
let dueTotal = $state(0);
|
||||
let recoveryMode = $state(false);
|
||||
let showRecovery = $state(false);
|
||||
let fsrsTooltipOpen = $state(false);
|
||||
let undoCardId = $state<string | null>(null);
|
||||
let undoSubIndex = $state<number | null>(null);
|
||||
let undoTimer = $state<ReturnType<typeof setTimeout> | null>(null);
|
||||
let undoProgress = $state(0); // 0→100 für den Ablauf-Balken
|
||||
let undoRafId = $state<number | null>(null);
|
||||
|
||||
const current = $derived(queue[queueIndex]);
|
||||
const isDone = $derived(!loading && queueIndex >= queue.length);
|
||||
|
|
@ -131,6 +140,14 @@
|
|||
};
|
||||
});
|
||||
|
||||
const STATE_LABELS: Record<string, string> = {
|
||||
new: 'Neu', learning: 'Lernend', review: 'Wiederholen', relearning: 'Nachlernen'
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if (!revealed) fsrsTooltipOpen = false;
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
if (!devUser.id) {
|
||||
goto('/');
|
||||
|
|
@ -143,7 +160,9 @@
|
|||
]);
|
||||
deckName = d.name;
|
||||
deckColor = d.color ?? null;
|
||||
dueTotal = due.total;
|
||||
queue = due.reviews;
|
||||
if (due.total > 30 && !recoveryMode) showRecovery = true;
|
||||
} catch (e) {
|
||||
toasts.error(`Sitzung konnte nicht geladen werden: ${apiErrorMessage(e)}`);
|
||||
goto('/study');
|
||||
|
|
@ -153,10 +172,27 @@
|
|||
window.addEventListener('keydown', onKey);
|
||||
});
|
||||
|
||||
async function startRecovery() {
|
||||
showRecovery = false;
|
||||
recoveryMode = true;
|
||||
loading = true;
|
||||
try {
|
||||
const due = await listDueReviews({ deckId, recovery: true });
|
||||
queue = due.reviews;
|
||||
queueIndex = 0;
|
||||
revealed = false;
|
||||
} catch (e) {
|
||||
toasts.error(`Fehler: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('keydown', onKey);
|
||||
}
|
||||
clearUndoTimer();
|
||||
});
|
||||
|
||||
function onKey(e: KeyboardEvent) {
|
||||
|
|
@ -166,6 +202,12 @@
|
|||
if (isTyping) return; // TypingView übernimmt per svelte:window
|
||||
if (isMultipleChoice) return; // MultipleChoiceView übernimmt per svelte:window
|
||||
|
||||
if (e.key === 'z' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
void undo();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!revealed) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
|
@ -190,12 +232,59 @@
|
|||
if (rating === 'again') stats.again += 1;
|
||||
queueIndex += 1;
|
||||
revealed = false;
|
||||
// Undo-Fenster öffnen
|
||||
clearUndoTimer();
|
||||
undoCardId = c.card_id;
|
||||
undoSubIndex = c.sub_index;
|
||||
undoProgress = 100;
|
||||
const startTime = Date.now();
|
||||
const DURATION = 5000;
|
||||
function tick() {
|
||||
const elapsed = Date.now() - startTime;
|
||||
undoProgress = Math.max(0, 100 - (elapsed / DURATION) * 100);
|
||||
if (elapsed < DURATION) {
|
||||
undoRafId = requestAnimationFrame(tick);
|
||||
} else {
|
||||
clearUndoTimer();
|
||||
}
|
||||
}
|
||||
undoRafId = requestAnimationFrame(tick);
|
||||
undoTimer = setTimeout(clearUndoTimer, DURATION);
|
||||
} catch (e) {
|
||||
toasts.error(t('card_edit.save_failed', { msg: apiErrorMessage(e) }));
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearUndoTimer() {
|
||||
if (undoTimer !== null) { clearTimeout(undoTimer); undoTimer = null; }
|
||||
if (undoRafId !== null) { cancelAnimationFrame(undoRafId); undoRafId = null; }
|
||||
undoCardId = null;
|
||||
undoSubIndex = null;
|
||||
undoProgress = 0;
|
||||
}
|
||||
|
||||
async function undo() {
|
||||
if (undoCardId === null || undoSubIndex === null || busy) return;
|
||||
const cid = undoCardId;
|
||||
const si = undoSubIndex;
|
||||
clearUndoTimer();
|
||||
busy = true;
|
||||
try {
|
||||
await undoReview(cid, si);
|
||||
// Einen Schritt zurück — queue-Index dekrementieren, Karte wieder zeigen
|
||||
queueIndex = Math.max(0, queueIndex - 1);
|
||||
revealed = false;
|
||||
stats.reviewed = Math.max(0, stats.reviewed - 1);
|
||||
// War die Bewertung 'again'? Nicht eindeutig rückwirkend bestimmbar,
|
||||
// daher konservativ: kein stats.again decrement
|
||||
} catch (e) {
|
||||
toasts.error(`Undo fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="study-page">
|
||||
|
|
@ -213,6 +302,19 @@
|
|||
{queueIndex + 1} / {queue.length}
|
||||
{/if}
|
||||
</p>
|
||||
{#if showRecovery}
|
||||
<div class="recovery-banner">
|
||||
<p class="recovery-text">
|
||||
{dueTotal > 50 ? 'Großer Rückstand' : 'Rückstand'}: {queue.length} Karten fällig.
|
||||
</p>
|
||||
<button class="recovery-btn" onclick={startRecovery}>
|
||||
Sanfter Einstieg <span class="recovery-sub">25 schwächste zuerst</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if recoveryMode}
|
||||
<p class="recovery-active">⚡ Sanfter Einstieg · {queue.length} Karten</p>
|
||||
{/if}
|
||||
<a class="aside-manage" href="/decks/{deckId}">{t('study_session.manage_link')}</a>
|
||||
</aside>
|
||||
|
||||
|
|
@ -290,6 +392,34 @@
|
|||
<div class="prose answer">{@html answerHtml}</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if revealed && current && !isTyping && !isMultipleChoice && !isImageOcclusion}
|
||||
<div class="fsrs-info-wrap">
|
||||
<button
|
||||
class="fsrs-info-btn"
|
||||
onclick={() => (fsrsTooltipOpen = !fsrsTooltipOpen)}
|
||||
aria-label="FSRS-Daten anzeigen"
|
||||
aria-expanded={fsrsTooltipOpen}
|
||||
>ⓘ</button>
|
||||
{#if fsrsTooltipOpen}
|
||||
<div class="fsrs-popover" role="status" aria-live="polite">
|
||||
<div class="fsrs-row">
|
||||
<span class="fsrs-label">Zustand</span>
|
||||
<span class="fsrs-val">{STATE_LABELS[current.state] ?? current.state}</span>
|
||||
</div>
|
||||
<div class="fsrs-row">
|
||||
<span class="fsrs-label">Stabilität</span>
|
||||
<span class="fsrs-val">
|
||||
{current.stability > 0 ? current.stability.toFixed(1) + ' Tage' : '–'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="fsrs-row">
|
||||
<span class="fsrs-label">Schwierigkeit</span>
|
||||
<span class="fsrs-val">{current.difficulty.toFixed(1)} / 10</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
</CardSurface>
|
||||
</div>
|
||||
|
|
@ -327,6 +457,15 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if undoCardId !== null}
|
||||
<div class="undo-toast" role="status" aria-live="polite">
|
||||
<div class="undo-bar" style:width="{undoProgress}%"></div>
|
||||
<span class="undo-text">Bewertet</span>
|
||||
<button class="undo-btn" onclick={undo} disabled={busy}>
|
||||
Rückgängig <kbd>⌘Z</kbd>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -437,6 +576,7 @@
|
|||
Portrait-Format (5:7) an: Inhalt vertikal zentriert, Padding rechts
|
||||
vom Color-Stripe. */
|
||||
.study-inner {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
|
@ -667,4 +807,169 @@
|
|||
.grade.grade-easy:hover:not(:disabled) {
|
||||
background: hsl(var(--color-success) / 0.08);
|
||||
}
|
||||
|
||||
.fsrs-info-wrap {
|
||||
position: absolute;
|
||||
bottom: 0.75rem;
|
||||
right: 0.75rem;
|
||||
}
|
||||
|
||||
.fsrs-info-btn {
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.fsrs-info-btn:hover { opacity: 1; }
|
||||
|
||||
.fsrs-popover {
|
||||
position: absolute;
|
||||
bottom: 1.875rem;
|
||||
right: 0;
|
||||
background: hsl(var(--color-surface));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.625rem 0.875rem;
|
||||
min-width: 10rem;
|
||||
z-index: 20;
|
||||
box-shadow: 0 4px 16px hsl(var(--color-foreground) / 0.1);
|
||||
}
|
||||
|
||||
.fsrs-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.125rem 0;
|
||||
}
|
||||
|
||||
.fsrs-label {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.fsrs-val {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-foreground));
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.recovery-banner {
|
||||
margin-top: 0.25rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: hsl(45 96% 58% / 0.12);
|
||||
border: 1px solid hsl(45 96% 58% / 0.35);
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.recovery-text {
|
||||
margin: 0;
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recovery-btn {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.375rem;
|
||||
background: hsl(45 96% 58%);
|
||||
color: hsl(30 60% 15%);
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.3125rem 0.625rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.recovery-btn:hover { opacity: 0.85; }
|
||||
|
||||
.recovery-sub {
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.recovery-active {
|
||||
margin: 0.25rem 0 0;
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(45 96% 45%);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.undo-toast {
|
||||
position: relative;
|
||||
margin-top: 0.75rem;
|
||||
width: 100%;
|
||||
max-width: 24rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.625rem 0.875rem;
|
||||
background: hsl(var(--color-surface));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 0.625rem;
|
||||
box-shadow: 0 2px 8px hsl(var(--color-foreground) / 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.undo-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
background: hsl(var(--color-primary));
|
||||
transition: width 0.1s linear;
|
||||
border-radius: 0 0 0 0.625rem;
|
||||
}
|
||||
|
||||
.undo-text {
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.undo-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.3125rem 0.75rem;
|
||||
background: hsl(var(--color-primary) / 0.1);
|
||||
color: hsl(var(--color-primary));
|
||||
border: 1px solid hsl(var(--color-primary) / 0.25);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.undo-btn:hover:not(:disabled) {
|
||||
background: hsl(var(--color-primary) / 0.18);
|
||||
}
|
||||
.undo-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.undo-btn kbd {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--color-primary) / 0.7);
|
||||
font-family: inherit;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -77,17 +77,23 @@ aber im Code längst gelandet:
|
|||
|
||||
### Scheduler-Verbesserungen
|
||||
|
||||
- **FSRS-Parameter pro User optimieren** — `ts-fsrs` liefert
|
||||
`computeParameters()` aus Review-History; Schema (`decks.fsrs_settings`)
|
||||
und Per-Deck-Override sind vorbereitet, aber kein Endpoint und kein UI.
|
||||
Größter messbarer Retention-Gewinn pro Aufwand.
|
||||
- **FSRS-Parameter pro User optimieren** — `ts-fsrs@5.3.2` hat
|
||||
**kein** `computeParameters()` aus Review-History (das erfordert den
|
||||
Python `fsrs-optimizer`). Der praktisch nutzbare Hebel ist
|
||||
`request_retention` (0.5–0.99): höherer Wert → mehr Wiederholungen,
|
||||
besser für prüfungsrelevantes Lernen; niedrigerer Wert → lockerer,
|
||||
gut für Allgemeinwissen. Schema (`decks.fsrs_settings`) und
|
||||
Per-Deck-PATCH-Endpoint sind da; **Retention-Slider im Deck-Edit
|
||||
gebaut 2026-05-13.** w-Gewichte-Optimierung (Offline-Python-Skript
|
||||
→ JSON-Upload) bleibt offen.
|
||||
- **Leech-Detection — gebaut 2026-05-12.** `me/stats` liefert
|
||||
`leech_cards` (Threshold 4 Lapses, Limit 20, sortiert nach
|
||||
Lapses desc, mit Front-Snippet + Deck-Name). Stats-Page hat eine
|
||||
rote Leech-Sektion mit Link in den Card-Editor. i18n in DE/EN/
|
||||
FR/IT/ES. Suspension/Aufteilen-Vorschläge sind nächste Welle.
|
||||
- **Undo letzte Bewertung** — wichtiger UX-Reflex; aktuell muss man
|
||||
das Review händisch zurückbauen.
|
||||
- **Undo letzte Bewertung — gebaut 2026-05-13.** `prevSnapshot`-
|
||||
Spalte in `reviews`, API `POST /:cardId/:subIndex/undo`, 5-s-Toast
|
||||
mit Fortschrittsbalken im Study-View, Ctrl/Cmd+Z-Shortcut.
|
||||
- **Card Burial / Suspension** — Karten temporär deaktivieren ohne
|
||||
Löschen; häufig angefragtes Anki-Feature.
|
||||
- **Geschwister-Burial** — Cloze-Cluster und basic-reverse-Seiten
|
||||
|
|
@ -100,9 +106,9 @@ aber im Code längst gelandet:
|
|||
Fehler" als saubere Filter im Queue-Loader.
|
||||
- **Subdeck-Unterstützung** — hierarchische Deck-Struktur
|
||||
(z. B. Vokabeln → Nomen / Verben).
|
||||
- **Cloze-Hint-Anzeige** — `{{c1::answer::hint}}` wird aktuell beim
|
||||
Rendern fallen gelassen; Hint-Anzeige steht in `STATUS.md` als
|
||||
verbleibender Phase-9-Punkt.
|
||||
- **Cloze-Hint-Anzeige — bereits implementiert.** `renderClozePrompt`
|
||||
in `packages/cards-domain/src/cloze.ts` rendert `{{c1::answer::hint}}`
|
||||
bereits als `[hint]`. `STATUS.md`-Eintrag ist veraltet.
|
||||
|
||||
### Kartentypen, Schema vorhanden / vorbereitet
|
||||
|
||||
|
|
@ -119,8 +125,9 @@ aber im Code längst gelandet:
|
|||
|
||||
- **Streak-Freeze-Token** — ein Streak-Schutztag pro Woche, optional
|
||||
durch Credits kaufbar.
|
||||
- **Streak im Header** — heute nur im Stats-Dashboard sichtbar;
|
||||
Header-Glyph mit Zahl wäre Mikro-Aufwand, sichtbare Wirkung.
|
||||
- **Streak im Header — gebaut 2026-05-13.** `GET /api/v1/me/summary`
|
||||
liefert `streak_days` + `due_now`; Header zeigt Flammen-Pill und
|
||||
fällige-Karten-Pill via `onMount`.
|
||||
- **XP + Badges** — Meilensteine (erstes Deck, 100 Karten, 30-Tage-
|
||||
Streak).
|
||||
- **Tages-Ziele** — „Heute: 20 Karten" mit Progress-Bar im Dashboard.
|
||||
|
|
@ -152,17 +159,21 @@ aber im Code längst gelandet:
|
|||
|
||||
## Analytics & Insights
|
||||
|
||||
- **Vergessenskurven-Visualisierung** — pro Deck und Tag, aus
|
||||
FSRS-State ableitbar.
|
||||
- **Vergessenskurven-Visualisierung / Graceful Backlog Recovery —
|
||||
gebaut 2026-05-13.** Stats-Page zeigt Difficulty-Distribution
|
||||
(5 Buckets, Farb-Bars) + Deck-Fortschritt (Mastery %, Stability>21).
|
||||
Study-View zeigt Recovery-Banner wenn total>30 fällige Karten,
|
||||
startet Queue nach Stability sortiert (25er-Batch).
|
||||
- **Retention-Rate** — aufgeschlüsselt nach Kategorie und Sprache.
|
||||
- **Lernzeit-Tracking** — Minuten pro Session, Wochentrend.
|
||||
- **Karten-Schwierigkeits-Heatmap** — welche Karten kosten die
|
||||
meiste Review-Zeit.
|
||||
- **Wöchentliche Zusammenfassung** — In-App oder per Email via
|
||||
`mana-notify`.
|
||||
- **Algorithmus-Transparenz pro Karte** — kleines „Wieso wurde ich
|
||||
befragt?"-Tooltip mit Stability/Difficulty/letztem Rating. Macht
|
||||
FSRS sichtbar (Mission-Wert „Souveränität").
|
||||
- **Algorithmus-Transparenz pro Karte — gebaut 2026-05-13.**
|
||||
FSRS-Tooltip im Study-View: ⓘ-Button nach Reveal öffnet Popover
|
||||
mit State, Stability, Difficulty, letztem Rating. Macht FSRS
|
||||
sichtbar (Mission-Wert „Souveränität").
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -181,10 +181,10 @@ weil sie den breitesten Markt haben.
|
|||
|
||||
| # | Deck-Slug | Titel | Karten | Lizenz | Sprache |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | `cardecky/englisch-a2-grundwortschatz` | Englisch A2 — 500 häufigste Wörter | 500 (basic-reverse) | CC-BY-4.0 | de |
|
||||
| 2 | `cardecky/englisch-b1-aufbauwortschatz` | Englisch B1 — Aufbau-Wortschatz | 600 (basic-reverse) | CC-BY-4.0 | de |
|
||||
| 3 | `cardecky/franzoesisch-a2-grundwortschatz` | Französisch A2 — Grundwortschatz | 500 (basic-reverse) | CC-BY-4.0 | de |
|
||||
| 4 | `cardecky/mathematik-sek1-grundbegriffe` | Mathematik Sek 1 — Begriffe + Formeln | 200 (basic + cloze) | CC-BY-4.0 | de |
|
||||
| 1 | `cardecky/englisch-a2-grundwortschatz` | Englisch A2 — 500 häufigste Wörter | 500 (basic-reverse) | CC-BY-4.0 | de | ✅ live 2026-05-12 (557 Karten) |
|
||||
| 2 | `cardecky/englisch-b1-aufbauwortschatz` | Englisch B1 — Aufbau-Wortschatz | 600 (basic-reverse) | CC-BY-4.0 | de | ✅ live 2026-05-13 (689 Karten) |
|
||||
| 3 | `cardecky/franzoesisch-a2-grundwortschatz` | Französisch A2 — Grundwortschatz | 500 (basic-reverse) | CC-BY-4.0 | de | ✅ live 2026-05-13 (575 Karten) |
|
||||
| 4 | `cardecky/mathematik-sek1-grundbegriffe` | Mathematik Sek 1 — Begriffe + Formeln | 200 (basic + cloze) | CC-BY-4.0 | de | ✅ live 2026-05-13 (178 Karten) |
|
||||
| 5 | `cardecky/geografie-welt-hauptstaedte` | Geografie Welt — Länder + Hauptstädte | 195 (basic-reverse) | CC-BY-4.0 | de |
|
||||
| 6 | `cardecky/periodensystem-elemente` | Periodensystem — 118 Elemente | 118 (basic-reverse) | CC-BY-4.0 | de |
|
||||
| 7 | `cardecky/biologie-zelle-grundlagen` | Biologie — Zelle und Zellbiologie | 80 (basic + cloze) | CC-BY-4.0 | de |
|
||||
|
|
@ -206,6 +206,26 @@ weil sie den breitesten Markt haben.
|
|||
geschätzt 30–40 Stunden über mehrere Sessions, weil Recherche +
|
||||
Quellen-Belege + Stufen-Validierung pro Deck Zeit kosten.
|
||||
|
||||
## 8b. Bonus-Decks (außerhalb Phase-1-Seed)
|
||||
|
||||
| # | Deck-Slug | Titel | Karten | Lizenz | Sprache | Status |
|
||||
|---|---|---|---|---|---|---|
|
||||
| B-1 | `cardecky/taegerwilen-bodensee` | Tägerwilen am Bodensee | 66 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-2 | `cardecky/kreuzlingen-thurgau` | Kreuzlingen am Bodensee | 70 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-3 | `cardecky/gottlieben-seerhein` | Gottlieben am Seerhein | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-4 | `cardecky/konstanz-bodensee` | Konstanz am Bodensee | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-5 | `cardecky/meersburg-bodensee` | Meersburg am Bodensee | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-6 | `cardecky/muensterlingen-bodensee` | Münsterlingen am Bodensee | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-7 | `cardecky/reichenau-bodensee` | Reichenau am Bodensee | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-8 | `cardecky/friedrichshafen-bodensee` | Friedrichshafen am Bodensee | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-9 | `cardecky/lindau-bodensee` | Lindau am Bodensee | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-10 | `cardecky/bregenz-vorarlberg` | Bregenz am Bodensee | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-11 | `cardecky/ueberlingen-bodensee` | Überlingen am Bodensee | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-12 | `cardecky/romanshorn-bodensee` | Romanshorn am Bodensee | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-13 | `cardecky/allensbach-bodensee` | Allensbach am Bodensee | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-14 | `cardecky/bodensee` | Der Bodensee | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
| B-15 | `cardecky/thurgau` | Kanton Thurgau | 50 (basic + cloze + mc) | CC0-1.0 | de | ✅ live 2026-05-12 |
|
||||
|
||||
**Lizenzen:**
|
||||
- Default `CC-BY-4.0` mit Attribution „Cardecky / mana e.V." erlaubt
|
||||
Re-Use durch Lehrer/innen, Forks, Schul-Repositories.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue