diff --git a/apps/api/src/db/schema/decks.ts b/apps/api/src/db/schema/decks.ts index 71045da..0dba171 100644 --- a/apps/api/src/db/schema/decks.ts +++ b/apps/api/src/db/schema/decks.ts @@ -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'), diff --git a/apps/api/src/db/schema/marketplace/decks.ts b/apps/api/src/db/schema/marketplace/decks.ts index 087b31a..90a4bb5 100644 --- a/apps/api/src/db/schema/marketplace/decks.ts +++ b/apps/api/src/db/schema/marketplace/decks.ts @@ -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'), diff --git a/apps/api/src/routes/decks.ts b/apps/api/src/routes/decks.ts index 10d684e..0593130 100644 --- a/apps/api/src/routes/decks.ts +++ b/apps/api/src/routes/decks.ts @@ -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, diff --git a/apps/api/src/routes/marketplace/decks.ts b/apps/api/src/routes/marketplace/decks.ts index c66aeb4..70b1331 100644 --- a/apps/api/src/routes/marketplace/decks.ts +++ b/apps/api/src/routes/marketplace/decks.ts @@ -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 }), }) diff --git a/apps/api/src/routes/marketplace/explore.ts b/apps/api/src/routes/marketplace/explore.ts index a0927f9..ba759c7 100644 --- a/apps/api/src/routes/marketplace/explore.ts +++ b/apps/api/src/routes/marketplace/explore.ts @@ -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), diff --git a/apps/web/package.json b/apps/web/package.json index 813e524..b522392 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,7 +21,8 @@ "marked": "^18.0.3", "sql.js": "^1.14.1", "@mana/themes": "^0.1.0", - "@mana/shared-ui-2": "^0.1.0" + "@mana/shared-ui-2": "^0.1.0", + "@mana/shared-icons": "^1.0.0" }, "devDependencies": { "@sveltejs/adapter-node": "^5.2.0", diff --git a/apps/web/src/lib/api/marketplace.ts b/apps/web/src/lib/api/marketplace.ts index 4c2f5fb..ba06f79 100644 --- a/apps/web/src/lib/api/marketplace.ts +++ b/apps/web/src/lib/api/marketplace.ts @@ -29,6 +29,7 @@ export interface MarketplaceDeck { title: string; description: string | null; language: string | null; + category: string | null; license: string; price_credits: number; owner_user_id: string; @@ -61,6 +62,7 @@ export interface DeckListEntry { title: string; description: string | null; language: string | null; + category: string | null; license: string; price_credits: number; card_count: number; diff --git a/apps/web/src/lib/components/DeckCategoryIcon.svelte b/apps/web/src/lib/components/DeckCategoryIcon.svelte new file mode 100644 index 0000000..3ac7848 --- /dev/null +++ b/apps/web/src/lib/components/DeckCategoryIcon.svelte @@ -0,0 +1,47 @@ + + +{#if IconComponent} + +{/if} diff --git a/apps/web/src/lib/components/marketplace/DeckListGrid.svelte b/apps/web/src/lib/components/marketplace/DeckListGrid.svelte index d488308..6c2f265 100644 --- a/apps/web/src/lib/components/marketplace/DeckListGrid.svelte +++ b/apps/web/src/lib/components/marketplace/DeckListGrid.svelte @@ -1,6 +1,9 @@