refactor(account): Profil-Karte, Meta-Grid, Action-Karten

- Avatar mit Initialen, Name, E-Mail, Tier-Badge, Logout-Button
- Meta-Grid: User-ID (gekürzt, full on hover) + Tier
- Export- und Danger-Zone als saubere Action-Karten mit Icon
- Einheitliche Btn-Styles (outline, primary, danger)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-10 16:18:57 +02:00
parent b182bac2fb
commit f3a148171a

View file

@ -13,6 +13,18 @@
if (!devUser.id) goto('/'); if (!devUser.id) goto('/');
}); });
const initials = $derived.by(() => {
const name = devUser.user?.name ?? devUser.user?.email ?? '?';
return name
.split(/[\s@.]+/)
.filter(Boolean)
.slice(0, 2)
.map((w) => w[0].toUpperCase())
.join('');
});
const shortId = $derived(devUser.id ? devUser.id.slice(0, 8) + '…' : '—');
async function onExport() { async function onExport() {
exporting = true; exporting = true;
try { try {
@ -31,7 +43,7 @@
decks: data.data.decks.length, decks: data.data.decks.length,
cards: data.data.cards.length, cards: data.data.cards.length,
reviews: data.data.reviews.length, reviews: data.data.reviews.length,
}) }),
); );
} catch (e) { } catch (e) {
toasts.error(t('account.export_failed', { msg: (e as Error).message })); toasts.error(t('account.export_failed', { msg: (e as Error).message }));
@ -46,7 +58,9 @@
deleting = true; deleting = true;
try { try {
const r = await deleteMe(); const r = await deleteMe();
toasts.success(t('account.delete_done', { decks: r.counts.decks, imports: r.counts.import_jobs })); toasts.success(
t('account.delete_done', { decks: r.counts.decks, imports: r.counts.import_jobs }),
);
devUser.clear(); devUser.clear();
goto('/'); goto('/');
} catch (e) { } catch (e) {
@ -65,75 +79,309 @@
<title>{t('account.title')} · {t('app.title_suffix')}</title> <title>{t('account.title')} · {t('app.title_suffix')}</title>
</svelte:head> </svelte:head>
<div class="mx-auto max-w-2xl px-4 py-8"> <div class="page">
<h1 class="text-2xl font-semibold">{t('account.title')}</h1>
<section class="mt-6 rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4"> <!-- Profil-Karte -->
{#if devUser.user} <div class="card profile-card">
<div class="space-y-2 text-sm"> <div class="avatar" aria-hidden="true">{initials}</div>
<div> <div class="profile-info">
<div class="text-[hsl(var(--color-muted-foreground))]">E-Mail</div> {#if devUser.user?.name}
<div class="mt-1 font-medium">{devUser.user.email}</div> <p class="profile-name">{devUser.user.name}</p>
</div> {/if}
{#if devUser.user.name} <p class="profile-email">{devUser.user?.email ?? '—'}</p>
<div> {#if devUser.user?.tier}
<div class="text-[hsl(var(--color-muted-foreground))]">Name</div> <span class="tier-badge">{devUser.user.tier}</span>
<div class="mt-1">{devUser.user.name}</div> {/if}
</div>
{/if}
<div>
<div class="text-[hsl(var(--color-muted-foreground))]">Tier</div>
<div class="mt-1">
<span
class="inline-flex rounded bg-[hsl(var(--color-border))]/40 px-2 py-0.5 text-xs"
>{devUser.user.tier}</span>
</div>
</div>
<div>
<div class="text-[hsl(var(--color-muted-foreground))]">{t('account.user_id_label')}</div>
<code class="mt-1 block break-all text-xs text-[hsl(var(--color-muted-foreground))]">{devUser.user.id}</code>
</div>
</div>
{:else}
<div class="text-sm">
<div class="text-[hsl(var(--color-muted-foreground))]">{t('account.user_id_label')} (Stub)</div>
<code class="mt-1 block break-all text-sm">{devUser.id ?? '—'}</code>
</div>
{/if}
<div class="mt-3 flex gap-3">
<button
type="button"
onclick={logout}
class="rounded border border-[hsl(var(--color-border))] px-3 py-1.5 text-sm hover:bg-[hsl(var(--color-border))]/40"
>
{t('account.logout')}
</button>
</div> </div>
</section> <button type="button" class="btn-outline logout-btn" onclick={logout}>
{t('account.logout')}
</button>
</div>
<section class="mt-6 rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4"> <!-- Meta-Grid -->
<h2 class="text-lg font-medium">{t('account.export_title')}</h2> <div class="meta-grid">
<p class="mt-1 text-sm text-[hsl(var(--color-muted-foreground))]">{t('account.export_intro')}</p> <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>
<!-- 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>
</div>
</div>
<button <button
type="button" type="button"
class="btn-primary"
onclick={onExport} onclick={onExport}
disabled={exporting} disabled={exporting}
class="mt-3 rounded bg-[hsl(var(--color-primary))] px-4 py-1.5 text-sm text-[hsl(var(--color-primary-foreground))] disabled:opacity-50"
> >
{exporting ? t('account.export_loading') : t('account.export_button')} {exporting ? t('account.export_loading') : t('account.export_button')}
</button> </button>
</section> </div>
<section class="mt-6 rounded-lg border border-[hsl(var(--color-error))]/30 bg-[hsl(var(--color-card))] p-4"> <!-- Danger-Karte -->
<h2 class="text-lg font-medium text-[hsl(var(--color-error))]">{t('account.delete_title')}</h2> <div class="card danger-card">
<p class="mt-1 text-sm text-[hsl(var(--color-muted-foreground))]">{t('account.delete_intro')}</p> <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>
</div>
</div>
<button <button
type="button" type="button"
class="btn-danger"
onclick={onDelete} onclick={onDelete}
disabled={deleting} disabled={deleting}
class="mt-3 rounded border border-[hsl(var(--color-error))] px-4 py-1.5 text-sm text-[hsl(var(--color-error))] hover:bg-[hsl(var(--color-error))]/10 disabled:opacity-50"
> >
{deleting ? t('account.delete_loading') : t('account.delete_button')} {deleting ? t('account.delete_loading') : t('account.delete_button')}
</button> </button>
</section> </div>
</div> </div>
<style>
.page {
max-width: 36rem;
margin: 0 auto;
padding: 2rem 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
/* ── 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));
}
.avatar {
flex-shrink: 0;
width: 3.5rem;
height: 3.5rem;
border-radius: 50%;
background: hsl(var(--color-primary) / 0.15);
color: hsl(var(--color-primary));
font-size: 1.25rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: -0.02em;
}
.profile-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.profile-name {
margin: 0;
font-size: 1.0625rem;
font-weight: 600;
line-height: 1.3;
color: hsl(var(--color-foreground));
}
.profile-email {
margin: 0;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tier-badge {
align-self: flex-start;
margin-top: 0.25rem;
padding: 0.125rem 0.625rem;
border-radius: 9999px;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.04em;
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));
}
/* ── Buttons ─────────────────────────────────────────────────────── */
.btn-outline {
padding: 0.4375rem 0.875rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--color-border));
background: transparent;
font-size: 0.8125rem;
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-danger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>