feat(cards): deck management UI + production auth portal wiring
Deck schema, API routes, and SvelteKit UI for creating and browsing decks (DeckStack component, inline creation, floating nav). Production compose updated with PUBLIC_AUTH_WEB_URL so cards-web redirects to auth.mana.how for login/register instead of the raw API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7116bd66b4
commit
5859e202c5
9 changed files with 271 additions and 28 deletions
|
|
@ -3,9 +3,11 @@
|
|||
import { DECK_CATEGORY_LABELS } from '@cards/domain';
|
||||
import { stackLayers } from '$lib/utils/deck-tilt';
|
||||
import { t, tn } from '$lib/i18n/index.svelte.ts';
|
||||
import { archiveDeck, unarchiveDeck } from '$lib/api/decks.ts';
|
||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
import CardSurface from './CardSurface.svelte';
|
||||
import DeckCategoryIcon from './DeckCategoryIcon.svelte';
|
||||
import { PencilSimple } from '@mana/shared-icons';
|
||||
import { PencilSimple, Archive, ArrowCounterClockwise, DotsThree } from '@mana/shared-icons';
|
||||
|
||||
interface Props {
|
||||
deck: Deck;
|
||||
|
|
@ -14,9 +16,35 @@
|
|||
href?: string;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
ariaLabel?: string;
|
||||
onarchive?: (deck: Deck) => void;
|
||||
}
|
||||
|
||||
let { deck, cardCount = 0, dueCount = 0, href, onclick, ariaLabel }: Props = $props();
|
||||
let { deck, cardCount = 0, dueCount = 0, href, onclick, ariaLabel, onarchive }: Props = $props();
|
||||
|
||||
let menuOpen = $state(false);
|
||||
const isArchived = $derived(deck.archived_at != null);
|
||||
|
||||
function toggleMenu(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
menuOpen = !menuOpen;
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menuOpen = false;
|
||||
}
|
||||
|
||||
async function handleArchiveToggle(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
try {
|
||||
const updated = isArchived ? await unarchiveDeck(deck.id) : await archiveDeck(deck.id);
|
||||
onarchive?.(updated);
|
||||
} catch {
|
||||
toasts.error(isArchived ? 'Wiederherstellen fehlgeschlagen' : 'Archivieren fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
const layers = $derived(stackLayers(deck.id, 3));
|
||||
const hasContent = $derived(cardCount > 0);
|
||||
|
|
@ -44,15 +72,41 @@
|
|||
{/each}
|
||||
{/if}
|
||||
|
||||
<a
|
||||
href="/decks/{deck.id}/edit"
|
||||
class="edit-btn"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
aria-label="Deck bearbeiten"
|
||||
title="Deck bearbeiten"
|
||||
>
|
||||
<PencilSimple size={13} weight="bold" />
|
||||
</a>
|
||||
<div class="actions">
|
||||
<a
|
||||
href="/decks/{deck.id}/edit"
|
||||
class="action-btn"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
aria-label="Deck bearbeiten"
|
||||
title="Deck bearbeiten"
|
||||
>
|
||||
<PencilSimple size={13} weight="bold" />
|
||||
</a>
|
||||
<button
|
||||
class="action-btn"
|
||||
onclick={toggleMenu}
|
||||
aria-label="Weitere Aktionen"
|
||||
title="Weitere Aktionen"
|
||||
aria-expanded={menuOpen}
|
||||
>
|
||||
<DotsThree size={13} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if menuOpen}
|
||||
<div class="menu-backdrop" role="presentation" onclick={closeMenu} onkeydown={() => {}}></div>
|
||||
<div class="menu" role="menu">
|
||||
<button class="menu-item" role="menuitem" onclick={handleArchiveToggle}>
|
||||
{#if isArchived}
|
||||
<ArrowCounterClockwise size={14} weight="bold" />
|
||||
Wiederherstellen
|
||||
{:else}
|
||||
<Archive size={14} weight="bold" />
|
||||
Archivieren
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<CardSurface
|
||||
size="md"
|
||||
|
|
@ -182,11 +236,22 @@
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
.actions {
|
||||
position: absolute;
|
||||
bottom: 0.625rem;
|
||||
right: 0.625rem;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.stack-wrap:hover .actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 1.625rem;
|
||||
height: 1.625rem;
|
||||
border-radius: 0.375rem;
|
||||
|
|
@ -197,18 +262,54 @@
|
|||
justify-content: center;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
text-decoration: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s, color 0.12s, background 0.12s, border-color 0.12s;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: color 0.12s, background 0.12s, border-color 0.12s;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.stack-wrap:hover .edit-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.edit-btn:hover {
|
||||
.action-btn:hover {
|
||||
color: hsl(var(--color-foreground));
|
||||
background: hsl(var(--color-surface));
|
||||
border-color: hsl(var(--color-primary) / 0.5);
|
||||
}
|
||||
|
||||
.menu-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 19;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
bottom: 2.5rem;
|
||||
right: 0.625rem;
|
||||
z-index: 20;
|
||||
background: hsl(var(--color-surface));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 16px hsl(var(--color-foreground) / 0.1);
|
||||
padding: 0.25rem;
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-foreground));
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: hsl(var(--color-muted));
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue