feat(quiz): edit existing questions + wire up guest seed

- Pencil button on each question opens the bottom form pre-filled;
  submit updates in place instead of appending.
- Guest users now see the demo quiz on first visit (QUIZ_GUEST_SEED
  registered with seedAllGuestData).
- Silence state_referenced_locally warnings with svelte-ignore to
  match the pattern used in cards / landing / rsvp views.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-15 21:24:53 +02:00
parent 3b99356464
commit 988c17a678
13 changed files with 144 additions and 421 deletions

View file

@ -184,7 +184,7 @@ export function useAiTierItems() {
id: 'ai-settings',
label: 'KI-Einstellungen',
icon: 'settings',
onClick: () => goto('/settings#ai-options'),
onClick: () => goto('/'),
},
]);

View file

@ -84,7 +84,7 @@
</a>
<a
href="/settings"
href="/"
class="p-4 rounded-xl bg-card border hover:border-primary/50 hover:shadow-md transition-all group"
>
<div class="flex items-center gap-3">

View file

@ -1,7 +1,7 @@
<!--
SettingsSidebar — vertical category nav (lg+) and horizontal pill row
(mobile), with an inline search field that surfaces a quick result list.
Owns the search query; the parent owns the active category.
SettingsSidebar — horizontal category chip row with an inline search
field that surfaces a quick result list. Owns the search query; the
parent owns the active category.
-->
<script lang="ts">
import { MagnifyingGlass, X } from '@mana/shared-icons';
@ -17,7 +17,7 @@
activeCategory: CategoryId;
onSelect: (id: CategoryId) => void;
onJump: (entry: SearchEntry) => void;
/** Override the default categories list (e.g. to exclude profile in workbench). */
/** Override the default categories list. */
categories?: Category[];
}
@ -78,7 +78,6 @@
<div class="no-results">Keine Treffer für „{query}"</div>
{/if}
<!-- Mobile horizontal chips (hidden on lg+ via local media query) -->
<div class="chip-row" role="tablist">
{#each categories as cat (cat.id)}
{@const Icon = cat.icon}
@ -98,36 +97,6 @@
</button>
{/each}
</div>
<!-- Desktop vertical sidebar -->
<nav class="hidden lg:block">
<ul class="cat-list" role="tablist">
{#each categories as cat (cat.id)}
{@const Icon = cat.icon}
{@const isActive = activeCategory === cat.id}
{@const dim = query.length > 0 && !highlightedCategoryIds.has(cat.id)}
<li>
<button
type="button"
role="tab"
aria-selected={isActive}
onclick={() => onSelect(cat.id)}
class="cat-btn"
class:active={isActive}
class:dim
>
<span class="cat-icon" class:icon-active={isActive}>
<Icon size={18} />
</span>
<span class="cat-text">
<span class="cat-label">{cat.label}</span>
<span class="cat-desc">{cat.description}</span>
</span>
</button>
</li>
{/each}
</ul>
</nav>
</aside>
<style>
@ -136,14 +105,6 @@
flex-direction: column;
gap: 0.75rem;
}
@media (min-width: 1024px) {
.settings-sidebar {
position: sticky;
top: 6rem;
width: 17rem;
flex-shrink: 0;
}
}
/* ── Search field ───────────────────────────────────────────────── */
.search-wrapper {
@ -245,7 +206,7 @@
color: hsl(var(--color-muted-foreground));
}
/* ── Mobile chips ───────────────────────────────────────────────── */
/* ── Category chips ─────────────────────────────────────────────── */
.chip-row {
display: flex;
gap: 0.5rem;
@ -257,14 +218,6 @@
.chip-row::-webkit-scrollbar {
display: none;
}
/* Hide the mobile chip row on desktop. A media query inside the
scoped <style> block beats the unscoped Tailwind .lg\:hidden,
which would otherwise lose the specificity battle. */
@media (min-width: 1024px) {
.chip-row {
display: none;
}
}
.chip-btn {
flex-shrink: 0;
display: flex;
@ -292,85 +245,4 @@
.chip-btn.dim {
opacity: 0.4;
}
/* ── Desktop sidebar buttons ────────────────────────────────────── */
.cat-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.cat-btn {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.625rem 0.75rem;
background: hsl(var(--color-card));
border: 1px solid hsl(var(--color-border));
border-radius: 0.875rem;
text-align: left;
cursor: pointer;
color: hsl(var(--color-foreground));
transition:
background 0.15s,
border-color 0.15s,
box-shadow 0.15s,
transform 0.15s;
}
.cat-btn:hover {
background: hsl(var(--color-surface-hover));
border-color: hsl(var(--color-border-strong, var(--color-border)));
transform: translateY(-1px);
}
.cat-btn.active {
background: hsl(var(--color-primary) / 0.12);
border-color: hsl(var(--color-primary) / 0.35);
box-shadow:
inset 0 0 0 1px hsl(var(--color-primary) / 0.2),
0 2px 6px hsl(var(--color-primary) / 0.12);
}
.cat-btn.dim {
opacity: 0.45;
}
.cat-icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.625rem;
background: hsl(var(--color-muted) / 0.5);
color: hsl(var(--color-muted-foreground));
transition:
background 0.15s,
color 0.15s;
}
.icon-active {
background: hsl(var(--color-primary) / 0.18);
color: hsl(var(--color-primary));
}
.cat-text {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.cat-label {
font-size: 0.875rem;
font-weight: 600;
line-height: 1.2;
}
.cat-desc {
margin-top: 0.125rem;
font-size: 0.75rem;
color: hsl(var(--color-muted-foreground));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View file

@ -4,9 +4,9 @@
* updates both the navigation and the search results.
*/
import type { Component } from 'svelte';
import { User, Gear, Robot, ShieldCheck, CurrencyCircleDollar, Cloud } from '@mana/shared-icons';
import { Gear, Robot, ShieldCheck, CurrencyCircleDollar, Cloud } from '@mana/shared-icons';
export type CategoryId = 'profile' | 'general' | 'ai' | 'security' | 'credits' | 'data';
export type CategoryId = 'general' | 'ai' | 'security' | 'credits' | 'data';
export interface Category {
id: CategoryId;
@ -18,13 +18,6 @@ export interface Category {
}
export const categories: Category[] = [
{
id: 'profile',
label: 'Profil',
description: 'Persönliche Daten & Konto',
icon: User,
anchors: ['profile', 'account'],
},
{
id: 'general',
label: 'Allgemein',
@ -72,18 +65,6 @@ export interface SearchEntry {
}
export const searchIndex: SearchEntry[] = [
// Profile
{ label: 'E-Mail', keywords: ['email', 'mail'], category: 'profile', anchor: 'profile' },
{ label: 'Vorname', keywords: ['name'], category: 'profile', anchor: 'profile' },
{ label: 'Nachname', keywords: ['name'], category: 'profile', anchor: 'profile' },
{
label: 'Konto-Status',
keywords: ['rolle', 'role', 'aktiv'],
category: 'profile',
anchor: 'account',
},
{ label: 'Benutzer-ID', keywords: ['id', 'uid'], category: 'profile', anchor: 'account' },
// General
{
label: 'Theme',

View file

@ -1,134 +0,0 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { Button, Input } from '@mana/shared-ui';
import { User, ShieldCheck } from '@mana/shared-icons';
import { authStore } from '$lib/stores/auth.svelte';
import { profileService } from '$lib/api/profile';
import { ManaEvents } from '@mana/shared-utils/analytics';
import SettingsPanel from '../SettingsPanel.svelte';
import SettingsSectionHeader from '../SettingsSectionHeader.svelte';
let savingProfile = $state(false);
let profileSuccess = $state(false);
let profileError = $state<string | null>(null);
let firstName = $state('');
let lastName = $state('');
async function handleUpdateProfile() {
const name = `${firstName} ${lastName}`.trim();
if (!name) {
profileError = 'Bitte gib einen Namen ein';
return;
}
savingProfile = true;
profileSuccess = false;
profileError = null;
try {
await profileService.updateProfile({ name });
profileSuccess = true;
ManaEvents.profileUpdated();
} catch (e) {
profileError = e instanceof Error ? e.message : $_('common.error_saving');
} finally {
savingProfile = false;
}
}
</script>
<SettingsPanel id="profile">
<SettingsSectionHeader
icon={User}
title="Profil"
description="Deine persönlichen Informationen"
tone="primary"
/>
{#if profileSuccess}
<div
class="mb-4 rounded-lg bg-green-50 p-4 text-sm text-green-800 dark:bg-green-900/20 dark:text-green-400"
>
Profil erfolgreich aktualisiert!
</div>
{/if}
{#if profileError}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{profileError}
</div>
{/if}
<div class="space-y-4">
<div>
<label for="email" class="mb-2 block text-sm font-medium">E-Mail</label>
<Input
type="email"
id="email"
value={authStore.user?.email || ''}
disabled
class="bg-muted"
/>
<p class="mt-1 text-xs text-muted-foreground">E-Mail kann nicht geändert werden</p>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label for="firstName" class="mb-2 block text-sm font-medium">Vorname</label>
<Input type="text" id="firstName" bind:value={firstName} placeholder="Max" />
</div>
<div>
<label for="lastName" class="mb-2 block text-sm font-medium">Nachname</label>
<Input type="text" id="lastName" bind:value={lastName} placeholder="Mustermann" />
</div>
</div>
<Button onclick={handleUpdateProfile} loading={savingProfile} class="w-full sm:w-auto">
{savingProfile ? $_('common.saving') : 'Änderungen speichern'}
</Button>
</div>
</SettingsPanel>
<SettingsPanel id="account">
<SettingsSectionHeader
icon={ShieldCheck}
title="Konto"
description="Konto- und Sicherheitsinformationen"
tone="blue"
/>
<div>
<div class="flex items-center justify-between border-b border-border py-3">
<div>
<p class="font-medium">Konto-Status</p>
<p class="text-sm text-muted-foreground">Dein aktueller Kontostatus</p>
</div>
<span
class="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-800 dark:bg-green-900/20 dark:text-green-400"
>
Aktiv
</span>
</div>
<div class="flex items-center justify-between border-b border-border py-3">
<div>
<p class="font-medium">Rolle</p>
<p class="text-sm text-muted-foreground">Deine Berechtigungsstufe</p>
</div>
<span
class="rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
>
{authStore.user?.role || 'user'}
</span>
</div>
<div class="flex items-center justify-between py-3">
<div>
<p class="font-medium">Benutzer-ID</p>
<p class="text-sm text-muted-foreground">Deine eindeutige Kennung</p>
</div>
<code class="rounded bg-muted px-2 py-1 font-mono text-xs">
{authStore.user?.id?.slice(0, 8) || '...'}...
</code>
</div>
</div>
</SettingsPanel>

View file

@ -34,6 +34,7 @@ import { STRETCH_GUEST_SEED } from '$lib/modules/stretch/collections';
import { MEDITATE_GUEST_SEED } from '$lib/modules/meditate/collections';
import { SLEEP_GUEST_SEED } from '$lib/modules/sleep/collections';
import { MOOD_GUEST_SEED } from '$lib/modules/mood/collections';
import { QUIZ_GUEST_SEED } from '$lib/modules/quiz/collections';
/**
* Flat list of { tableName, rows } entries. Only modules with non-empty
@ -72,6 +73,7 @@ register(STRETCH_GUEST_SEED);
register(MEDITATE_GUEST_SEED);
register(SLEEP_GUEST_SEED);
register(MOOD_GUEST_SEED);
register(QUIZ_GUEST_SEED);
/**
* Seed all module guest data into empty tables. Idempotent: tables

View file

@ -8,15 +8,17 @@
import { quizzesStore } from './stores/quizzes.svelte';
import { QUESTION_TYPE_LABELS } from './types';
import type { QuestionType, QuestionOption, QuizQuestion } from './types';
import { ArrowLeft, Plus, Trash, Check, Play } from '@mana/shared-icons';
import { ArrowLeft, Plus, Trash, Check, Play, PencilSimple, X } from '@mana/shared-icons';
interface Props {
quizId: string;
}
let { quizId }: Props = $props();
// svelte-ignore state_referenced_locally
const quiz$ = useQuiz(quizId);
const quiz = $derived(quiz$.value);
// svelte-ignore state_referenced_locally
const questions$ = useQuestions(quizId);
const questions = $derived(questions$.value);
@ -49,7 +51,8 @@
});
}
// ── New question form ───────────────────────────────
// ── Question form (new OR edit) ─────────────────────
let editingId = $state<string | null>(null);
let newType = $state<QuestionType>('single');
let newText = $state('');
let newExplanation = $state('');
@ -59,30 +62,52 @@
]);
let newTextAnswer = $state('');
function resetNewForm() {
newText = '';
newExplanation = '';
newTextAnswer = '';
if (newType === 'truefalse') {
newOptions = [
function defaultOptions(type: QuestionType): QuestionOption[] {
if (type === 'truefalse') {
return [
{ id: 't', text: 'Wahr', isCorrect: true },
{ id: 'f', text: 'Falsch', isCorrect: false },
];
} else if (newType === 'text') {
}
if (type === 'text') return [];
return [
{ id: crypto.randomUUID(), text: '', isCorrect: type === 'single' },
{ id: crypto.randomUUID(), text: '', isCorrect: false },
];
}
function resetNewForm() {
editingId = null;
newText = '';
newExplanation = '';
newTextAnswer = '';
newOptions = defaultOptions(newType);
}
function onTypeChange() {
// Manual onchange so we don't rebuild options on every unrelated rerender
// (an $effect on newType would wipe the buffer when loading a question for edit).
newOptions = defaultOptions(newType);
newTextAnswer = '';
}
function startEdit(q: QuizQuestion) {
editingId = q.id;
newType = q.type;
newText = q.questionText;
newExplanation = q.explanation ?? '';
if (q.type === 'text') {
newTextAnswer = q.options[0]?.text ?? '';
newOptions = [];
} else {
newOptions = [
{ id: crypto.randomUUID(), text: '', isCorrect: newType === 'single' },
{ id: crypto.randomUUID(), text: '', isCorrect: false },
];
newTextAnswer = '';
newOptions = q.options.map((o) => ({ ...o }));
}
}
$effect(() => {
// Rebuild option slots when question type changes.
newType;
function cancelEdit() {
resetNewForm();
});
}
function toggleNewCorrect(id: string) {
if (newType === 'single' || newType === 'truefalse') {
@ -100,7 +125,7 @@
newOptions = newOptions.filter((o) => o.id !== id);
}
async function submitNewQuestion() {
async function submitQuestion() {
const text = newText.trim();
if (!text) return;
@ -116,12 +141,21 @@
options = valid.map((o) => ({ ...o, text: o.text.trim() }));
}
await quizzesStore.addQuestion(quizId, {
type: newType,
questionText: text,
options,
explanation: newExplanation.trim() || null,
});
if (editingId) {
await quizzesStore.updateQuestion(editingId, {
type: newType,
questionText: text,
options,
explanation: newExplanation.trim() || null,
});
} else {
await quizzesStore.addQuestion(quizId, {
type: newType,
questionText: text,
options,
explanation: newExplanation.trim() || null,
});
}
resetNewForm();
}
@ -198,10 +232,18 @@
{:else}
<ol class="question-list">
{#each questions as q, i (q.id)}
<li class="question-item">
<li class="question-item" class:editing={editingId === q.id}>
<div class="q-header">
<span class="q-num">{i + 1}</span>
<span class="q-type">{QUESTION_TYPE_LABELS[q.type]}</span>
<button
class="icon-btn"
title="Bearbeiten"
aria-label="Bearbeiten"
onclick={() => startEdit(q)}
>
<PencilSimple size={14} />
</button>
<button
class="icon-btn"
title="Löschen"
@ -224,11 +266,22 @@
{/if}
</section>
<section class="new-section">
<h2>Neue Frage</h2>
<section class="new-section" class:is-editing={editingId}>
<div class="new-header">
<h2>
{editingId
? `Frage ${questions.findIndex((x) => x.id === editingId) + 1} bearbeiten`
: 'Neue Frage'}
</h2>
{#if editingId}
<button class="cancel-btn" onclick={cancelEdit}>
<X size={12} /> Abbrechen
</button>
{/if}
</div>
<label class="field">
<span>Typ</span>
<select bind:value={newType}>
<select bind:value={newType} onchange={onTypeChange}>
<option value="single">Single Choice</option>
<option value="multi">Multiple Choice</option>
<option value="truefalse">Wahr / Falsch</option>
@ -295,8 +348,12 @@
></textarea>
</label>
<button class="submit-btn" onclick={submitNewQuestion}>
<Plus size={14} /> Frage hinzufügen
<button class="submit-btn" onclick={submitQuestion}>
{#if editingId}
<Check size={14} /> Änderungen speichern
{:else}
<Plus size={14} /> Frage hinzufügen
{/if}
</button>
</section>
{/if}
@ -417,6 +474,10 @@
border: 1px solid hsl(var(--color-border));
background: hsl(var(--color-surface));
}
.question-item.editing {
border-color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.05);
}
.q-header {
display: flex;
align-items: center;
@ -478,6 +539,35 @@
border-radius: 0.5rem;
border: 1px dashed hsl(var(--color-border));
}
.new-section.is-editing {
border-style: solid;
border-color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.03);
}
.new-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.new-header h2 {
margin: 0;
}
.cancel-btn {
display: inline-flex;
align-items: center;
gap: 0.25rem;
background: transparent;
border: 1px solid hsl(var(--color-border));
color: hsl(var(--color-muted-foreground));
padding: 0.25rem 0.625rem;
border-radius: 9999px;
font-size: 0.75rem;
cursor: pointer;
}
.cancel-btn:hover {
color: hsl(var(--color-error));
border-color: hsl(var(--color-error));
}
.field {
display: flex;
flex-direction: column;

View file

@ -14,8 +14,10 @@
}
let { quizId }: Props = $props();
// svelte-ignore state_referenced_locally
const quiz$ = useQuiz(quizId);
const quiz = $derived(quiz$.value);
// svelte-ignore state_referenced_locally
const questions$ = useQuestions(quizId);
const questions = $derived(questions$.value);

View file

@ -1,7 +1,6 @@
<!--
Settings — Workbench-embedded settings panel with category sidebar,
search, and all setting sections (general, AI, security, credits, data).
Profile and Themes are separate workbench apps — not duplicated here.
Settings — the single home for app settings (general, AI, security,
credits, data). Profile and Themes live in their own workbench apps.
-->
<script lang="ts">
import { onMount } from 'svelte';
@ -10,19 +9,13 @@
import { GlobalSettingsSection } from '@mana/shared-ui';
import { userSettings } from '$lib/stores/user-settings.svelte';
import SettingsSidebar from '$lib/components/settings/SettingsSidebar.svelte';
import {
categories,
type CategoryId,
type SearchEntry,
} from '$lib/components/settings/searchIndex';
import type { CategoryId, SearchEntry } from '$lib/components/settings/searchIndex';
import AiSection from '$lib/components/settings/sections/AiSection.svelte';
import SecuritySection from '$lib/components/settings/sections/SecuritySection.svelte';
import CreditsSection from '$lib/components/settings/sections/CreditsSection.svelte';
import DataSection from '$lib/components/settings/sections/DataSection.svelte';
import SettingsPanel from '$lib/components/settings/SettingsPanel.svelte';
// Filter out 'profile' — it's a separate workbench app now
const workbenchCategories = categories.filter((c) => c.id !== 'profile');
let activeCategory = $state<CategoryId>('general');
onMount(() => {
@ -30,7 +23,6 @@
});
function jumpTo(entry: SearchEntry) {
if (entry.category === 'profile') return;
activeCategory = entry.category;
void tick().then(() => {
const target = document.getElementById(entry.anchor);
@ -44,7 +36,6 @@
{activeCategory}
onSelect={(id) => (activeCategory = id)}
onJump={jumpTo}
categories={workbenchCategories}
/>
<div class="settings-content">
@ -71,21 +62,14 @@
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: 1rem;
height: 100%;
overflow-y: auto;
}
@media (min-width: 1024px) {
.settings-page {
flex-direction: row;
align-items: flex-start;
}
}
.settings-content {
min-width: 0;
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;

View file

@ -746,7 +746,7 @@
id: 'settings',
label: 'Einstellungen',
category: 'Navigation',
onExecute: () => goto('/settings'),
onExecute: () => goto('/'),
},
];
</script>
@ -974,7 +974,7 @@
currentSyncLabel={syncStatus.label}
{appItems}
{userEmail}
settingsHref="/settings"
settingsHref="/"
manaHref="/mana"
profileHref="/profile"
spiralHref="/spiral"

View file

@ -1,74 +0,0 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { onMount, tick } from 'svelte';
import { page } from '$app/stores';
import { PageHeader } from '@mana/shared-ui';
import { APP_VERSION } from '$lib/version';
import SettingsSidebar from '$lib/components/settings/SettingsSidebar.svelte';
import {
categories,
type CategoryId,
type SearchEntry,
} from '$lib/components/settings/searchIndex';
import ProfileSection from '$lib/components/settings/sections/ProfileSection.svelte';
import GeneralSection from '$lib/components/settings/sections/GeneralSection.svelte';
import AiSection from '$lib/components/settings/sections/AiSection.svelte';
import SecuritySection from '$lib/components/settings/sections/SecuritySection.svelte';
import CreditsSection from '$lib/components/settings/sections/CreditsSection.svelte';
import DataSection from '$lib/components/settings/sections/DataSection.svelte';
let activeCategory = $state<CategoryId>('profile');
let mounted = $state(false);
onMount(() => {
mounted = true;
});
// Map URL hash → active category and scroll the matching anchor into view.
// Re-runs on every hash change so the pill-nav `/settings#ai-options`
// shortcut still works when the user is already on /settings.
$effect(() => {
const hash = $page.url.hash?.slice(1);
if (!hash || !mounted) return;
const cat = categories.find((c) => c.anchors.includes(hash));
if (cat) activeCategory = cat.id;
void tick().then(() => {
const target = document.getElementById(hash);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
function jumpTo(entry: SearchEntry) {
activeCategory = entry.category;
void tick().then(() => {
const target = document.getElementById(entry.anchor);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
}
</script>
<div class="mx-auto w-full max-w-4xl px-4 sm:px-6">
<PageHeader title={$_('common.settings')} backHref="/" sticky size="lg" />
<div class="mt-6 flex flex-col gap-6 lg:flex-row lg:items-start">
<SettingsSidebar {activeCategory} onSelect={(id) => (activeCategory = id)} onJump={jumpTo} />
<div class="min-w-0 flex-1 space-y-6">
{#if activeCategory === 'profile'}
<ProfileSection />
{:else if activeCategory === 'general'}
<GeneralSection />
{:else if activeCategory === 'ai'}
<AiSection />
{:else if activeCategory === 'security'}
<SecuritySection />
{:else if activeCategory === 'credits'}
<CreditsSection />
{:else if activeCategory === 'data'}
<DataSection />
{/if}
</div>
</div>
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
</div>

View file

@ -186,7 +186,7 @@
<div class="flex items-center justify-between gap-4">
<div>
<Breadcrumbs
items={[{ label: 'Einstellungen', href: '/settings' }, { label: 'Meine Daten' }]}
items={[{ label: 'Home', href: '/' }, { label: 'Meine Daten' }]}
/>
<h1 class="text-2xl font-bold">Meine Daten</h1>
<p class="text-muted-foreground">

View file

@ -106,7 +106,7 @@
</script>
<div>
<PageHeader title="Cloud Sync" backHref="/settings" sticky size="lg" />
<PageHeader title="Cloud Sync" backHref="/" sticky size="lg" />
{#if syncBilling.loading}
<div class="flex items-center justify-center py-12">
@ -268,8 +268,8 @@
<!-- Back link -->
<div class="mt-6">
<a href="/settings" class="text-sm text-primary hover:underline">
← Zurück zu Einstellungen
<a href="/" class="text-sm text-primary hover:underline">
← Zurück
</a>
</div>
{/if}