Phase 12 G1-G4: Marketplace-Polish — svelte-ignore + Skeleton/Empty-State + Server-Filter + Owner-Info
G1 — svelte-ignore für 5 benigne Init-Capture-Warnings:
- PublishVersionModal: state(latestSemver ? bumpMinor(latestSemver) : '1.0.0')
ist intentional, weil das Modal pro Click frisch gemountet wird
- SuggestEditModal: state(card.fields.front…) + state({ ...card.fields })
gleicher Lebenszyklus
Kein Refactor auf $derived, weil das die Bind-Semantik kaputtmachen
würde — Direktive plus ein Kommentar reicht.
G2 — Loading + Empty-States:
- Neue Components SkeletonGrid + EmptyState in lib/components/marketplace/
- /explore: SkeletonGrid statt „Lade Featured + Trending…"-String,
EmptyState wenn weder Featured noch Trending da
- /me/subscribed + /me/forks: EmptyState statt inline-Box
- Konsistentes Vereins-Vokabular (icon + Title + Description + CTA)
G3 — Server-side Fork-Filter:
- GET /api/v1/decks akzeptiert ?forked_from_marketplace=true
- Drizzle isNotNull-Filter auf decks.forked_from_marketplace_deck_id
- toDeckDto exposed jetzt forked_from_marketplace_{deck,version}_id
(vorher schwiegen die Spalten, mussten client-side via Cast
rausgefischt werden)
- /me/forks ruft listDecks({ forkedFromMarketplace: true }) statt
listDecks() + client-side Filter
G4 — Owner-Author-Info im Deck-Detail-Endpoint:
- GET /api/v1/marketplace/decks/:slug returned jetzt zusätzlich
owner: { slug, display_name, verified_mana, verified_community,
pseudonym } — gejoint aus marketplace.authors via deck.owner_user_id
- toOwnerDto-Helper, identisches Shape wie in /authors/:slug
- /d/[slug] verbraucht den neuen owner-Block für AuthorBadge mit
echtem Profil-Link statt user_id-Slice (vorher: kaputter Link
/u/<empty-slug> + nur „SEAiKLkPZ…" als Display-Name)
Verifikation:
- API: type-check + 89 Tests grün
- Web: svelte-check 0 errors, 0 warnings (von 5 → 0)
- Live-Smoke: GET /marketplace/decks/r5-stoa-grundlagen liefert
owner={slug:'cardecky', display_name:'Cardecky', verified_*:false}
- ?forked_from_marketplace=true Filter mit Till's JWT liefert 0
(weil Till keine Forks hat) — 401 ohne JWT bestätigt
Bewusst nicht angefasst: Header-Nav-Link (WIP-Konflikt), Image-
Occlusion in Marketplace (Player-Side komplex), Auth-Guard im
+layout.svelte (page-level guards reichen), Anki-Import→Marketplace-
Publish-Hook (eigene Welle).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
40861710bf
commit
17871ba2a4
13 changed files with 174 additions and 63 deletions
33
apps/web/src/lib/components/marketplace/EmptyState.svelte
Normal file
33
apps/web/src/lib/components/marketplace/EmptyState.svelte
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
icon?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
ctaHref?: string;
|
||||
ctaLabel?: string;
|
||||
}
|
||||
|
||||
const { icon, title, description, ctaHref, ctaLabel }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="rounded-lg border border-dashed border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))]/40 p-10 text-center"
|
||||
>
|
||||
{#if icon}
|
||||
<div class="mx-auto mb-3 text-4xl" aria-hidden="true">{icon}</div>
|
||||
{/if}
|
||||
<h3 class="text-lg font-medium">{title}</h3>
|
||||
{#if description}
|
||||
<p class="mx-auto mt-1 max-w-md text-sm text-[hsl(var(--color-muted-foreground))]">
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
{#if ctaHref && ctaLabel}
|
||||
<a
|
||||
href={ctaHref}
|
||||
class="mt-4 inline-block text-sm text-[hsl(var(--color-primary))] hover:underline"
|
||||
>
|
||||
{ctaLabel} →
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -17,6 +17,9 @@
|
|||
return `${m[1]}.${Number(m[2]) + 1}.0`;
|
||||
}
|
||||
|
||||
// Modal-Lebenszyklus: pro Klick frisch gemountet, Props sind invariant.
|
||||
// Init-Capture aus `latestSemver` ist daher gewollt — keine Re-Reactivity.
|
||||
// svelte-ignore state_referenced_locally
|
||||
let semver = $state(latestSemver ? bumpMinor(latestSemver) : '1.0.0');
|
||||
let changelog = $state('');
|
||||
let cardsJson = $state('[]');
|
||||
|
|
|
|||
24
apps/web/src/lib/components/marketplace/SkeletonGrid.svelte
Normal file
24
apps/web/src/lib/components/marketplace/SkeletonGrid.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const { count = 6 }: Props = $props();
|
||||
</script>
|
||||
|
||||
<ul class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3" aria-busy="true" aria-label="Lade Decks">
|
||||
{#each Array.from({ length: count }) as _, i (i)}
|
||||
<li
|
||||
class="animate-pulse rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4"
|
||||
>
|
||||
<div class="h-4 w-3/4 rounded bg-[hsl(var(--color-border))]"></div>
|
||||
<div class="mt-2 h-3 w-full rounded bg-[hsl(var(--color-border))]/60"></div>
|
||||
<div class="mt-1 h-3 w-5/6 rounded bg-[hsl(var(--color-border))]/60"></div>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<div class="h-2 w-16 rounded bg-[hsl(var(--color-border))]/40"></div>
|
||||
<div class="h-2 w-12 rounded bg-[hsl(var(--color-border))]/40"></div>
|
||||
<div class="h-2 w-20 rounded bg-[hsl(var(--color-border))]/40"></div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
|
@ -13,9 +13,12 @@
|
|||
const { slug, card, onSubmitted, onClose }: Props = $props();
|
||||
|
||||
let mode = $state<'modify' | 'remove'>('modify');
|
||||
let title = $state(`„${(card.fields.front ?? card.fields.text ?? '').slice(0, 60)}" verbessern`);
|
||||
let body = $state('');
|
||||
// Map des aktuellen field-Werts → editierbarer Wert.
|
||||
// Modal-Lebenszyklus: pro Klick frisch gemountet, `card`-Prop ist
|
||||
// invariant. Init-Capture aus card.fields ist daher gewollt.
|
||||
// svelte-ignore state_referenced_locally
|
||||
let title = $state(`„${(card.fields.front ?? card.fields.text ?? '').slice(0, 60)}" verbessern`);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let editedFields = $state<Record<string, string>>({ ...card.fields });
|
||||
let busy = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue