mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 19:21:23 +02:00
✨ feat(questions): unify auth pages with shared components
- Add Questions branding to shared-branding package (logo, colors) - Create QuestionsLogo.svelte component - Refactor login page to use shared LoginPage component - Refactor register page to use shared RegisterPage component - Refactor forgot-password page to use shared ForgotPasswordPage component - Fix Svelte 5 class: directive on components (+page.svelte) The Questions app now uses the same auth UI as Calendar, Chat, and other apps.
This commit is contained in:
parent
da4b1e696b
commit
91143a497b
9 changed files with 136 additions and 255 deletions
|
|
@ -142,8 +142,9 @@
|
|||
<!-- Status Icon -->
|
||||
<div class="mt-1">
|
||||
<StatusIcon
|
||||
class="h-5 w-5 {statusColor}"
|
||||
class:animate-spin={question.status === 'researching'}
|
||||
class="h-5 w-5 {statusColor} {question.status === 'researching'
|
||||
? 'animate-spin'
|
||||
: ''}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,81 +1,30 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
|
||||
import { getForgotPasswordTranslations } from '@manacore/shared-i18n';
|
||||
import { QuestionsLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { ArrowLeft } from '@manacore/shared-icons';
|
||||
|
||||
let email = $state('');
|
||||
let error = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let success = $state(false);
|
||||
// Get translations (default to English)
|
||||
const translations = $derived(getForgotPasswordTranslations('en'));
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
const result = await authStore.resetPassword(email);
|
||||
|
||||
if (result.success) {
|
||||
success = true;
|
||||
} else {
|
||||
error = result.error || 'Failed to send reset email';
|
||||
}
|
||||
|
||||
loading = false;
|
||||
async function handleForgotPassword(email: string) {
|
||||
return authStore.resetPassword(email);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl bg-card p-8 shadow-lg">
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="text-2xl font-bold text-foreground">Reset Password</h1>
|
||||
<p class="mt-2 text-muted-foreground">Enter your email to receive a reset link</p>
|
||||
</div>
|
||||
<svelte:head>
|
||||
<title>{translations.titleForm} | Questions</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if success}
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-4xl">📧</div>
|
||||
<h2 class="mb-2 text-lg font-semibold text-foreground">Check your email</h2>
|
||||
<p class="mb-4 text-muted-foreground">
|
||||
We've sent a password reset link to <strong>{email}</strong>. Please check your inbox.
|
||||
</p>
|
||||
<a href="/login" class="text-primary hover:underline">Back to login</a>
|
||||
</div>
|
||||
{:else}
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
{#if error}
|
||||
<div class="rounded-lg bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="email" class="mb-1 block text-sm font-medium text-foreground">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
bind:value={email}
|
||||
required
|
||||
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="w-full rounded-lg bg-primary px-4 py-2 font-medium text-primary-foreground transition-colors hover:bg-primary-hover disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Sending...' : 'Send Reset Link'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<a
|
||||
href="/login"
|
||||
class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
Back to login
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<ForgotPasswordPage
|
||||
appName="Questions"
|
||||
logo={QuestionsLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onForgotPassword={handleForgotPassword}
|
||||
{goto}
|
||||
loginPath="/login"
|
||||
lightBackground="#f3e8ff"
|
||||
darkBackground="#1e1033"
|
||||
{translations}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,81 +1,64 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||
import { QuestionsLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiClient } from '$lib/api/client';
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let error = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
// Get redirect URL from query params or sessionStorage
|
||||
const redirectTo = $derived.by(() => {
|
||||
const queryRedirect = $page.url.searchParams.get('redirectTo');
|
||||
if (queryRedirect) return queryRedirect;
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = null;
|
||||
loading = true;
|
||||
if (browser) {
|
||||
const sessionRedirect = sessionStorage.getItem('auth-return-url');
|
||||
if (sessionRedirect) {
|
||||
sessionStorage.removeItem('auth-return-url');
|
||||
return sessionRedirect;
|
||||
}
|
||||
}
|
||||
|
||||
return '/';
|
||||
});
|
||||
|
||||
// Get translations (default to English)
|
||||
const translations = $derived(getLoginTranslations('en'));
|
||||
|
||||
// Read verification status from query params
|
||||
const verified = $derived($page.url.searchParams.get('verified') === 'true');
|
||||
const initialEmail = $derived($page.url.searchParams.get('email') || '');
|
||||
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
const result = await authStore.signIn(email, password);
|
||||
|
||||
if (result.success) {
|
||||
const token = await authStore.getValidToken();
|
||||
apiClient.setAccessToken(token);
|
||||
goto('/');
|
||||
} else {
|
||||
error = result.error || 'Login failed';
|
||||
}
|
||||
|
||||
loading = false;
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl bg-card p-8 shadow-lg">
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="text-2xl font-bold text-foreground">Questions</h1>
|
||||
<p class="mt-2 text-muted-foreground">Sign in to your account</p>
|
||||
</div>
|
||||
<svelte:head>
|
||||
<title>{translations.title} | Questions</title>
|
||||
</svelte:head>
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
{#if error}
|
||||
<div class="rounded-lg bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="email" class="mb-1 block text-sm font-medium text-foreground">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
bind:value={email}
|
||||
required
|
||||
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="mb-1 block text-sm font-medium text-foreground">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
bind:value={password}
|
||||
required
|
||||
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="w-full rounded-lg bg-primary px-4 py-2 font-medium text-primary-foreground transition-colors hover:bg-primary-hover disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Signing in...' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center text-sm text-muted-foreground">
|
||||
<a href="/forgot-password" class="text-primary hover:underline">Forgot password?</a>
|
||||
<span class="mx-2">·</span>
|
||||
<a href="/register" class="text-primary hover:underline">Create account</a>
|
||||
</div>
|
||||
</div>
|
||||
<LoginPage
|
||||
appName="Questions"
|
||||
logo={QuestionsLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onSignIn={handleSignIn}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
lightBackground="#f3e8ff"
|
||||
darkBackground="#1e1033"
|
||||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,125 +1,44 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { RegisterPage } from '@manacore/shared-auth-ui';
|
||||
import { getRegisterTranslations } from '@manacore/shared-i18n';
|
||||
import { QuestionsLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiClient } from '$lib/api/client';
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let confirmPassword = $state('');
|
||||
let error = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let needsVerification = $state(false);
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = null;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
error = 'Passwords do not match';
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
error = 'Password must be at least 8 characters';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
const result = await authStore.signUp(email, password);
|
||||
|
||||
if (result.success) {
|
||||
if (result.needsVerification) {
|
||||
needsVerification = true;
|
||||
} else {
|
||||
const token = await authStore.getValidToken();
|
||||
apiClient.setAccessToken(token);
|
||||
goto('/');
|
||||
// Get redirect URL from sessionStorage
|
||||
const redirectTo = $derived.by(() => {
|
||||
if (browser) {
|
||||
const sessionRedirect = sessionStorage.getItem('auth-return-url');
|
||||
if (sessionRedirect) {
|
||||
sessionStorage.removeItem('auth-return-url');
|
||||
return sessionRedirect;
|
||||
}
|
||||
} else {
|
||||
error = result.error || 'Registration failed';
|
||||
}
|
||||
return '/';
|
||||
});
|
||||
|
||||
loading = false;
|
||||
// Get translations (default to English)
|
||||
const translations = $derived(getRegisterTranslations('en'));
|
||||
|
||||
async function handleSignUp(email: string, password: string) {
|
||||
return authStore.signUp(email, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl bg-card p-8 shadow-lg">
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="text-2xl font-bold text-foreground">Questions</h1>
|
||||
<p class="mt-2 text-muted-foreground">Create your account</p>
|
||||
</div>
|
||||
<svelte:head>
|
||||
<title>{translations.title} | Questions</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if needsVerification}
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-4xl">📧</div>
|
||||
<h2 class="mb-2 text-lg font-semibold">Check your email</h2>
|
||||
<p class="text-muted-foreground">
|
||||
We've sent a verification link to <strong>{email}</strong>. Please check your inbox and
|
||||
click the link to verify your account.
|
||||
</p>
|
||||
<a href="/login" class="mt-4 inline-block text-primary hover:underline">Back to login</a>
|
||||
</div>
|
||||
{:else}
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
{#if error}
|
||||
<div class="rounded-lg bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="email" class="mb-1 block text-sm font-medium text-foreground">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
bind:value={email}
|
||||
required
|
||||
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="mb-1 block text-sm font-medium text-foreground">Password</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
bind:value={password}
|
||||
required
|
||||
minlength="8"
|
||||
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirmPassword" class="mb-1 block text-sm font-medium text-foreground"
|
||||
>Confirm Password</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
bind:value={confirmPassword}
|
||||
required
|
||||
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="w-full rounded-lg bg-primary px-4 py-2 font-medium text-primary-foreground transition-colors hover:bg-primary-hover disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Creating account...' : 'Create account'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center text-sm text-muted-foreground">
|
||||
Already have an account?
|
||||
<a href="/login" class="text-primary hover:underline">Sign in</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<RegisterPage
|
||||
appName="Questions"
|
||||
logo={QuestionsLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onSignUp={handleSignUp}
|
||||
{goto}
|
||||
successRedirect={redirectTo}
|
||||
loginPath="/login"
|
||||
lightBackground="#f3e8ff"
|
||||
darkBackground="#1e1033"
|
||||
{translations}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -220,6 +220,19 @@ export const APP_BRANDING: Record<AppId, AppBranding> = {
|
|||
logoStroke: true,
|
||||
logoStrokeWidth: 1.5,
|
||||
},
|
||||
questions: {
|
||||
id: 'questions',
|
||||
name: 'Questions',
|
||||
tagline: 'AI Research Assistant',
|
||||
primaryColor: '#8b5cf6',
|
||||
secondaryColor: '#a78bfa',
|
||||
// Question mark / search icon
|
||||
logoPath:
|
||||
'M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z',
|
||||
logoViewBox: '0 0 24 24',
|
||||
logoStroke: true,
|
||||
logoStrokeWidth: 1.5,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export {
|
|||
MoodlitLogo,
|
||||
InventoryLogo,
|
||||
ClockLogo,
|
||||
QuestionsLogo,
|
||||
} from './logos';
|
||||
|
||||
// Configuration
|
||||
|
|
|
|||
13
packages/shared-branding/src/logos/QuestionsLogo.svelte
Normal file
13
packages/shared-branding/src/logos/QuestionsLogo.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import AppLogo from '../AppLogo.svelte';
|
||||
|
||||
interface Props {
|
||||
size?: number;
|
||||
color?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { size = 55, color, class: className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<AppLogo app="questions" {size} {color} class={className} />
|
||||
|
|
@ -18,3 +18,4 @@ export { default as MailLogo } from './MailLogo.svelte';
|
|||
export { default as MoodlitLogo } from './MoodlitLogo.svelte';
|
||||
export { default as InventoryLogo } from './InventoryLogo.svelte';
|
||||
export { default as ClockLogo } from './ClockLogo.svelte';
|
||||
export { default as QuestionsLogo } from './QuestionsLogo.svelte';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ export type AppId =
|
|||
| 'todo'
|
||||
| 'mail'
|
||||
| 'moodlit'
|
||||
| 'inventory';
|
||||
| 'inventory'
|
||||
| 'questions';
|
||||
|
||||
/**
|
||||
* App branding configuration
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue