feat(manacore): add avatar upload frontend and onboarding wizard

- Add avatar upload to EditProfileModal with presigned URL flow
- Create onboarding wizard with 5 steps: Welcome, Profile, Apps, Credits, Complete
- Add onboarding store with localStorage persistence
- Integrate wizard into app layout (shows for new users)
- Update MANACORE-TODOS.md to mark completed tasks
This commit is contained in:
Till-JS 2026-02-13 23:18:10 +01:00
parent 405084b52d
commit 5118235e0b
12 changed files with 1339 additions and 39 deletions

View file

@ -170,7 +170,7 @@ Archivierte Apps (memoro, storyteller) wurden bereits entfernt.
| Profil bearbeiten | ✅ | ✅ | Hoch |
| Passwort ändern | ✅ | ✅ | Hoch |
| Konto löschen | ✅ | ✅ | Mittel |
| Avatar hochladen | ✅ | | Niedrig |
| Avatar hochladen | ✅ | | Niedrig |
| 2FA aktivieren | ❌ | ❌ | Niedrig |
**Dateien:**
@ -193,7 +193,7 @@ Archivierte Apps (memoro, storyteller) wurden bereits entfernt.
- [x] Presigned URL Endpoint: `POST /api/v1/storage/avatar/upload-url`
- [x] Direct Upload Endpoint: `POST /api/v1/storage/avatar`
- [x] `manacore-storage` Bucket konfiguriert
- [ ] Frontend-Integration (EditProfileModal) noch offen
- [x] Frontend-Integration (EditProfileModal mit Avatar-Picker und Presigned-URL-Upload)
---
@ -291,27 +291,34 @@ GET /api/v1/subscriptions/invoices # Rechnungen
---
### 8. Onboarding-Flow
### 8. ✅ Onboarding-Flow (ERLEDIGT)
**Status:** Implementiert am 2026-02-13
**Beschreibung:** Welcome-Wizard für neue Benutzer
**Schritte:**
**Implementierte Schritte:**
1. Willkommen & Kurze Einführung
2. Profil vervollständigen (Name, Avatar)
3. Bevorzugte Apps auswählen
4. Dashboard personalisieren
5. Credits-System erklären
6. Tour durch wichtigste Features
1. Willkommen & Kurze Einführung (WelcomeStep)
2. Profil vervollständigen - Name, Avatar (ProfileStep)
3. Apps-Übersicht - alle Mana-Apps zeigen (AppsStep)
4. Credits-System erklären (CreditsStep)
5. Fertig - Quick Actions zum Start (CompleteStep)
**Aufgaben:**
- [ ] Onboarding-Wizard Komponente
- [ ] Progress-Tracking (User hat Onboarding abgeschlossen)
- [ ] Skip-Option
- [ ] Feature-Tour (Tooltip-basiert)
- [x] Onboarding-Store (`lib/stores/onboarding.svelte.ts`)
- [x] OnboardingWizard Komponente mit Step-Navigation
- [x] 5 Step-Komponenten (Welcome, Profile, Apps, Credits, Complete)
- [x] Progress-Tracking (LocalStorage-basiert)
- [x] Skip-Option
- [x] Integration in App-Layout (zeigt Wizard für neue User)
**Geschätzter Aufwand:** 2-3 Tage
**Dateien:**
- `apps/manacore/apps/web/src/lib/stores/onboarding.svelte.ts`
- `apps/manacore/apps/web/src/lib/components/onboarding/OnboardingWizard.svelte`
- `apps/manacore/apps/web/src/lib/components/onboarding/steps/*.svelte`
---
@ -378,16 +385,16 @@ GET /api/v1/subscriptions/invoices # Rechnungen
## Empfohlene Reihenfolge
| # | Task | Aufwand | Impact | Abhängigkeiten |
| --- | --------------------------- | -------- | -------- | -------------- |
| 1 | App-Config aktualisieren | 2-4h | Hoch | Keine |
| 2 | Stripe-Integration | 2-3 Tage | Kritisch | mana-core-auth |
| 3 | Dashboard-Widgets erweitern | 1-2 Tage | Hoch | App-Config |
| 4 | Profil-Features | 1-2 Tage | Mittel | Keine |
| 5 | Notifications | 3-5 Tage | Hoch | Backend-Arbeit |
| 6 | Onboarding | 2-3 Tage | Mittel | Keine |
| 7 | Subscription-Management | 2-3 Tage | Mittel | Stripe |
| 8 | API-Keys | 2-3 Tage | Niedrig | Keine |
| # | Task | Aufwand | Impact | Status |
| --- | --------------------------- | -------- | -------- | ----------- |
| 1 | App-Config aktualisieren | 2-4h | Hoch | ✅ Erledigt |
| 2 | Stripe-Integration | 2-3 Tage | Kritisch | ✅ Erledigt |
| 3 | Dashboard-Widgets erweitern | 1-2 Tage | Hoch | ✅ Erledigt |
| 4 | Profil-Features | 1-2 Tage | Mittel | ✅ Erledigt |
| 5 | Notifications | 3-5 Tage | Hoch | ⏳ Offen |
| 6 | Onboarding | 2-3 Tage | Mittel | ✅ Erledigt |
| 7 | Subscription-Management | 2-3 Tage | Mittel | ✅ Erledigt |
| 8 | API-Keys | 2-3 Tage | Niedrig | ⏳ Offen |
---
@ -414,4 +421,4 @@ Diese Tasks können schnell erledigt werden:
---
_Zuletzt aktualisiert: 2026-02-13 (Avatar Storage Backend + Subscription Plans Seed)_
_Zuletzt aktualisiert: 2026-02-13 (Avatar-Upload Frontend + Onboarding-Flow implementiert)_

View file

@ -1,6 +1,6 @@
/**
* Profile Service for ManaCore Web App
* Handles profile updates, password changes, and account deletion
* Handles profile updates, password changes, account deletion, and avatar uploads
*/
import { authStore } from '$lib/stores/auth.svelte';
@ -33,6 +33,13 @@ export interface DeleteAccountRequest {
reason?: string;
}
export interface AvatarUploadUrlResponse {
uploadUrl: string;
fileUrl: string;
key: string;
expiresIn: number;
}
// Helper function for authenticated requests
async function fetchWithAuth<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const token = await authStore.getAccessToken();
@ -96,4 +103,38 @@ export const profileService = {
body: JSON.stringify(data),
});
},
/**
* Get presigned URL for avatar upload
*/
async getAvatarUploadUrl(filename: string): Promise<AvatarUploadUrlResponse> {
return fetchWithAuth('/api/v1/storage/avatar/upload-url', {
method: 'POST',
body: JSON.stringify({ filename }),
});
},
/**
* Upload avatar file using presigned URL, then update profile
*/
async uploadAvatar(file: File): Promise<{ success: boolean; user: UserProfile }> {
// 1. Get presigned upload URL
const { uploadUrl, fileUrl } = await this.getAvatarUploadUrl(file.name);
// 2. Upload file directly to S3/MinIO
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
});
if (!uploadResponse.ok) {
throw new Error('Avatar-Upload fehlgeschlagen');
}
// 3. Update profile with new image URL
return this.updateProfile({ image: fileUrl });
},
};

View file

@ -0,0 +1,167 @@
<script lang="ts">
import { onboardingStore } from '$lib/stores/onboarding.svelte';
import WelcomeStep from './steps/WelcomeStep.svelte';
import ProfileStep from './steps/ProfileStep.svelte';
import AppsStep from './steps/AppsStep.svelte';
import CreditsStep from './steps/CreditsStep.svelte';
import CompleteStep from './steps/CompleteStep.svelte';
interface Props {
onComplete: () => void;
}
let { onComplete }: Props = $props();
const STEPS = [
{ id: 'welcome', label: 'Willkommen', component: WelcomeStep },
{ id: 'profile', label: 'Profil', component: ProfileStep },
{ id: 'apps', label: 'Apps', component: AppsStep },
{ id: 'credits', label: 'Credits', component: CreditsStep },
{ id: 'complete', label: 'Fertig', component: CompleteStep },
];
let currentStep = $derived(onboardingStore.currentStep);
let currentStepData = $derived(STEPS[currentStep] || STEPS[0]);
let isFirstStep = $derived(currentStep === 0);
let isLastStep = $derived(currentStep === STEPS.length - 1);
let progress = $derived(((currentStep + 1) / STEPS.length) * 100);
function handleNext() {
if (isLastStep) {
onboardingStore.complete();
onComplete();
} else {
onboardingStore.completeStep(currentStepData.id);
onboardingStore.nextStep();
}
}
function handlePrev() {
onboardingStore.prevStep();
}
function handleSkip() {
onboardingStore.skip();
onComplete();
}
function handleStepClick(index: number) {
// Only allow going to completed steps or the next step
if (index <= currentStep) {
onboardingStore.goToStep(index);
}
}
</script>
<div class="fixed inset-0 z-50 bg-background flex flex-col">
<!-- Header with progress -->
<header class="border-b px-6 py-4">
<div class="max-w-3xl mx-auto">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<div
class="h-10 w-10 rounded-xl bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center"
>
<span class="text-xl">M</span>
</div>
<div>
<h1 class="font-semibold text-lg">Willkommen bei ManaCore</h1>
<p class="text-sm text-muted-foreground">
Schritt {currentStep + 1} von {STEPS.length}
</p>
</div>
</div>
<button
onclick={handleSkip}
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Überspringen
</button>
</div>
<!-- Progress bar -->
<div class="h-1.5 bg-muted rounded-full overflow-hidden">
<div
class="h-full bg-primary transition-all duration-300 ease-out rounded-full"
style="width: {progress}%"
></div>
</div>
<!-- Step indicators -->
<div class="flex justify-between mt-3">
{#each STEPS as step, index}
<button
onclick={() => handleStepClick(index)}
disabled={index > currentStep}
class="flex flex-col items-center gap-1 group"
>
<div
class="h-8 w-8 rounded-full flex items-center justify-center text-sm font-medium transition-all
{index < currentStep
? 'bg-primary text-primary-foreground'
: index === currentStep
? 'bg-primary/20 text-primary border-2 border-primary'
: 'bg-muted text-muted-foreground'}"
>
{#if index < currentStep}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
{:else}
{index + 1}
{/if}
</div>
<span
class="text-xs {index === currentStep
? 'text-foreground font-medium'
: 'text-muted-foreground'} hidden sm:block"
>
{step.label}
</span>
</button>
{/each}
</div>
</div>
</header>
<!-- Step content -->
<main class="flex-1 overflow-y-auto">
<div class="max-w-3xl mx-auto px-6 py-8">
{#if currentStepData.id === 'welcome'}
<WelcomeStep />
{:else if currentStepData.id === 'profile'}
<ProfileStep />
{:else if currentStepData.id === 'apps'}
<AppsStep />
{:else if currentStepData.id === 'credits'}
<CreditsStep />
{:else if currentStepData.id === 'complete'}
<CompleteStep />
{/if}
</div>
</main>
<!-- Footer with navigation -->
<footer class="border-t px-6 py-4">
<div class="max-w-3xl mx-auto flex justify-between">
<button
onclick={handlePrev}
disabled={isFirstStep}
class="px-6 py-2.5 border rounded-lg hover:bg-muted transition-colors disabled:opacity-0 disabled:pointer-events-none"
>
Zurück
</button>
<button
onclick={handleNext}
class="px-6 py-2.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium"
>
{isLastStep ? "Los geht's!" : 'Weiter'}
</button>
</div>
</footer>
</div>

View file

@ -0,0 +1,6 @@
export { default as OnboardingWizard } from './OnboardingWizard.svelte';
export { default as WelcomeStep } from './steps/WelcomeStep.svelte';
export { default as ProfileStep } from './steps/ProfileStep.svelte';
export { default as AppsStep } from './steps/AppsStep.svelte';
export { default as CreditsStep } from './steps/CreditsStep.svelte';
export { default as CompleteStep } from './steps/CompleteStep.svelte';

View file

@ -0,0 +1,125 @@
<script lang="ts">
// Apps overview step - shows available Mana apps
const APPS = [
{
id: 'todo',
name: 'Todo',
description: 'Aufgaben verwalten mit Projekten und Prioritäten',
icon: '✓',
color: 'bg-blue-500',
category: 'Produktivität',
},
{
id: 'calendar',
name: 'Calendar',
description: 'Termine und Ereignisse planen',
icon: '📅',
color: 'bg-green-500',
category: 'Produktivität',
},
{
id: 'contacts',
name: 'Contacts',
description: 'Kontakte organisieren und verwalten',
icon: '👤',
color: 'bg-purple-500',
category: 'Produktivität',
},
{
id: 'chat',
name: 'Chat',
description: 'KI-gestützte Konversationen',
icon: '💬',
color: 'bg-pink-500',
category: 'AI',
},
{
id: 'picture',
name: 'Picture',
description: 'Bilder mit KI generieren',
icon: '🎨',
color: 'bg-orange-500',
category: 'AI',
},
{
id: 'zitare',
name: 'Zitare',
description: 'Tägliche Inspiration mit Zitaten',
icon: '💡',
color: 'bg-yellow-500',
category: 'Lifestyle',
},
{
id: 'clock',
name: 'Clock',
description: 'Timer, Stoppuhr und Weltuhr',
icon: '⏰',
color: 'bg-red-500',
category: 'Utility',
},
{
id: 'manadeck',
name: 'ManaDeck',
description: 'Karteikarten für effektives Lernen',
icon: '📚',
color: 'bg-indigo-500',
category: 'Bildung',
},
];
const categories = [...new Set(APPS.map((app) => app.category))];
</script>
<div class="max-w-2xl mx-auto">
<div class="text-center mb-8">
<h2 class="text-2xl font-bold mb-2">Entdecke die Mana-Apps</h2>
<p class="text-muted-foreground">Alle Apps sind mit deinem ManaCore-Konto verbunden.</p>
</div>
{#each categories as category}
<div class="mb-6">
<h3 class="text-sm font-medium text-muted-foreground mb-3">{category}</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
{#each APPS.filter((a) => a.category === category) as app}
<div
class="p-4 rounded-xl bg-card border hover:border-primary/50 hover:shadow-sm transition-all cursor-pointer group"
>
<div class="flex items-start gap-3">
<div
class="{app.color} h-10 w-10 rounded-lg flex items-center justify-center text-white text-lg"
>
{app.icon}
</div>
<div class="flex-1 min-w-0">
<h4 class="font-semibold group-hover:text-primary transition-colors">{app.name}</h4>
<p class="text-sm text-muted-foreground line-clamp-2">{app.description}</p>
</div>
</div>
</div>
{/each}
</div>
</div>
{/each}
<div class="mt-8 p-4 rounded-xl bg-primary/5 border border-primary/20">
<div class="flex gap-3">
<div class="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
<svg class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div>
<h4 class="font-medium">Ein Konto, alle Apps</h4>
<p class="text-sm text-muted-foreground">
Dein ManaCore-Login funktioniert in allen Apps. Deine Credits werden geteilt.
</p>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,160 @@
<script lang="ts">
// Completion step with celebration
</script>
<div class="text-center max-w-md mx-auto">
<!-- Celebration animation -->
<div class="mb-8 relative">
<div
class="inline-flex h-24 w-24 rounded-full bg-gradient-to-br from-green-400 to-emerald-500 items-center justify-center shadow-lg shadow-green-500/25 animate-bounce"
>
<svg class="h-12 w-12 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<!-- Confetti-like decorations -->
<div class="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-2">
<span class="text-2xl animate-pulse">🎉</span>
</div>
<div class="absolute top-4 left-8">
<span class="text-xl animate-bounce" style="animation-delay: 0.1s;"></span>
</div>
<div class="absolute top-4 right-8">
<span class="text-xl animate-bounce" style="animation-delay: 0.2s;">🌟</span>
</div>
</div>
<h2 class="text-3xl font-bold mb-3">Alles bereit!</h2>
<p class="text-lg text-muted-foreground mb-8">
Du kannst jetzt loslegen und alle Mana-Apps nutzen.
</p>
<!-- Quick actions -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-left mb-8">
<a
href="/dashboard"
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">
<div
class="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors"
>
<svg class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
</div>
<div>
<div class="font-semibold group-hover:text-primary transition-colors">Dashboard</div>
<div class="text-xs text-muted-foreground">Dein Überblick</div>
</div>
</div>
</a>
<a
href="/apps"
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">
<div
class="h-10 w-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center group-hover:bg-blue-200 dark:group-hover:bg-blue-900/50 transition-colors"
>
<svg
class="h-5 w-5 text-blue-600 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
/>
</svg>
</div>
<div>
<div class="font-semibold group-hover:text-blue-600 transition-colors">Alle Apps</div>
<div class="text-xs text-muted-foreground">Entdecke mehr</div>
</div>
</div>
</a>
<a
href="/credits"
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">
<div
class="h-10 w-10 rounded-lg bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center group-hover:bg-yellow-200 dark:group-hover:bg-yellow-900/50 transition-colors"
>
<svg
class="h-5 w-5 text-yellow-600 dark:text-yellow-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div>
<div class="font-semibold group-hover:text-yellow-600 transition-colors">Credits</div>
<div class="text-xs text-muted-foreground">150 Gratis-Credits</div>
</div>
</div>
</a>
<a
href="/settings"
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">
<div
class="h-10 w-10 rounded-lg bg-gray-100 dark:bg-gray-800 flex items-center justify-center group-hover:bg-gray-200 dark:group-hover:bg-gray-700 transition-colors"
>
<svg
class="h-5 w-5 text-gray-600 dark:text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<div>
<div
class="font-semibold group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors"
>
Einstellungen
</div>
<div class="text-xs text-muted-foreground">Personalisieren</div>
</div>
</div>
</a>
</div>
<p class="text-sm text-muted-foreground">
Klicke auf "Los geht's!" um zum Dashboard zu gelangen.
</p>
</div>

View file

@ -0,0 +1,169 @@
<script lang="ts">
// Credits explanation step
</script>
<div class="max-w-xl mx-auto">
<div class="text-center mb-8">
<div
class="inline-flex h-16 w-16 rounded-2xl bg-gradient-to-br from-yellow-400 to-orange-500 items-center justify-center mb-4 shadow-lg shadow-orange-500/25"
>
<svg class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h2 class="text-2xl font-bold mb-2">Das Credits-System</h2>
<p class="text-muted-foreground">Einfach und transparent für alle AI-Features.</p>
</div>
<div class="space-y-4">
<!-- How it works -->
<div class="p-5 rounded-xl bg-card border">
<h3 class="font-semibold mb-3 flex items-center gap-2">
<span
class="h-6 w-6 rounded-full bg-primary/10 flex items-center justify-center text-sm text-primary"
>1</span
>
So funktioniert's
</h3>
<ul class="space-y-2 text-sm text-muted-foreground">
<li class="flex items-start gap-2">
<svg
class="h-4 w-4 text-green-500 mt-0.5 shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<span>Du hast ein Credit-Guthaben für alle Mana-Apps</span>
</li>
<li class="flex items-start gap-2">
<svg
class="h-4 w-4 text-green-500 mt-0.5 shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<span>AI-Features wie Chat, Bildgenerierung etc. kosten Credits</span>
</li>
<li class="flex items-start gap-2">
<svg
class="h-4 w-4 text-green-500 mt-0.5 shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<span>Einfache Preisregel: <strong class="text-foreground">1 Mana = 1 Cent</strong></span>
</li>
</ul>
</div>
<!-- Free credits -->
<div
class="p-5 rounded-xl bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border border-green-200 dark:border-green-800"
>
<h3 class="font-semibold mb-3 flex items-center gap-2">
<span
class="h-6 w-6 rounded-full bg-green-500 flex items-center justify-center text-sm text-white"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"
/>
</svg>
</span>
Gratis-Credits
</h3>
<div class="grid grid-cols-2 gap-4">
<div class="bg-white dark:bg-background rounded-lg p-3 text-center">
<div class="text-2xl font-bold text-green-600">150</div>
<div class="text-xs text-muted-foreground">Willkommensbonus</div>
</div>
<div class="bg-white dark:bg-background rounded-lg p-3 text-center">
<div class="text-2xl font-bold text-green-600">+5</div>
<div class="text-xs text-muted-foreground">Täglich gratis</div>
</div>
</div>
</div>
<!-- Credit packages -->
<div class="p-5 rounded-xl bg-card border">
<h3 class="font-semibold mb-3 flex items-center gap-2">
<span
class="h-6 w-6 rounded-full bg-primary/10 flex items-center justify-center text-sm text-primary"
>2</span
>
Credit-Pakete
</h3>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div class="p-3 rounded-lg bg-muted text-center">
<div class="font-bold">100</div>
<div class="text-xs text-muted-foreground">€1</div>
</div>
<div class="p-3 rounded-lg bg-muted text-center">
<div class="font-bold">500</div>
<div class="text-xs text-muted-foreground">€5</div>
</div>
<div class="p-3 rounded-lg bg-muted text-center">
<div class="font-bold">1.500</div>
<div class="text-xs text-muted-foreground">€15</div>
</div>
<div class="p-3 rounded-lg bg-muted text-center">
<div class="font-bold">5.000</div>
<div class="text-xs text-muted-foreground">€50</div>
</div>
</div>
</div>
<!-- Pro tip -->
<div
class="p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800"
>
<div class="flex gap-3">
<svg
class="h-5 w-5 text-blue-500 shrink-0 mt-0.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
<p class="text-sm text-blue-800 dark:text-blue-200">
<strong>Tipp:</strong> Mit einem Pro-Abo erhältst du monatlich 2.000 Credits und weitere Vorteile.
</p>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,218 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { profileService } from '$lib/api/profile';
let name = $state(authStore.user?.name || '');
let avatarPreview = $state<string | null>(authStore.user?.image || null);
let selectedFile = $state<File | null>(null);
let saving = $state(false);
let saved = $state(false);
let error = $state<string | null>(null);
let fileInput: HTMLInputElement;
function handleFileSelect(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
error = 'Bitte wähle ein Bild aus';
return;
}
if (file.size > 5 * 1024 * 1024) {
error = 'Max. 5MB erlaubt';
return;
}
selectedFile = file;
error = null;
const reader = new FileReader();
reader.onload = (e) => {
avatarPreview = e.target?.result as string;
};
reader.readAsDataURL(file);
}
function triggerFileInput() {
fileInput?.click();
}
async function saveProfile() {
if (!name.trim()) {
error = 'Bitte gib deinen Namen ein';
return;
}
saving = true;
error = null;
try {
// Upload avatar if selected
if (selectedFile) {
await profileService.uploadAvatar(selectedFile);
}
// Update name
await profileService.updateProfile({ name: name.trim() });
saved = true;
// Refresh auth store
// The profile update should update the user in authStore
} catch (e) {
error = e instanceof Error ? e.message : 'Fehler beim Speichern';
} finally {
saving = false;
}
}
function getInitials(name: string): string {
return (
name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2) || 'U'
);
}
</script>
<div class="max-w-md mx-auto">
<div class="text-center mb-8">
<h2 class="text-2xl font-bold mb-2">Vervollständige dein Profil</h2>
<p class="text-muted-foreground">Füge ein Profilbild und deinen Namen hinzu.</p>
</div>
<div class="space-y-6">
<!-- Avatar Upload -->
<div class="flex flex-col items-center">
<input
bind:this={fileInput}
type="file"
accept="image/*"
class="hidden"
onchange={handleFileSelect}
/>
<button onclick={triggerFileInput} class="relative group">
{#if avatarPreview}
<img
src={avatarPreview}
alt="Avatar"
class="h-28 w-28 rounded-full object-cover border-4 border-border group-hover:border-primary/50 transition-colors"
/>
{:else}
<div
class="h-28 w-28 rounded-full bg-primary/10 flex items-center justify-center text-primary text-3xl font-semibold border-4 border-border group-hover:border-primary/50 transition-colors"
>
{getInitials(name || 'U')}
</div>
{/if}
<!-- Camera overlay -->
<div
class="absolute inset-0 rounded-full bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
>
<svg class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
</button>
<p class="mt-2 text-sm text-muted-foreground">Klicke zum Ändern</p>
</div>
<!-- Name Input -->
<div>
<label for="onboarding-name" class="block text-sm font-medium mb-2">Dein Name</label>
<input
id="onboarding-name"
type="text"
bind:value={name}
placeholder="Max Mustermann"
class="w-full px-4 py-3 border rounded-xl bg-background focus:outline-none focus:ring-2 focus:ring-primary/50 text-lg"
/>
</div>
<!-- Email (readonly) -->
<div>
<label class="block text-sm font-medium mb-2 text-muted-foreground">E-Mail</label>
<div class="px-4 py-3 border rounded-xl bg-muted text-muted-foreground">
{authStore.user?.email || 'Nicht verfügbar'}
</div>
</div>
{#if error}
<div
class="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"
>
<p class="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
{/if}
{#if saved}
<div
class="p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg"
>
<p class="text-sm text-green-600 dark:text-green-400 flex items-center gap-2">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
Profil gespeichert!
</p>
</div>
{:else}
<button
onclick={saveProfile}
disabled={saving || !name.trim()}
class="w-full px-4 py-3 bg-card border rounded-xl hover:bg-muted transition-colors disabled:opacity-50 font-medium flex items-center justify-center gap-2"
>
{#if saving}
<svg class="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
></path>
</svg>
Speichern...
{:else}
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
Jetzt speichern
{/if}
</button>
{/if}
<p class="text-center text-sm text-muted-foreground">
Du kannst dein Profil jederzeit in den Einstellungen ändern.
</p>
</div>
</div>

View file

@ -0,0 +1,93 @@
<script lang="ts">
// Welcome step - introduces ManaCore
</script>
<div class="text-center max-w-xl mx-auto">
<!-- Hero Icon -->
<div class="mb-8">
<div
class="inline-flex h-24 w-24 rounded-2xl bg-gradient-to-br from-primary via-primary/80 to-primary/60 items-center justify-center shadow-lg shadow-primary/25"
>
<span class="text-5xl text-primary-foreground font-bold">M</span>
</div>
</div>
<h2 class="text-3xl font-bold mb-4">Willkommen bei ManaCore!</h2>
<p class="text-lg text-muted-foreground mb-8">
Dein zentrales Dashboard für alle Mana-Apps. Verwalte deine Aufgaben, Termine, Kontakte und mehr
- alles an einem Ort.
</p>
<!-- Feature highlights -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 text-left">
<div class="p-4 rounded-xl bg-card border">
<div
class="h-10 w-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center mb-3"
>
<svg
class="h-5 w-5 text-blue-600 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
/>
</svg>
</div>
<h3 class="font-semibold mb-1">Alle Apps</h3>
<p class="text-sm text-muted-foreground">
Zugriff auf Todo, Kalender, Chat, Picture und mehr.
</p>
</div>
<div class="p-4 rounded-xl bg-card border">
<div
class="h-10 w-10 rounded-lg bg-green-100 dark:bg-green-900/30 flex items-center justify-center mb-3"
>
<svg
class="h-5 w-5 text-green-600 dark:text-green-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h3 class="font-semibold mb-1">Credits-System</h3>
<p class="text-sm text-muted-foreground">Ein Guthaben für alle AI-Features in allen Apps.</p>
</div>
<div class="p-4 rounded-xl bg-card border">
<div
class="h-10 w-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center mb-3"
>
<svg
class="h-5 w-5 text-purple-600 dark:text-purple-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
/>
</svg>
</div>
<h3 class="font-semibold mb-1">Personalisierung</h3>
<p class="text-sm text-muted-foreground">
Themes, Widgets und Einstellungen nach deinem Geschmack.
</p>
</div>
</div>
</div>

View file

@ -12,22 +12,71 @@
let name = $state('');
let saving = $state(false);
let uploadingAvatar = $state(false);
let error = $state<string | null>(null);
let avatarPreview = $state<string | null>(null);
let selectedFile = $state<File | null>(null);
// File input ref
let fileInput: HTMLInputElement;
// Initialize form when modal opens
$effect(() => {
if (show && user) {
name = user.name || '';
avatarPreview = user.image || null;
selectedFile = null;
error = null;
}
});
function handleBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget && !saving) {
if (event.target === event.currentTarget && !saving && !uploadingAvatar) {
onClose();
}
}
function handleFileSelect(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
error = 'Bitte wähle ein Bild aus (JPG, PNG, GIF, WebP)';
return;
}
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
error = 'Das Bild darf maximal 5MB groß sein';
return;
}
selectedFile = file;
error = null;
// Create preview
const reader = new FileReader();
reader.onload = (e) => {
avatarPreview = e.target?.result as string;
};
reader.readAsDataURL(file);
}
function triggerFileInput() {
fileInput?.click();
}
function removeAvatar() {
selectedFile = null;
avatarPreview = null;
if (fileInput) {
fileInput.value = '';
}
}
async function handleSubmit(e: Event) {
e.preventDefault();
if (!name.trim()) {
@ -39,15 +88,52 @@
error = null;
try {
const result = await profileService.updateProfile({ name: name.trim() });
onSuccess(result.user);
let updatedUser: UserProfile;
// If new avatar selected, upload first
if (selectedFile) {
uploadingAvatar = true;
const result = await profileService.uploadAvatar(selectedFile);
updatedUser = result.user;
uploadingAvatar = false;
// Now update name if changed
if (name.trim() !== updatedUser.name) {
const nameResult = await profileService.updateProfile({ name: name.trim() });
updatedUser = nameResult.user;
}
} else if (avatarPreview === null && user?.image) {
// Avatar was removed
const result = await profileService.updateProfile({
name: name.trim(),
image: '',
});
updatedUser = result.user;
} else {
// Just update name
const result = await profileService.updateProfile({ name: name.trim() });
updatedUser = result.user;
}
onSuccess(updatedUser);
onClose();
} catch (e) {
error = e instanceof Error ? e.message : 'Fehler beim Speichern';
} finally {
saving = false;
uploadingAvatar = false;
}
}
// Get initials for avatar placeholder
function getInitials(name: string): string {
return name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2);
}
</script>
{#if show}
@ -59,9 +145,7 @@
<div class="bg-card rounded-xl shadow-xl max-w-md w-full" role="dialog" aria-modal="true">
<div class="p-6">
<div class="flex items-center gap-3 mb-6">
<div
class="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center"
>
<div class="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
<svg class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
@ -76,6 +160,82 @@
<form onsubmit={handleSubmit}>
<div class="space-y-4">
<!-- Avatar Upload -->
<div>
<label class="block text-sm font-medium mb-2">Profilbild</label>
<div class="flex items-center gap-4">
<!-- Avatar Preview -->
<div class="relative">
{#if avatarPreview}
<img
src={avatarPreview}
alt="Avatar"
class="h-20 w-20 rounded-full object-cover border-2 border-border"
/>
{:else}
<div
class="h-20 w-20 rounded-full bg-primary/10 flex items-center justify-center text-primary text-xl font-semibold"
>
{getInitials(name || user?.name || 'U')}
</div>
{/if}
{#if uploadingAvatar}
<div
class="absolute inset-0 bg-black/50 rounded-full flex items-center justify-center"
>
<svg class="animate-spin h-6 w-6 text-white" fill="none" viewBox="0 0 24 24">
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
{/if}
</div>
<!-- Upload/Remove Buttons -->
<div class="flex flex-col gap-2">
<input
bind:this={fileInput}
type="file"
accept="image/*"
class="hidden"
onchange={handleFileSelect}
/>
<button
type="button"
onclick={triggerFileInput}
disabled={saving || uploadingAvatar}
class="px-3 py-1.5 text-sm border rounded-lg hover:bg-muted transition-colors disabled:opacity-50"
>
Bild auswählen
</button>
{#if avatarPreview}
<button
type="button"
onclick={removeAvatar}
disabled={saving || uploadingAvatar}
class="px-3 py-1.5 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors disabled:opacity-50"
>
Entfernen
</button>
{/if}
</div>
</div>
<p class="mt-2 text-xs text-muted-foreground">JPG, PNG, GIF oder WebP. Max. 5MB.</p>
</div>
<!-- Email (readonly) -->
<div>
<label for="profile-email" class="block text-sm font-medium mb-2">E-Mail</label>
<input
@ -88,20 +248,23 @@
<p class="mt-1 text-xs text-muted-foreground">E-Mail kann nicht geändert werden</p>
</div>
<!-- Name -->
<div>
<label for="profile-name" class="block text-sm font-medium mb-2">Name</label>
<input
id="profile-name"
type="text"
bind:value={name}
disabled={saving}
disabled={saving || uploadingAvatar}
placeholder="Dein Name"
class="w-full px-3 py-2 border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50"
/>
</div>
{#if error}
<div class="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div
class="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"
>
<p class="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
{/if}
@ -111,17 +274,17 @@
<button
type="button"
onclick={onClose}
disabled={saving}
disabled={saving || uploadingAvatar}
class="flex-1 px-4 py-2 border rounded-lg hover:bg-muted transition-colors disabled:opacity-50"
>
Abbrechen
</button>
<button
type="submit"
disabled={saving}
disabled={saving || uploadingAvatar}
class="flex-1 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 transition-colors flex items-center justify-center gap-2"
>
{#if saving}
{#if saving || uploadingAvatar}
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle
class="opacity-25"
@ -137,7 +300,7 @@
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span>Speichern...</span>
<span>{uploadingAvatar ? 'Hochladen...' : 'Speichern...'}</span>
{:else}
Speichern
{/if}

View file

@ -0,0 +1,122 @@
/**
* Onboarding Store
* Tracks user onboarding progress and completion status
*/
const STORAGE_KEY = 'manacore-onboarding';
interface OnboardingState {
completed: boolean;
currentStep: number;
completedSteps: string[];
skipped: boolean;
startedAt: string | null;
completedAt: string | null;
}
function createOnboardingStore() {
let state = $state<OnboardingState>({
completed: false,
currentStep: 0,
completedSteps: [],
skipped: false,
startedAt: null,
completedAt: null,
});
// Load from localStorage on init
function load() {
if (typeof localStorage === 'undefined') return;
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
const parsed = JSON.parse(saved);
state = { ...state, ...parsed };
} catch {
// Ignore parse errors
}
}
}
// Save to localStorage
function save() {
if (typeof localStorage === 'undefined') return;
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
return {
get completed() {
return state.completed;
},
get currentStep() {
return state.currentStep;
},
get completedSteps() {
return state.completedSteps;
},
get skipped() {
return state.skipped;
},
get shouldShow() {
return !state.completed && !state.skipped;
},
load,
start() {
state.startedAt = new Date().toISOString();
state.currentStep = 0;
save();
},
nextStep() {
state.currentStep++;
save();
},
prevStep() {
if (state.currentStep > 0) {
state.currentStep--;
save();
}
},
goToStep(step: number) {
state.currentStep = step;
save();
},
completeStep(stepId: string) {
if (!state.completedSteps.includes(stepId)) {
state.completedSteps = [...state.completedSteps, stepId];
save();
}
},
complete() {
state.completed = true;
state.completedAt = new Date().toISOString();
save();
},
skip() {
state.skipped = true;
save();
},
reset() {
state = {
completed: false,
currentStep: 0,
completedSteps: [],
skipped: false,
startedAt: null,
completedAt: null,
};
save();
},
};
}
export const onboardingStore = createOnboardingStore();

View file

@ -22,6 +22,8 @@
isNavCollapsed as collapsedStore,
} from '$lib/stores/navigation';
import { getPillAppItems } from '@manacore/shared-branding';
import { onboardingStore } from '$lib/stores/onboarding.svelte';
import { OnboardingWizard } from '$lib/components/onboarding';
let { children }: { children: Snippet } = $props();
@ -148,6 +150,17 @@
// Track initialization state
let isInitializing = $state(true);
let showOnboarding = $state(false);
function handleOnboardingComplete() {
onboardingStore.complete();
showOnboarding = false;
}
function handleOnboardingSkip() {
onboardingStore.skip();
showOnboarding = false;
}
onMount(async () => {
// Initialize auth store first
@ -182,6 +195,13 @@
// Settings API not available - use defaults
});
// Load onboarding state and show wizard if needed
onboardingStore.load();
if (onboardingStore.shouldShow) {
onboardingStore.start();
showOnboarding = true;
}
loading = false;
});
</script>
@ -198,6 +218,15 @@
</div>
</div>
{:else if authStore.isAuthenticated}
<!-- Onboarding Wizard Modal -->
{#if showOnboarding}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-background/95 backdrop-blur-sm"
>
<OnboardingWizard onComplete={handleOnboardingComplete} onSkip={handleOnboardingSkip} />
</div>
{/if}
<div class="min-h-screen bg-background">
<!-- Pill Navigation -->
<PillNavigation