mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:01:09 +02:00
feat(guides): complete Phase 1 — auth routes, i18n, collection detail, import fixes
- Add auth routes: login, register, forgot-password, reset-password (teal branding) - Add i18n setup with de/en locales - Add version.ts and theme store - Add collections/[id] detail page with path progress and per-guide actions - Fix import naming conflict (guidesStore → dbStore alias in app layout) - Fix BaseRecord inline imports → top-level imports in stores and components - Fix authStore.getAccessToken() → getValidToken() - Fix theme import to use local store wrapper - Update plan doc: Phase 1 complete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6d2509c258
commit
a893b07b70
15 changed files with 480 additions and 13 deletions
|
|
@ -133,19 +133,22 @@ apps/guides/apps/web/src/routes/
|
|||
|
||||
## Phasen
|
||||
|
||||
### Phase 1 — MVP (jetzt implementiert) ✓
|
||||
### Phase 1 — MVP ✅ Abgeschlossen
|
||||
- [x] Monorepo-Skelett (package.json, config-files)
|
||||
- [x] Local-Store (5 Collections)
|
||||
- [x] Guest-Seed (3 Demo-Guides)
|
||||
- [x] i18n Setup (de + en)
|
||||
- [x] version.ts
|
||||
- [x] theme Store
|
||||
- [x] Root-Layout + Auth-Layout
|
||||
- [x] Auth-Routes (login, register, forgot-password, reset-password)
|
||||
- [x] Bibliothek-View (+page.svelte)
|
||||
- [x] Guide-Detail-View
|
||||
- [x] Run-Modus (Scroll + Fokus)
|
||||
- [x] Collections-View
|
||||
- [x] Collections-View + Collections-Detail [id]
|
||||
- [x] Verlauf-View
|
||||
- [x] GuideCard-Komponente
|
||||
- [x] GuideEditModal
|
||||
- [x] RunView-Komponente
|
||||
- [x] Registrierung in mana-apps.ts + app-icons.ts
|
||||
|
||||
### Phase 2 — Web-Import & Sharing
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts">
|
||||
import type { LocalGuide, Difficulty } from '$lib/data/local-store.js';
|
||||
import type { BaseRecord } from '@manacore/local-store';
|
||||
|
||||
type GuideInput = Omit<LocalGuide, keyof import('@manacore/local-store').BaseRecord>;
|
||||
type GuideInput = Omit<LocalGuide, keyof BaseRecord>;
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
|
|
|||
23
apps/guides/apps/web/src/lib/i18n/index.ts
Normal file
23
apps/guides/apps/web/src/lib/i18n/index.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { addMessages, init, getLocaleFromNavigator } from 'svelte-i18n';
|
||||
|
||||
import de from './locales/de.json';
|
||||
import en from './locales/en.json';
|
||||
|
||||
const LOCALE_KEY = 'guides_locale';
|
||||
|
||||
addMessages('de', de);
|
||||
addMessages('en', en);
|
||||
|
||||
const saved = typeof localStorage !== 'undefined' ? localStorage.getItem(LOCALE_KEY) : null;
|
||||
const locale = saved ?? getLocaleFromNavigator() ?? 'de';
|
||||
|
||||
init({
|
||||
fallbackLocale: 'de',
|
||||
initialLocale: locale.startsWith('de') ? 'de' : 'en',
|
||||
});
|
||||
|
||||
export function setLocale(lang: 'de' | 'en') {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(LOCALE_KEY, lang);
|
||||
}
|
||||
}
|
||||
22
apps/guides/apps/web/src/lib/i18n/locales/de.json
Normal file
22
apps/guides/apps/web/src/lib/i18n/locales/de.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"app.name": "Guides",
|
||||
"app.description": "Schritt-für-Schritt Anleitungen",
|
||||
"nav.library": "Bibliothek",
|
||||
"nav.collections": "Sammlungen",
|
||||
"nav.history": "Verlauf",
|
||||
"guide.new": "Neue Anleitung",
|
||||
"guide.start_run": "Durchlauf starten",
|
||||
"guide.continue_run": "Fortsetzen",
|
||||
"guide.finish_run": "Durchlauf abschließen",
|
||||
"guide.steps": "Schritte",
|
||||
"guide.difficulty.easy": "Einfach",
|
||||
"guide.difficulty.medium": "Mittel",
|
||||
"guide.difficulty.hard": "Schwer",
|
||||
"run.mode.scroll": "Scroll-Modus",
|
||||
"run.mode.focus": "Fokus-Modus",
|
||||
"common.save": "Speichern",
|
||||
"common.cancel": "Abbrechen",
|
||||
"common.delete": "Löschen",
|
||||
"common.create": "Erstellen",
|
||||
"common.back": "Zurück"
|
||||
}
|
||||
22
apps/guides/apps/web/src/lib/i18n/locales/en.json
Normal file
22
apps/guides/apps/web/src/lib/i18n/locales/en.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"app.name": "Guides",
|
||||
"app.description": "Step-by-Step Guides",
|
||||
"nav.library": "Library",
|
||||
"nav.collections": "Collections",
|
||||
"nav.history": "History",
|
||||
"guide.new": "New Guide",
|
||||
"guide.start_run": "Start Run",
|
||||
"guide.continue_run": "Continue",
|
||||
"guide.finish_run": "Complete Run",
|
||||
"guide.steps": "Steps",
|
||||
"guide.difficulty.easy": "Easy",
|
||||
"guide.difficulty.medium": "Medium",
|
||||
"guide.difficulty.hard": "Hard",
|
||||
"run.mode.scroll": "Scroll Mode",
|
||||
"run.mode.focus": "Focus Mode",
|
||||
"common.save": "Save",
|
||||
"common.cancel": "Cancel",
|
||||
"common.delete": "Delete",
|
||||
"common.create": "Create",
|
||||
"common.back": "Back"
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
* Reads happen via Dexie liveQuery in components.
|
||||
*/
|
||||
|
||||
import type { BaseRecord } from '@manacore/local-store';
|
||||
import {
|
||||
guideCollection,
|
||||
sectionCollection,
|
||||
|
|
@ -39,7 +40,7 @@ export const guidesStore = {
|
|||
},
|
||||
|
||||
async createGuide(
|
||||
data: Omit<LocalGuide, keyof import('@manacore/local-store').BaseRecord>
|
||||
data: Omit<LocalGuide, keyof BaseRecord>
|
||||
): Promise<LocalGuide | null> {
|
||||
return withErrorHandling(async () => {
|
||||
return guideCollection.insert({
|
||||
|
|
@ -70,7 +71,7 @@ export const guidesStore = {
|
|||
|
||||
// ─── Sections ──────────────────────────────────────────
|
||||
|
||||
async createSection(data: Omit<LocalSection, keyof import('@manacore/local-store').BaseRecord>): Promise<LocalSection | null> {
|
||||
async createSection(data: Omit<LocalSection, keyof BaseRecord>): Promise<LocalSection | null> {
|
||||
return withErrorHandling(
|
||||
() => sectionCollection.insert({ id: crypto.randomUUID(), ...data }),
|
||||
'Abschnitt konnte nicht erstellt werden'
|
||||
|
|
@ -94,7 +95,7 @@ export const guidesStore = {
|
|||
|
||||
// ─── Steps ─────────────────────────────────────────────
|
||||
|
||||
async createStep(data: Omit<LocalStep, keyof import('@manacore/local-store').BaseRecord>): Promise<LocalStep | null> {
|
||||
async createStep(data: Omit<LocalStep, keyof BaseRecord>): Promise<LocalStep | null> {
|
||||
return withErrorHandling(
|
||||
() => stepCollection.insert({ id: crypto.randomUUID(), ...data }),
|
||||
'Schritt konnte nicht erstellt werden'
|
||||
|
|
@ -118,7 +119,7 @@ export const guidesStore = {
|
|||
// ─── Collections ───────────────────────────────────────
|
||||
|
||||
async createCollection(
|
||||
data: Omit<LocalCollection, keyof import('@manacore/local-store').BaseRecord>
|
||||
data: Omit<LocalCollection, keyof BaseRecord>
|
||||
): Promise<LocalCollection | null> {
|
||||
return withErrorHandling(
|
||||
() => collectionCollection.insert({ id: crypto.randomUUID(), ...data }),
|
||||
|
|
|
|||
3
apps/guides/apps/web/src/lib/stores/theme.ts
Normal file
3
apps/guides/apps/web/src/lib/stores/theme.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { createThemeStore } from '@manacore/shared-theme';
|
||||
|
||||
export const theme = createThemeStore('guides_theme');
|
||||
4
apps/guides/apps/web/src/lib/version.ts
Normal file
4
apps/guides/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const APP_VERSION = '0.1.0';
|
||||
export const BUILD_TIME: string =
|
||||
typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : new Date().toISOString();
|
||||
export const BUILD_HASH: string = typeof __BUILD_HASH__ !== 'undefined' ? __BUILD_HASH__ : 'dev';
|
||||
|
|
@ -5,8 +5,7 @@
|
|||
import { AuthGate } from '@manacore/shared-auth-ui';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { guidesStore } from '$lib/stores/guides.svelte';
|
||||
import { guidesStore as localStore } from '$lib/data/local-store.js';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { guidesStore as dbStore } from '$lib/data/local-store.js';
|
||||
import { BookOpen, StackSimple, ClockCounterClockwise, Plus } from '@manacore/shared-icons';
|
||||
|
||||
let { children } = $props();
|
||||
|
|
@ -27,9 +26,9 @@
|
|||
href === '/' ? currentPath === '/' : currentPath.startsWith(href);
|
||||
|
||||
onMount(async () => {
|
||||
await localStore.initialize();
|
||||
await dbStore.initialize();
|
||||
if (authStore.isLoggedIn) {
|
||||
localStore.startSync(() => authStore.getAccessToken());
|
||||
dbStore.startSync(() => authStore.getValidToken());
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
collectionCollection,
|
||||
guideCollection,
|
||||
runCollection,
|
||||
type LocalCollection,
|
||||
type LocalGuide,
|
||||
type LocalRun,
|
||||
} from '$lib/data/local-store.js';
|
||||
import { runsStore } from '$lib/stores/runs.svelte';
|
||||
|
||||
let colId = $derived($page.params.id);
|
||||
let collection = $state<LocalCollection | null>(null);
|
||||
let guides = $state<LocalGuide[]>([]);
|
||||
let runs = $state<LocalRun[]>([]);
|
||||
|
||||
$effect(() => {
|
||||
const id = colId;
|
||||
const sub = liveQuery(async () => {
|
||||
const [col, allGuides, allRuns] = await Promise.all([
|
||||
collectionCollection.get(id),
|
||||
guideCollection.getAll(),
|
||||
runCollection.getAll(),
|
||||
]);
|
||||
return { col, allGuides, allRuns };
|
||||
}).subscribe(({ col, allGuides, allRuns }) => {
|
||||
collection = col ?? null;
|
||||
if (!col) return;
|
||||
if (col.type === 'path') {
|
||||
guides = col.guideOrder
|
||||
.map((gid) => allGuides.find((g) => g.id === gid))
|
||||
.filter(Boolean) as LocalGuide[];
|
||||
} else {
|
||||
guides = allGuides.filter((g) => g.collectionId === col.id);
|
||||
}
|
||||
runs = allRuns;
|
||||
});
|
||||
return () => sub.unsubscribe();
|
||||
});
|
||||
|
||||
function isGuideCompleted(guideId: string) {
|
||||
return runs.some((r) => r.guideId === guideId && r.completedAt);
|
||||
}
|
||||
|
||||
function getLastRunDate(guideId: string): string | null {
|
||||
const guideRuns = runs.filter((r) => r.guideId === guideId && r.completedAt);
|
||||
if (!guideRuns.length) return null;
|
||||
const latest = guideRuns.sort((a, b) => b.completedAt!.localeCompare(a.completedAt!))[0];
|
||||
return new Date(latest.completedAt!).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
|
||||
let completedCount = $derived(guides.filter((g) => isGuideCompleted(g.id)).length);
|
||||
let progress = $derived(guides.length > 0 ? Math.round((completedCount / guides.length) * 100) : 0);
|
||||
|
||||
const difficultyConfig = {
|
||||
easy: { label: 'Einfach', color: 'text-green-600' },
|
||||
medium: { label: 'Mittel', color: 'text-amber-600' },
|
||||
hard: { label: 'Schwer', color: 'text-red-600' },
|
||||
};
|
||||
|
||||
async function startGuide(guide: LocalGuide) {
|
||||
// Find or create active run
|
||||
const activeRun = await runsStore.getActiveRun(guide.id);
|
||||
if (activeRun) {
|
||||
goto(`/guide/${guide.id}/run?runId=${activeRun.id}&mode=${activeRun.mode}`);
|
||||
} else {
|
||||
const run = await runsStore.startRun(guide.id, 'scroll');
|
||||
if (run) goto(`/guide/${guide.id}/run?runId=${run.id}&mode=scroll`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !collection}
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<p class="text-muted-foreground">Sammlung nicht gefunden.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mx-auto max-w-2xl p-4 md:p-8">
|
||||
<a href="/collections" class="mb-6 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
||||
← Sammlungen
|
||||
</a>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8 rounded-2xl p-6" style="background-color: {collection.coverColor ?? '#0d9488'}18">
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="text-5xl">{collection.coverEmoji ?? (collection.type === 'path' ? '🗺' : '📚')}</span>
|
||||
<div class="flex-1">
|
||||
<div class="mb-1 flex items-center gap-2">
|
||||
<h1 class="text-2xl font-bold text-foreground">{collection.title}</h1>
|
||||
<span class="rounded-full bg-surface px-2 py-0.5 text-xs text-muted-foreground">
|
||||
{collection.type === 'path' ? 'Lernpfad' : 'Bibliothek'}
|
||||
</span>
|
||||
</div>
|
||||
{#if collection.description}
|
||||
<p class="mb-3 text-sm text-muted-foreground">{collection.description}</p>
|
||||
{/if}
|
||||
<div class="text-sm text-muted-foreground">{guides.length} Anleitungen</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if collection.type === 'path'}
|
||||
<div class="mt-5">
|
||||
<div class="mb-1.5 flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">{completedCount} von {guides.length} abgeschlossen</span>
|
||||
<span class="font-semibold text-primary">{progress}%</span>
|
||||
</div>
|
||||
<div class="h-2 overflow-hidden rounded-full bg-surface">
|
||||
<div class="h-full rounded-full bg-primary transition-all" style="width: {progress}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Guide list -->
|
||||
<div class="space-y-3">
|
||||
{#each guides as guide, i (guide.id)}
|
||||
{@const completed = isGuideCompleted(guide.id)}
|
||||
{@const lastRun = getLastRunDate(guide.id)}
|
||||
<div
|
||||
class="flex items-center gap-4 rounded-xl border p-4 transition-all
|
||||
{completed ? 'border-green-200 bg-green-50/50 dark:border-green-900/30 dark:bg-green-950/20' : 'border-border bg-surface'}"
|
||||
>
|
||||
<!-- Index / Check -->
|
||||
{#if collection.type === 'path'}
|
||||
<div
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-sm font-semibold
|
||||
{completed ? 'bg-green-500 text-white' : 'bg-muted text-muted-foreground'}"
|
||||
>
|
||||
{completed ? '✓' : i + 1}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-2xl">{guide.coverEmoji ?? '📖'}</span>
|
||||
{/if}
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<a href="/guide/{guide.id}" class="block truncate font-medium text-foreground hover:text-primary">
|
||||
{guide.title}
|
||||
</a>
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span class="{difficultyConfig[guide.difficulty].color}">{difficultyConfig[guide.difficulty].label}</span>
|
||||
{#if guide.estimatedMinutes}
|
||||
<span>· {guide.estimatedMinutes}min</span>
|
||||
{/if}
|
||||
{#if lastRun}
|
||||
<span>· ✓ {lastRun}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action -->
|
||||
<button
|
||||
onclick={() => startGuide(guide)}
|
||||
class="flex-shrink-0 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors
|
||||
{completed
|
||||
? 'border border-border text-muted-foreground hover:bg-accent'
|
||||
: 'bg-primary text-white hover:bg-primary-hover'}"
|
||||
>
|
||||
{completed ? 'Wiederholen' : 'Starten'}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if guides.length === 0}
|
||||
<div class="py-16 text-center">
|
||||
<p class="text-muted-foreground">Diese Sammlung enthält noch keine Anleitungen.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
let error = $state('');
|
||||
let success = $state(false);
|
||||
let loading = $state(false);
|
||||
|
||||
async function handleResetPassword(email: string) {
|
||||
loading = true;
|
||||
error = '';
|
||||
success = false;
|
||||
const result = await authStore.resetPassword(email);
|
||||
if (result.success) success = true;
|
||||
else error = result.error || 'Passwort-Zurücksetzung fehlgeschlagen';
|
||||
loading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ForgotPasswordPage
|
||||
appName="Guides"
|
||||
appLogo=""
|
||||
{loading}
|
||||
{error}
|
||||
{success}
|
||||
onSubmit={handleResetPassword}
|
||||
loginHref="/login"
|
||||
/>
|
||||
56
apps/guides/apps/web/src/routes/(auth)/login/+page.svelte
Normal file
56
apps/guides/apps/web/src/routes/(auth)/login/+page.svelte
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { APP_VERSION, BUILD_TIME } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
const verified = $derived($page.url.searchParams.get('verified') === 'true');
|
||||
const initialEmail = $derived($page.url.searchParams.get('email') || '');
|
||||
|
||||
const redirectTo = $derived.by(() => {
|
||||
const queryRedirect = $page.url.searchParams.get('redirectTo');
|
||||
if (queryRedirect) return queryRedirect;
|
||||
if (browser) {
|
||||
const sessionRedirect = sessionStorage.getItem('auth-return-url');
|
||||
if (sessionRedirect) {
|
||||
sessionStorage.removeItem('auth-return-url');
|
||||
return sessionRedirect;
|
||||
}
|
||||
}
|
||||
return '/';
|
||||
});
|
||||
|
||||
const translations = $derived(getLoginTranslations($locale || 'de'));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{translations.title} | Guides</title>
|
||||
</svelte:head>
|
||||
|
||||
<LoginPage
|
||||
appName="Guides"
|
||||
primaryColor="#0d9488"
|
||||
onSignIn={(email, password) => authStore.signIn(email, password)}
|
||||
onResendVerification={(email) => authStore.resendVerificationEmail(email)}
|
||||
passkeyAvailable={authStore.isPasskeyAvailable()}
|
||||
onSignInWithPasskey={() => authStore.signInWithPasskey()}
|
||||
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
|
||||
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
|
||||
onSendMagicLink={(email) => authStore.sendMagicLink(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
lightBackground="#f0fdfa"
|
||||
darkBackground="#042f2e"
|
||||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
buildTime={BUILD_TIME}
|
||||
/>
|
||||
39
apps/guides/apps/web/src/routes/(auth)/register/+page.svelte
Normal file
39
apps/guides/apps/web/src/routes/(auth)/register/+page.svelte
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { RegisterPage } from '@manacore/shared-auth-ui';
|
||||
import { getRegisterTranslations } from '@manacore/shared-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
const redirectTo = $derived.by(() => {
|
||||
if (browser) {
|
||||
const sessionRedirect = sessionStorage.getItem('auth-return-url');
|
||||
if (sessionRedirect) {
|
||||
sessionStorage.removeItem('auth-return-url');
|
||||
return sessionRedirect;
|
||||
}
|
||||
}
|
||||
return '/';
|
||||
});
|
||||
|
||||
const translations = $derived(getRegisterTranslations($locale || 'de'));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{translations.title} | Guides</title>
|
||||
</svelte:head>
|
||||
|
||||
<RegisterPage
|
||||
appName="Guides"
|
||||
primaryColor="#0d9488"
|
||||
onSignUp={(email, password) => authStore.signUp(email, password)}
|
||||
onResendVerification={(email) => authStore.resendVerificationEmail(email)}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
loginPath="/login"
|
||||
lightBackground="#f0fdfa"
|
||||
darkBackground="#042f2e"
|
||||
{translations}
|
||||
/>
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
let loading = $state(false);
|
||||
let hasToken = $state(false);
|
||||
let token = $state<string | null>(null);
|
||||
let password = $state('');
|
||||
let confirmPassword = $state('');
|
||||
let error = $state<string | null>(null);
|
||||
let success = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
token = $page.url.searchParams.get('token');
|
||||
hasToken = !!token;
|
||||
});
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = null;
|
||||
if (!token) { error = 'Ungültiger Link'; return; }
|
||||
if (password !== confirmPassword) { error = 'Passwörter stimmen nicht überein'; return; }
|
||||
if (password.length < 8) { error = 'Mindestens 8 Zeichen'; return; }
|
||||
loading = true;
|
||||
try {
|
||||
const result = await authStore.resetPasswordWithToken(token, password);
|
||||
if (!result.success) error = result.error || 'Fehler beim Zurücksetzen';
|
||||
else { success = true; setTimeout(() => goto('/login'), 3000); }
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Unbekannter Fehler';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Passwort zurücksetzen | Guides</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex min-h-screen flex-col bg-gradient-to-b from-teal-50 to-white dark:from-teal-950 dark:to-neutral-900">
|
||||
<header class="p-4">
|
||||
<a href="/" class="text-xl font-semibold text-teal-600">📖 Guides</a>
|
||||
</header>
|
||||
|
||||
<main class="flex flex-1 items-center justify-center p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<h1 class="mb-8 text-center text-2xl font-bold text-foreground">Passwort zurücksetzen</h1>
|
||||
|
||||
{#if success}
|
||||
<div class="rounded-2xl bg-surface p-8 text-center shadow-lg">
|
||||
<div class="mb-4 text-5xl">✅</div>
|
||||
<p class="mb-6 text-sm text-muted-foreground">Passwort erfolgreich geändert. Du wirst weitergeleitet...</p>
|
||||
<a href="/login" class="rounded-lg bg-primary px-6 py-3 text-sm font-medium text-white">Zum Login</a>
|
||||
</div>
|
||||
{:else if hasToken}
|
||||
<div class="rounded-2xl bg-surface p-8 shadow-lg">
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
{#if error}
|
||||
<div class="rounded-lg bg-red-50 p-3 text-sm text-red-700">{error}</div>
|
||||
{/if}
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-foreground">Neues Passwort</label>
|
||||
<input type="password" bind:value={password} required minlength={8} autocomplete="new-password"
|
||||
class="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-foreground">Passwort bestätigen</label>
|
||||
<input type="password" bind:value={confirmPassword} required minlength={8} autocomplete="new-password"
|
||||
class="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30" />
|
||||
</div>
|
||||
<button type="submit" disabled={loading}
|
||||
class="w-full rounded-xl bg-primary py-3 text-sm font-semibold text-white hover:bg-primary-hover disabled:opacity-50">
|
||||
{loading ? 'Wird gespeichert...' : 'Passwort ändern'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-2xl bg-surface p-8 text-center shadow-lg">
|
||||
<div class="mb-4 text-5xl">⚠️</div>
|
||||
<p class="mb-6 text-sm text-muted-foreground">Dieser Link ist ungültig oder abgelaufen.</p>
|
||||
<a href="/forgot-password" class="rounded-lg bg-primary px-6 py-3 text-sm font-medium text-white">
|
||||
Neuen Link anfordern
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
import '$lib/i18n';
|
||||
import { onMount } from 'svelte';
|
||||
import { isLoading as i18nLoading } from 'svelte-i18n';
|
||||
import { theme } from '@manacore/shared-theme';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue