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 { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
import { t } from '$lib/i18n/index.svelte.ts';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
let {
|
||||
imageRef = $bindable(''),
|
||||
|
|
@ -44,7 +45,7 @@
|
|||
imageRef = r.id;
|
||||
maskRegionsJson = '[]';
|
||||
} catch (err) {
|
||||
toasts.error(`${(err as Error).message}`);
|
||||
toasts.error(`${apiErrorMessage(err)}`);
|
||||
} finally {
|
||||
uploading = false;
|
||||
input.value = '';
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
let options = $state<string[]>([]);
|
||||
let selected = $state<string | null>(null);
|
||||
let loading = $state(true);
|
||||
let tooFewOptions = $state(false);
|
||||
|
||||
const correct = $derived(selected === answer);
|
||||
|
||||
|
|
@ -51,6 +52,13 @@
|
|||
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)]);
|
||||
loading = false;
|
||||
});
|
||||
|
|
@ -100,6 +108,15 @@
|
|||
|
||||
{#if loading}
|
||||
<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}
|
||||
<div class="options" role="group" aria-label="Antwortmöglichkeiten">
|
||||
{#each options as opt, i (opt)}
|
||||
|
|
@ -168,6 +185,47 @@
|
|||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { i18n, t } from '$lib/i18n/index.svelte.ts';
|
||||
import CardSurface from './CardSurface.svelte';
|
||||
import DeckCategoryIcon from './DeckCategoryIcon.svelte';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
let open = $state(false);
|
||||
let catOpen = $state(false);
|
||||
|
|
@ -95,7 +96,7 @@
|
|||
toasts.success(`🖼 "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
||||
goto(`/decks/${result.deck.id}`);
|
||||
} catch (err) {
|
||||
imageError = (err as Error).message;
|
||||
imageError = apiErrorMessage(err);
|
||||
imageGenerating = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -119,7 +120,7 @@
|
|||
toasts.success(`${deck.name} ✓`);
|
||||
goto(`/decks/${deck.id}`);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +134,7 @@
|
|||
toasts.success(`✨ "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
||||
goto(`/decks/${result.deck.id}`);
|
||||
} catch (err) {
|
||||
aiError = (err as Error).message;
|
||||
aiError = apiErrorMessage(err);
|
||||
generating = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
|
||||
import {
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
hideDiscussion,
|
||||
listDiscussions,
|
||||
postDiscussion,
|
||||
|
|
@ -37,7 +38,7 @@
|
|||
const result = await listDiscussions(cardContentHash);
|
||||
comments = result.discussions;
|
||||
} catch (e) {
|
||||
toasts.error(`Discussions laden fehlgeschlagen: ${(e as Error).message}`);
|
||||
toasts.error(`Discussions laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -53,7 +54,7 @@
|
|||
replyToId = null;
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
posting = false;
|
||||
}
|
||||
|
|
@ -65,7 +66,7 @@
|
|||
await hideDiscussion(id);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import type { Card, Deck } from '@cards/domain';
|
||||
import {
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
getMyAuthorProfile,
|
||||
getMarketplaceDeck,
|
||||
initMarketplaceDeck,
|
||||
|
|
@ -125,7 +126,7 @@
|
|||
);
|
||||
onPublished(slug.trim());
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
error = apiErrorMessage(e);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { publishMarketplaceVersion } from '$lib/api/marketplace.ts';
|
||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
|
|
@ -45,7 +46,7 @@
|
|||
}
|
||||
cards = parsed;
|
||||
} catch (e) {
|
||||
error = `JSON-Parse-Fehler: ${(e as Error).message}`;
|
||||
error = `JSON-Parse-Fehler: ${apiErrorMessage(e)}`;
|
||||
busy = false;
|
||||
return;
|
||||
}
|
||||
|
|
@ -59,7 +60,7 @@
|
|||
toasts.success(`Version ${result.version.semver} veröffentlicht (${result.version.card_count} Karten)`);
|
||||
onPublished?.();
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
error = apiErrorMessage(e);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
|
||||
import {
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
closePullRequest,
|
||||
listPullRequests,
|
||||
mergePullRequest,
|
||||
|
|
@ -37,7 +38,7 @@
|
|||
const result = await listPullRequests(slug, statusFilter);
|
||||
prs = result.pull_requests;
|
||||
} catch (e) {
|
||||
toasts.error(`PRs laden fehlgeschlagen: ${(e as Error).message}`);
|
||||
toasts.error(`PRs laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -54,7 +55,7 @@
|
|||
toasts.success('PR gemerged — neue Version live.');
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
busyId = null;
|
||||
}
|
||||
|
|
@ -67,7 +68,7 @@
|
|||
await rejectPullRequest(id);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
busyId = null;
|
||||
}
|
||||
|
|
@ -79,7 +80,7 @@
|
|||
await closePullRequest(id);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
busyId = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { createPullRequest } from '$lib/api/marketplace.ts';
|
||||
import type { MarketplaceVersionCard } from '$lib/api/marketplace.ts';
|
||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
|
|
@ -59,7 +60,7 @@
|
|||
toasts.success('Pull-Request eingereicht — Author bekommt Bescheid.');
|
||||
onSubmitted?.();
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
error = apiErrorMessage(e);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||
import { t } from '$lib/i18n/index.svelte.ts';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
|
|
@ -22,7 +23,7 @@
|
|||
await devUser.login(email.trim(), password);
|
||||
goto('/decks');
|
||||
} catch (err) {
|
||||
error = (err as Error).message;
|
||||
error = apiErrorMessage(err);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
import { exportMe, deleteMe } from '$lib/api/me.ts';
|
||||
import { toasts } from '$lib/stores/toasts.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 deleting = $state(false);
|
||||
|
|
@ -23,7 +26,9 @@
|
|||
.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() {
|
||||
exporting = true;
|
||||
|
|
@ -46,7 +51,7 @@
|
|||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
toasts.error(t('account.export_failed', { msg: (e as Error).message }));
|
||||
toasts.error(t('account.export_failed', { msg: apiErrorMessage(e) }));
|
||||
} finally {
|
||||
exporting = false;
|
||||
}
|
||||
|
|
@ -64,7 +69,7 @@
|
|||
devUser.clear();
|
||||
goto('/');
|
||||
} catch (e) {
|
||||
toasts.error(t('account.delete_failed', { msg: (e as Error).message }));
|
||||
toasts.error(t('account.delete_failed', { msg: apiErrorMessage(e) }));
|
||||
deleting = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -79,112 +84,179 @@
|
|||
<title>{t('account.title')} · {t('app.title_suffix')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="page">
|
||||
<h1 class="page-title">{t('account.title')}</h1>
|
||||
|
||||
<ul class="card-row" aria-label="Account-Karten">
|
||||
|
||||
<!-- Profil-Karte -->
|
||||
<div class="card profile-card">
|
||||
<div class="avatar" aria-hidden="true">{initials}</div>
|
||||
<div class="profile-info">
|
||||
{#if devUser.user?.name}
|
||||
<p class="profile-name">{devUser.user.name}</p>
|
||||
{/if}
|
||||
<p class="profile-email">{devUser.user?.email ?? '—'}</p>
|
||||
{#if devUser.user?.tier}
|
||||
<span class="tier-badge">{devUser.user.tier}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button type="button" class="btn-outline logout-btn" onclick={logout}>
|
||||
{t('account.logout')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Meta-Grid -->
|
||||
<div class="meta-grid">
|
||||
<div class="card meta-card">
|
||||
<p class="meta-label">{t('account.user_id_label')}</p>
|
||||
<code class="meta-value">{shortId}</code>
|
||||
<code class="meta-full">{devUser.id ?? '—'}</code>
|
||||
</div>
|
||||
<div class="card meta-card">
|
||||
<p class="meta-label">Tier</p>
|
||||
<p class="meta-value">{devUser.user?.tier ?? '—'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<li class="stack-wrap">
|
||||
{#each profileLayers as layer, i (i)}
|
||||
<div class="layer" style:transform="translate({layer.dx}px,{layer.dy}px) rotate({layer.tilt}deg)" aria-hidden="true"></div>
|
||||
{/each}
|
||||
<CardSurface size="md" colorAccent="hsl(var(--color-primary))">
|
||||
<div class="card-inner">
|
||||
<div class="card-corner">
|
||||
<div class="avatar" aria-hidden="true">{initials}</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{#if devUser.user?.name}
|
||||
<p class="card-title">{devUser.user.name}</p>
|
||||
{/if}
|
||||
<p class="card-sub">{devUser.user?.email ?? '—'}</p>
|
||||
{#if devUser.user?.tier}
|
||||
<span class="tier-badge">{devUser.user.tier}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="card-meta">
|
||||
<button type="button" class="btn-ghost" onclick={logout}>
|
||||
{t('account.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardSurface>
|
||||
</li>
|
||||
|
||||
<!-- Export-Karte -->
|
||||
<div class="card action-card">
|
||||
<div class="action-header">
|
||||
<div class="action-icon">📦</div>
|
||||
<div>
|
||||
<h2 class="action-title">{t('account.export_title')}</h2>
|
||||
<p class="action-desc">{t('account.export_intro')}</p>
|
||||
<li class="stack-wrap">
|
||||
{#each exportLayers as layer, i (i)}
|
||||
<div class="layer" style:transform="translate({layer.dx}px,{layer.dy}px) rotate({layer.tilt}deg)" aria-hidden="true"></div>
|
||||
{/each}
|
||||
<CardSurface size="md" colorAccent="#22C55E">
|
||||
<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>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
onclick={onExport}
|
||||
disabled={exporting}
|
||||
>
|
||||
{exporting ? t('account.export_loading') : t('account.export_button')}
|
||||
</button>
|
||||
</div>
|
||||
</CardSurface>
|
||||
</li>
|
||||
|
||||
<!-- Danger-Karte -->
|
||||
<div class="card danger-card">
|
||||
<div class="action-header">
|
||||
<div class="action-icon danger-icon">⚠️</div>
|
||||
<div>
|
||||
<h2 class="action-title danger-title">{t('account.delete_title')}</h2>
|
||||
<p class="action-desc">{t('account.delete_intro')}</p>
|
||||
<li class="stack-wrap">
|
||||
{#each dangerLayers as layer, i (i)}
|
||||
<div class="layer layer-danger" style:transform="translate({layer.dx}px,{layer.dy}px) rotate({layer.tilt}deg)" aria-hidden="true"></div>
|
||||
{/each}
|
||||
<CardSurface size="md" colorAccent="hsl(var(--color-error))">
|
||||
<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>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
onclick={onDelete}
|
||||
disabled={deleting}
|
||||
>
|
||||
{deleting ? t('account.delete_loading') : t('account.delete_button')}
|
||||
</button>
|
||||
</div>
|
||||
</CardSurface>
|
||||
</li>
|
||||
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
max-width: 36rem;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
.page-title {
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 1.75rem;
|
||||
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;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1rem 1.125rem 1.375rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Basis-Karte ─────────────────────────────────────────────────── */
|
||||
.card {
|
||||
border-radius: 0.875rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
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));
|
||||
.card-corner {
|
||||
position: absolute;
|
||||
top: 0.875rem;
|
||||
right: 0.875rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
background: hsl(var(--color-primary) / 0.15);
|
||||
color: hsl(var(--color-primary));
|
||||
font-size: 1.25rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -192,196 +264,121 @@
|
|||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
.card-icon {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.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;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.profile-email {
|
||||
.card-sub {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
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 {
|
||||
align-self: flex-start;
|
||||
margin-top: 0.25rem;
|
||||
padding: 0.125rem 0.625rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
background: hsl(var(--color-primary) / 0.1);
|
||||
color: hsl(var(--color-primary));
|
||||
font-size: 0.6875rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
letter-spacing: 0.05em;
|
||||
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 {
|
||||
color: hsl(var(--color-error));
|
||||
}
|
||||
|
||||
.action-desc {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.55;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
.card-meta {
|
||||
flex: 0 0 auto;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────────── */
|
||||
.btn-outline {
|
||||
padding: 0.4375rem 0.875rem;
|
||||
border-radius: 0.5rem;
|
||||
/* Buttons */
|
||||
.btn-ghost {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.4375rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: transparent;
|
||||
font-size: 0.8125rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-foreground));
|
||||
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;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: hsl(var(--color-error) / 0.08);
|
||||
.btn-ghost:hover {
|
||||
background: hsl(var(--color-border) / 0.4);
|
||||
}
|
||||
|
||||
.btn-danger:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
.btn-primary {
|
||||
padding: 0.4375rem 0.875rem;
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import {
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
extractClusterIds,
|
||||
maskRegionCount,
|
||||
renderClozePrompt,
|
||||
|
|
@ -60,7 +61,7 @@
|
|||
back = fields.back ?? '';
|
||||
}
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
error = apiErrorMessage(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -98,7 +99,7 @@
|
|||
toasts.success(t('card_edit.updated'));
|
||||
goto(`/decks/${updated.deck_id}`);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -111,7 +112,7 @@
|
|||
toasts.success(t('card_edit.deleted'));
|
||||
goto(`/decks/${card.deck_id}`);
|
||||
} 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>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import {
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
extractClusterIds,
|
||||
maskRegionCount,
|
||||
renderClozePrompt,
|
||||
|
|
@ -82,7 +83,7 @@
|
|||
}
|
||||
}
|
||||
} 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);
|
||||
goto(`/decks/${card.deck_id}`);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
|
||||
import {
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
forkDeck,
|
||||
getMarketplaceDeck,
|
||||
getMarketplaceVersion,
|
||||
|
|
@ -80,7 +81,7 @@
|
|||
cards = version;
|
||||
discussionCounts = counts;
|
||||
} catch (e) {
|
||||
toasts.error(`Deck laden fehlgeschlagen: ${(e as Error).message}`);
|
||||
toasts.error(`Deck laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -101,7 +102,7 @@
|
|||
starred = true;
|
||||
}
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
|
|
@ -124,7 +125,7 @@
|
|||
toasts.success('Abonniert. Update-Benachrichtigung an.');
|
||||
}
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
|
|
@ -141,7 +142,7 @@
|
|||
toasts.success(`Deck geforkt — ${result.cards_created} Karten kopiert.`);
|
||||
await goto(`/decks/${result.deck.id}`);
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
import PublishDeckModal from '$lib/components/marketplace/PublishDeckModal.svelte';
|
||||
import { Image, CaretRight, DotsThree, PencilSimple, Trash } from '@mana/shared-icons';
|
||||
import { marked } from 'marked';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
function md(text: string): string {
|
||||
return marked.parse(text, { async: false }) as string;
|
||||
|
|
@ -62,7 +63,7 @@
|
|||
dueCount = due.total;
|
||||
error = null;
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
error = apiErrorMessage(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -75,7 +76,7 @@
|
|||
deck = await updateDeck(deck.id, { category: next ?? undefined });
|
||||
categoryOpen = false;
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +87,7 @@
|
|||
toasts.success(t('card_edit.deleted'));
|
||||
await refresh();
|
||||
} 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 { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
import { t } from '$lib/i18n/index.svelte.ts';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
const deckId = $derived(page.params.id ?? '');
|
||||
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
description = deck.description ?? '';
|
||||
color = deck.color ?? '#6366f1';
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
goto(`/decks/${deckId}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
|
@ -45,7 +46,7 @@
|
|||
toasts.success(t('deck_edit.saved'));
|
||||
goto(`/decks/${deckId}`);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
import { i18n, t } from '$lib/i18n/index.svelte.ts';
|
||||
import DeckCategoryIcon from '$lib/components/DeckCategoryIcon.svelte';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
let name = $state('');
|
||||
let description = $state('');
|
||||
|
|
@ -68,7 +69,7 @@
|
|||
toasts.success(`🖼 "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
||||
goto(`/decks/${result.deck.id}`);
|
||||
} catch (err) {
|
||||
imageError = (err as Error).message;
|
||||
imageError = apiErrorMessage(err);
|
||||
imageGenerating = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +96,7 @@
|
|||
toasts.success(`${deck.name} ✓`);
|
||||
goto(`/decks/${deck.id}`);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +110,7 @@
|
|||
toasts.success(`✨ "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
|
||||
goto(`/decks/${result.deck.id}`);
|
||||
} catch (err) {
|
||||
aiError = (err as Error).message;
|
||||
aiError = apiErrorMessage(err);
|
||||
generating = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
|
||||
import {
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
browseDecks,
|
||||
getExplore,
|
||||
type DeckListEntry,
|
||||
|
|
@ -31,7 +32,7 @@
|
|||
featured = explore.featured;
|
||||
trending = explore.trending;
|
||||
} catch (e) {
|
||||
toasts.error(`Explore-Liste laden fehlgeschlagen: ${(e as Error).message}`);
|
||||
toasts.error(`Explore-Liste laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
loadingExplore = false;
|
||||
}
|
||||
|
|
@ -59,7 +60,7 @@
|
|||
}
|
||||
browseTotal = result.total;
|
||||
} catch (e) {
|
||||
toasts.error(`Browse fehlgeschlagen: ${(e as Error).message}`);
|
||||
toasts.error(`Browse fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
loadingBrowse = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import EmptyState from '$lib/components/marketplace/EmptyState.svelte';
|
||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
import { GitFork } from '@mana/shared-icons';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
let forks = $state<Deck[]>([]);
|
||||
let loading = $state(true);
|
||||
|
|
@ -29,7 +30,7 @@
|
|||
const result = await listDecks({ forkedFromMarketplace: true });
|
||||
forks = result.decks;
|
||||
} catch (e) {
|
||||
toasts.error(`Forks laden fehlgeschlagen: ${(e as Error).message}`);
|
||||
toasts.error(`Forks laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -48,7 +49,7 @@
|
|||
await refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
busyId = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
browseDecks,
|
||||
getMyAuthorProfile,
|
||||
upsertMyAuthorProfile,
|
||||
|
|
@ -42,7 +43,7 @@
|
|||
decks = result.items;
|
||||
}
|
||||
} catch (e) {
|
||||
toasts.error(`Profil laden fehlgeschlagen: ${(e as Error).message}`);
|
||||
toasts.error(`Profil laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -85,7 +86,7 @@
|
|||
decks = result.items;
|
||||
}
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import EmptyState from '$lib/components/marketplace/EmptyState.svelte';
|
||||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
import { ArrowCounterClockwise } from '@mana/shared-icons';
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
|
||||
let items = $state<SubscriptionEntry[]>([]);
|
||||
let loading = $state(true);
|
||||
|
|
@ -21,7 +22,7 @@
|
|||
const result = await getMySubscriptions();
|
||||
items = result.subscriptions;
|
||||
} catch (e) {
|
||||
toasts.error(`Subs laden fehlgeschlagen: ${(e as Error).message}`);
|
||||
toasts.error(`Subs laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||
import { loadStats, type UserStats } from '$lib/api/me.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 loading = $state(true);
|
||||
|
|
@ -27,7 +28,7 @@
|
|||
try {
|
||||
stats = await loadStats();
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
error = apiErrorMessage(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
clusterIdForSubIndex,
|
||||
maskForSubIndex,
|
||||
renderClozePrompt,
|
||||
|
|
@ -143,7 +144,7 @@
|
|||
deckColor = d.color ?? null;
|
||||
queue = due.reviews;
|
||||
} catch (e) {
|
||||
toasts.error(`Sitzung konnte nicht geladen werden: ${(e as Error).message}`);
|
||||
toasts.error(`Sitzung konnte nicht geladen werden: ${apiErrorMessage(e)}`);
|
||||
goto('/study');
|
||||
return;
|
||||
}
|
||||
|
|
@ -189,7 +190,7 @@
|
|||
queueIndex += 1;
|
||||
revealed = false;
|
||||
} 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 {
|
||||
busy = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { page } from '$app/state';
|
||||
|
||||
import {
|
||||
import { apiErrorMessage } from '$lib/api/error.ts';
|
||||
browseDecks,
|
||||
followAuthor,
|
||||
getAuthor,
|
||||
|
|
@ -44,7 +45,7 @@
|
|||
decks = d.items;
|
||||
following = follow.following;
|
||||
} catch (e) {
|
||||
toasts.error(`Author laden fehlgeschlagen: ${(e as Error).message}`);
|
||||
toasts.error(`Author laden fehlgeschlagen: ${apiErrorMessage(e)}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -65,7 +66,7 @@
|
|||
following = true;
|
||||
}
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
toasts.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue