feat(web): apiErrorMessage-Utility + MultipleChoice-Fallback
- Neue Utility `apiErrorMessage()` in `$lib/api/error.ts`: liest `body.detail` / `body.error` aus ApiError-Responses statt generischer "(err as Error).message" — 22 Dateien auf die Utility umgestellt, keine rohen Type-Casts mehr - MultipleChoiceView: Fallback-UI wenn < 1 Distractor verfügbar — zeigt Antwort direkt + Nochmal/Gewusst-Buttons statt kaputter 1-Option-Auswahl Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f3a148171a
commit
f2f752e9ee
24 changed files with 381 additions and 284 deletions
21
apps/web/src/lib/api/error.ts
Normal file
21
apps/web/src/lib/api/error.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ApiError } from './client.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiert die beste verfügbare Fehlermeldung aus einem unbekannten Fehler.
|
||||||
|
* Bei ApiError wird `body.detail` oder `body.error` bevorzugt — das sind die
|
||||||
|
* vom Server gesetzten, lesbaren Fehlertexte. Fallback auf Error.message bzw.
|
||||||
|
* String-Konvertierung.
|
||||||
|
*/
|
||||||
|
export function apiErrorMessage(err: unknown): string {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
const b = err.body;
|
||||||
|
if (b && typeof b === 'object') {
|
||||||
|
const body = b as Record<string, unknown>;
|
||||||
|
if (typeof body.detail === 'string' && body.detail) return body.detail;
|
||||||
|
if (typeof body.error === 'string' && body.error) return body.error;
|
||||||
|
}
|
||||||
|
return `Fehler ${err.status}`;
|
||||||
|
}
|
||||||
|
if (err instanceof Error) return err.message;
|
||||||
|
return String(err);
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
import { API_BASE } from '$lib/api/client.ts';
|
import { API_BASE } from '$lib/api/client.ts';
|
||||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||||
import { t } from '$lib/i18n/index.svelte.ts';
|
import { t } from '$lib/i18n/index.svelte.ts';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
imageRef = $bindable(''),
|
imageRef = $bindable(''),
|
||||||
|
|
@ -44,7 +45,7 @@
|
||||||
imageRef = r.id;
|
imageRef = r.id;
|
||||||
maskRegionsJson = '[]';
|
maskRegionsJson = '[]';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toasts.error(`${(err as Error).message}`);
|
toasts.error(`${apiErrorMessage(err)}`);
|
||||||
} finally {
|
} finally {
|
||||||
uploading = false;
|
uploading = false;
|
||||||
input.value = '';
|
input.value = '';
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
let options = $state<string[]>([]);
|
let options = $state<string[]>([]);
|
||||||
let selected = $state<string | null>(null);
|
let selected = $state<string | null>(null);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
let tooFewOptions = $state(false);
|
||||||
|
|
||||||
const correct = $derived(selected === answer);
|
const correct = $derived(selected === answer);
|
||||||
|
|
||||||
|
|
@ -51,6 +52,13 @@
|
||||||
distractors = [...distractors, ...poolItems].slice(0, 3);
|
distractors = [...distractors, ...poolItems].slice(0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zu wenig Distractors: Fallback auf Antwort-Anzeige mit manueller Bewertung
|
||||||
|
if (distractors.length < 1) {
|
||||||
|
tooFewOptions = true;
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
options = shuffle([answer, ...distractors.slice(0, 3)]);
|
options = shuffle([answer, ...distractors.slice(0, 3)]);
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
|
@ -100,6 +108,15 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="loading" aria-live="polite">Optionen werden geladen …</div>
|
<div class="loading" aria-live="polite">Optionen werden geladen …</div>
|
||||||
|
{:else if tooFewOptions}
|
||||||
|
<div class="few-options">
|
||||||
|
<p class="few-hint">Deck hat zu wenig Karten für Multiple Choice.</p>
|
||||||
|
<div class="few-answer">{answer}</div>
|
||||||
|
<div class="few-actions">
|
||||||
|
<button class="few-btn few-btn-again" onclick={() => ongrade('again')}>Nochmal</button>
|
||||||
|
<button class="few-btn few-btn-good" onclick={() => ongrade('good')}>Gewusst</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="options" role="group" aria-label="Antwortmöglichkeiten">
|
<div class="options" role="group" aria-label="Antwortmöglichkeiten">
|
||||||
{#each options as opt, i (opt)}
|
{#each options as opt, i (opt)}
|
||||||
|
|
@ -168,6 +185,47 @@
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.few-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.few-hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
}
|
||||||
|
.few-answer {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: hsl(var(--color-surface));
|
||||||
|
border: 1px solid hsl(var(--color-border));
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.few-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.few-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid hsl(var(--color-border));
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.few-btn-again {
|
||||||
|
background: hsl(var(--color-error) / 0.1);
|
||||||
|
border-color: hsl(var(--color-error) / 0.4);
|
||||||
|
color: hsl(var(--color-foreground));
|
||||||
|
}
|
||||||
|
.few-btn-good {
|
||||||
|
background: hsl(var(--color-success) / 0.1);
|
||||||
|
border-color: hsl(var(--color-success) / 0.4);
|
||||||
|
color: hsl(var(--color-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
.options {
|
.options {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import { i18n, t } from '$lib/i18n/index.svelte.ts';
|
import { i18n, t } from '$lib/i18n/index.svelte.ts';
|
||||||
import CardSurface from './CardSurface.svelte';
|
import CardSurface from './CardSurface.svelte';
|
||||||
import DeckCategoryIcon from './DeckCategoryIcon.svelte';
|
import DeckCategoryIcon from './DeckCategoryIcon.svelte';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
let open = $state(false);
|
let open = $state(false);
|
||||||
let catOpen = $state(false);
|
let catOpen = $state(false);
|
||||||
|
|
@ -95,7 +96,7 @@
|
||||||
toasts.success(`🖼 "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
toasts.success(`🖼 "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
||||||
goto(`/decks/${result.deck.id}`);
|
goto(`/decks/${result.deck.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
imageError = (err as Error).message;
|
imageError = apiErrorMessage(err);
|
||||||
imageGenerating = false;
|
imageGenerating = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +120,7 @@
|
||||||
toasts.success(`${deck.name} ✓`);
|
toasts.success(`${deck.name} ✓`);
|
||||||
goto(`/decks/${deck.id}`);
|
goto(`/decks/${deck.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toasts.error(t('deck_new.create_failed', { msg: (err as Error).message }));
|
toasts.error(t('deck_new.create_failed', { msg: apiErrorMessage(err) }));
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +134,7 @@
|
||||||
toasts.success(`✨ "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
toasts.success(`✨ "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
||||||
goto(`/decks/${result.deck.id}`);
|
goto(`/decks/${result.deck.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
aiError = (err as Error).message;
|
aiError = apiErrorMessage(err);
|
||||||
generating = false;
|
generating = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
hideDiscussion,
|
hideDiscussion,
|
||||||
listDiscussions,
|
listDiscussions,
|
||||||
postDiscussion,
|
postDiscussion,
|
||||||
|
|
@ -37,7 +38,7 @@
|
||||||
const result = await listDiscussions(cardContentHash);
|
const result = await listDiscussions(cardContentHash);
|
||||||
comments = result.discussions;
|
comments = result.discussions;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(`Discussions laden fehlgeschlagen: ${(e as Error).message}`);
|
toasts.error(`Discussions laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +54,7 @@
|
||||||
replyToId = null;
|
replyToId = null;
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
} finally {
|
} finally {
|
||||||
posting = false;
|
posting = false;
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +66,7 @@
|
||||||
await hideDiscussion(id);
|
await hideDiscussion(id);
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { Card, Deck } from '@cards/domain';
|
import type { Card, Deck } from '@cards/domain';
|
||||||
import {
|
import {
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
getMyAuthorProfile,
|
getMyAuthorProfile,
|
||||||
getMarketplaceDeck,
|
getMarketplaceDeck,
|
||||||
initMarketplaceDeck,
|
initMarketplaceDeck,
|
||||||
|
|
@ -125,7 +126,7 @@
|
||||||
);
|
);
|
||||||
onPublished(slug.trim());
|
onPublished(slug.trim());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = (e as Error).message;
|
error = apiErrorMessage(e);
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { publishMarketplaceVersion } from '$lib/api/marketplace.ts';
|
import { publishMarketplaceVersion } from '$lib/api/marketplace.ts';
|
||||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|
@ -45,7 +46,7 @@
|
||||||
}
|
}
|
||||||
cards = parsed;
|
cards = parsed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = `JSON-Parse-Fehler: ${(e as Error).message}`;
|
error = `JSON-Parse-Fehler: ${apiErrorMessage(e)}`;
|
||||||
busy = false;
|
busy = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +60,7 @@
|
||||||
toasts.success(`Version ${result.version.semver} veröffentlicht (${result.version.card_count} Karten)`);
|
toasts.success(`Version ${result.version.semver} veröffentlicht (${result.version.card_count} Karten)`);
|
||||||
onPublished?.();
|
onPublished?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = (e as Error).message;
|
error = apiErrorMessage(e);
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
closePullRequest,
|
closePullRequest,
|
||||||
listPullRequests,
|
listPullRequests,
|
||||||
mergePullRequest,
|
mergePullRequest,
|
||||||
|
|
@ -37,7 +38,7 @@
|
||||||
const result = await listPullRequests(slug, statusFilter);
|
const result = await listPullRequests(slug, statusFilter);
|
||||||
prs = result.pull_requests;
|
prs = result.pull_requests;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(`PRs laden fehlgeschlagen: ${(e as Error).message}`);
|
toasts.error(`PRs laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +55,7 @@
|
||||||
toasts.success('PR gemerged — neue Version live.');
|
toasts.success('PR gemerged — neue Version live.');
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
} finally {
|
} finally {
|
||||||
busyId = null;
|
busyId = null;
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +68,7 @@
|
||||||
await rejectPullRequest(id);
|
await rejectPullRequest(id);
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
} finally {
|
} finally {
|
||||||
busyId = null;
|
busyId = null;
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +80,7 @@
|
||||||
await closePullRequest(id);
|
await closePullRequest(id);
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
} finally {
|
} finally {
|
||||||
busyId = null;
|
busyId = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { createPullRequest } from '$lib/api/marketplace.ts';
|
import { createPullRequest } from '$lib/api/marketplace.ts';
|
||||||
import type { MarketplaceVersionCard } from '$lib/api/marketplace.ts';
|
import type { MarketplaceVersionCard } from '$lib/api/marketplace.ts';
|
||||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|
@ -59,7 +60,7 @@
|
||||||
toasts.success('Pull-Request eingereicht — Author bekommt Bescheid.');
|
toasts.success('Pull-Request eingereicht — Author bekommt Bescheid.');
|
||||||
onSubmitted?.();
|
onSubmitted?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = (e as Error).message;
|
error = apiErrorMessage(e);
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||||
import { t } from '$lib/i18n/index.svelte.ts';
|
import { t } from '$lib/i18n/index.svelte.ts';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
let email = $state('');
|
let email = $state('');
|
||||||
let password = $state('');
|
let password = $state('');
|
||||||
|
|
@ -22,7 +23,7 @@
|
||||||
await devUser.login(email.trim(), password);
|
await devUser.login(email.trim(), password);
|
||||||
goto('/decks');
|
goto('/decks');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = (err as Error).message;
|
error = apiErrorMessage(err);
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
import { exportMe, deleteMe } from '$lib/api/me.ts';
|
import { exportMe, deleteMe } from '$lib/api/me.ts';
|
||||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||||
import { t } from '$lib/i18n/index.svelte.ts';
|
import { t } from '$lib/i18n/index.svelte.ts';
|
||||||
|
import { stackLayers } from '$lib/utils/deck-tilt';
|
||||||
|
import CardSurface from '$lib/components/CardSurface.svelte';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
let exporting = $state(false);
|
let exporting = $state(false);
|
||||||
let deleting = $state(false);
|
let deleting = $state(false);
|
||||||
|
|
@ -23,7 +26,9 @@
|
||||||
.join('');
|
.join('');
|
||||||
});
|
});
|
||||||
|
|
||||||
const shortId = $derived(devUser.id ? devUser.id.slice(0, 8) + '…' : '—');
|
const profileLayers = stackLayers('account-profile', 3);
|
||||||
|
const exportLayers = stackLayers('account-export', 3);
|
||||||
|
const dangerLayers = stackLayers('account-danger', 3);
|
||||||
|
|
||||||
async function onExport() {
|
async function onExport() {
|
||||||
exporting = true;
|
exporting = true;
|
||||||
|
|
@ -46,7 +51,7 @@
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(t('account.export_failed', { msg: (e as Error).message }));
|
toasts.error(t('account.export_failed', { msg: apiErrorMessage(e) }));
|
||||||
} finally {
|
} finally {
|
||||||
exporting = false;
|
exporting = false;
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +69,7 @@
|
||||||
devUser.clear();
|
devUser.clear();
|
||||||
goto('/');
|
goto('/');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(t('account.delete_failed', { msg: (e as Error).message }));
|
toasts.error(t('account.delete_failed', { msg: apiErrorMessage(e) }));
|
||||||
deleting = false;
|
deleting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,112 +84,179 @@
|
||||||
<title>{t('account.title')} · {t('app.title_suffix')}</title>
|
<title>{t('account.title')} · {t('app.title_suffix')}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="page">
|
<h1 class="page-title">{t('account.title')}</h1>
|
||||||
|
|
||||||
|
<ul class="card-row" aria-label="Account-Karten">
|
||||||
|
|
||||||
<!-- Profil-Karte -->
|
<!-- Profil-Karte -->
|
||||||
<div class="card profile-card">
|
<li class="stack-wrap">
|
||||||
<div class="avatar" aria-hidden="true">{initials}</div>
|
{#each profileLayers as layer, i (i)}
|
||||||
<div class="profile-info">
|
<div class="layer" style:transform="translate({layer.dx}px,{layer.dy}px) rotate({layer.tilt}deg)" aria-hidden="true"></div>
|
||||||
{#if devUser.user?.name}
|
{/each}
|
||||||
<p class="profile-name">{devUser.user.name}</p>
|
<CardSurface size="md" colorAccent="hsl(var(--color-primary))">
|
||||||
{/if}
|
<div class="card-inner">
|
||||||
<p class="profile-email">{devUser.user?.email ?? '—'}</p>
|
<div class="card-corner">
|
||||||
{#if devUser.user?.tier}
|
<div class="avatar" aria-hidden="true">{initials}</div>
|
||||||
<span class="tier-badge">{devUser.user.tier}</span>
|
</div>
|
||||||
{/if}
|
<div class="card-body">
|
||||||
</div>
|
{#if devUser.user?.name}
|
||||||
<button type="button" class="btn-outline logout-btn" onclick={logout}>
|
<p class="card-title">{devUser.user.name}</p>
|
||||||
{t('account.logout')}
|
{/if}
|
||||||
</button>
|
<p class="card-sub">{devUser.user?.email ?? '—'}</p>
|
||||||
</div>
|
{#if devUser.user?.tier}
|
||||||
|
<span class="tier-badge">{devUser.user.tier}</span>
|
||||||
<!-- Meta-Grid -->
|
{/if}
|
||||||
<div class="meta-grid">
|
</div>
|
||||||
<div class="card meta-card">
|
<div class="card-meta">
|
||||||
<p class="meta-label">{t('account.user_id_label')}</p>
|
<button type="button" class="btn-ghost" onclick={logout}>
|
||||||
<code class="meta-value">{shortId}</code>
|
{t('account.logout')}
|
||||||
<code class="meta-full">{devUser.id ?? '—'}</code>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card meta-card">
|
</div>
|
||||||
<p class="meta-label">Tier</p>
|
</CardSurface>
|
||||||
<p class="meta-value">{devUser.user?.tier ?? '—'}</p>
|
</li>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Export-Karte -->
|
<!-- Export-Karte -->
|
||||||
<div class="card action-card">
|
<li class="stack-wrap">
|
||||||
<div class="action-header">
|
{#each exportLayers as layer, i (i)}
|
||||||
<div class="action-icon">📦</div>
|
<div class="layer" style:transform="translate({layer.dx}px,{layer.dy}px) rotate({layer.tilt}deg)" aria-hidden="true"></div>
|
||||||
<div>
|
{/each}
|
||||||
<h2 class="action-title">{t('account.export_title')}</h2>
|
<CardSurface size="md" colorAccent="#22C55E">
|
||||||
<p class="action-desc">{t('account.export_intro')}</p>
|
<div class="card-inner">
|
||||||
|
<div class="card-corner">
|
||||||
|
<span class="card-icon" aria-hidden="true">📦</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-title">{t('account.export_title')}</p>
|
||||||
|
<p class="card-desc">{t('account.export_intro')}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-primary"
|
||||||
|
onclick={onExport}
|
||||||
|
disabled={exporting}
|
||||||
|
>
|
||||||
|
{exporting ? t('account.export_loading') : t('account.export_button')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardSurface>
|
||||||
<button
|
</li>
|
||||||
type="button"
|
|
||||||
class="btn-primary"
|
|
||||||
onclick={onExport}
|
|
||||||
disabled={exporting}
|
|
||||||
>
|
|
||||||
{exporting ? t('account.export_loading') : t('account.export_button')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Danger-Karte -->
|
<!-- Danger-Karte -->
|
||||||
<div class="card danger-card">
|
<li class="stack-wrap">
|
||||||
<div class="action-header">
|
{#each dangerLayers as layer, i (i)}
|
||||||
<div class="action-icon danger-icon">⚠️</div>
|
<div class="layer layer-danger" style:transform="translate({layer.dx}px,{layer.dy}px) rotate({layer.tilt}deg)" aria-hidden="true"></div>
|
||||||
<div>
|
{/each}
|
||||||
<h2 class="action-title danger-title">{t('account.delete_title')}</h2>
|
<CardSurface size="md" colorAccent="hsl(var(--color-error))">
|
||||||
<p class="action-desc">{t('account.delete_intro')}</p>
|
<div class="card-inner">
|
||||||
|
<div class="card-corner">
|
||||||
|
<span class="card-icon" aria-hidden="true">⚠️</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-title danger-title">{t('account.delete_title')}</p>
|
||||||
|
<p class="card-desc">{t('account.delete_intro')}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-danger"
|
||||||
|
onclick={onDelete}
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{deleting ? t('account.delete_loading') : t('account.delete_button')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardSurface>
|
||||||
<button
|
</li>
|
||||||
type="button"
|
|
||||||
class="btn-danger"
|
|
||||||
onclick={onDelete}
|
|
||||||
disabled={deleting}
|
|
||||||
>
|
|
||||||
{deleting ? t('account.delete_loading') : t('account.delete_button')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</ul>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page {
|
.page-title {
|
||||||
max-width: 36rem;
|
margin: 0 0 1.5rem;
|
||||||
margin: 0 auto;
|
font-size: 1.75rem;
|
||||||
padding: 2rem 1rem;
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal-Scroll-Reihe — gleiche Mechanik wie DeckListGrid */
|
||||||
|
.card-row {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding-block: 1.25rem 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scroll-padding-inline-start: max(1rem, calc((100dvw - 72rem) / 2 + 1rem));
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: hsl(var(--color-border)) transparent;
|
||||||
|
width: 100dvw;
|
||||||
|
margin-left: calc(50% - 50dvw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-row::before,
|
||||||
|
.card-row::after {
|
||||||
|
content: '';
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: max(1rem, calc((100dvw - 72rem) / 2 + 1rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-row::-webkit-scrollbar { height: 4px; }
|
||||||
|
.card-row::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
.card-row::-webkit-scrollbar-thumb {
|
||||||
|
background: hsl(var(--color-border));
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stack-Wrapper — identisch zu DeckStack / MarketplaceDeckStack */
|
||||||
|
.stack-wrap {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 16rem;
|
||||||
|
aspect-ratio: 5 / 7;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: hsl(var(--color-surface));
|
||||||
|
border: 1px solid hsl(var(--color-border));
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
box-shadow: 0 1px 3px hsl(var(--color-foreground) / 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-danger {
|
||||||
|
border-color: hsl(var(--color-error) / 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Karten-Innenlayout — gleiche Struktur wie MarketplaceDeckStack */
|
||||||
|
.card-inner {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
padding: 1rem 1rem 1.125rem 1.375rem;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Basis-Karte ─────────────────────────────────────────────────── */
|
.card-corner {
|
||||||
.card {
|
position: absolute;
|
||||||
border-radius: 0.875rem;
|
top: 0.875rem;
|
||||||
border: 1px solid hsl(var(--color-border));
|
right: 0.875rem;
|
||||||
background: hsl(var(--color-card));
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Profil-Karte ────────────────────────────────────────────────── */
|
|
||||||
.profile-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1.25rem;
|
|
||||||
border-left: 4px solid hsl(var(--color-primary));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
flex-shrink: 0;
|
width: 2.5rem;
|
||||||
width: 3.5rem;
|
height: 2.5rem;
|
||||||
height: 3.5rem;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: hsl(var(--color-primary) / 0.15);
|
background: hsl(var(--color-primary) / 0.15);
|
||||||
color: hsl(var(--color-primary));
|
color: hsl(var(--color-primary));
|
||||||
font-size: 1.25rem;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -192,196 +264,121 @@
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-info {
|
.card-icon {
|
||||||
flex: 1;
|
font-size: 1.5rem;
|
||||||
min-width: 0;
|
line-height: 1;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-name {
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.625rem;
|
||||||
|
padding: 2.75rem 0.5rem 0 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.0625rem;
|
font-size: 1.0625rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
color: hsl(var(--color-foreground));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-email {
|
.card-sub {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.75rem;
|
||||||
color: hsl(var(--color-muted-foreground));
|
color: hsl(var(--color-muted-foreground));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 5;
|
||||||
|
line-clamp: 5;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.tier-badge {
|
.tier-badge {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
margin-top: 0.25rem;
|
padding: 0.125rem 0.5rem;
|
||||||
padding: 0.125rem 0.625rem;
|
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background: hsl(var(--color-primary) / 0.1);
|
background: hsl(var(--color-primary) / 0.1);
|
||||||
color: hsl(var(--color-primary));
|
color: hsl(var(--color-primary));
|
||||||
font-size: 0.6875rem;
|
font-size: 0.625rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.05em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn {
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Meta-Grid ───────────────────────────────────────────────────── */
|
|
||||||
.meta-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-card {
|
|
||||||
padding: 1.125rem 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-label {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: hsl(var(--color-muted-foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-value {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: hsl(var(--color-foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-full {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
color: hsl(var(--color-muted-foreground));
|
|
||||||
word-break: break-all;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-card:hover .meta-full {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-card:hover .meta-value:has(+ .meta-full) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Action-Karten ───────────────────────────────────────────────── */
|
|
||||||
.action-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger-card {
|
|
||||||
border-color: hsl(var(--color-error) / 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-header {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 1;
|
|
||||||
margin-top: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-title {
|
|
||||||
margin: 0 0 0.375rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: hsl(var(--color-foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger-title {
|
.danger-title {
|
||||||
color: hsl(var(--color-error));
|
color: hsl(var(--color-error));
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-desc {
|
.card-meta {
|
||||||
margin: 0;
|
flex: 0 0 auto;
|
||||||
font-size: 0.8125rem;
|
padding-top: 0.5rem;
|
||||||
line-height: 1.55;
|
|
||||||
color: hsl(var(--color-muted-foreground));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Buttons ─────────────────────────────────────────────────────── */
|
/* Buttons */
|
||||||
.btn-outline {
|
.btn-ghost {
|
||||||
padding: 0.4375rem 0.875rem;
|
padding: 0.375rem 0.75rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.4375rem;
|
||||||
border: 1px solid hsl(var(--color-border));
|
border: 1px solid hsl(var(--color-border));
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: hsl(var(--color-foreground));
|
color: hsl(var(--color-foreground));
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s, border-color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline:hover {
|
|
||||||
background: hsl(var(--color-border) / 0.4);
|
|
||||||
border-color: hsl(var(--color-border));
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
align-self: flex-start;
|
|
||||||
padding: 0.5rem 1.25rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border: none;
|
|
||||||
background: hsl(var(--color-primary));
|
|
||||||
color: hsl(var(--color-primary-foreground));
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
opacity: 0.88;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
align-self: flex-start;
|
|
||||||
padding: 0.5rem 1.25rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border: 1px solid hsl(var(--color-error));
|
|
||||||
background: transparent;
|
|
||||||
color: hsl(var(--color-error));
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover:not(:disabled) {
|
.btn-ghost:hover {
|
||||||
background: hsl(var(--color-error) / 0.08);
|
background: hsl(var(--color-border) / 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:disabled {
|
.btn-primary {
|
||||||
opacity: 0.5;
|
padding: 0.4375rem 0.875rem;
|
||||||
cursor: not-allowed;
|
border-radius: 0.4375rem;
|
||||||
|
border: none;
|
||||||
|
background: hsl(var(--color-primary));
|
||||||
|
color: hsl(var(--color-primary-foreground));
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) { opacity: 0.88; }
|
||||||
|
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
padding: 0.4375rem 0.875rem;
|
||||||
|
border-radius: 0.4375rem;
|
||||||
|
border: 1px solid hsl(var(--color-error));
|
||||||
|
background: transparent;
|
||||||
|
color: hsl(var(--color-error));
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover:not(:disabled) { background: hsl(var(--color-error) / 0.08); }
|
||||||
|
.btn-danger:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import {
|
import {
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
extractClusterIds,
|
extractClusterIds,
|
||||||
maskRegionCount,
|
maskRegionCount,
|
||||||
renderClozePrompt,
|
renderClozePrompt,
|
||||||
|
|
@ -60,7 +61,7 @@
|
||||||
back = fields.back ?? '';
|
back = fields.back ?? '';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = (e as Error).message;
|
error = apiErrorMessage(e);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +99,7 @@
|
||||||
toasts.success(t('card_edit.updated'));
|
toasts.success(t('card_edit.updated'));
|
||||||
goto(`/decks/${updated.deck_id}`);
|
goto(`/decks/${updated.deck_id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(t('card_edit.save_failed', { msg: (e as Error).message }));
|
toasts.error(t('card_edit.save_failed', { msg: apiErrorMessage(e) }));
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +112,7 @@
|
||||||
toasts.success(t('card_edit.deleted'));
|
toasts.success(t('card_edit.deleted'));
|
||||||
goto(`/decks/${card.deck_id}`);
|
goto(`/decks/${card.deck_id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(t('card_edit.delete_failed', { msg: (e as Error).message }));
|
toasts.error(t('card_edit.delete_failed', { msg: apiErrorMessage(e) }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import {
|
import {
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
extractClusterIds,
|
extractClusterIds,
|
||||||
maskRegionCount,
|
maskRegionCount,
|
||||||
renderClozePrompt,
|
renderClozePrompt,
|
||||||
|
|
@ -82,7 +83,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(t('card_new.decks_load_failed', { msg: (e as Error).message }));
|
toasts.error(t('card_new.decks_load_failed', { msg: apiErrorMessage(e) }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -145,7 +146,7 @@
|
||||||
toasts.success(msg);
|
toasts.success(msg);
|
||||||
goto(`/decks/${card.deck_id}`);
|
goto(`/decks/${card.deck_id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(t('card_new.create_failed', { msg: (e as Error).message }));
|
toasts.error(t('card_new.create_failed', { msg: apiErrorMessage(e) }));
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
forkDeck,
|
forkDeck,
|
||||||
getMarketplaceDeck,
|
getMarketplaceDeck,
|
||||||
getMarketplaceVersion,
|
getMarketplaceVersion,
|
||||||
|
|
@ -80,7 +81,7 @@
|
||||||
cards = version;
|
cards = version;
|
||||||
discussionCounts = counts;
|
discussionCounts = counts;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(`Deck laden fehlgeschlagen: ${(e as Error).message}`);
|
toasts.error(`Deck laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +102,7 @@
|
||||||
starred = true;
|
starred = true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +125,7 @@
|
||||||
toasts.success('Abonniert. Update-Benachrichtigung an.');
|
toasts.success('Abonniert. Update-Benachrichtigung an.');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +142,7 @@
|
||||||
toasts.success(`Deck geforkt — ${result.cards_created} Karten kopiert.`);
|
toasts.success(`Deck geforkt — ${result.cards_created} Karten kopiert.`);
|
||||||
await goto(`/decks/${result.deck.id}`);
|
await goto(`/decks/${result.deck.id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
import PublishDeckModal from '$lib/components/marketplace/PublishDeckModal.svelte';
|
import PublishDeckModal from '$lib/components/marketplace/PublishDeckModal.svelte';
|
||||||
import { Image, CaretRight, DotsThree, PencilSimple, Trash } from '@mana/shared-icons';
|
import { Image, CaretRight, DotsThree, PencilSimple, Trash } from '@mana/shared-icons';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
function md(text: string): string {
|
function md(text: string): string {
|
||||||
return marked.parse(text, { async: false }) as string;
|
return marked.parse(text, { async: false }) as string;
|
||||||
|
|
@ -62,7 +63,7 @@
|
||||||
dueCount = due.total;
|
dueCount = due.total;
|
||||||
error = null;
|
error = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = (e as Error).message;
|
error = apiErrorMessage(e);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +76,7 @@
|
||||||
deck = await updateDeck(deck.id, { category: next ?? undefined });
|
deck = await updateDeck(deck.id, { category: next ?? undefined });
|
||||||
categoryOpen = false;
|
categoryOpen = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +87,7 @@
|
||||||
toasts.success(t('card_edit.deleted'));
|
toasts.success(t('card_edit.deleted'));
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(t('card_edit.delete_failed', { msg: (e as Error).message }));
|
toasts.error(t('card_edit.delete_failed', { msg: apiErrorMessage(e) }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
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 } from '$lib/i18n/index.svelte.ts';
|
import { t } from '$lib/i18n/index.svelte.ts';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
const deckId = $derived(page.params.id ?? '');
|
const deckId = $derived(page.params.id ?? '');
|
||||||
|
|
||||||
|
|
@ -23,7 +24,7 @@
|
||||||
description = deck.description ?? '';
|
description = deck.description ?? '';
|
||||||
color = deck.color ?? '#6366f1';
|
color = deck.color ?? '#6366f1';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
goto(`/decks/${deckId}`);
|
goto(`/decks/${deckId}`);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
@ -45,7 +46,7 @@
|
||||||
toasts.success(t('deck_edit.saved'));
|
toasts.success(t('deck_edit.saved'));
|
||||||
goto(`/decks/${deckId}`);
|
goto(`/decks/${deckId}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(t('deck_edit.save_failed', { msg: (e as Error).message }));
|
toasts.error(t('deck_edit.save_failed', { msg: apiErrorMessage(e) }));
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||||
import { i18n, t } from '$lib/i18n/index.svelte.ts';
|
import { i18n, t } from '$lib/i18n/index.svelte.ts';
|
||||||
import DeckCategoryIcon from '$lib/components/DeckCategoryIcon.svelte';
|
import DeckCategoryIcon from '$lib/components/DeckCategoryIcon.svelte';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
let name = $state('');
|
let name = $state('');
|
||||||
let description = $state('');
|
let description = $state('');
|
||||||
|
|
@ -68,7 +69,7 @@
|
||||||
toasts.success(`🖼 "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
toasts.success(`🖼 "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
||||||
goto(`/decks/${result.deck.id}`);
|
goto(`/decks/${result.deck.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
imageError = (err as Error).message;
|
imageError = apiErrorMessage(err);
|
||||||
imageGenerating = false;
|
imageGenerating = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +96,7 @@
|
||||||
toasts.success(`${deck.name} ✓`);
|
toasts.success(`${deck.name} ✓`);
|
||||||
goto(`/decks/${deck.id}`);
|
goto(`/decks/${deck.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toasts.error(t('deck_new.create_failed', { msg: (err as Error).message }));
|
toasts.error(t('deck_new.create_failed', { msg: apiErrorMessage(err) }));
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +110,7 @@
|
||||||
toasts.success(`✨ "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
toasts.success(`✨ "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
||||||
goto(`/decks/${result.deck.id}`);
|
goto(`/decks/${result.deck.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
aiError = (err as Error).message;
|
aiError = apiErrorMessage(err);
|
||||||
generating = false;
|
generating = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
browseDecks,
|
browseDecks,
|
||||||
getExplore,
|
getExplore,
|
||||||
type DeckListEntry,
|
type DeckListEntry,
|
||||||
|
|
@ -31,7 +32,7 @@
|
||||||
featured = explore.featured;
|
featured = explore.featured;
|
||||||
trending = explore.trending;
|
trending = explore.trending;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(`Explore-Liste laden fehlgeschlagen: ${(e as Error).message}`);
|
toasts.error(`Explore-Liste laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||||
} finally {
|
} finally {
|
||||||
loadingExplore = false;
|
loadingExplore = false;
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +60,7 @@
|
||||||
}
|
}
|
||||||
browseTotal = result.total;
|
browseTotal = result.total;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(`Browse fehlgeschlagen: ${(e as Error).message}`);
|
toasts.error(`Browse fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||||
} finally {
|
} finally {
|
||||||
loadingBrowse = false;
|
loadingBrowse = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
import EmptyState from '$lib/components/marketplace/EmptyState.svelte';
|
import EmptyState from '$lib/components/marketplace/EmptyState.svelte';
|
||||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||||
import { GitFork } from '@mana/shared-icons';
|
import { GitFork } from '@mana/shared-icons';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
let forks = $state<Deck[]>([]);
|
let forks = $state<Deck[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|
@ -29,7 +30,7 @@
|
||||||
const result = await listDecks({ forkedFromMarketplace: true });
|
const result = await listDecks({ forkedFromMarketplace: true });
|
||||||
forks = result.decks;
|
forks = result.decks;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(`Forks laden fehlgeschlagen: ${(e as Error).message}`);
|
toasts.error(`Forks laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +49,7 @@
|
||||||
await refresh();
|
await refresh();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
} finally {
|
} finally {
|
||||||
busyId = null;
|
busyId = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import {
|
import {
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
browseDecks,
|
browseDecks,
|
||||||
getMyAuthorProfile,
|
getMyAuthorProfile,
|
||||||
upsertMyAuthorProfile,
|
upsertMyAuthorProfile,
|
||||||
|
|
@ -42,7 +43,7 @@
|
||||||
decks = result.items;
|
decks = result.items;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(`Profil laden fehlgeschlagen: ${(e as Error).message}`);
|
toasts.error(`Profil laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +86,7 @@
|
||||||
decks = result.items;
|
decks = result.items;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import EmptyState from '$lib/components/marketplace/EmptyState.svelte';
|
import EmptyState from '$lib/components/marketplace/EmptyState.svelte';
|
||||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||||
import { ArrowCounterClockwise } from '@mana/shared-icons';
|
import { ArrowCounterClockwise } from '@mana/shared-icons';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
let items = $state<SubscriptionEntry[]>([]);
|
let items = $state<SubscriptionEntry[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|
@ -21,7 +22,7 @@
|
||||||
const result = await getMySubscriptions();
|
const result = await getMySubscriptions();
|
||||||
items = result.subscriptions;
|
items = result.subscriptions;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(`Subs laden fehlgeschlagen: ${(e as Error).message}`);
|
toasts.error(`Subs laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||||
import { loadStats, type UserStats } from '$lib/api/me.ts';
|
import { loadStats, type UserStats } from '$lib/api/me.ts';
|
||||||
import { i18n, t, tn } from '$lib/i18n/index.svelte.ts';
|
import { i18n, t, tn } from '$lib/i18n/index.svelte.ts';
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
|
|
||||||
let stats = $state<UserStats | null>(null);
|
let stats = $state<UserStats | null>(null);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|
@ -27,7 +28,7 @@
|
||||||
try {
|
try {
|
||||||
stats = await loadStats();
|
stats = await loadStats();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = (e as Error).message;
|
error = apiErrorMessage(e);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import {
|
import {
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
clusterIdForSubIndex,
|
clusterIdForSubIndex,
|
||||||
maskForSubIndex,
|
maskForSubIndex,
|
||||||
renderClozePrompt,
|
renderClozePrompt,
|
||||||
|
|
@ -143,7 +144,7 @@
|
||||||
deckColor = d.color ?? null;
|
deckColor = d.color ?? null;
|
||||||
queue = due.reviews;
|
queue = due.reviews;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(`Sitzung konnte nicht geladen werden: ${(e as Error).message}`);
|
toasts.error(`Sitzung konnte nicht geladen werden: ${apiErrorMessage(e)}`);
|
||||||
goto('/study');
|
goto('/study');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +190,7 @@
|
||||||
queueIndex += 1;
|
queueIndex += 1;
|
||||||
revealed = false;
|
revealed = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(t('card_edit.save_failed', { msg: (e as Error).message }));
|
toasts.error(t('card_edit.save_failed', { msg: apiErrorMessage(e) }));
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||||
browseDecks,
|
browseDecks,
|
||||||
followAuthor,
|
followAuthor,
|
||||||
getAuthor,
|
getAuthor,
|
||||||
|
|
@ -44,7 +45,7 @@
|
||||||
decks = d.items;
|
decks = d.items;
|
||||||
following = follow.following;
|
following = follow.following;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error(`Author laden fehlgeschlagen: ${(e as Error).message}`);
|
toasts.error(`Author laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +66,7 @@
|
||||||
following = true;
|
following = true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.error((e as Error).message);
|
toasts.error(apiErrorMessage(e));
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue