diff --git a/common/messages/de.json b/common/messages/de.json index b3691424..fdadaac2 100644 --- a/common/messages/de.json +++ b/common/messages/de.json @@ -1326,6 +1326,7 @@ "signin.seo.title": "Anmelden", "signin.submit": "Anmelden", "signin.title": "Anmelden", + "signin.subtitle": "Willkommen zurück — mach da weiter, wo du aufgehört hast.", "signup.creating_profile": "Profil wird erstellt...", "signup.email": "E-Mail", "signup.email_placeholder": "sie@beispiel.de", diff --git a/common/messages/fr.json b/common/messages/fr.json index f29eb190..d274cc8d 100644 --- a/common/messages/fr.json +++ b/common/messages/fr.json @@ -1325,6 +1325,7 @@ "signin.seo.title": "Se connecter", "signin.submit": "Se connecter", "signin.title": "Se connecter", + "signin.subtitle": "Bon retour — reprends là où tu t'étais arrêté.", "signup.creating_profile": "Création de votre profil...", "signup.email": "E-mail", "signup.email_placeholder": "vous@exemple.com", diff --git a/web/components/auth/auth-form.tsx b/web/components/auth/auth-form.tsx new file mode 100644 index 00000000..047116c3 --- /dev/null +++ b/web/components/auth/auth-form.tsx @@ -0,0 +1,146 @@ +import clsx from 'clsx' +import {InputHTMLAttributes, ReactNode} from 'react' + +// Shared building blocks for the /register and /signin pages so the two stay in +// visual lockstep. Mirrors the home page design language: warm tokens, a soft +// radial glow for depth, and staggered fade-up entrances. + +/** Page wrapper: centers the card and paints the hero-style radial glow behind it. */ +export function AuthShell({children}: {children: ReactNode}) { + return ( +
+ {/* Soft radial glow behind the card for depth — mirrors the home hero. + Dark mode uses a brighter, lighter-hued, faster-falloff core so it reads + as light, not brown haze. */} + {/**/} +
{children}
+
+ ) +} + +/** Favicon + title + optional subtitle. */ +export function AuthHeader({title, subtitle}: {title: string; subtitle?: string}) { + return ( +
+ {/*
*/} + {/* */} + {/*
*/} +

{title}

+ {subtitle &&

{subtitle}

} +
+ ) +} + +/** The form element, with the standard entrance animation. */ +export function AuthForm({ + onSubmit, + children, +}: { + onSubmit: (event: React.FormEvent) => void + children: ReactNode +}) { + return ( +
+ {children} +
+ ) +} + +/** Wraps stacked inputs so adjacent borders collapse into one. */ +export function AuthFieldGroup({children}: {children: ReactNode}) { + return
{children}
+} + +const authInputBase = + 'bg-canvas-50 appearance-none relative block w-full px-4 py-3 border-[1.5px] border-canvas-200 text-ink-1000 placeholder-ink-400 transition-colors focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:z-10 sm:text-sm' + +/** Email/password input with a screen-reader label. `position` rounds the outer corner. */ +export function AuthInput({ + position, + label, + below, + className, + ...props +}: { + position: 'top' | 'bottom' + label: string + below?: ReactNode +} & InputHTMLAttributes) { + return ( +
+ + + {below} +
+ ) +} + +/** Gradient rule with a centered label. */ +export function AuthDivider({label}: {label: string}) { + return ( +
+
+
+
+
+ {label} +
+
+ ) +} + +/** Primary CTA, matching the home hero button (amber, shadow, hover lift). */ +export function AuthSubmitButton({ + isLoading, + children, +}: { + isLoading?: boolean + children: ReactNode +}) { + return ( + + ) +} + +/** Centered error message. Renders nothing when empty. */ +export function AuthError({children}: {children: ReactNode}) { + if (!children) return null + return
{children}
+} + +/** Footer line ("Already have an account?" etc.) with link styling + entrance. */ +export function AuthFooter({children}: {children: ReactNode}) { + return ( +
+

{children}

+
+ ) +} diff --git a/web/pages/register.tsx b/web/pages/register.tsx index 2e56ea90..8f81e509 100644 --- a/web/pages/register.tsx +++ b/web/pages/register.tsx @@ -6,10 +6,21 @@ import Link from 'next/link' import {useSearchParams} from 'next/navigation' import React, {Suspense, useState} from 'react' import toast from 'react-hot-toast' +import { + AuthDivider, + AuthError, + AuthFieldGroup, + AuthFooter, + AuthForm, + AuthHeader, + AuthInput, + AuthShell, + AuthSubmitButton, +} from 'web/components/auth/auth-form' import {GoogleButton} from 'web/components/buttons/sign-up-button' -import FavIconBlack from 'web/components/FavIcon' import {PageBase} from 'web/components/page-base' import {SEO} from 'web/components/SEO' +import {NewTabLink} from 'web/components/widgets/new-tab-link' import {auth} from 'web/lib/firebase/users' import {useT} from 'web/lib/locale' import {googleSigninSignup, setOnboardingFlag, signinSignupRedirect} from 'web/lib/util/signup' @@ -112,146 +123,110 @@ function RegisterComponent() { description={t('register.seo.description', 'Register for a new account')} url={`/register`} /> -
-
- {registrationSuccess ? ( -
-
- - - -
-

- {t('register.check_email.title', 'Check your email')} -

-

- {t('register.check_email.sent_prefix', 'We have sent a verification link to ')} - {registeredEmail} - {t('register.check_email.sent_suffix', '.')} -

-

- {t( - 'register.check_email.help_prefix', - 'Did not receive the email? Check your spam folder or ', - )} - - {t('register.check_email.help_suffix', '.')} -

-
- - {t('register.back_to_login', 'Back to Login')} - -
+ + {registrationSuccess ? ( +
+
+ + +
- ) : ( -
-
- {/*

*/} - {/* The project is still in development...*/} - {/*

*/} -
- -
-

- {t('register.get_started', 'Get Started')} -

-
-
-
-
- - -
-
- - -
-
- -
-

- {t('register.agreement.prefix', 'By signing up, I agree to the ')} - {t('register.terms', 'Terms and Conditions')} - {t('register.agreement.and', ' and ')} - {t('register.privacy', 'Privacy Policy')} - {t('register.agreement.suffix', '.')} -

-
- - {error &&
{error}
} - -
- - -
-
-
-
-
- - {t('register.or_sign_up_with', 'Or sign up with')} - -
-
- -
-
-
-
-

- {t('register.already_account', 'Already have an account?')}{' '} - {t('register.link_signin', 'Sign in')} -

-
+

+ {t('register.check_email.title', 'Check your email')} +

+

+ {t('register.check_email.sent_prefix', 'We have sent a verification link to ')} + {registeredEmail} + {t('register.check_email.sent_suffix', '.')} +

+

+ {t( + 'register.check_email.help_prefix', + 'Did not receive the email? Check your spam folder or ', + )} + + {t('register.check_email.help_suffix', '.')} +

+
+ + {t('register.back_to_login', 'Back to Login')} +
- )} -
-
+
+ ) : ( + <> + + + + + + + +

+ {t('register.agreement.prefix', 'By signing up, I agree to the ')} + {t('register.terms', 'Terms and Conditions')} + {t('register.agreement.and', ' and ')} + {t('register.privacy', 'Privacy Policy')} + {t('register.agreement.suffix', '.')} +

+ + {error} + +
+ + {isLoading + ? t('register.button.creating', 'Creating account...') + : t('register.button.email', 'Sign up with Email')} + + + +
+
+ + {t('register.already_account', 'Already have an account?')}{' '} + {t('register.link_signin', 'Sign in')} + + + )} +
) } diff --git a/web/pages/signin.tsx b/web/pages/signin.tsx index 98f0310d..d03e8c20 100644 --- a/web/pages/signin.tsx +++ b/web/pages/signin.tsx @@ -5,8 +5,18 @@ import {signInWithEmailAndPassword} from 'firebase/auth' import Link from 'next/link' import {useSearchParams} from 'next/navigation' import React, {Suspense, useEffect, useState} from 'react' +import { + AuthDivider, + AuthError, + AuthFieldGroup, + AuthFooter, + AuthForm, + AuthHeader, + AuthInput, + AuthShell, + AuthSubmitButton, +} from 'web/components/auth/auth-form' import {GoogleButton} from 'web/components/buttons/sign-up-button' -import FavIconBlack from 'web/components/FavIcon' import {InfoIcon} from 'web/components/icons' import {PageBase} from 'web/components/page-base' import {SEO} from 'web/components/SEO' @@ -129,55 +139,43 @@ function RegisterComponent() { description={t('signin.seo.description', 'Sign in to your account')} url={`/signin`} /> -
-
- {redirectPath && ( -
- -

- {t( - 'signin.prompt.sign_in_to_access', - 'Please sign in to access the {redirectPath} page', - {redirectPath: redirectPath.replace('/', '')}, - )} -

-
- )} -
-
- -
-

- {t('signin.title', 'Sign in')} -

+ + {redirectPath && ( +
+ +

+ {t( + 'signin.prompt.sign_in_to_access', + 'Please sign in to access the {redirectPath} page', + {redirectPath: redirectPath.replace('/', '')}, + )} +

-
-
-
- - -
-
- - + )} + + + + +
-
-
+ } + /> + - {error &&
{error}
} + {error} -
- - -
-
-
-
-
- - {t('signin.continue', 'Or continue with')} - -
-
- -
- -
-

- {t('signin.no_account', "Don't have an account?")}{' '} - {t('signin.link_sign_up', 'Register')} -

+
+ + {isLoading ? 'Signing in...' : t('signin.submit', 'Sign in with Email')} + + +
-
-
+ + + {t('signin.no_account', "Don't have an account?")}{' '} + {t('signin.link_sign_up', 'Register')} + + ) }