Phase 9c: Inbox-Banner auf /decks und /study
InboxBanner.svelte zeigt einen klickbaren Hinweis, wenn der User ein Inbox-Deck hat und es Karten enthält. Linkt aufs Inbox-Deck, wo die Karten in andere Decks umsortiert werden können. API-Pfad bleibt schmal: kein neuer Endpunkt — die Komponente nutzt listDecks() + listCards(inbox.id) und filtert clientseitig auf name === "Inbox" (der stabile API-Konstantenname). Wenn das später Hot-Path wird, ist GET /api/v1/inbox/stats ein additiver Fix. svelte-check 356 files 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
35366ed4f2
commit
47419b3cac
4 changed files with 63 additions and 0 deletions
23
apps/web/src/lib/api/inbox.ts
Normal file
23
apps/web/src/lib/api/inbox.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import type { Deck } from '@cards/domain';
|
||||||
|
import { listDecks } from './decks.ts';
|
||||||
|
import { listCards } from './cards.ts';
|
||||||
|
|
||||||
|
const INBOX_NAME = 'Inbox';
|
||||||
|
|
||||||
|
export interface InboxStats {
|
||||||
|
deck: Deck | null;
|
||||||
|
cardCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt das Inbox-Deck (Name = "Inbox", auto-erzeugt vom API beim
|
||||||
|
* ersten Share-Receive) und zählt die Karten darin. Liefert ein
|
||||||
|
* neutrales Result, wenn der User noch keinen Share empfangen hat.
|
||||||
|
*/
|
||||||
|
export async function loadInboxStats(): Promise<InboxStats> {
|
||||||
|
const decks = await listDecks();
|
||||||
|
const inbox = decks.decks.find((d) => d.name === INBOX_NAME) ?? null;
|
||||||
|
if (!inbox) return { deck: null, cardCount: 0 };
|
||||||
|
const cards = await listCards(inbox.id);
|
||||||
|
return { deck: inbox, cardCount: cards.total };
|
||||||
|
}
|
||||||
30
apps/web/src/lib/components/InboxBanner.svelte
Normal file
30
apps/web/src/lib/components/InboxBanner.svelte
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { loadInboxStats, type InboxStats } from '$lib/api/inbox.ts';
|
||||||
|
|
||||||
|
let stats = $state<InboxStats | null>(null);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
stats = await loadInboxStats();
|
||||||
|
} catch {
|
||||||
|
// Inbox ist optional — Fehler still verschlucken, Banner bleibt weg.
|
||||||
|
stats = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if stats && stats.deck && stats.cardCount > 0}
|
||||||
|
<a
|
||||||
|
href="/decks/{stats.deck.id}"
|
||||||
|
class="block rounded-lg border border-[var(--color-primary)]/40 bg-[var(--color-primary)]/10 px-4 py-3 text-sm hover:bg-[var(--color-primary)]/15"
|
||||||
|
>
|
||||||
|
<span class="font-medium">📥 Inbox</span>
|
||||||
|
<span class="text-[var(--color-muted)]">·</span>
|
||||||
|
<span>
|
||||||
|
{stats.cardCount} eingegangene
|
||||||
|
{stats.cardCount === 1 ? 'Karte' : 'Karten'} aus anderen Apps
|
||||||
|
</span>
|
||||||
|
<span class="ml-1 text-[var(--color-muted)]">— sortieren →</span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { listDecks, deleteDeck } from '$lib/api/decks.ts';
|
import { listDecks, deleteDeck } from '$lib/api/decks.ts';
|
||||||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||||
|
import InboxBanner from '$lib/components/InboxBanner.svelte';
|
||||||
|
|
||||||
let decks = $state<Deck[]>([]);
|
let decks = $state<Deck[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|
@ -54,6 +55,10 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<InboxBanner />
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="mt-8 text-[var(--color-muted)]">Lade…</p>
|
<p class="mt-8 text-[var(--color-muted)]">Lade…</p>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { listDecks } from '$lib/api/decks.ts';
|
import { listDecks } from '$lib/api/decks.ts';
|
||||||
import { listDueReviews } from '$lib/api/reviews.ts';
|
import { listDueReviews } from '$lib/api/reviews.ts';
|
||||||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||||
|
import InboxBanner from '$lib/components/InboxBanner.svelte';
|
||||||
|
|
||||||
type Item = { deck: Deck; due: number };
|
type Item = { deck: Deck; due: number };
|
||||||
|
|
||||||
|
|
@ -37,6 +38,10 @@
|
||||||
|
|
||||||
<h1 class="text-2xl font-semibold">Lernen</h1>
|
<h1 class="text-2xl font-semibold">Lernen</h1>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<InboxBanner />
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="mt-8 text-[var(--color-muted)]">Lade…</p>
|
<p class="mt-8 text-[var(--color-muted)]">Lade…</p>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue