mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
refactor(mana): remove post-signup onboarding wizard
Drop the 8-step wizard (Welcome, Profile, Context, Apps, AI-Tier, Sync, Credits, Complete) in favor of contextual, per-module intros — todo and news already own their first-run flows, and the workbench empty state handles the initial surface for new users. Removes components/onboarding/, stores/onboarding.svelte.ts, the ONBOARDING storage key, and all trigger/render wiring in (app)/+layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8d00ee0697
commit
a44e3df1d0
13 changed files with 1 additions and 1329 deletions
|
|
@ -1,196 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onboardingStore } from '$lib/stores/onboarding.svelte';
|
||||
import { ManaEvents } from '@mana/shared-utils/analytics';
|
||||
import { profileService } from '$lib/api/profile';
|
||||
import WelcomeStep from './steps/WelcomeStep.svelte';
|
||||
import ProfileStep from './steps/ProfileStep.svelte';
|
||||
import AppsStep from './steps/AppsStep.svelte';
|
||||
import AiTierStep from './steps/AiTierStep.svelte';
|
||||
import SyncStep from './steps/SyncStep.svelte';
|
||||
import CreditsStep from './steps/CreditsStep.svelte';
|
||||
import ContextStep from './steps/ContextStep.svelte';
|
||||
import CompleteStep from './steps/CompleteStep.svelte';
|
||||
import { Check } from '@mana/shared-icons';
|
||||
|
||||
interface Props {
|
||||
onComplete: () => void;
|
||||
/**
|
||||
* Optional callback fired when the user explicitly skips the wizard
|
||||
* (vs. completing it normally). Layout consumers use it to track
|
||||
* skip-rate analytics; the default skip path still calls onComplete
|
||||
* so the modal closes either way.
|
||||
*/
|
||||
onSkip?: () => void;
|
||||
}
|
||||
|
||||
let { onComplete, onSkip }: Props = $props();
|
||||
|
||||
// Reference to profile name for auto-save on step transition
|
||||
let profileNameRef = $state('');
|
||||
|
||||
const STEPS = [
|
||||
{ id: 'welcome', label: 'Willkommen', component: WelcomeStep },
|
||||
{ id: 'profile', label: 'Profil', component: ProfileStep },
|
||||
{ id: 'context', label: 'Über dich', component: ContextStep },
|
||||
{ id: 'apps', label: 'Apps', component: AppsStep },
|
||||
{ id: 'ai-tier', label: 'KI', component: AiTierStep },
|
||||
{ id: 'sync', label: 'Sync', component: SyncStep },
|
||||
{ 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);
|
||||
|
||||
async function handleNext() {
|
||||
// Auto-save profile name when leaving the profile step
|
||||
if (currentStepData.id === 'profile' && profileNameRef.trim()) {
|
||||
try {
|
||||
await profileService.updateProfile({ name: profileNameRef.trim() });
|
||||
} catch {
|
||||
// Non-blocking: profile save failure shouldn't block onboarding
|
||||
}
|
||||
}
|
||||
|
||||
if (isLastStep) {
|
||||
onboardingStore.complete();
|
||||
onComplete();
|
||||
} else {
|
||||
ManaEvents.onboardingStepCompleted(currentStepData.id, currentStep + 1);
|
||||
onboardingStore.completeStep(currentStepData.id);
|
||||
onboardingStore.nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
onboardingStore.prevStep();
|
||||
}
|
||||
|
||||
function handleSkip() {
|
||||
onboardingStore.skip();
|
||||
onSkip?.();
|
||||
onComplete();
|
||||
}
|
||||
|
||||
function handleStepClick(index: number) {
|
||||
// Only allow going to completed steps or the next step
|
||||
if (index <= currentStep) {
|
||||
onboardingStore.goToStep(index);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Backdrop -->
|
||||
<div class="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<!-- Modal Container - uses surface-elevated-2 for proper elevation hierarchy -->
|
||||
<div
|
||||
class="bg-surface-elevated-2 rounded-2xl shadow-2xl w-full max-w-lg max-h-[85vh] flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200 border border-border"
|
||||
>
|
||||
<!-- Header with progress -->
|
||||
<header class="border-b px-5 py-4 flex-shrink-0">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="h-9 w-9 rounded-lg bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center"
|
||||
>
|
||||
<span class="text-lg font-semibold text-primary-foreground">M</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-semibold">Willkommen bei Mana</h1>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Schritt {currentStep + 1} von {STEPS.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onclick={handleSkip}
|
||||
class="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Überspringen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="h-1 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 (compact) -->
|
||||
<div class="flex justify-between mt-2">
|
||||
{#each STEPS as step, index}
|
||||
<button
|
||||
onclick={() => handleStepClick(index)}
|
||||
disabled={index > currentStep}
|
||||
class="flex flex-col items-center gap-0.5 group"
|
||||
>
|
||||
<div
|
||||
class="h-6 w-6 rounded-full flex items-center justify-center text-xs 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}
|
||||
<Check size={20} />
|
||||
{:else}
|
||||
{index + 1}
|
||||
{/if}
|
||||
</div>
|
||||
<span
|
||||
class="text-[10px] {index === currentStep
|
||||
? 'text-foreground font-medium'
|
||||
: 'text-muted-foreground'} hidden sm:block"
|
||||
>
|
||||
{step.label}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Step content -->
|
||||
<main class="flex-1 overflow-y-auto px-5 py-4">
|
||||
{#if currentStepData.id === 'welcome'}
|
||||
<WelcomeStep />
|
||||
{:else if currentStepData.id === 'profile'}
|
||||
<ProfileStep bind:nameValue={profileNameRef} />
|
||||
{:else if currentStepData.id === 'apps'}
|
||||
<AppsStep />
|
||||
{:else if currentStepData.id === 'ai-tier'}
|
||||
<AiTierStep />
|
||||
{:else if currentStepData.id === 'sync'}
|
||||
<SyncStep />
|
||||
{:else if currentStepData.id === 'credits'}
|
||||
<CreditsStep />
|
||||
{:else if currentStepData.id === 'complete'}
|
||||
<CompleteStep />
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<!-- Footer with navigation -->
|
||||
<footer class="border-t px-5 py-3 flex-shrink-0">
|
||||
<div class="flex justify-between">
|
||||
<button
|
||||
onclick={handlePrev}
|
||||
disabled={isFirstStep}
|
||||
class="px-4 py-2 text-sm border rounded-lg hover:bg-muted transition-colors disabled:opacity-0 disabled:pointer-events-none"
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
<button
|
||||
onclick={handleNext}
|
||||
class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium"
|
||||
>
|
||||
{isLastStep ? "Los geht's!" : 'Weiter'}
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
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';
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Onboarding step: introduces the four LLM tiers and lets the user
|
||||
* pick which ones Mana is allowed to use. Same routing semantics as
|
||||
* the AiSettings card on the main settings page, but compressed into
|
||||
* a single onboarding screen with a "you can change this anytime"
|
||||
* note at the bottom.
|
||||
*
|
||||
* Default: nothing selected → user explicitly opts in.
|
||||
*/
|
||||
import { llmSettingsState, updateLlmSettings, tierLabel, type LlmTier } from '@mana/shared-llm';
|
||||
import { isLocalLlmSupported } from '@mana/local-llm';
|
||||
import { Robot, Cpu, HardDrive, Cloud, CheckCircle, Info } from '@mana/shared-icons';
|
||||
|
||||
const settings = $derived(llmSettingsState.current);
|
||||
const webgpuSupported = isLocalLlmSupported();
|
||||
|
||||
function toggleTier(tier: LlmTier) {
|
||||
const current = settings.allowedTiers;
|
||||
const next = current.includes(tier) ? current.filter((t) => t !== tier) : [...current, tier];
|
||||
updateLlmSettings({ allowedTiers: next });
|
||||
}
|
||||
|
||||
const cards = [
|
||||
{
|
||||
tier: 'browser' as LlmTier,
|
||||
icon: Cpu,
|
||||
title: 'Auf deinem Gerät',
|
||||
tagline: 'Maximale Privatsphäre',
|
||||
description: 'Gemma 4 läuft direkt im Browser. ~500 MB einmaliger Download.',
|
||||
privacyDot: 'bg-emerald-500',
|
||||
disabled: !webgpuSupported,
|
||||
disabledHint: 'Braucht WebGPU (Chrome 113+, Safari 18+).',
|
||||
},
|
||||
{
|
||||
tier: 'mana-server' as LlmTier,
|
||||
icon: HardDrive,
|
||||
title: 'Mana-Server',
|
||||
tagline: 'Selbst-gehostet',
|
||||
description: 'Anfragen laufen zu unserem Server. Schneller, immer noch privat.',
|
||||
privacyDot: 'bg-blue-500',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
tier: 'cloud' as LlmTier,
|
||||
icon: Cloud,
|
||||
title: 'Google Gemini',
|
||||
tagline: 'Stärkstes Modell',
|
||||
description: 'Beste Qualität für komplexe Aufgaben — Daten gehen zu Google.',
|
||||
privacyDot: 'bg-amber-500',
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<div class="mb-6 text-center">
|
||||
<div class="mx-auto mb-3 flex h-14 w-14 items-center justify-center rounded-full bg-primary/10">
|
||||
<Robot size={28} class="text-primary" />
|
||||
</div>
|
||||
<div class="mb-2 text-2xl font-bold">Wie soll Mana KI nutzen?</div>
|
||||
<p class="text-muted-foreground">
|
||||
Mana bietet KI-Funktionen auf vier Ebenen — von "gar keine" bis zu allem. Du entscheidest,
|
||||
welche Schichten dein Vertrauen haben.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Always-on tier 0 -->
|
||||
<div class="mb-3 rounded-xl border border-emerald-500/30 bg-emerald-500/5 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<CheckCircle size={20} weight="fill" class="text-emerald-500" />
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">Lokal (ohne KI) — immer aktiv</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Datum-Erkennung, Suche und einfache Klassifikation laufen offline ohne KI. Brauchst du
|
||||
nichts auswählen — das ist immer da.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggleable tiers -->
|
||||
<div class="space-y-3">
|
||||
{#each cards as card}
|
||||
{@const enabled = settings.allowedTiers.includes(card.tier)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => !card.disabled && toggleTier(card.tier)}
|
||||
disabled={card.disabled}
|
||||
class="w-full rounded-xl border p-4 text-left transition-all {enabled
|
||||
? 'border-primary bg-primary/5 ring-1 ring-primary/30'
|
||||
: 'border-border bg-card hover:border-primary/40'} {card.disabled
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: 'cursor-pointer'}"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg {enabled
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-muted text-muted-foreground'}"
|
||||
>
|
||||
<card.icon size={20} />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-base font-semibold">{card.title}</div>
|
||||
<span class="h-1.5 w-1.5 rounded-full {card.privacyDot}"></span>
|
||||
<span class="text-xs text-muted-foreground">{card.tagline}</span>
|
||||
{#if enabled}
|
||||
<span
|
||||
class="ml-auto rounded-full bg-primary px-2 py-0.5 text-[10px] font-medium text-primary-foreground"
|
||||
>
|
||||
aktiv
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-muted-foreground">{card.description}</p>
|
||||
{#if card.disabled && card.disabledHint}
|
||||
<p class="mt-1 text-xs text-amber-600 dark:text-amber-400">
|
||||
{card.disabledHint}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Live chain summary -->
|
||||
{#if settings.allowedTiers.length > 0}
|
||||
<div class="mt-4 rounded-lg bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
||||
<span class="font-medium text-foreground">Reihenfolge: </span>
|
||||
{settings.allowedTiers.map((t) => tierLabel(t)).join(' → ')} → Lokal (Fallback)
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-6 flex items-start gap-3 rounded-xl bg-primary/5 p-4">
|
||||
<Info size={20} class="mt-0.5 shrink-0 text-primary" />
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Du kannst diese Auswahl jederzeit in den Einstellungen ändern. Es ist auch komplett okay, hier
|
||||
nichts auszuwählen — KI-Funktionen sind in Mana optional und alle Kern-Features funktionieren
|
||||
ohne sie.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { Info } from '@mana/shared-icons';
|
||||
// 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: 'quotes',
|
||||
name: 'Quotes',
|
||||
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: 'cards',
|
||||
name: 'Cards',
|
||||
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 Mana-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">
|
||||
<Info size={20} class="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium">Ein Konto, alle Apps</h4>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Dein Mana-Login funktioniert in allen Apps. Deine Credits werden geteilt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { Check, Gear, House, SquaresFour, CurrencyCircleDollar } from '@mana/shared-icons';
|
||||
|
||||
// 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"
|
||||
>
|
||||
<Check size={20} class="text-white" />
|
||||
</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="/"
|
||||
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"
|
||||
>
|
||||
<House size={20} class="text-primary" />
|
||||
</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="/"
|
||||
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"
|
||||
>
|
||||
<SquaresFour size={20} class="text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold group-hover:text-blue-600 transition-colors">Workbench</div>
|
||||
<div class="text-xs text-muted-foreground">Deine Apps entdecken</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/?app=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"
|
||||
>
|
||||
<CurrencyCircleDollar size={20} class="text-yellow-600 dark:text-yellow-400" />
|
||||
</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="/?app=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"
|
||||
>
|
||||
<Gear size={20} class="text-gray-600 dark:text-gray-400" />
|
||||
</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>
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
<!--
|
||||
Onboarding Context Step — 5 key questions to populate the user context.
|
||||
Uses the same ContextInterview component in compact mode.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import ContextInterview from '$lib/modules/profile/ContextInterview.svelte';
|
||||
</script>
|
||||
|
||||
<div class="context-step">
|
||||
<div class="step-header">
|
||||
<h2 class="step-title">Erzähl Mana über dich</h2>
|
||||
<p class="step-subtitle">
|
||||
Je besser Mana dich kennt, desto relevanter werden Vorschläge und Automatisierungen. Du kannst
|
||||
alles jederzeit im Profil ändern oder ergänzen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="interview-area">
|
||||
<ContextInterview
|
||||
compact
|
||||
limitCategories={['about', 'routine', 'leisure', 'nutrition', 'goals']}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.context-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.step-header {
|
||||
text-align: center;
|
||||
}
|
||||
.step-title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.step-subtitle {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
max-width: 32rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.interview-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { Check, CurrencyCircleDollar, Gift, Lightbulb } from '@mana/shared-icons';
|
||||
|
||||
// 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"
|
||||
>
|
||||
<CurrencyCircleDollar size={32} class="text-white" />
|
||||
</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">
|
||||
<Check size={20} class="text-green-500 mt-0.5 shrink-0" />
|
||||
<span>Du hast ein Credit-Guthaben für alle Mana-Apps</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<Check size={20} class="text-green-500 mt-0.5 shrink-0" />
|
||||
<span>AI-Features wie Chat, Bildgenerierung etc. kosten Credits</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<Check size={20} class="text-green-500 mt-0.5 shrink-0" />
|
||||
<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"
|
||||
>
|
||||
<Gift size={16} />
|
||||
</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">
|
||||
<Lightbulb size={20} class="text-blue-500 shrink-0 mt-0.5" />
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||
<strong>Tipp:</strong> Mit einer Mana Quelle M erhältst du monatlich 1.000 Mana ab 9,99€/Monat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { profileService } from '$lib/api/profile';
|
||||
import { Camera, Check } from '@mana/shared-icons';
|
||||
|
||||
let { nameValue = $bindable('') }: { nameValue?: string } = $props();
|
||||
|
||||
let name = $state(authStore.user?.name || '');
|
||||
|
||||
// Sync name to parent via bindable
|
||||
$effect(() => {
|
||||
nameValue = 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 : $_('common.error_saving');
|
||||
} 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"
|
||||
>
|
||||
<Camera size={20} class="text-white" />
|
||||
</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>
|
||||
<p class="block text-sm font-medium mb-2 text-muted-foreground">E-Mail</p>
|
||||
<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">
|
||||
<Check size={20} />
|
||||
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}
|
||||
<Check size={20} />
|
||||
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>
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Onboarding step: introduces Cloud Sync and lets the user decide
|
||||
* whether to activate it. Shows pricing and explains the local-first
|
||||
* model — sync is optional, data always stays local.
|
||||
*/
|
||||
import { syncBilling } from '$lib/stores/sync-billing.svelte';
|
||||
import { creditsService, type CreditBalance } from '$lib/api/credits';
|
||||
import { Cloud, DeviceMobile, Info, CheckCircle, Warning } from '@mana/shared-icons';
|
||||
import type { BillingInterval } from '$lib/api/sync';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const SYNC_PRICES: Record<BillingInterval, { credits: number; label: string }> = {
|
||||
monthly: { credits: 30, label: 'Monatlich' },
|
||||
quarterly: { credits: 90, label: 'Quartalsweise' },
|
||||
yearly: { credits: 360, label: 'Jährlich' },
|
||||
};
|
||||
|
||||
let balance = $state<CreditBalance | null>(null);
|
||||
let selectedInterval = $state<BillingInterval>('monthly');
|
||||
let activating = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
balance = await creditsService.getBalance();
|
||||
} catch {
|
||||
// Non-critical
|
||||
}
|
||||
});
|
||||
|
||||
async function handleActivate() {
|
||||
activating = true;
|
||||
error = null;
|
||||
try {
|
||||
await syncBilling.activate(selectedInterval);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Aktivierung fehlgeschlagen';
|
||||
} finally {
|
||||
activating = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<div class="mb-6 text-center">
|
||||
<div class="mx-auto mb-3 flex h-14 w-14 items-center justify-center rounded-full bg-primary/10">
|
||||
<Cloud size={28} class="text-primary" />
|
||||
</div>
|
||||
<div class="mb-2 text-2xl font-bold">Cloud Sync</div>
|
||||
<p class="text-muted-foreground">
|
||||
Synchronisiere deine Daten verschlüsselt über alle Geräte — oder nutze Mana nur lokal.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Local-first info -->
|
||||
<div class="mb-4 rounded-xl border border-emerald-500/30 bg-emerald-500/5 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<DeviceMobile size={20} class="text-emerald-500" />
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">Lokal — immer verfügbar</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Alle deine Daten sind lokal auf deinem Gerät gespeichert. Mana funktioniert vollständig
|
||||
offline — auch ohne Cloud Sync.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync option -->
|
||||
{#if syncBilling.active}
|
||||
<div class="rounded-xl border border-primary bg-primary/5 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<CheckCircle size={20} weight="fill" class="text-primary" />
|
||||
<div>
|
||||
<div class="font-semibold">Cloud Sync ist aktiv</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Deine Daten werden über alle Geräte synchronisiert.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex items-start gap-3 mb-4">
|
||||
<div
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary"
|
||||
>
|
||||
<Cloud size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-base font-semibold">Cloud Sync aktivieren</div>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Multi-Device-Sync, automatische Backups, Ende-zu-Ende-Verschlüsselung.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interval selection -->
|
||||
<div class="space-y-2 mb-4">
|
||||
{#each ['monthly', 'quarterly', 'yearly'] as const as iv}
|
||||
<label
|
||||
class="flex items-center justify-between rounded-lg border p-3 cursor-pointer transition-colors {selectedInterval ===
|
||||
iv
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-primary/40'}"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name="sync-interval"
|
||||
value={iv}
|
||||
checked={selectedInterval === iv}
|
||||
onchange={() => (selectedInterval = iv)}
|
||||
class="h-3.5 w-3.5 text-primary"
|
||||
/>
|
||||
<span class="text-sm font-medium">{SYNC_PRICES[iv].label}</span>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-primary">{SYNC_PRICES[iv].credits} Credits</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="mb-3 flex items-center gap-2 rounded-lg bg-red-50 dark:bg-red-900/20 p-3">
|
||||
<Warning size={16} class="text-red-600 dark:text-red-400" />
|
||||
<p class="text-sm text-red-800 dark:text-red-200">{error}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
onclick={handleActivate}
|
||||
disabled={activating ||
|
||||
(balance !== null && balance.balance < SYNC_PRICES[selectedInterval].credits)}
|
||||
class="w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{activating
|
||||
? 'Wird aktiviert...'
|
||||
: `Sync aktivieren (${SYNC_PRICES[selectedInterval].credits} Credits)`}
|
||||
</button>
|
||||
|
||||
{#if balance !== null && balance.balance < SYNC_PRICES[selectedInterval].credits}
|
||||
<p class="mt-2 text-center text-xs text-amber-600 dark:text-amber-400">
|
||||
Nicht genügend Credits ({balance.balance} verfügbar). Du kannst Sync jederzeit später in den
|
||||
Einstellungen aktivieren.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-6 flex items-start gap-3 rounded-xl bg-primary/5 p-4">
|
||||
<Info size={20} class="mt-0.5 shrink-0 text-primary" />
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Sync ist optional — du kannst diesen Schritt überspringen und Mana nur lokal nutzen. Alle
|
||||
Features funktionieren auch ohne Sync. Du kannst jederzeit in den Einstellungen aktivieren.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { Palette, SquaresFour, CurrencyCircleDollar } from '@mana/shared-icons';
|
||||
|
||||
// Welcome step - introduces Mana
|
||||
</script>
|
||||
|
||||
<div class="text-center">
|
||||
<!-- Hero Icon -->
|
||||
<div class="mb-4">
|
||||
<div
|
||||
class="inline-flex h-16 w-16 rounded-xl bg-gradient-to-br from-primary via-primary/80 to-primary/60 items-center justify-center shadow-lg shadow-primary/25"
|
||||
>
|
||||
<span class="text-3xl text-primary-foreground font-bold">M</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-bold mb-2">Willkommen bei Mana!</h2>
|
||||
<p class="text-sm text-muted-foreground mb-4">
|
||||
Dein zentrales Dashboard für alle Mana-Apps. Verwalte deine Aufgaben, Termine, Kontakte und mehr
|
||||
- alles an einem Ort.
|
||||
</p>
|
||||
|
||||
<!-- Feature highlights - using surface-elevated-1 (one level below the modal) -->
|
||||
<div class="space-y-2 text-left">
|
||||
<div class="p-3 rounded-lg bg-surface-elevated-1 border flex items-start gap-3">
|
||||
<div
|
||||
class="h-8 w-8 rounded-md bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<SquaresFour size={16} class="text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-sm">Alle Apps</h3>
|
||||
<p class="text-xs text-muted-foreground">Todo, Kalender, Chat, Picture und mehr.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 rounded-lg bg-surface-elevated-1 border flex items-start gap-3">
|
||||
<div
|
||||
class="h-8 w-8 rounded-md bg-green-100 dark:bg-green-900/30 flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<CurrencyCircleDollar size={16} class="text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-sm">Credits-System</h3>
|
||||
<p class="text-xs text-muted-foreground">Ein Guthaben für alle AI-Features.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 rounded-lg bg-surface-elevated-1 border flex items-start gap-3">
|
||||
<div
|
||||
class="h-8 w-8 rounded-md bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<Palette size={20} class="text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-sm">Personalisierung</h3>
|
||||
<p class="text-xs text-muted-foreground">Themes und Einstellungen nach deinem Geschmack.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -9,8 +9,6 @@ export const STORAGE_KEYS = {
|
|||
NAV_COLLAPSED: 'mana-nav-collapsed',
|
||||
/** Whether the welcome page has been seen */
|
||||
HAS_SEEN_WELCOME: 'hasSeenWelcome',
|
||||
/** Onboarding wizard state (JSON) */
|
||||
ONBOARDING: 'mana-onboarding',
|
||||
/** Dashboard widget layout (JSON) — defined in default-dashboard.ts */
|
||||
// DASHBOARD: 'mana-dashboard-config',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
/**
|
||||
* Onboarding Store
|
||||
* Tracks user onboarding progress and completion status
|
||||
*/
|
||||
|
||||
import { STORAGE_KEYS } from '$lib/config/storage-keys';
|
||||
|
||||
const STORAGE_KEY = STORAGE_KEYS.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();
|
||||
|
|
@ -80,7 +80,6 @@
|
|||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
|
||||
import { getPillAppItems } from '@mana/shared-branding';
|
||||
import { onboardingStore } from '$lib/stores/onboarding.svelte';
|
||||
import { STORAGE_KEYS } from '$lib/config/storage-keys';
|
||||
import { SearchRegistry } from '$lib/search/registry';
|
||||
import { registerAllProviders } from '$lib/search/providers';
|
||||
|
|
@ -146,7 +145,6 @@
|
|||
// ── UI State ────────────────────────────────────────────
|
||||
let isCollapsed = $state(false);
|
||||
let showShortcuts = $state(false);
|
||||
let showOnboarding = $state(false);
|
||||
|
||||
// ── Theme ───────────────────────────────────────────────
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
|
@ -459,7 +457,6 @@
|
|||
// where {@const} narrows the component back to its concrete type.
|
||||
type AnyComponent = Component<any>;
|
||||
let KeyboardShortcutsModalC = $state<AnyComponent | null>(null);
|
||||
let OnboardingWizardC = $state<AnyComponent | null>(null);
|
||||
let GuestWelcomeModalC = $state<AnyComponent | null>(null);
|
||||
let SessionWarningC = $state<AnyComponent | null>(null);
|
||||
let EncryptionIntroBannerC = $state<AnyComponent | null>(null);
|
||||
|
|
@ -474,13 +471,6 @@
|
|||
});
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
if (showOnboarding && !OnboardingWizardC) {
|
||||
import('$lib/components/onboarding').then((m) => {
|
||||
OnboardingWizardC = m.OnboardingWizard;
|
||||
});
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
if (guestMode?.showWelcome && !GuestWelcomeModalC) {
|
||||
import('@mana/shared-auth-ui').then((m) => {
|
||||
|
|
@ -507,19 +497,6 @@
|
|||
});
|
||||
});
|
||||
|
||||
// ── Onboarding ──────────────────────────────────────────
|
||||
function handleOnboardingComplete() {
|
||||
onboardingStore.complete();
|
||||
ManaEvents.onboardingCompleted();
|
||||
showOnboarding = false;
|
||||
}
|
||||
|
||||
function handleOnboardingSkip() {
|
||||
ManaEvents.onboardingSkipped(onboardingStore.currentStep);
|
||||
onboardingStore.skip();
|
||||
showOnboarding = false;
|
||||
}
|
||||
|
||||
// ── Auth Ready (replaces monolith onMount) ──────────────
|
||||
async function handleAuthReady() {
|
||||
// Wire the unified guest-prompt singleton to SvelteKit's `goto`
|
||||
|
|
@ -623,19 +600,10 @@
|
|||
// value (0 on a fresh tab) until a sync actually runs.
|
||||
refreshPendingCount();
|
||||
|
||||
// Phase B-idle: settings, onboarding gating and return-visit
|
||||
// telemetry. None of this gates rendering — onboarding shows
|
||||
// via showOnboarding after the store resolves, which is fine
|
||||
// on a delay.
|
||||
// Phase B-idle: settings + return-visit telemetry. Non-gating.
|
||||
idle(async () => {
|
||||
trackReturnVisit();
|
||||
userSettings.load().catch(() => {});
|
||||
await onboardingStore.load();
|
||||
if (onboardingStore.shouldShow) {
|
||||
onboardingStore.start();
|
||||
ManaEvents.onboardingStarted();
|
||||
showOnboarding = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
// IMPORTANT: do NOT call notificationService.requestPermission() here.
|
||||
|
|
@ -775,16 +743,6 @@
|
|||
appName="Mana"
|
||||
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
|
||||
>
|
||||
<!-- Onboarding Wizard (auth only) — loaded on demand -->
|
||||
{#if showOnboarding && authStore.isAuthenticated && OnboardingWizardC}
|
||||
{@const OnboardingWizard = OnboardingWizardC}
|
||||
<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" class:bg-background={!wallpaperStore.hasWallpaper}>
|
||||
<WallpaperLayer config={wallpaperStore.effective} />
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue