diff --git a/STATUS.md b/STATUS.md index e537869..a95a24a 100644 --- a/STATUS.md +++ b/STATUS.md @@ -98,7 +98,7 @@ Vollständiger Plan: [`mana/docs/playbooks/CARDS_GREENFIELD.md`](../mana/docs/pl | 9 | Polish (DSGVO-UI, Settings, Account, Statistik, i18n, A11y, Media, Image-Occlusion) | 🟡 weit | Card-Edit + Cloze-Editor + Inbox-Banner + Account/DSGVO + Statistik + Pre-Flight-Swap + i18n DE/EN + A11y-Pass + Cloze-Hint-Anzeige + Anki-Re-Import-Dedupe + MinIO-Media-Upload + Image-Occlusion durch (9a–9l). Verbleibend: type-in, audio, multiple-choice (Schema vorbereitet) | | 10 | Production-Deploy (Mac Mini, Cloudflare-Tunnel) | ✅ live 2026-05-08 | cardecky.mana.how + cardecky-api.mana.how, alte cards.* via nginx-301-Redirect | | 11 | Decommission Cards-Modul aus mana-monorepo | ✅ 2026-05-08 | apps/cards, services/cards-server, packages/cards-core, mana-app cards-Modul + cross-refs entfernt (4 Commits, type-check 0 errors) | -| 12 | Marketplace-Restore (R0–R6) | 🟡 R0+R1+R2+R3+R4 durch | Plan: [`docs/playbooks/MARKETPLACE_RESTORE.md`](docs/playbooks/MARKETPLACE_RESTORE.md). R0 (Doku): ✅. R1 (Schema): ✅. R2 (α+β Authors + Publish): ✅. R3 (γ+δ Discovery + Engagement + Subscribe + Smart-Merge mit FSRS-State-Erhalt): ✅. **R4 (ε Pull-Requests + Card-Discussions): ✅** — `POST/GET /decks/:slug/pull-requests` (PR-Erstellung mit add/modify/remove-Diff, public List), `GET /pull-requests/:id`, `POST /pull-requests/:id/{close,reject,merge}` mit Lifecycle-Enforcement (open→merged|closed|rejected), Merge ist Owner-only und erzeugt atomar eine neue Version mit semver-minor-Bump (1.0.0→1.1.0 default), bumpt `latest_version_id`, schreibt PR-Resolution. Card-Discussions: `POST /decks/:slug/cards/:hash/discussions` (auth, Threads keyed auf `card_content_hash` damit sie Versions-Bumps überleben), `GET /cards/:hash/discussions` (public read, hidden filtered), `GET /decks/:slug/discussions/counts` (Bulk pro Karte), `POST /discussions/:id/hide` (Author oder Deck-Owner). 11 neue Semver-Unit-Tests, 89 gesamt grün. **E2E-Smoke**: Cardecky publisht v1.0.0 (Apatheia, Eudaimonia, Logos) → Till submitted PR (modify Eudaimonia-Back, remove Logos, add Tugendlehre) → Till's Merge-Versuch wird mit 403 abgelehnt (deck_owner_only) → Cardecky merged → v1.1.0 entsteht atomar mit korrektem Karten-Mix in Ord-Reihenfolge → re-merge wird mit 409 abgelehnt → Till postet Frage zur Apatheia-Karte → Cardecky antwortet mit parent_id (Threading) → Cross-Card-Parent wird mit 422 abgelehnt → Hide-Operation versteckt vom Read aus → Bulk-Counts korrekt → Smart-Merge-Pull gegen v1.0.0→v1.1.0 zeigt 2 changed (Eudaimonia + Logos↔Tugendlehre über ord-Heuristik), 0 cards_inserted weil bereits-private-via-Fork. Bug-Fix: `r.use('*', authMiddleware)` in fork.ts wäre an dem `/api/v1/marketplace`-Mount-Punkt nachfolgende Router-Mounts (PRs, Discussions) versehentlich gefangen — Refactor auf per-route Middleware. Verbleibend: R5 Frontend-Routes, R6 voller UI-E2E. | +| 12 | Marketplace-Restore (R0–R6) | 🟡 R0+R1+R2+R3+R4+R5 durch | Plan: [`docs/playbooks/MARKETPLACE_RESTORE.md`](docs/playbooks/MARKETPLACE_RESTORE.md). R0–R4 (Backend-Stack): ✅. **R5 (Frontend-Routes): ✅** — `apps/web/src/lib/api/marketplace.ts` (~340 Z. Client mit Authors, Discovery, Engagement, Subscribe, Fork, PR, Discussions), Components in `lib/components/marketplace/` (AuthorBadge, DeckListGrid, PublishVersionModal, SuggestEditModal, DiscussionThread, PullRequestList — eigener Namespace ohne Konflikt zu Tills WIP-DeckGrid.svelte), Routes: `/explore` (Featured + Trending + Browse mit Suche + Sortierung + Pagination), `/d/[slug]` (Public-Detail mit Star/Subscribe/Fork-Buttons + Karten-Liste mit Discussion-Counts + Suggest-Edit-Modal pro Karte + PR-Liste mit Owner-Merge/Reject + Publish-Modal für Owner), `/u/[slug]` (Author-Profil + Verified-Badges + Follow-Button + eigene Decks), `/me/published` (Author-Profil-CRUD + eigene Veröffentlichungen), `/me/subscribed` (Subs mit update_available-Banner), `/me/forks` (geforkte Decks mit „Update ziehen"-Button → Smart-Merge-Pull). svelte-check: 4017 Files, **0 errors, 5 Svelte-5-rune-Warnings** (benign — Modals capturen Init-Values von Props, gewollt). SSR-Smoke: alle 4 Marketplace-URLs (`/explore`, `/d/r5-stoa-grundlagen`, `/u/cardecky`, `/me/published`) liefern 200. Test-Decks `r5-stoa-grundlagen` (Stoische Grundbegriffe, 4 Karten v1.0.0) + `r5-deutsche-historie` (2 Karten) bewusst in der lokalen `cards`-DB liegen gelassen für Browser-Spielwiese. Header-Nav-Link auf `/explore` **nicht** gesetzt — `Header.svelte` ist in Tills uncommitted WIP, Link wird beim Theming-WIP-Commit nachgezogen. Verbleibend: R6 voller UI-E2E + ggf. Polish (Modal-Warnings, Empty-States, Loading-Skeletons). | Legende: ✅ erledigt + verifiziert · 🚧 blockiert · ⏸ noch nicht begonnen diff --git a/apps/web/src/lib/api/marketplace.ts b/apps/web/src/lib/api/marketplace.ts new file mode 100644 index 0000000..40d01c1 --- /dev/null +++ b/apps/web/src/lib/api/marketplace.ts @@ -0,0 +1,440 @@ +/** + * Marketplace-API-Client. Konsumiert das `/api/v1/marketplace/*`- + * Backend (siehe `cards/apps/api/src/routes/marketplace/`). + * + * Pattern wie `decks.ts` — dünne Wrapper um den `api()`-Helper aus + * `client.ts`, der JWT/X-User-Id-Auth und 401-Refresh bereits + * abhandelt. + */ + +import { api } from './client.ts'; + +// ─── Types ─────────────────────────────────────────────────────────── + +export interface MarketplaceAuthor { + slug: string; + display_name: string; + bio: string | null; + avatar_url: string | null; + joined_at: string; + pseudonym: boolean; + verified_mana: boolean; + verified_community: boolean; + banned: boolean; +} + +export interface MarketplaceDeck { + id: string; + slug: string; + title: string; + description: string | null; + language: string | null; + license: string; + price_credits: number; + owner_user_id: string; + latest_version_id: string | null; + is_featured: boolean; + is_takedown: boolean; + created_at: string; +} + +export interface MarketplaceVersion { + id: string; + deck_id: string; + semver: string; + changelog: string | null; + content_hash: string; + card_count: number; + published_at: string; + deprecated_at?: string | null; +} + +export interface MarketplaceVersionCard { + content_hash: string; + type: string; + fields: Record; + ord: number; +} + +export interface DeckListEntry { + slug: string; + title: string; + description: string | null; + language: string | null; + license: string; + price_credits: number; + card_count: number; + star_count: number; + subscriber_count: number; + is_featured: boolean; + created_at: string; + owner: { + slug: string; + display_name: string; + verified_mana: boolean; + verified_community: boolean; + }; +} + +export interface PullRequest { + id: string; + deck_id: string; + author_user_id: string; + status: 'open' | 'merged' | 'closed' | 'rejected'; + title: string; + body: string | null; + diff: { + add: { type: string; fields: Record }[]; + modify: { contentHash: string; fields: Record }[]; + remove: { contentHash: string }[]; + }; + merged_into_version_id: string | null; + created_at: string; + resolved_at: string | null; +} + +export interface Discussion { + id: string; + card_content_hash: string; + deck_id: string; + author_user_id: string; + parent_id: string | null; + body: string; + hidden: boolean; + created_at: string; +} + +export interface DiffPayload { + from: { semver?: string; versionId?: string }; + to: { semver: string; versionId: string }; + added: MarketplaceVersionCard[]; + changed: { previous: { contentHash: string }; next: MarketplaceVersionCard }[]; + unchanged: { contentHash: string; ord: number }[]; + removed: { contentHash: string }[]; +} + +export interface SubscriptionEntry { + deck_slug: string; + deck_title: string; + deck_description: string | null; + subscribed_at: string; + notify_updates: boolean; + current_version_id: string | null; + latest_version_id: string | null; + update_available: boolean; +} + +// ─── Authors ───────────────────────────────────────────────────────── + +export function getMyAuthorProfile() { + return api('/api/v1/marketplace/authors/me'); +} + +export function upsertMyAuthorProfile(input: { + slug: string; + displayName: string; + bio?: string; + avatarUrl?: string; + pseudonym?: boolean; +}) { + return api('/api/v1/marketplace/authors/me', { + method: 'POST', + body: input, + }); +} + +export function getAuthor(slug: string) { + return api(`/api/v1/marketplace/authors/${slug}`); +} + +// ─── Discovery ─────────────────────────────────────────────────────── + +export function getExplore() { + return api<{ featured: DeckListEntry[]; trending: DeckListEntry[] }>( + '/api/v1/marketplace/explore' + ); +} + +export interface BrowseQuery { + q?: string; + tag?: string; + language?: string; + author?: string; + sort?: 'recent' | 'popular' | 'trending'; + limit?: number; + offset?: number; +} + +export function browseDecks(query: BrowseQuery = {}) { + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(query)) { + if (value !== undefined && value !== null && value !== '') { + params.set(key, String(value)); + } + } + const qs = params.toString(); + return api<{ items: DeckListEntry[]; total: number }>( + `/api/v1/marketplace/decks${qs ? '?' + qs : ''}` + ); +} + +export function getTags() { + return api<{ + tags: { + id: string; + slug: string; + name: string; + parent_id: string | null; + description: string | null; + curated: boolean; + }[]; + }>('/api/v1/marketplace/tags'); +} + +// ─── Deck (Public) ─────────────────────────────────────────────────── + +export function getMarketplaceDeck(slug: string) { + return api<{ deck: MarketplaceDeck; latest_version: MarketplaceVersion | null }>( + `/api/v1/marketplace/decks/${slug}` + ); +} + +export function getMarketplaceVersion(slug: string, semver: string) { + return api<{ version: MarketplaceVersion; cards: MarketplaceVersionCard[] }>( + `/api/v1/marketplace/decks/${slug}/versions/${semver}` + ); +} + +export function getDiff(slug: string, fromSemver: string) { + return api( + `/api/v1/marketplace/decks/${slug}/diff?from=${encodeURIComponent(fromSemver)}` + ); +} + +export function initMarketplaceDeck(input: { + slug: string; + title: string; + description?: string; + language?: string; + license?: string; + priceCredits?: number; +}) { + return api('/api/v1/marketplace/decks', { + method: 'POST', + body: input, + }); +} + +export function patchMarketplaceDeck( + slug: string, + patch: Partial<{ + title: string; + description: string; + language: string; + license: string; + priceCredits: number; + }> +) { + return api(`/api/v1/marketplace/decks/${slug}`, { + method: 'PATCH', + body: patch, + }); +} + +export function publishMarketplaceVersion( + slug: string, + input: { + semver: string; + changelog?: string; + cards: { type: string; fields: Record }[]; + } +) { + return api<{ + deck: MarketplaceDeck; + version: MarketplaceVersion; + moderation: { verdict: string; categories: string[]; model: string }; + }>(`/api/v1/marketplace/decks/${slug}/publish`, { + method: 'POST', + body: input, + }); +} + +// ─── Engagement: Stars + Follows ───────────────────────────────────── + +export function starDeck(slug: string) { + return api<{ starred: true }>(`/api/v1/marketplace/decks/${slug}/star`, { + method: 'POST', + }); +} + +export function unstarDeck(slug: string) { + return api<{ starred: false }>(`/api/v1/marketplace/decks/${slug}/star`, { + method: 'DELETE', + }); +} + +export function getStarState(slug: string) { + return api<{ starred: boolean }>(`/api/v1/marketplace/decks/${slug}/star`); +} + +export function followAuthor(slug: string) { + return api<{ following: true }>(`/api/v1/marketplace/authors/${slug}/follow`, { + method: 'POST', + }); +} + +export function unfollowAuthor(slug: string) { + return api<{ following: false }>(`/api/v1/marketplace/authors/${slug}/follow`, { + method: 'DELETE', + }); +} + +export function getFollowState(slug: string) { + return api<{ following: boolean }>(`/api/v1/marketplace/authors/${slug}/follow`); +} + +// ─── Subscriptions + Fork + Pull-Update ────────────────────────────── + +export function subscribe(slug: string) { + return api<{ subscribed: true; deck_slug: string; current_version_id: string }>( + `/api/v1/marketplace/decks/${slug}/subscribe`, + { method: 'POST' } + ); +} + +export function unsubscribe(slug: string) { + return api<{ subscribed: false }>(`/api/v1/marketplace/decks/${slug}/subscribe`, { + method: 'DELETE', + }); +} + +export function getSubscribeState(slug: string) { + return api<{ subscribed: boolean; current_version_id?: string }>( + `/api/v1/marketplace/decks/${slug}/subscribe` + ); +} + +export function getMySubscriptions() { + return api<{ subscriptions: SubscriptionEntry[] }>('/api/v1/marketplace/me/subscriptions'); +} + +export function forkDeck(slug: string, color?: string) { + return api<{ + deck: { + id: string; + name: string; + description: string | null; + color: string | null; + forked_from_marketplace_deck_id: string; + forked_from_marketplace_version_id: string; + }; + cards_created: number; + }>(`/api/v1/marketplace/decks/${slug}/fork`, { + method: 'POST', + body: color ? { color } : {}, + }); +} + +export function pullUpdate(privateDeckId: string) { + return api<{ + up_to_date: boolean; + from?: { semver: string; versionId: string }; + to?: { semver: string; versionId: string }; + added: number; + changed: number; + removed: number; + cards_inserted?: number; + }>(`/api/v1/marketplace/private/${privateDeckId}/pull-update`, { + method: 'POST', + }); +} + +// ─── Pull-Requests ─────────────────────────────────────────────────── + +export interface PrCreateInput { + title: string; + body?: string; + diff: { + add: { type: string; fields: Record }[]; + modify: { type: string; previousContentHash: string; fields: Record }[]; + remove: { contentHash: string }[]; + }; +} + +export function listPullRequests( + slug: string, + status?: 'open' | 'merged' | 'closed' | 'rejected' +) { + const qs = status ? `?status=${status}` : ''; + return api<{ pull_requests: PullRequest[]; total: number }>( + `/api/v1/marketplace/decks/${slug}/pull-requests${qs}` + ); +} + +export function getPullRequest(id: string) { + return api(`/api/v1/marketplace/pull-requests/${id}`); +} + +export function createPullRequest(slug: string, input: PrCreateInput) { + return api(`/api/v1/marketplace/decks/${slug}/pull-requests`, { + method: 'POST', + body: input, + }); +} + +export function closePullRequest(id: string) { + return api<{ status: 'closed' }>(`/api/v1/marketplace/pull-requests/${id}/close`, { + method: 'POST', + }); +} + +export function rejectPullRequest(id: string) { + return api<{ status: 'rejected' }>(`/api/v1/marketplace/pull-requests/${id}/reject`, { + method: 'POST', + }); +} + +export function mergePullRequest( + id: string, + opts: { newSemver?: string; mergeNote?: string } = {} +) { + return api<{ + pull_request: PullRequest; + version: MarketplaceVersion; + }>(`/api/v1/marketplace/pull-requests/${id}/merge`, { + method: 'POST', + body: opts, + }); +} + +// ─── Card-Discussions ──────────────────────────────────────────────── + +export function listDiscussions(cardContentHash: string) { + return api<{ discussions: Discussion[]; total: number }>( + `/api/v1/marketplace/cards/${cardContentHash}/discussions` + ); +} + +export function postDiscussion( + deckSlug: string, + cardContentHash: string, + body: string, + parentId?: string +) { + return api( + `/api/v1/marketplace/decks/${deckSlug}/cards/${cardContentHash}/discussions`, + { method: 'POST', body: { body, parentId } } + ); +} + +export function getDiscussionCounts(slug: string) { + return api<{ counts: Record }>( + `/api/v1/marketplace/decks/${slug}/discussions/counts` + ); +} + +export function hideDiscussion(id: string) { + return api<{ hidden: true }>(`/api/v1/marketplace/discussions/${id}/hide`, { + method: 'POST', + }); +} diff --git a/apps/web/src/lib/components/marketplace/AuthorBadge.svelte b/apps/web/src/lib/components/marketplace/AuthorBadge.svelte new file mode 100644 index 0000000..8193dfa --- /dev/null +++ b/apps/web/src/lib/components/marketplace/AuthorBadge.svelte @@ -0,0 +1,37 @@ + + + + {displayName} + {#if verifiedMana} + 🛡️ + {/if} + {#if verifiedCommunity} + + {/if} + diff --git a/apps/web/src/lib/components/marketplace/DeckListGrid.svelte b/apps/web/src/lib/components/marketplace/DeckListGrid.svelte new file mode 100644 index 0000000..d488308 --- /dev/null +++ b/apps/web/src/lib/components/marketplace/DeckListGrid.svelte @@ -0,0 +1,79 @@ + + +{#if items.length === 0} +

+ {emptyMessage} +

+{:else} + +{/if} diff --git a/apps/web/src/lib/components/marketplace/DiscussionThread.svelte b/apps/web/src/lib/components/marketplace/DiscussionThread.svelte new file mode 100644 index 0000000..fd1196c --- /dev/null +++ b/apps/web/src/lib/components/marketplace/DiscussionThread.svelte @@ -0,0 +1,155 @@ + + +
+

💬 Diskussion zur Karte

+ + {#if loading} +

Lade…

+ {:else if comments.length === 0} +

Noch keine Kommentare.

+ {:else} +
    + {#each comments as comment (comment.id)} +
  • +
    +
    {comment.body}
    + {#if canHide(comment)} + + {/if} +
    +
    + {new Date(comment.created_at).toLocaleString()} + {#if myUserId && replyToId !== comment.id} + + {/if} +
    +
  • + {/each} +
+ {/if} + + {#if myUserId} +
+ {#if replyToId} +

+ ↪ Antwort auf einen Kommentar. + +

+ {/if} + + +
+ {:else} +

+ Anmelden, um zu kommentieren. +

+ {/if} +
diff --git a/apps/web/src/lib/components/marketplace/PublishVersionModal.svelte b/apps/web/src/lib/components/marketplace/PublishVersionModal.svelte new file mode 100644 index 0000000..8e16f33 --- /dev/null +++ b/apps/web/src/lib/components/marketplace/PublishVersionModal.svelte @@ -0,0 +1,156 @@ + + + diff --git a/apps/web/src/lib/components/marketplace/PullRequestList.svelte b/apps/web/src/lib/components/marketplace/PullRequestList.svelte new file mode 100644 index 0000000..15fb4b8 --- /dev/null +++ b/apps/web/src/lib/components/marketplace/PullRequestList.svelte @@ -0,0 +1,189 @@ + + +
+
+

Pull-Requests

+ +
+ + {#if loading} +

Lade…

+ {:else if prs.length === 0} +

+ Keine {statusFilter} PRs. +

+ {:else} +
    + {#each prs as pr (pr.id)} +
  • +
    +
    +

    {pr.title}

    + {#if pr.body} +

    + {pr.body} +

    + {/if} +

    + {diffSummary(pr.diff)} + · + {pr.status} + · + + {#if pr.status === 'merged' && pr.merged_into_version_id} + · + in einer neuen Version live + {/if} +

    +
    + + {#if pr.status === 'open'} +
    + {#if isOwner} + + + {:else if pr.author_user_id === myUserId} + + {/if} +
    + {/if} +
    +
  • + {/each} +
+ {/if} + + {#if isOwner && statusFilter === 'open' && latestSemver} +

+ Tipp: Beim Merge wird automatisch auf v{latestSemver + .split('.') + .map((n, i) => (i === 1 ? Number(n) + 1 : i === 2 ? '0' : n)) + .join('.')} gebumpt (semver-minor). +

+ {/if} +
diff --git a/apps/web/src/lib/components/marketplace/SuggestEditModal.svelte b/apps/web/src/lib/components/marketplace/SuggestEditModal.svelte new file mode 100644 index 0000000..704d436 --- /dev/null +++ b/apps/web/src/lib/components/marketplace/SuggestEditModal.svelte @@ -0,0 +1,168 @@ + + + diff --git a/apps/web/src/routes/d/[slug]/+page.svelte b/apps/web/src/routes/d/[slug]/+page.svelte new file mode 100644 index 0000000..1af26ce --- /dev/null +++ b/apps/web/src/routes/d/[slug]/+page.svelte @@ -0,0 +1,366 @@ + + + + {deck?.title ?? slug} · Cardecky + + +{#if loading} +

Lade…

+{:else if !deck} +

Deck nicht gefunden.

+{:else} +
+ + ← zurück zur Library + + +
+
+

{deck.title}

+ {#if deck.is_takedown} + + Take-Down + + {/if} + {#if deck.is_featured} + + ★ Featured + + {/if} +
+ + {#if deck.description} +

{deck.description}

+ {/if} + +
+ + {deck.license} + + {#if deck.language} + {deck.language} + {/if} + {#if latestVersion} + v{latestVersion.semver} · {latestVersion.card_count} Karten + {#if latestVersion.changelog} + + 📝 „{latestVersion.changelog.slice(0, 60)}…" + + {/if} + {:else} + noch nicht published + {/if} +
+
+ +
+ + + + + {#if isOwner} + + {/if} +
+ + {#if deck} +
+
+ Author: + + + (Profil-Slug auf Author-Page sichtbar) + +
+
+ {/if} + +
+

Karten

+ {#if cards.length === 0} +

Noch keine Karten.

+ {:else} +
    + {#each cards as card (card.content_hash)} +
  • +
    +
    +

    + {previewCard(card)} +

    +

    + {card.type} + · ord {card.ord} + · hash {card.content_hash.slice(0, 8)} + {#if discussionCounts[card.content_hash] > 0} + · 💬 {discussionCounts[card.content_hash]} + {/if} +

    +
    +
    + + {#if myUserId && !isOwner} + + {/if} +
    +
    + + {#if openDiscussionFor === card.content_hash && deck} +
    + +
    + {/if} +
  • + {/each} +
+ {/if} +
+ + {#if deck} +
+ +
+ {/if} +
+ + {#if publishOpen && deck} + (publishOpen = false)} + onPublished={async () => { + publishOpen = false; + await load(); + }} + /> + {/if} + + {#if suggestOpen && suggestCard} + { + suggestOpen = false; + suggestCard = null; + }} + onSubmitted={() => { + suggestOpen = false; + suggestCard = null; + }} + /> + {/if} +{/if} diff --git a/apps/web/src/routes/explore/+page.svelte b/apps/web/src/routes/explore/+page.svelte new file mode 100644 index 0000000..20e7a73 --- /dev/null +++ b/apps/web/src/routes/explore/+page.svelte @@ -0,0 +1,167 @@ + + + + Explore · Cardecky + + +
+
+

Cardecky-Library

+

+ Decks von der Verein-Community + KI-kuratierten Cardecky-Author. Subscribe für Live-Updates, + fork für eigenen Lern-Stand, ✏️ Verbesserungen via Pull-Request einreichen. +

+
+ + {#if loadingExplore} +

Lade Featured + Trending…

+ {:else} + {#if featured.length > 0} +
+

★ Featured

+ +
+ {/if} + + {#if trending.length > 0} +
+

🔥 Trending

+ +
+ {/if} + {/if} + +
+

🔎 Stöbern

+ +
+ + + + +
+ + + + {#if browseResults.length < browseTotal} + + {/if} +
+
diff --git a/apps/web/src/routes/me/forks/+page.svelte b/apps/web/src/routes/me/forks/+page.svelte new file mode 100644 index 0000000..cdcff05 --- /dev/null +++ b/apps/web/src/routes/me/forks/+page.svelte @@ -0,0 +1,122 @@ + + + + Meine Forks · Cardecky + + +
+
+

Meine Forks

+

+ Marketplace-Decks, die du in deine private Lern-Welt kopiert hast. Smart-Merge: bei + „Update ziehen" werden neue/geänderte Karten ergänzt — unveränderte Karten samt FSRS-Stand + bleiben unangetastet. +

+
+ + {#if loading} +

Lade…

+ {:else if forks.length === 0} +
+

Noch nichts geforkt.

+ + Library durchstöbern → + +
+ {:else} +
    + {#each forks as fork (fork.id)} +
  • +
    +
    + + {fork.name} + + {#if fork.description} +

    + {fork.description} +

    + {/if} +
    + +
    +
  • + {/each} +
+ {/if} +
diff --git a/apps/web/src/routes/me/published/+page.svelte b/apps/web/src/routes/me/published/+page.svelte new file mode 100644 index 0000000..1f4d8e8 --- /dev/null +++ b/apps/web/src/routes/me/published/+page.svelte @@ -0,0 +1,192 @@ + + + + Meine Veröffentlichungen · Cardecky + + +
+
+

Meine Veröffentlichungen

+

+ Decks, die du als Marketplace-Author published hast. +

+
+ + {#if loading} +

Lade…

+ {:else} +
+

+ {author ? 'Author-Profil' : 'Author-Profil anlegen'} +

+
+ + + + +
+ +
+
+
+ + {#if author} +
+

Decks ({decks.length})

+ {#if decks.length === 0} +

+ Noch nichts veröffentlicht. Decks werden über die Marketplace-API initialisiert + (z.B. via Cardecky-Skill); danach erscheinen sie hier. +

+ {:else} +
    + {#each decks as deck (deck.slug)} +
  • +
    +
    + + {deck.title} + +

    + {deck.card_count} Karten · {deck.star_count} ★ · {deck.subscriber_count} ↩︎ · + {deck.license} +

    +
    + {#if deck.is_featured} + + ★ Featured + + {/if} +
    +
  • + {/each} +
+ {/if} +
+ {/if} + {/if} +
diff --git a/apps/web/src/routes/me/subscribed/+page.svelte b/apps/web/src/routes/me/subscribed/+page.svelte new file mode 100644 index 0000000..c485476 --- /dev/null +++ b/apps/web/src/routes/me/subscribed/+page.svelte @@ -0,0 +1,92 @@ + + + + Meine Abos · Cardecky + + +
+
+

Meine Abonnements

+

+ Decks, die du beobachtest. Wenn der Author eine neue Version publisht, siehst du hier den + Update-Indikator. +

+
+ + {#if loading} +

Lade…

+ {:else if items.length === 0} +
+

+ Noch keine Abos. +

+ + Library durchstöbern → + +
+ {:else} +
    + {#each items as sub (sub.deck_slug)} +
  • +
    +
    + + {sub.deck_title} + + {#if sub.deck_description} +

    + {sub.deck_description} +

    + {/if} +

    + Abonniert seit {new Date(sub.subscribed_at).toLocaleDateString()} +

    +
    + {#if sub.update_available} + + Update verfügbar + + {/if} +
    +
  • + {/each} +
+ {/if} +
diff --git a/apps/web/src/routes/u/[slug]/+page.svelte b/apps/web/src/routes/u/[slug]/+page.svelte new file mode 100644 index 0000000..3221022 --- /dev/null +++ b/apps/web/src/routes/u/[slug]/+page.svelte @@ -0,0 +1,146 @@ + + + + {author?.display_name ?? slug} · Cardecky + + +{#if loading} +

Lade…

+{:else if !author} +

Author nicht gefunden.

+{:else} +
+ + ← zurück zur Library + + +
+
+

{author.display_name}

+ {#if author.verified_mana} + + 🛡️ Mana Verifiziert + + {/if} + {#if author.verified_community} + + ⭐ Community Verifiziert + + {/if} + {#if author.banned} + + Gesperrt + + {/if} +
+ +

+ @{author.slug} + · seit {new Date(author.joined_at).toLocaleDateString()} + {#if author.pseudonym} + · Pseudonym + {/if} +

+ + {#if author.bio} +

{author.bio}

+ {/if} + + {#if myUserId} + + {/if} +
+ +
+

Decks ({decks.length})

+ +
+
+{/if}