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:
parent
b182bac2fb
commit
f3a148171a
1 changed files with 303 additions and 55 deletions
|
|
@ -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 devUser.user.name}
|
|
||||||
<div>
|
|
||||||
<div class="text-[hsl(var(--color-muted-foreground))]">Name</div>
|
|
||||||
<div class="mt-1">{devUser.user.name}</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
<p class="profile-email">{devUser.user?.email ?? '—'}</p>
|
||||||
<div class="text-[hsl(var(--color-muted-foreground))]">Tier</div>
|
{#if devUser.user?.tier}
|
||||||
<div class="mt-1">
|
<span class="tier-badge">{devUser.user.tier}</span>
|
||||||
<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}
|
{/if}
|
||||||
<div class="mt-3 flex gap-3">
|
</div>
|
||||||
<button
|
<button type="button" class="btn-outline logout-btn" onclick={logout}>
|
||||||
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')}
|
{t('account.logout')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue