feat(decks): Deck-Kategorien über den ganzen Stack
Some checks are pending
CI / validate (push) Waiting to run
Some checks are pending
CI / validate (push) Waiting to run
- cards-domain: DECK_CATEGORY_IDS, Labels, DeckCategorySchema, category-Feld im DeckSchema - DB-Schema (decks + marketplace/decks): category-Spalte - API-Routen: category in create/update/list/explore - Web: DeckCategoryIcon-Komponente, Kategorie-Picker auf Deck-Detail, Kategorie-Icon in DeckListGrid (Marketplace) - Layout: Bottom-Padding für floating Nav-Bar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5876f95d85
commit
7bf61315b5
13 changed files with 251 additions and 11 deletions
|
|
@ -16,6 +16,21 @@ export const decks = cardsSchema.table(
|
|||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
color: text('color'),
|
||||
category: text('category', {
|
||||
enum: [
|
||||
'language',
|
||||
'medicine',
|
||||
'science',
|
||||
'math',
|
||||
'history',
|
||||
'law',
|
||||
'technology',
|
||||
'arts',
|
||||
'music',
|
||||
'sport',
|
||||
'other',
|
||||
],
|
||||
}),
|
||||
visibility: text('visibility', { enum: ['private', 'space', 'public'] })
|
||||
.notNull()
|
||||
.default('private'),
|
||||
|
|
|
|||
|
|
@ -53,6 +53,21 @@ export const publicDecks = marketplaceSchema.table(
|
|||
description: text('description'),
|
||||
// ISO-639-1 (z.B. 'de', 'en', 'es'). Nullable für mixed-language.
|
||||
language: text('language'),
|
||||
category: text('category', {
|
||||
enum: [
|
||||
'language',
|
||||
'medicine',
|
||||
'science',
|
||||
'math',
|
||||
'history',
|
||||
'law',
|
||||
'technology',
|
||||
'arts',
|
||||
'music',
|
||||
'sport',
|
||||
'other',
|
||||
],
|
||||
}),
|
||||
// SPDX-style ID. CC0-1.0, CC-BY-4.0, CC-BY-SA-4.0,
|
||||
// Cardecky-Personal-Use-1.0 (default für free), Cardecky-Pro-Only-1.0 (paid).
|
||||
license: text('license').notNull().default('Cardecky-Personal-Use-1.0'),
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }>
|
|||
name: parsed.data.name,
|
||||
description: parsed.data.description,
|
||||
color: parsed.data.color,
|
||||
category: parsed.data.category,
|
||||
visibility: parsed.data.visibility ?? 'private',
|
||||
fsrsSettings: parsed.data.fsrs_settings ?? {},
|
||||
createdAt: now,
|
||||
|
|
@ -89,6 +90,7 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }>
|
|||
...(parsed.data.name !== undefined && { name: parsed.data.name }),
|
||||
...(parsed.data.description !== undefined && { description: parsed.data.description }),
|
||||
...(parsed.data.color !== undefined && { color: parsed.data.color }),
|
||||
...(parsed.data.category !== undefined && { category: parsed.data.category }),
|
||||
...(parsed.data.visibility !== undefined && { visibility: parsed.data.visibility }),
|
||||
...(parsed.data.fsrs_settings !== undefined && {
|
||||
fsrsSettings: parsed.data.fsrs_settings,
|
||||
|
|
@ -122,6 +124,7 @@ function toDeckDto(row: typeof decks.$inferSelect) {
|
|||
name: row.name,
|
||||
description: row.description,
|
||||
color: row.color,
|
||||
category: row.category,
|
||||
visibility: row.visibility,
|
||||
fsrs_settings: row.fsrsSettings,
|
||||
content_hash: row.contentHash,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ export type MarketplaceDecksDeps = { db?: CardsDb };
|
|||
|
||||
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
|
||||
|
||||
const MarketplaceCategorySchema = z.enum([
|
||||
'language', 'medicine', 'science', 'math', 'history',
|
||||
'law', 'technology', 'arts', 'music', 'sport', 'other',
|
||||
]);
|
||||
|
||||
const InitSchema = z.object({
|
||||
slug: z.string(),
|
||||
title: z.string().min(1).max(120),
|
||||
|
|
@ -51,6 +56,7 @@ const InitSchema = z.object({
|
|||
.optional(),
|
||||
license: z.string().max(60).optional(),
|
||||
priceCredits: z.number().int().min(0).max(100_000).optional(),
|
||||
category: MarketplaceCategorySchema.optional(),
|
||||
});
|
||||
|
||||
const PatchSchema = z.object({
|
||||
|
|
@ -59,6 +65,7 @@ const PatchSchema = z.object({
|
|||
language: z.string().regex(/^[a-z]{2}$/).optional(),
|
||||
license: z.string().max(60).optional(),
|
||||
priceCredits: z.number().int().min(0).max(100_000).optional(),
|
||||
category: MarketplaceCategorySchema.optional(),
|
||||
});
|
||||
|
||||
const CardTypeSchema = z.enum([
|
||||
|
|
@ -104,6 +111,7 @@ function toDeckDto(row: typeof publicDecks.$inferSelect) {
|
|||
title: row.title,
|
||||
description: row.description,
|
||||
language: row.language,
|
||||
category: row.category,
|
||||
license: row.license,
|
||||
price_credits: row.priceCredits,
|
||||
owner_user_id: row.ownerUserId,
|
||||
|
|
@ -236,6 +244,7 @@ export function marketplaceDecksRouter(
|
|||
title: parsed.data.title,
|
||||
description: parsed.data.description,
|
||||
language: parsed.data.language,
|
||||
category: parsed.data.category,
|
||||
license,
|
||||
priceCredits,
|
||||
ownerUserId: userId,
|
||||
|
|
@ -275,6 +284,7 @@ export function marketplaceDecksRouter(
|
|||
...(parsed.data.title !== undefined && { title: parsed.data.title }),
|
||||
...(parsed.data.description !== undefined && { description: parsed.data.description }),
|
||||
...(parsed.data.language !== undefined && { language: parsed.data.language }),
|
||||
...(parsed.data.category !== undefined && { category: parsed.data.category }),
|
||||
...(parsed.data.license !== undefined && { license }),
|
||||
...(parsed.data.priceCredits !== undefined && { priceCredits }),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ interface DeckListEntry {
|
|||
title: string;
|
||||
description: string | null;
|
||||
language: string | null;
|
||||
category: string | null;
|
||||
license: string;
|
||||
price_credits: number;
|
||||
card_count: number;
|
||||
|
|
@ -117,6 +118,7 @@ async function browseImpl(
|
|||
title: publicDecks.title,
|
||||
description: publicDecks.description,
|
||||
language: publicDecks.language,
|
||||
category: publicDecks.category,
|
||||
license: publicDecks.license,
|
||||
priceCredits: publicDecks.priceCredits,
|
||||
cardCount: cardCountExpr,
|
||||
|
|
@ -148,6 +150,7 @@ async function browseImpl(
|
|||
title: r.title,
|
||||
description: r.description,
|
||||
language: r.language,
|
||||
category: r.category,
|
||||
license: r.license,
|
||||
price_credits: r.priceCredits,
|
||||
card_count: Number(r.cardCount),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue