feat(decks): Deck-Kategorien über den ganzen Stack
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:
Till JS 2026-05-09 20:24:47 +02:00
parent 5876f95d85
commit 7bf61315b5
13 changed files with 251 additions and 11 deletions

View file

@ -0,0 +1,47 @@
<script lang="ts">
import type { DeckCategoryId } from '@cards/domain';
import {
Globe,
Heartbeat,
Leaf,
Brain,
Flag,
Medal,
Lightning,
Palette,
MusicNote,
Barbell,
Sparkle,
} from '@mana/shared-icons';
type Weight = 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone';
interface Props {
category: DeckCategoryId;
size?: number;
color?: string | null;
weight?: Weight;
}
let { category, size = 32, color = null, weight = 'duotone' }: Props = $props();
const ICON_MAP: Record<DeckCategoryId, typeof Globe> = {
language: Globe,
medicine: Heartbeat,
science: Leaf,
math: Brain,
history: Flag,
law: Medal,
technology: Lightning,
arts: Palette,
music: MusicNote,
sport: Barbell,
other: Sparkle,
};
const IconComponent = $derived(ICON_MAP[category]);
</script>
{#if IconComponent}
<IconComponent {size} {weight} color={color ?? undefined} />
{/if}

View file

@ -1,6 +1,9 @@
<script lang="ts">
import type { DeckListEntry } from '$lib/api/marketplace.ts';
import type { DeckCategoryId } from '@cards/domain';
import { DECK_CATEGORY_IDS } from '@cards/domain';
import AuthorBadge from './AuthorBadge.svelte';
import DeckCategoryIcon from '$lib/components/DeckCategoryIcon.svelte';
interface Props {
items: DeckListEntry[];
@ -9,6 +12,10 @@
const { items, emptyMessage = 'Noch keine Decks gefunden.' }: Props = $props();
function isValidCategory(c: string | null): c is DeckCategoryId {
return c !== null && (DECK_CATEGORY_IDS as readonly string[]).includes(c);
}
function languageLabel(code: string | null): string {
if (!code) return '';
const map: Record<string, string> = { de: 'Deutsch', en: 'English', es: 'Español', fr: 'Français' };
@ -29,14 +36,21 @@
<a href="/d/{deck.slug}" class="block">
<div class="flex items-start justify-between gap-2">
<h3 class="truncate font-medium">{deck.title}</h3>
{#if deck.is_featured}
<span
class="shrink-0 rounded-full bg-[hsl(var(--color-primary))]/15 px-2 py-0.5 text-[10px] font-medium text-[hsl(var(--color-primary))]"
title="Editorial Pick"
>
★ Featured
</span>
{/if}
<div class="flex shrink-0 items-center gap-1.5">
{#if isValidCategory(deck.category)}
<span class="text-[hsl(var(--color-muted-foreground))]" aria-hidden="true">
<DeckCategoryIcon category={deck.category} size={16} weight="duotone" />
</span>
{/if}
{#if deck.is_featured}
<span
class="rounded-full bg-[hsl(var(--color-primary))]/15 px-2 py-0.5 text-[10px] font-medium text-[hsl(var(--color-primary))]"
title="Editorial Pick"
>
★ Featured
</span>
{/if}
</div>
</div>
{#if deck.description}
<p