wordeck/apps/web/src/routes/decks/+page.svelte
Till JS c25c1d0dc4 Phase 9g: i18n DE/EN über alle Routes
Schmale Eigen-Lösung statt svelte-i18n: $lib/i18n mit de.ts + en.ts
(je ~150 Strings, flat-with-nesting), index.svelte.ts liefert reaktiven
i18n-Store als Svelte-5-Rune plus t()/tn()-Helper. Locale wird in
localStorage persistiert, Default = navigator.language (DE/EN-Erkennung),
Fallback = de.

Header bekommt einen DE/EN-Toggle-Switcher (role=group, aria-pressed).
Alle Routes (decks, decks/[id], decks/new, cards/new, cards/[id]/edit,
study, study/[deckId], import, account, stats, +page) und alle
Komponenten (Header, AnkiImport, InboxBanner) ziehen jetzt durch t().

tn(key, n) wählt zwischen `<key>_one` (n=1) und `<key>` — minimaler
Plural-Helper, reicht für DE/EN-MVP. Komplexere Pluralregeln (FR/IT)
bräuchten Intl.PluralRules, kommen wenn die Locales dazukommen.

Begleitend ein paar A11y-Vorbereitungen mitgenommen: aria-label am
Locale-Switcher, role=group für Grade-Buttons, role=button +
keyboard-handler für die Anki-Dropzone, role=progressbar mit
aria-valuemin/max/now, role=alert für Error-States,
aria-live=polite für Lade-Meldungen, sr-only-Heading im
Study-Card-Render, aria-hidden für rein dekorative Elemente.

svelte-check 379 files 0 errors, 94 Tests grün (41 Domain + 48 API
+ 5 Web).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:22:00 +02:00

109 lines
3.1 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import type { Deck } from '@cards/domain';
import { listDecks, deleteDeck } from '$lib/api/decks.ts';
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import { toasts } from '$lib/stores/toasts.svelte.ts';
import InboxBanner from '$lib/components/InboxBanner.svelte';
import { t } from '$lib/i18n/index.svelte.ts';
let decks = $state<Deck[]>([]);
let loading = $state(true);
let error = $state<string | null>(null);
onMount(async () => {
if (!devUser.id) {
goto('/');
return;
}
await refresh();
});
async function refresh() {
try {
loading = true;
const r = await listDecks();
decks = r.decks;
error = null;
} catch (e) {
error = (e as Error).message;
} finally {
loading = false;
}
}
async function onDelete(id: string, name: string) {
if (!confirm(t('decks.delete_confirm', { name }))) return;
try {
await deleteDeck(id);
toasts.success(t('decks.deleted', { name }));
await refresh();
} catch (e) {
toasts.error(t('decks.delete_failed', { msg: (e as Error).message }));
}
}
</script>
<div class="flex items-center justify-between">
<h1 class="text-2xl font-semibold">{t('decks.title')}</h1>
<a
href="/decks/new"
class="rounded bg-[var(--color-primary)] px-4 py-2 text-sm text-[var(--color-primary-fg)]"
>{t('decks.new')}</a
>
</div>
<div class="mt-4">
<InboxBanner />
</div>
{#if loading}
<p class="mt-8 text-[var(--color-muted)]">{t('decks.loading')}</p>
{:else if error}
<p class="mt-8 text-[var(--color-danger)]">{t('decks.error', { msg: error })}</p>
{:else if decks.length === 0}
<div
class="mt-8 rounded-lg border border-dashed border-[var(--color-border)] p-12 text-center"
>
<p class="text-[var(--color-muted)]">{t('decks.empty')}</p>
<a href="/decks/new" class="mt-4 inline-block text-[var(--color-primary)] hover:underline"
>{t('decks.empty_cta')}</a
>
</div>
{:else}
<ul class="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{#each decks as deck (deck.id)}
<li
class="group relative rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4 hover:border-[var(--color-primary)]"
>
<a href="/decks/{deck.id}" class="block">
<div class="flex items-start gap-3">
{#if deck.color}
<span
class="mt-1 h-3 w-3 shrink-0 rounded-full"
style="background:{deck.color}"
></span>
{/if}
<div class="min-w-0 flex-1">
<h2 class="truncate font-medium">{deck.name}</h2>
{#if deck.description}
<p class="mt-1 line-clamp-2 text-sm text-[var(--color-muted)]">
{deck.description}
</p>
{/if}
</div>
</div>
</a>
<button
class="absolute right-2 top-2 hidden rounded p-1 text-[var(--color-muted)] hover:bg-[var(--color-border)] hover:text-[var(--color-danger)] group-hover:block"
onclick={() => onDelete(deck.id, deck.name)}
aria-label={t('decks.delete_confirm', { name: deck.name })}
title={t('decks.delete_confirm', { name: deck.name })}
>
🗑
</button>
</li>
{/each}
</ul>
{/if}