feat(cards): deck management UI + production auth portal wiring

Deck schema, API routes, and SvelteKit UI for creating and browsing decks
(DeckStack component, inline creation, floating nav). Production compose
updated with PUBLIC_AUTH_WEB_URL so cards-web redirects to auth.mana.how
for login/register instead of the raw API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-11 18:50:27 +02:00
parent 7116bd66b4
commit 5859e202c5
9 changed files with 271 additions and 28 deletions

View file

@ -44,6 +44,7 @@ export const decks = cardsSchema.table(
// Quelle nachzuladen.
forkedFromMarketplaceDeckId: text('forked_from_marketplace_deck_id'),
forkedFromMarketplaceVersionId: text('forked_from_marketplace_version_id'),
archivedAt: timestamp('archived_at', { withTimezone: true, mode: 'date' }),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'date' })
.notNull()
.defaultNow(),

View file

@ -13,6 +13,7 @@ export function toDeckDto(row: typeof decks.$inferSelect) {
content_hash: row.contentHash,
forked_from_marketplace_deck_id: row.forkedFromMarketplaceDeckId,
forked_from_marketplace_version_id: row.forkedFromMarketplaceVersionId,
archived_at: row.archivedAt?.toISOString() ?? null,
created_at: row.createdAt.toISOString(),
updated_at: row.updatedAt.toISOString(),
};

View file

@ -1,4 +1,4 @@
import { and, eq, isNotNull, ne } from 'drizzle-orm';
import { and, eq, isNotNull, isNull, ne } from 'drizzle-orm';
import { sql } from 'drizzle-orm';
import { Hono } from 'hono';
@ -52,10 +52,17 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }>
r.get('/', async (c) => {
const userId = c.get('userId');
const forkedFromMarketplace = c.req.query('forked_from_marketplace');
const archivedParam = c.req.query('archived');
const conditions = [eq(decks.userId, userId)];
if (forkedFromMarketplace === 'true') {
conditions.push(isNotNull(decks.forkedFromMarketplaceDeckId));
}
// archived=true → nur archivierte; default → nur aktive
if (archivedParam === 'true') {
conditions.push(isNotNull(decks.archivedAt));
} else {
conditions.push(isNull(decks.archivedAt));
}
const rows = await dbOf()
.select()
.from(decks)
@ -86,6 +93,7 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }>
422
);
}
const now = new Date();
const [row] = await dbOf()
.update(decks)
.set({
@ -97,7 +105,9 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }>
...(parsed.data.fsrs_settings !== undefined && {
fsrsSettings: parsed.data.fsrs_settings,
}),
updatedAt: new Date(),
...(parsed.data.archived === true && { archivedAt: now }),
...(parsed.data.archived === false && { archivedAt: null }),
updatedAt: now,
})
.where(and(eq(decks.id, id), eq(decks.userId, userId)))
.returning();