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
|
|
@ -1,4 +1,4 @@
|
|||
import { and, eq } from 'drizzle-orm';
|
||||
import { and, eq, isNotNull } from 'drizzle-orm';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import { DeckCreateSchema, DeckUpdateSchema } from '@cards/domain';
|
||||
|
|
@ -48,7 +48,15 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }>
|
|||
|
||||
r.get('/', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const rows = await dbOf().select().from(decks).where(eq(decks.userId, userId));
|
||||
const forkedFromMarketplace = c.req.query('forked_from_marketplace');
|
||||
const conditions = [eq(decks.userId, userId)];
|
||||
if (forkedFromMarketplace === 'true') {
|
||||
conditions.push(isNotNull(decks.forkedFromMarketplaceDeckId));
|
||||
}
|
||||
const rows = await dbOf()
|
||||
.select()
|
||||
.from(decks)
|
||||
.where(and(...conditions));
|
||||
return c.json({ decks: rows.map(toDeckDto), total: rows.length });
|
||||
});
|
||||
|
||||
|
|
@ -117,6 +125,8 @@ function toDeckDto(row: typeof decks.$inferSelect) {
|
|||
visibility: row.visibility,
|
||||
fsrs_settings: row.fsrsSettings,
|
||||
content_hash: row.contentHash,
|
||||
forked_from_marketplace_deck_id: row.forkedFromMarketplaceDeckId,
|
||||
forked_from_marketplace_version_id: row.forkedFromMarketplaceVersionId,
|
||||
created_at: row.createdAt.toISOString(),
|
||||
updated_at: row.updatedAt.toISOString(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
publicDeckVersions,
|
||||
publicDecks,
|
||||
} from '../../db/schema/index.ts';
|
||||
import type { AuthorRow } from '../../db/schema/marketplace/index.ts';
|
||||
import { authMiddleware, type AuthVars } from '../../middleware/auth.ts';
|
||||
import { optionalAuthMiddleware } from '../../middleware/marketplace/optional-auth.ts';
|
||||
import { moderateDeckContent } from '../../lib/marketplace/ai-moderation.ts';
|
||||
|
|
@ -113,6 +114,16 @@ function toDeckDto(row: typeof publicDecks.$inferSelect) {
|
|||
};
|
||||
}
|
||||
|
||||
function toOwnerDto(row: AuthorRow) {
|
||||
return {
|
||||
slug: row.slug,
|
||||
display_name: row.displayName,
|
||||
verified_mana: row.verifiedMana,
|
||||
verified_community: row.verifiedCommunity,
|
||||
pseudonym: row.pseudonym,
|
||||
};
|
||||
}
|
||||
|
||||
function toVersionDto(row: typeof publicDeckVersions.$inferSelect) {
|
||||
return {
|
||||
id: row.id,
|
||||
|
|
@ -140,19 +151,27 @@ export function marketplaceDecksRouter(
|
|||
const [deck] = await db.select().from(publicDecks).where(eq(publicDecks.slug, slug)).limit(1);
|
||||
if (!deck) return c.json({ error: 'not_found' }, 404);
|
||||
|
||||
let version: typeof publicDeckVersions.$inferSelect | null = null;
|
||||
if (deck.latestVersionId) {
|
||||
const [v] = await db
|
||||
.select()
|
||||
.from(publicDeckVersions)
|
||||
.where(eq(publicDeckVersions.id, deck.latestVersionId))
|
||||
.limit(1);
|
||||
version = v ?? null;
|
||||
}
|
||||
const [versionAndOwner] = await Promise.all([
|
||||
deck.latestVersionId
|
||||
? db
|
||||
.select()
|
||||
.from(publicDeckVersions)
|
||||
.where(eq(publicDeckVersions.id, deck.latestVersionId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0] ?? null)
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const [ownerRow] = await db
|
||||
.select()
|
||||
.from(authors)
|
||||
.where(eq(authors.userId, deck.ownerUserId))
|
||||
.limit(1);
|
||||
|
||||
return c.json({
|
||||
deck: toDeckDto(deck),
|
||||
latest_version: version ? toVersionDto(version) : null,
|
||||
latest_version: versionAndOwner ? toVersionDto(versionAndOwner) : null,
|
||||
owner: ownerRow ? toOwnerDto(ownerRow) : null,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue