feat(nutriphi): migrate to shared auth UI components

- Add nutriphi branding to shared-branding package (types, config, logo)
- Add nutriphi icon to app-icons and MANA_APPS for AppSlider
- Replace custom login/register pages with shared LoginPage/RegisterPage
- Add forgot-password page using shared ForgotPasswordPage component
- Create AppSlider component for nutriphi web
- Update vite.config.ts with SSR config for shared packages
- Add nutriphi env variables to generate-env.mjs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-29 06:04:35 +01:00
parent f1e27f3beb
commit 8b61399a64
13 changed files with 295 additions and 263 deletions

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { AppSlider, type AppItem } from '@manacore/shared-ui';
import { MANA_APPS, APP_STATUS_LABELS, APP_SLIDER_LABELS } from '@manacore/shared-branding';
// Convert MANA_APPS to AppItem format (German)
const apps: AppItem[] = MANA_APPS.map((app) => ({
name: app.name,
description: app.description.de,
longDescription: app.longDescription.de,
icon: app.icon,
color: app.color,
comingSoon: app.comingSoon,
status: app.status,
}));
const statusLabels = APP_STATUS_LABELS.de;
const labels = APP_SLIDER_LABELS.de;
function handleAppClick(app: AppItem, index: number) {
console.log('Opening app:', app.name);
}
</script>
<AppSlider
{apps}
title={labels.title}
isDark={false}
{statusLabels}
comingSoonLabel={labels.comingSoon}
openAppLabel={labels.openApp}
onAppClick={handleAppClick}
/>

View file

@ -0,0 +1,48 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
import { NutriPhiLogo } from '@manacore/shared-branding';
import { auth } from '$lib/stores/auth';
import AppSlider from '$lib/components/AppSlider.svelte';
// German translations
const translations = {
titleForm: 'Passwort zurücksetzen',
titleSuccess: 'E-Mail gesendet',
description:
'Gib deine E-Mail-Adresse ein und wir senden dir einen Link zum Zurücksetzen deines Passworts.',
emailPlaceholder: 'E-Mail',
sendResetLinkButton: 'Link senden',
sending: 'Wird gesendet...',
backToLogin: 'Zurück zur Anmeldung',
resendEmail: 'E-Mail erneut senden',
successMessage:
'Wir haben einen Link zum Zurücksetzen deines Passworts an {email} gesendet. Bitte überprüfe deinen Posteingang.',
emailRequired: 'E-Mail ist erforderlich',
sendFailed: 'Fehler beim Senden der E-Mail',
};
async function handleForgotPassword(email: string) {
return auth.forgotPassword(email);
}
</script>
<svelte:head>
<title>Passwort zurücksetzen | Nutriphi</title>
</svelte:head>
<ForgotPasswordPage
appName="Nutriphi"
logo={NutriPhiLogo}
primaryColor="#10b981"
onForgotPassword={handleForgotPassword}
{goto}
loginPath="/login"
lightBackground="#d1fae5"
darkBackground="#022c22"
{translations}
>
{#snippet appSlider()}
<AppSlider />
{/snippet}
</ForgotPasswordPage>

View file

@ -1,113 +1,65 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { LoginPage } from '@manacore/shared-auth-ui';
import { NutriPhiLogo } from '@manacore/shared-branding';
import { auth } from '$lib/stores/auth';
import AppSlider from '$lib/components/AppSlider.svelte';
let email = $state('');
let password = $state('');
let error = $state('');
let isLoading = $state(false);
// Get redirect URL from query params
const redirectTo = $derived($page.url.searchParams.get('redirectTo') || '/meals');
async function handleSubmit(e: Event) {
e.preventDefault();
error = '';
isLoading = true;
// German translations
const translations = {
title: 'Anmelden',
subtitle: 'Melde dich mit deinem Konto an',
emailPlaceholder: 'E-Mail',
passwordPlaceholder: 'Passwort',
rememberMe: 'Angemeldet bleiben',
forgotPassword: 'Passwort vergessen?',
signInButton: 'Anmelden',
signingIn: 'Wird angemeldet...',
success: 'Erfolgreich!',
orDivider: 'oder',
noAccount: 'Noch kein Konto?',
createAccount: 'Jetzt registrieren',
skipToForm: 'Zum Login-Formular springen',
showPassword: 'Passwort anzeigen',
hidePassword: 'Passwort verbergen',
emailRequired: 'E-Mail ist erforderlich',
emailInvalid: 'Bitte gib eine gültige E-Mail-Adresse ein',
passwordRequired: 'Passwort ist erforderlich',
signInFailed: 'Anmeldung fehlgeschlagen',
googleSignInFailed: 'Google-Anmeldung fehlgeschlagen',
signInSuccess: 'Erfolgreich angemeldet. Weiterleitung...',
googleSignInSuccess: 'Erfolgreich mit Google angemeldet. Weiterleitung...',
};
const result = await auth.signIn(email, password);
if (result.success) {
goto('/meals');
} else {
if (result.error === 'INVALID_CREDENTIALS') {
error = 'Ungültige E-Mail oder Passwort';
} else if (result.error === 'EMAIL_NOT_VERIFIED') {
error = 'Bitte bestätige zuerst deine E-Mail-Adresse';
} else {
error = result.error || 'Anmeldung fehlgeschlagen';
}
}
isLoading = false;
async function handleSignIn(email: string, password: string) {
return auth.signIn(email, password);
}
</script>
<main class="flex min-h-screen items-center justify-center p-4">
<div class="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl dark:bg-gray-800">
<div class="mb-6 text-center">
<div
class="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-green-400 to-emerald-500"
>
<span class="text-4xl">🥗</span>
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Anmelden</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">Willkommen zurück bei Nutriphi</p>
</div>
<svelte:head>
<title>Anmelden | Nutriphi</title>
</svelte:head>
{#if error}
<div class="mb-4 rounded-lg bg-red-100 p-3 text-red-700 dark:bg-red-900/30 dark:text-red-400">
{error}
</div>
{/if}
<form onsubmit={handleSubmit} class="space-y-4">
<div>
<label for="email" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
E-Mail
</label>
<input
type="email"
id="email"
bind:value={email}
required
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="deine@email.de"
/>
</div>
<div>
<label
for="password"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Passwort
</label>
<input
type="password"
id="password"
bind:value={password}
required
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Dein Passwort"
/>
</div>
<button
type="submit"
disabled={isLoading}
class="w-full rounded-xl bg-gradient-to-r from-green-500 to-emerald-600 px-6 py-3 font-semibold text-white shadow-lg transition-all hover:from-green-600 hover:to-emerald-700 hover:shadow-xl disabled:cursor-not-allowed disabled:opacity-50"
>
{isLoading ? 'Wird angemeldet...' : 'Anmelden'}
</button>
</form>
<div class="mt-6 text-center">
<p class="text-gray-600 dark:text-gray-400">
Noch kein Konto?
<a
href="/register"
class="font-semibold text-green-600 hover:text-green-700 dark:text-green-400"
>
Registrieren
</a>
</p>
</div>
<div class="mt-4 text-center">
<a
href="/"
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
Zurück zur Startseite
</a>
</div>
</div>
</main>
<LoginPage
appName="Nutriphi"
logo={NutriPhiLogo}
primaryColor="#10b981"
onSignIn={handleSignIn}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"
lightBackground="#d1fae5"
darkBackground="#022c22"
{translations}
>
{#snippet appSlider()}
<AppSlider />
{/snippet}
</LoginPage>

View file

@ -1,168 +1,56 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { RegisterPage } from '@manacore/shared-auth-ui';
import { NutriPhiLogo } from '@manacore/shared-branding';
import { auth } from '$lib/stores/auth';
import AppSlider from '$lib/components/AppSlider.svelte';
let email = $state('');
let password = $state('');
let confirmPassword = $state('');
let error = $state('');
let isLoading = $state(false);
let needsVerification = $state(false);
// German translations
const translations = {
title: 'Konto erstellen',
emailPlaceholder: 'E-Mail',
passwordPlaceholder: 'Passwort',
confirmPasswordPlaceholder: 'Passwort bestätigen',
passwordRequirements:
'Passwort muss mindestens 8 Zeichen mit Kleinbuchstaben, Großbuchstaben, Zahl und Sonderzeichen enthalten.',
createAccountButton: 'Konto erstellen',
creatingAccount: 'Wird erstellt...',
backToLogin: 'Zurück zur Anmeldung',
showPassword: 'Passwort anzeigen',
hidePassword: 'Passwort verbergen',
emailRequired: 'E-Mail ist erforderlich',
passwordRequired: 'Passwort ist erforderlich',
confirmPasswordRequired: 'Bitte bestätige dein Passwort',
passwordsDoNotMatch: 'Passwörter stimmen nicht überein',
passwordTooShort: 'Passwort muss mindestens 8 Zeichen lang sein',
passwordStrengthError:
'Passwort muss Kleinbuchstaben, Großbuchstaben, Zahl und Sonderzeichen enthalten',
registrationFailed: 'Registrierung fehlgeschlagen',
accountCreated: 'Konto erstellt! Bitte überprüfe deine E-Mails zur Bestätigung.',
};
async function handleSubmit(e: Event) {
e.preventDefault();
error = '';
if (password !== confirmPassword) {
error = 'Die Passwörter stimmen nicht überein';
return;
}
if (password.length < 8) {
error = 'Das Passwort muss mindestens 8 Zeichen lang sein';
return;
}
isLoading = true;
const result = await auth.signUp(email, password);
if (result.success) {
if (result.needsVerification) {
needsVerification = true;
} else {
goto('/meals');
}
} else {
if (result.error?.includes('already in use')) {
error = 'Diese E-Mail-Adresse wird bereits verwendet';
} else {
error = result.error || 'Registrierung fehlgeschlagen';
}
}
isLoading = false;
async function handleSignUp(email: string, password: string) {
return auth.signUp(email, password);
}
</script>
<main class="flex min-h-screen items-center justify-center p-4">
<div class="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl dark:bg-gray-800">
<div class="mb-6 text-center">
<div
class="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-green-400 to-emerald-500"
>
<span class="text-4xl">🥗</span>
</div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Registrieren</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">Erstelle dein Nutriphi Konto</p>
</div>
<svelte:head>
<title>Registrieren | Nutriphi</title>
</svelte:head>
{#if needsVerification}
<div
class="rounded-lg bg-green-100 p-4 text-center text-green-700 dark:bg-green-900/30 dark:text-green-400"
>
<p class="font-semibold">Bestätige deine E-Mail</p>
<p class="mt-2 text-sm">
Wir haben dir eine Bestätigungs-E-Mail gesendet. Bitte klicke auf den Link in der E-Mail,
um dein Konto zu aktivieren.
</p>
<a
href="/login"
class="mt-4 inline-block font-semibold text-green-600 hover:text-green-700 dark:text-green-400"
>
Zur Anmeldung
</a>
</div>
{:else}
{#if error}
<div
class="mb-4 rounded-lg bg-red-100 p-3 text-red-700 dark:bg-red-900/30 dark:text-red-400"
>
{error}
</div>
{/if}
<form onsubmit={handleSubmit} class="space-y-4">
<div>
<label
for="email"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
E-Mail
</label>
<input
type="email"
id="email"
bind:value={email}
required
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="deine@email.de"
/>
</div>
<div>
<label
for="password"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Passwort
</label>
<input
type="password"
id="password"
bind:value={password}
required
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Mindestens 8 Zeichen"
/>
</div>
<div>
<label
for="confirmPassword"
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Passwort bestätigen
</label>
<input
type="password"
id="confirmPassword"
bind:value={confirmPassword}
required
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Passwort wiederholen"
/>
</div>
<button
type="submit"
disabled={isLoading}
class="w-full rounded-xl bg-gradient-to-r from-green-500 to-emerald-600 px-6 py-3 font-semibold text-white shadow-lg transition-all hover:from-green-600 hover:to-emerald-700 hover:shadow-xl disabled:cursor-not-allowed disabled:opacity-50"
>
{isLoading ? 'Wird registriert...' : 'Registrieren'}
</button>
</form>
<div class="mt-6 text-center">
<p class="text-gray-600 dark:text-gray-400">
Bereits ein Konto?
<a
href="/login"
class="font-semibold text-green-600 hover:text-green-700 dark:text-green-400"
>
Anmelden
</a>
</p>
</div>
{/if}
<div class="mt-4 text-center">
<a
href="/"
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
Zurück zur Startseite
</a>
</div>
</div>
</main>
<RegisterPage
appName="Nutriphi"
logo={NutriPhiLogo}
primaryColor="#10b981"
onSignUp={handleSignUp}
{goto}
successRedirect="/meals"
loginPath="/login"
lightBackground="#d1fae5"
darkBackground="#022c22"
{translations}
>
{#snippet appSlider()}
<AppSlider />
{/snippet}
</RegisterPage>

View file

@ -3,4 +3,24 @@ import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
ssr: {
noExternal: [
'@manacore/shared-theme',
'@manacore/shared-auth-ui',
'@manacore/shared-branding',
'@manacore/shared-ui',
'@manacore/shared-theme-ui',
'@manacore/shared-i18n',
],
},
optimizeDeps: {
exclude: [
'@manacore/shared-theme',
'@manacore/shared-auth-ui',
'@manacore/shared-branding',
'@manacore/shared-ui',
'@manacore/shared-theme-ui',
'@manacore/shared-i18n',
],
},
});