feat(mana-games): add auth, settings, themes, help, submit, and onboarding pages

Adds login/register/forgot-password auth routes using shared-auth-ui,
settings page with theme/language/account controls, themes browser,
help page, community submit form, profile, and app onboarding modal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-29 14:54:31 +02:00
parent 732f2b8edd
commit 0806600bc0
14 changed files with 602 additions and 0 deletions

View file

@ -0,0 +1,41 @@
import { createAppOnboardingStore, type AppOnboardingStep } from '@manacore/shared-app-onboarding';
import { userSettings } from './user-settings.svelte';
const onboardingSteps: AppOnboardingStep[] = [
{
id: 'features',
type: 'info',
question: 'Willkommen bei Mana Games!',
description: 'Das erwartet dich:',
emoji: '🎮',
gradient: { from: 'green-500', to: 'green-700' },
bullets: [
'22+ Browser-Spiele direkt spielbar',
'KI-Spielgenerator: Erstelle eigene Games',
'Statistiken: Highscores & Spielzeit',
'Community: Reiche eigene Spiele ein',
],
},
{
id: 'welcome',
type: 'info',
question: "Los geht's!",
description: 'Tipps:',
emoji: '🕹️',
gradient: { from: 'primary', to: 'primary/70' },
bullets: [
'Cmd/Ctrl+K für Schnellsuche',
'Spiele laufen komplett im Browser',
'Stats werden lokal gespeichert',
'Anmelden synchronisiert deine Daten',
],
},
];
export const gamesOnboarding = createAppOnboardingStore({
appId: 'mana-games',
steps: onboardingSteps,
userSettings,
onComplete: async () => {},
onSkip: async () => {},
});

View file

@ -26,6 +26,8 @@
import { setLocale, supportedLocales } from '$lib/i18n';
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import { gamesOnboarding } from '$lib/stores/app-onboarding.svelte';
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
import { gamesStore } from '$lib/data/local-store';
import {
tagLocalStore,
@ -241,6 +243,10 @@
/>
</div>
{#if gamesOnboarding.shouldShow}
<MiniOnboardingModal store={gamesOnboarding} appName="Mana Games" appEmoji="🎮" />
{/if}
<GuestWelcomeModal
appId="mana-games"
visible={showGuestWelcome}

View file

@ -0,0 +1,47 @@
<svelte:head>
<title>Hilfe - Mana Games</title>
</svelte:head>
<div class="max-w-2xl mx-auto space-y-6">
<h1 class="text-2xl font-bold text-foreground">Hilfe</h1>
<section class="space-y-4">
<div class="rounded-xl border border-border bg-card p-4">
<h2 class="font-semibold text-foreground mb-2">Wie spiele ich?</h2>
<p class="text-sm text-muted-foreground">
Wähle ein Spiel auf der Startseite aus und klicke darauf. Das Spiel läuft direkt im Browser.
Die Steuerung wird auf der Spielseite angezeigt.
</p>
</div>
<div class="rounded-xl border border-border bg-card p-4">
<h2 class="font-semibold text-foreground mb-2">KI-Spielgenerator</h2>
<p class="text-sm text-muted-foreground">
Unter "Erstellen" kannst du eigene Spiele beschreiben und von verschiedenen KI-Modellen
generieren lassen. Generierte Spiele werden lokal in deinem Browser gespeichert.
</p>
</div>
<div class="rounded-xl border border-border bg-card p-4">
<h2 class="font-semibold text-foreground mb-2">Statistiken</h2>
<p class="text-sm text-muted-foreground">
Deine Highscores, Spielzeiten und Fortschritte werden automatisch gespeichert. Melde dich
an, um sie geräteübergreifend zu synchronisieren.
</p>
</div>
<div class="rounded-xl border border-border bg-card p-4">
<h2 class="font-semibold text-foreground mb-2">Tastaturkürzel</h2>
<div class="grid grid-cols-2 gap-2 mt-2">
{#each [['Cmd/Ctrl+K', 'Schnellsuche'], ['Esc', 'Suche schließen']] as [key, desc]}
<div class="flex items-center gap-2">
<kbd class="px-2 py-0.5 rounded bg-muted text-xs font-mono text-muted-foreground"
>{key}</kbd
>
<span class="text-sm text-foreground">{desc}</span>
</div>
{/each}
</div>
</div>
</section>
</div>

View file

@ -0,0 +1,9 @@
<svelte:head>
<title>Mana - Mana Games</title>
</svelte:head>
<div class="max-w-2xl mx-auto text-center py-12">
<p class="text-4xl mb-4">💎</p>
<h1 class="text-2xl font-bold text-foreground">Mana</h1>
<p class="text-muted-foreground mt-2">Demnächst verfügbar.</p>
</div>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { ProfilePage } from '@manacore/shared-profile-ui';
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
</script>
<svelte:head>
<title>Profil - Mana Games</title>
</svelte:head>
{#if authStore.isAuthenticated}
<ProfilePage {authStore} {goto} />
{:else}
<div class="max-w-2xl mx-auto text-center py-12">
<p class="text-muted-foreground">Bitte melde dich an.</p>
<a href="/login" class="text-primary hover:underline mt-2 inline-block">Anmelden</a>
</div>
{/if}

View file

@ -0,0 +1,165 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { locale } from 'svelte-i18n';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { setLocale, supportedLocales } from '$lib/i18n';
import { goto } from '$app/navigation';
import { gameStatsCollection } from '$lib/data/local-store';
async function clearStats() {
const all = await gameStatsCollection.getAll();
for (const stat of all) {
await gameStatsCollection.remove(stat.id);
}
}
async function handleLogout() {
await authStore.signOut();
goto('/login');
}
</script>
<svelte:head>
<title>{$_('nav.settings')} - Mana Games</title>
</svelte:head>
<div class="settings-page">
<header class="mb-8">
<h1 class="text-2xl font-bold text-foreground">{$_('nav.settings')}</h1>
<p class="text-muted-foreground text-sm mt-1">Passe Mana Games an deine Bedürfnisse an</p>
</header>
<!-- Theme -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Darstellung</h2>
<div class="setting-row">
<div>
<div class="setting-label">Farbmodus</div>
<div class="setting-desc">Hell, Dunkel oder System</div>
</div>
<div class="flex gap-1">
{#each ['light', 'dark', 'system'] as mode}
<button
class="px-3 py-1.5 text-sm rounded-lg transition-colors {theme.mode === mode
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
onclick={() => theme.setMode(mode as 'light' | 'dark' | 'system')}
>
{mode === 'light' ? 'Hell' : mode === 'dark' ? 'Dunkel' : 'System'}
</button>
{/each}
</div>
</div>
</section>
<!-- Language -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Sprache</h2>
<div class="setting-row">
<div>
<div class="setting-label">App-Sprache</div>
<div class="setting-desc">Sprache der Benutzeroberfläche</div>
</div>
<select
value={$locale}
onchange={(e) => setLocale((e.target as HTMLSelectElement).value as any)}
class="h-9 px-3 rounded-lg bg-background border border-border text-foreground text-sm"
>
{#each supportedLocales as loc}
<option value={loc}>{loc === 'de' ? 'Deutsch' : 'English'}</option>
{/each}
</select>
</div>
</section>
<!-- Account -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Konto</h2>
{#if authStore.isAuthenticated}
<div class="setting-row">
<div>
<div class="setting-label">Eingeloggt als</div>
<div class="setting-desc">{authStore.user?.email}</div>
</div>
<button
class="px-4 py-2 rounded-lg bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-colors text-sm"
onclick={handleLogout}
>
Abmelden
</button>
</div>
{:else}
<div class="setting-row">
<div>
<div class="setting-label">Gast-Modus</div>
<div class="setting-desc">Melde dich an, um Stats zu synchronisieren</div>
</div>
<a
href="/login"
class="px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors text-sm"
>
Anmelden
</a>
</div>
{/if}
</section>
<!-- Data -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Daten</h2>
<div class="setting-row">
<div>
<div class="setting-label">Spielstatistiken löschen</div>
<div class="setting-desc">Alle Highscores und Spielzeiten zurücksetzen</div>
</div>
<button
class="px-4 py-2 rounded-lg bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-colors text-sm"
onclick={clearStats}
>
Löschen
</button>
</div>
</section>
</div>
<style>
.settings-page {
max-width: 600px;
margin: 0 auto;
}
.settings-section {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid hsl(var(--border));
}
.settings-section:last-child {
border-bottom: none;
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 0;
}
.setting-label {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--foreground));
}
.setting-desc {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
margin-top: 2px;
}
</style>

View file

@ -0,0 +1,202 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { authStore } from '$lib/stores/auth.svelte';
const BACKEND_URL = import.meta.env.DEV
? 'http://localhost:3011'
: import.meta.env.PUBLIC_MANA_GAMES_BACKEND_URL || '';
let title = $state('');
let description = $state('');
let controls = $state('');
let difficulty = $state<'Einfach' | 'Mittel' | 'Schwer'>('Mittel');
let tags = $state('');
let htmlCode = $state('');
let authorName = $state('');
let isSubmitting = $state(false);
let submitResult = $state<{ success: boolean; message: string } | null>(null);
async function handleSubmit() {
if (!title.trim() || !htmlCode.trim() || !authorName.trim()) return;
isSubmitting = true;
submitResult = null;
try {
const response = await fetch(`${BACKEND_URL}/api/games/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title,
description,
controls,
difficulty,
complexity: 'Mittel',
tags: tags
.split(',')
.map((t) => t.trim())
.filter(Boolean),
author: { name: authorName },
files: { html: htmlCode },
submittedAt: new Date().toISOString(),
}),
});
const data = await response.json();
submitResult = {
success: data.success,
message: data.success
? `Eingereicht! PR #${data.prNumber} erstellt.`
: data.error || 'Fehler beim Einreichen.',
};
if (data.success) {
title = '';
description = '';
controls = '';
tags = '';
htmlCode = '';
}
} catch {
submitResult = { success: false, message: 'Verbindungsfehler zum Backend.' };
} finally {
isSubmitting = false;
}
}
</script>
<svelte:head>
<title>Spiel einreichen - Mana Games</title>
</svelte:head>
<div class="max-w-2xl mx-auto space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">Spiel einreichen</h1>
<p class="text-muted-foreground mt-1">Reiche dein eigenes HTML5-Spiel bei der Community ein.</p>
</div>
{#if !authStore.isAuthenticated}
<div class="rounded-xl border border-border bg-card p-6 text-center">
<p class="text-muted-foreground mb-4">Bitte melde dich an, um ein Spiel einzureichen.</p>
<a
href="/login"
class="inline-block px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
Anmelden
</a>
</div>
{:else}
<form
onsubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
class="space-y-4"
>
<div>
<label for="title" class="block text-sm font-medium text-foreground mb-1">Titel *</label>
<input
id="title"
type="text"
bind:value={title}
required
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div>
<label for="author" class="block text-sm font-medium text-foreground mb-1">Autor *</label>
<input
id="author"
type="text"
bind:value={authorName}
required
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div>
<label for="desc" class="block text-sm font-medium text-foreground mb-1">Beschreibung</label
>
<textarea
id="desc"
bind:value={description}
rows="3"
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none"
></textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="controls" class="block text-sm font-medium text-foreground mb-1"
>Steuerung</label
>
<input
id="controls"
type="text"
bind:value={controls}
placeholder="Pfeiltasten, Maus..."
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div>
<label for="difficulty" class="block text-sm font-medium text-foreground mb-1"
>Schwierigkeit</label
>
<select
id="difficulty"
bind:value={difficulty}
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
>
<option value="Einfach">Einfach</option>
<option value="Mittel">Mittel</option>
<option value="Schwer">Schwer</option>
</select>
</div>
</div>
<div>
<label for="tags" class="block text-sm font-medium text-foreground mb-1"
>Tags (kommagetrennt)</label
>
<input
id="tags"
type="text"
bind:value={tags}
placeholder="Arcade, Action, Puzzle"
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div>
<label for="html" class="block text-sm font-medium text-foreground mb-1">HTML-Code *</label>
<textarea
id="html"
bind:value={htmlCode}
rows="12"
required
placeholder="<!DOCTYPE html>..."
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground font-mono text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none"
></textarea>
</div>
{#if submitResult}
<div
class="rounded-lg border p-3 text-sm {submitResult.success
? 'border-green-500/30 bg-green-500/10 text-green-400'
: 'border-red-500/30 bg-red-500/10 text-red-400'}"
>
{submitResult.message}
</div>
{/if}
<button
type="submit"
disabled={!title.trim() || !htmlCode.trim() || !authorName.trim() || isSubmitting}
class="w-full px-4 py-2.5 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Wird eingereicht...' : 'Spiel einreichen'}
</button>
</form>
{/if}
</div>

View file

@ -0,0 +1,9 @@
<svelte:head>
<title>Tags - Mana Games</title>
</svelte:head>
<div class="max-w-2xl mx-auto text-center py-12">
<p class="text-4xl mb-4">🏷️</p>
<h1 class="text-2xl font-bold text-foreground">Tags</h1>
<p class="text-muted-foreground mt-2">Demnächst verfügbar.</p>
</div>

View file

@ -0,0 +1,36 @@
<script lang="ts">
import { theme } from '$lib/stores/theme.svelte';
import { THEME_DEFINITIONS, EXTENDED_THEME_VARIANTS } from '@manacore/shared-theme';
import type { ThemeVariant } from '@manacore/shared-theme';
const allThemes = EXTENDED_THEME_VARIANTS;
</script>
<svelte:head>
<title>Themes - Mana Games</title>
</svelte:head>
<div class="max-w-4xl mx-auto space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">Themes</h1>
<p class="text-muted-foreground mt-1">Wähle ein Theme für Mana Games</p>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
{#each allThemes as variant}
{@const def = THEME_DEFINITIONS[variant]}
{#if def}
<button
onclick={() => theme.setVariant(variant)}
class="rounded-xl border p-4 text-left transition-all hover:-translate-y-0.5 {theme.variant ===
variant
? 'border-primary bg-primary/5 ring-2 ring-primary/30'
: 'border-border bg-card hover:border-primary/30'}"
>
<div class="text-2xl mb-2">{def.icon || '🎨'}</div>
<div class="font-medium text-foreground text-sm">{def.label}</div>
</button>
{/if}
{/each}
</div>
</div>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<svelte:head>
<title>Mana Games - Passwort vergessen</title>
</svelte:head>
<ForgotPasswordPage {authStore} {goto} appName="Mana Games" loginHref="/login" />

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { LoginPage } from '@manacore/shared-auth-ui';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<svelte:head>
<title>Mana Games - Login</title>
</svelte:head>
<LoginPage
{authStore}
{goto}
appName="Mana Games"
registerHref="/register"
forgotPasswordHref="/forgot-password"
primaryColor="#00ff88"
/>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { RegisterPage } from '@manacore/shared-auth-ui';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<svelte:head>
<title>Mana Games - Registrieren</title>
</svelte:head>
<RegisterPage {authStore} {goto} appName="Mana Games" loginHref="/login" primaryColor="#00ff88" />

View file

@ -0,0 +1,15 @@
<script lang="ts">
import { goto } from '$app/navigation';
</script>
<svelte:head>
<title>Mana Games - Passwort zurücksetzen</title>
</svelte:head>
<div class="min-h-screen flex items-center justify-center">
<div class="max-w-md w-full p-6 text-center">
<h1 class="text-xl font-bold text-foreground mb-4">Passwort zurücksetzen</h1>
<p class="text-muted-foreground mb-6">Funktion wird eingerichtet.</p>
<a href="/login" class="text-primary hover:underline">Zurück zum Login</a>
</div>
</div>

View file

@ -0,0 +1,14 @@
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
return new Response(
JSON.stringify({
status: 'ok',
service: 'mana-games-web',
timestamp: new Date().toISOString(),
}),
{
headers: { 'Content-Type': 'application/json' },
}
);
};