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 @@