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(),
|
name: text('name').notNull(),
|
||||||
description: text('description'),
|
description: text('description'),
|
||||||
color: text('color'),
|
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'] })
|
visibility: text('visibility', { enum: ['private', 'space', 'public'] })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default('private'),
|
.default('private'),
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,21 @@ export const publicDecks = marketplaceSchema.table(
|
||||||
description: text('description'),
|
description: text('description'),
|
||||||
// ISO-639-1 (z.B. 'de', 'en', 'es'). Nullable für mixed-language.
|
// ISO-639-1 (z.B. 'de', 'en', 'es'). Nullable für mixed-language.
|
||||||
language: text('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,
|
// 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).
|
// 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'),
|
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,
|
name: parsed.data.name,
|
||||||
description: parsed.data.description,
|
description: parsed.data.description,
|
||||||
color: parsed.data.color,
|
color: parsed.data.color,
|
||||||
|
category: parsed.data.category,
|
||||||
visibility: parsed.data.visibility ?? 'private',
|
visibility: parsed.data.visibility ?? 'private',
|
||||||
fsrsSettings: parsed.data.fsrs_settings ?? {},
|
fsrsSettings: parsed.data.fsrs_settings ?? {},
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|
@ -89,6 +90,7 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }>
|
||||||
...(parsed.data.name !== undefined && { name: parsed.data.name }),
|
...(parsed.data.name !== undefined && { name: parsed.data.name }),
|
||||||
...(parsed.data.description !== undefined && { description: parsed.data.description }),
|
...(parsed.data.description !== undefined && { description: parsed.data.description }),
|
||||||
...(parsed.data.color !== undefined && { color: parsed.data.color }),
|
...(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.visibility !== undefined && { visibility: parsed.data.visibility }),
|
||||||
...(parsed.data.fsrs_settings !== undefined && {
|
...(parsed.data.fsrs_settings !== undefined && {
|
||||||
fsrsSettings: parsed.data.fsrs_settings,
|
fsrsSettings: parsed.data.fsrs_settings,
|
||||||
|
|
@ -122,6 +124,7 @@ function toDeckDto(row: typeof decks.$inferSelect) {
|
||||||
name: row.name,
|
name: row.name,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
color: row.color,
|
color: row.color,
|
||||||
|
category: row.category,
|
||||||
visibility: row.visibility,
|
visibility: row.visibility,
|
||||||
fsrs_settings: row.fsrsSettings,
|
fsrs_settings: row.fsrsSettings,
|
||||||
content_hash: row.contentHash,
|
content_hash: row.contentHash,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,11 @@ export type MarketplaceDecksDeps = { db?: CardsDb };
|
||||||
|
|
||||||
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
|
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({
|
const InitSchema = z.object({
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
title: z.string().min(1).max(120),
|
title: z.string().min(1).max(120),
|
||||||
|
|
@ -51,6 +56,7 @@ const InitSchema = z.object({
|
||||||
.optional(),
|
.optional(),
|
||||||
license: z.string().max(60).optional(),
|
license: z.string().max(60).optional(),
|
||||||
priceCredits: z.number().int().min(0).max(100_000).optional(),
|
priceCredits: z.number().int().min(0).max(100_000).optional(),
|
||||||
|
category: MarketplaceCategorySchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const PatchSchema = z.object({
|
const PatchSchema = z.object({
|
||||||
|
|
@ -59,6 +65,7 @@ const PatchSchema = z.object({
|
||||||
language: z.string().regex(/^[a-z]{2}$/).optional(),
|
language: z.string().regex(/^[a-z]{2}$/).optional(),
|
||||||
license: z.string().max(60).optional(),
|
license: z.string().max(60).optional(),
|
||||||
priceCredits: z.number().int().min(0).max(100_000).optional(),
|
priceCredits: z.number().int().min(0).max(100_000).optional(),
|
||||||
|
category: MarketplaceCategorySchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const CardTypeSchema = z.enum([
|
const CardTypeSchema = z.enum([
|
||||||
|
|
@ -104,6 +111,7 @@ function toDeckDto(row: typeof publicDecks.$inferSelect) {
|
||||||
title: row.title,
|
title: row.title,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
language: row.language,
|
language: row.language,
|
||||||
|
category: row.category,
|
||||||
license: row.license,
|
license: row.license,
|
||||||
price_credits: row.priceCredits,
|
price_credits: row.priceCredits,
|
||||||
owner_user_id: row.ownerUserId,
|
owner_user_id: row.ownerUserId,
|
||||||
|
|
@ -236,6 +244,7 @@ export function marketplaceDecksRouter(
|
||||||
title: parsed.data.title,
|
title: parsed.data.title,
|
||||||
description: parsed.data.description,
|
description: parsed.data.description,
|
||||||
language: parsed.data.language,
|
language: parsed.data.language,
|
||||||
|
category: parsed.data.category,
|
||||||
license,
|
license,
|
||||||
priceCredits,
|
priceCredits,
|
||||||
ownerUserId: userId,
|
ownerUserId: userId,
|
||||||
|
|
@ -275,6 +284,7 @@ export function marketplaceDecksRouter(
|
||||||
...(parsed.data.title !== undefined && { title: parsed.data.title }),
|
...(parsed.data.title !== undefined && { title: parsed.data.title }),
|
||||||
...(parsed.data.description !== undefined && { description: parsed.data.description }),
|
...(parsed.data.description !== undefined && { description: parsed.data.description }),
|
||||||
...(parsed.data.language !== undefined && { language: parsed.data.language }),
|
...(parsed.data.language !== undefined && { language: parsed.data.language }),
|
||||||
|
...(parsed.data.category !== undefined && { category: parsed.data.category }),
|
||||||
...(parsed.data.license !== undefined && { license }),
|
...(parsed.data.license !== undefined && { license }),
|
||||||
...(parsed.data.priceCredits !== undefined && { priceCredits }),
|
...(parsed.data.priceCredits !== undefined && { priceCredits }),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ interface DeckListEntry {
|
||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
language: string | null;
|
language: string | null;
|
||||||
|
category: string | null;
|
||||||
license: string;
|
license: string;
|
||||||
price_credits: number;
|
price_credits: number;
|
||||||
card_count: number;
|
card_count: number;
|
||||||
|
|
@ -117,6 +118,7 @@ async function browseImpl(
|
||||||
title: publicDecks.title,
|
title: publicDecks.title,
|
||||||
description: publicDecks.description,
|
description: publicDecks.description,
|
||||||
language: publicDecks.language,
|
language: publicDecks.language,
|
||||||
|
category: publicDecks.category,
|
||||||
license: publicDecks.license,
|
license: publicDecks.license,
|
||||||
priceCredits: publicDecks.priceCredits,
|
priceCredits: publicDecks.priceCredits,
|
||||||
cardCount: cardCountExpr,
|
cardCount: cardCountExpr,
|
||||||
|
|
@ -148,6 +150,7 @@ async function browseImpl(
|
||||||
title: r.title,
|
title: r.title,
|
||||||
description: r.description,
|
description: r.description,
|
||||||
language: r.language,
|
language: r.language,
|
||||||
|
category: r.category,
|
||||||
license: r.license,
|
license: r.license,
|
||||||
price_credits: r.priceCredits,
|
price_credits: r.priceCredits,
|
||||||
card_count: Number(r.cardCount),
|
card_count: Number(r.cardCount),
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@
|
||||||
"marked": "^18.0.3",
|
"marked": "^18.0.3",
|
||||||
"sql.js": "^1.14.1",
|
"sql.js": "^1.14.1",
|
||||||
"@mana/themes": "^0.1.0",
|
"@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": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-node": "^5.2.0",
|
"@sveltejs/adapter-node": "^5.2.0",
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ export interface MarketplaceDeck {
|
||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
language: string | null;
|
language: string | null;
|
||||||
|
category: string | null;
|
||||||
license: string;
|
license: string;
|
||||||
price_credits: number;
|
price_credits: number;
|
||||||
owner_user_id: string;
|
owner_user_id: string;
|
||||||
|
|
@ -61,6 +62,7 @@ export interface DeckListEntry {
|
||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
language: string | null;
|
language: string | null;
|
||||||
|
category: string | null;
|
||||||
license: string;
|
license: string;
|
||||||
price_credits: number;
|
price_credits: number;
|
||||||
card_count: number;
|
card_count: number;
|
||||||
|
|
|
||||||
47
apps/web/src/lib/components/DeckCategoryIcon.svelte
Normal file
47
apps/web/src/lib/components/DeckCategoryIcon.svelte
Normal 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}
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { DeckListEntry } from '$lib/api/marketplace.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 AuthorBadge from './AuthorBadge.svelte';
|
||||||
|
import DeckCategoryIcon from '$lib/components/DeckCategoryIcon.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: DeckListEntry[];
|
items: DeckListEntry[];
|
||||||
|
|
@ -9,6 +12,10 @@
|
||||||
|
|
||||||
const { items, emptyMessage = 'Noch keine Decks gefunden.' }: Props = $props();
|
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 {
|
function languageLabel(code: string | null): string {
|
||||||
if (!code) return '';
|
if (!code) return '';
|
||||||
const map: Record<string, string> = { de: 'Deutsch', en: 'English', es: 'Español', fr: 'Français' };
|
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">
|
<a href="/d/{deck.slug}" class="block">
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<h3 class="truncate font-medium">{deck.title}</h3>
|
<h3 class="truncate font-medium">{deck.title}</h3>
|
||||||
{#if deck.is_featured}
|
<div class="flex shrink-0 items-center gap-1.5">
|
||||||
<span
|
{#if isValidCategory(deck.category)}
|
||||||
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))]"
|
<span class="text-[hsl(var(--color-muted-foreground))]" aria-hidden="true">
|
||||||
title="Editorial Pick"
|
<DeckCategoryIcon category={deck.category} size={16} weight="duotone" />
|
||||||
>
|
</span>
|
||||||
★ Featured
|
{/if}
|
||||||
</span>
|
{#if deck.is_featured}
|
||||||
{/if}
|
<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>
|
</div>
|
||||||
{#if deck.description}
|
{#if deck.description}
|
||||||
<p
|
<p
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
|
|
||||||
.default-pad {
|
.default-pad {
|
||||||
max-width: 72rem;
|
max-width: 72rem;
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus-Mode: keine max-width-Begrenzung, kein Außen-Padding —
|
/* Focus-Mode: keine max-width-Begrenzung, kein Außen-Padding —
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,15 @@
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import type { Card, Deck } from '@cards/domain';
|
import type { Card, Deck } from '@cards/domain';
|
||||||
import { getDeck } from '$lib/api/decks.ts';
|
import { type DeckCategoryId, DECK_CATEGORY_IDS, DECK_CATEGORY_LABELS } from '@cards/domain';
|
||||||
|
import { getDeck, updateDeck } from '$lib/api/decks.ts';
|
||||||
import { listCards, deleteCard } from '$lib/api/cards.ts';
|
import { listCards, deleteCard } from '$lib/api/cards.ts';
|
||||||
import { listDueReviews } from '$lib/api/reviews.ts';
|
import { listDueReviews } from '$lib/api/reviews.ts';
|
||||||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||||
import { t, tn } from '$lib/i18n/index.svelte.ts';
|
import { t, tn } from '$lib/i18n/index.svelte.ts';
|
||||||
import DeckFan from '$lib/components/DeckFan.svelte';
|
import DeckFan from '$lib/components/DeckFan.svelte';
|
||||||
|
import DeckCategoryIcon from '$lib/components/DeckCategoryIcon.svelte';
|
||||||
|
|
||||||
let deck = $state<Deck | null>(null);
|
let deck = $state<Deck | null>(null);
|
||||||
let cards = $state<Card[]>([]);
|
let cards = $state<Card[]>([]);
|
||||||
|
|
@ -46,6 +48,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onSetCategory(id: DeckCategoryId) {
|
||||||
|
if (!deck) return;
|
||||||
|
const next = deck.category === id ? null : id;
|
||||||
|
try {
|
||||||
|
deck = await updateDeck(deck.id, { category: next ?? undefined });
|
||||||
|
} catch (e) {
|
||||||
|
toasts.error((e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onDeleteCard(id: string) {
|
async function onDeleteCard(id: string) {
|
||||||
if (!confirm(t('deck_detail.card_delete_confirm'))) return;
|
if (!confirm(t('deck_detail.card_delete_confirm'))) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -112,6 +124,24 @@
|
||||||
{tn('decks.card_count', cards.length)} · {t('study.due_count', { n: dueCount })}
|
{tn('decks.card_count', cards.length)} · {t('study.due_count', { n: dueCount })}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="category-row mt-3">
|
||||||
|
{#each DECK_CATEGORY_IDS as id}
|
||||||
|
<button
|
||||||
|
class="cat-btn"
|
||||||
|
class:active={deck.category === id}
|
||||||
|
onclick={() => onSetCategory(id)}
|
||||||
|
>
|
||||||
|
<DeckCategoryIcon
|
||||||
|
category={id}
|
||||||
|
size={16}
|
||||||
|
color={deck.category === id ? (deck.color ?? null) : null}
|
||||||
|
weight={deck.category === id ? 'fill' : 'regular'}
|
||||||
|
/>
|
||||||
|
<span class="cat-label">{DECK_CATEGORY_LABELS[id]}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if cards.length === 0}
|
{#if cards.length === 0}
|
||||||
<div
|
<div
|
||||||
class="mt-8 rounded-lg border border-dashed border-[hsl(var(--color-border))] p-12 text-center"
|
class="mt-8 rounded-lg border border-dashed border-[hsl(var(--color-border))] p-12 text-center"
|
||||||
|
|
@ -183,6 +213,41 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.category-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.12s, background 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-btn:hover {
|
||||||
|
border-color: hsl(var(--color-border));
|
||||||
|
color: hsl(var(--color-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-btn.active {
|
||||||
|
border-color: hsl(var(--color-primary) / 0.4);
|
||||||
|
background: hsl(var(--color-primary) / 0.08);
|
||||||
|
color: hsl(var(--color-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.list-toggle {
|
.list-toggle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,37 @@ import { FsrsSettingsSchema } from './fsrs-settings.ts';
|
||||||
|
|
||||||
const VisibilitySchema = z.enum(['private', 'space', 'public']);
|
const VisibilitySchema = z.enum(['private', 'space', 'public']);
|
||||||
|
|
||||||
|
export const DECK_CATEGORY_IDS = [
|
||||||
|
'language',
|
||||||
|
'medicine',
|
||||||
|
'science',
|
||||||
|
'math',
|
||||||
|
'history',
|
||||||
|
'law',
|
||||||
|
'technology',
|
||||||
|
'arts',
|
||||||
|
'music',
|
||||||
|
'sport',
|
||||||
|
'other',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type DeckCategoryId = (typeof DECK_CATEGORY_IDS)[number];
|
||||||
|
export const DeckCategorySchema = z.enum(DECK_CATEGORY_IDS);
|
||||||
|
|
||||||
|
export const DECK_CATEGORY_LABELS: Record<DeckCategoryId, string> = {
|
||||||
|
language: 'Sprache',
|
||||||
|
medicine: 'Medizin',
|
||||||
|
science: 'Wissenschaft',
|
||||||
|
math: 'Mathematik',
|
||||||
|
history: 'Geschichte',
|
||||||
|
law: 'Recht',
|
||||||
|
technology: 'Technik',
|
||||||
|
arts: 'Kunst',
|
||||||
|
music: 'Musik',
|
||||||
|
sport: 'Sport',
|
||||||
|
other: 'Sonstiges',
|
||||||
|
};
|
||||||
|
|
||||||
export const DeckSchema = z
|
export const DeckSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
|
|
@ -15,6 +46,7 @@ export const DeckSchema = z
|
||||||
.regex(/^#[0-9a-fA-F]{6}$/)
|
.regex(/^#[0-9a-fA-F]{6}$/)
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
|
category: DeckCategorySchema.optional().nullable(),
|
||||||
visibility: VisibilitySchema.default('private'),
|
visibility: VisibilitySchema.default('private'),
|
||||||
fsrs_settings: FsrsSettingsSchema.default({}),
|
fsrs_settings: FsrsSettingsSchema.default({}),
|
||||||
content_hash: z.string().optional().nullable(),
|
content_hash: z.string().optional().nullable(),
|
||||||
|
|
@ -32,6 +64,7 @@ export const DeckCreateSchema = z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^#[0-9a-fA-F]{6}$/)
|
.regex(/^#[0-9a-fA-F]{6}$/)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
category: DeckCategorySchema.optional(),
|
||||||
visibility: VisibilitySchema.optional(),
|
visibility: VisibilitySchema.optional(),
|
||||||
fsrs_settings: FsrsSettingsSchema.optional(),
|
fsrs_settings: FsrsSettingsSchema.optional(),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
|
|
@ -69,6 +69,9 @@ importers:
|
||||||
'@cards/domain':
|
'@cards/domain':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/cards-domain
|
version: link:../../packages/cards-domain
|
||||||
|
'@mana/shared-icons':
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0(svelte@5.55.5)(vite@5.4.21(@types/node@22.19.18)(lightningcss@1.32.0))
|
||||||
'@mana/shared-ui-2':
|
'@mana/shared-ui-2':
|
||||||
specifier: ^0.1.0
|
specifier: ^0.1.0
|
||||||
version: 0.1.0(svelte@5.55.5)
|
version: 0.1.0(svelte@5.55.5)
|
||||||
|
|
@ -578,6 +581,11 @@ packages:
|
||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@mana/shared-icons@1.0.0':
|
||||||
|
resolution: {integrity: sha512-71L1dLO6tias8floLv8s0MYzv4cA5IvwftxdFnTYOsKMTMkJ2xEiJ4VxoV5Rj7iAG0oCdV5cmjGyOEKWaLVGEA==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.0.0
|
||||||
|
|
||||||
'@mana/shared-share-protocol@0.1.0':
|
'@mana/shared-share-protocol@0.1.0':
|
||||||
resolution: {integrity: sha512-I1fIDbS3nu++9LUXc08ICrLXE/cdV/n9D0Jm8LOhVH9izUXQSSg2EO4M2+m7K5vc5KdjGBcYrFPhAg48+KE6Kw==}
|
resolution: {integrity: sha512-I1fIDbS3nu++9LUXc08ICrLXE/cdV/n9D0Jm8LOhVH9izUXQSSg2EO4M2+m7K5vc5KdjGBcYrFPhAg48+KE6Kw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
@ -1513,6 +1521,15 @@ packages:
|
||||||
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||||
engines: {node: '>= 14.16'}
|
engines: {node: '>= 14.16'}
|
||||||
|
|
||||||
|
phosphor-svelte@3.1.0:
|
||||||
|
resolution: {integrity: sha512-nldtxx+XCgNREvrb7O5xgDsefytXpSkPTx8Rnu3f2qQCUZLDV1rLxYSd2Jcwckuo9lZB1qKMqGR17P4UDC0PrA==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.0.0 || ^5.0.0-next.96
|
||||||
|
vite: '>=5'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
|
@ -2095,6 +2112,13 @@ snapshots:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@mana/shared-icons@1.0.0(svelte@5.55.5)(vite@5.4.21(@types/node@22.19.18)(lightningcss@1.32.0))':
|
||||||
|
dependencies:
|
||||||
|
phosphor-svelte: 3.1.0(svelte@5.55.5)(vite@5.4.21(@types/node@22.19.18)(lightningcss@1.32.0))
|
||||||
|
svelte: 5.55.5
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- vite
|
||||||
|
|
||||||
'@mana/shared-share-protocol@0.1.0':
|
'@mana/shared-share-protocol@0.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
@ -2861,6 +2885,14 @@ snapshots:
|
||||||
|
|
||||||
pathval@2.0.1: {}
|
pathval@2.0.1: {}
|
||||||
|
|
||||||
|
phosphor-svelte@3.1.0(svelte@5.55.5)(vite@5.4.21(@types/node@22.19.18)(lightningcss@1.32.0)):
|
||||||
|
dependencies:
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.21
|
||||||
|
svelte: 5.55.5
|
||||||
|
optionalDependencies:
|
||||||
|
vite: 5.4.21(@types/node@22.19.18)(lightningcss@1.32.0)
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.4: {}
|
picomatch@4.0.4: {}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue