Skip to content

Commit a00446f

Browse files
authored
Merge pull request #990 from objectstack-ai/copilot/optimize-login-page-visuals
2 parents bc1303c + 53aba85 commit a00446f

22 files changed

Lines changed: 861 additions & 61 deletions
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Shared layout for authentication pages (login, register, forgot password).
3+
* Provides a widescreen-optimized split-panel design with branding on the left
4+
* and form content on the right, inspired by enterprise platforms like Airtable and Salesforce.
5+
*/
6+
7+
import type React from 'react';
8+
9+
export function AuthPageLayout({ children }: { children: React.ReactNode }) {
10+
return (
11+
<div className="flex min-h-screen">
12+
{/* Left branding panel - hidden on mobile, shown on lg+ */}
13+
<div className="hidden lg:flex lg:w-1/2 items-center justify-center bg-primary p-12">
14+
<div className="max-w-md space-y-6 text-primary-foreground">
15+
<div className="flex items-center gap-3">
16+
<svg
17+
xmlns="http://www.w3.org/2000/svg"
18+
viewBox="0 0 24 24"
19+
fill="none"
20+
stroke="currentColor"
21+
strokeWidth="2"
22+
strokeLinecap="round"
23+
strokeLinejoin="round"
24+
className="h-10 w-10"
25+
>
26+
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
27+
</svg>
28+
<span className="text-2xl font-bold">ObjectStack</span>
29+
</div>
30+
<h2 className="text-3xl font-bold leading-tight">
31+
Build powerful business applications, faster.
32+
</h2>
33+
<p className="text-lg opacity-90">
34+
The universal platform for enterprise data management, workflows, and analytics.
35+
</p>
36+
</div>
37+
</div>
38+
39+
{/* Right form panel */}
40+
<div className="flex w-full lg:w-1/2 items-center justify-center bg-background px-6 py-12">
41+
{children}
42+
</div>
43+
</div>
44+
);
45+
}

apps/console/src/pages/ForgotPasswordPage.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,37 @@
22
* Forgot Password Page for ObjectStack Console
33
*/
44

5-
import { ForgotPasswordForm } from '@object-ui/auth';
5+
import { Link } from 'react-router-dom';
6+
import { ForgotPasswordForm, type AuthLinkComponentProps } from '@object-ui/auth';
7+
import { useObjectTranslation } from '@object-ui/i18n';
8+
import { AuthPageLayout } from '../components/AuthPageLayout';
9+
10+
const RouterLink = ({ href, className, children }: AuthLinkComponentProps) => (
11+
<Link to={href} className={className}>{children}</Link>
12+
);
613

714
export function ForgotPasswordPage() {
15+
const { t } = useObjectTranslation();
16+
817
return (
9-
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
10-
<ForgotPasswordForm loginUrl="/login" />
11-
</div>
18+
<AuthPageLayout>
19+
<ForgotPasswordForm
20+
loginUrl="/login"
21+
title={t('auth.forgotPassword.title')}
22+
description={t('auth.forgotPassword.description')}
23+
linkComponent={RouterLink}
24+
labels={{
25+
emailLabel: t('auth.forgotPassword.emailLabel'),
26+
emailPlaceholder: t('auth.forgotPassword.emailPlaceholder'),
27+
submitButton: t('auth.forgotPassword.submitButton'),
28+
submittingButton: t('auth.forgotPassword.submittingButton'),
29+
successTitle: t('auth.forgotPassword.successTitle'),
30+
successDescription: t('auth.forgotPassword.successDescription'),
31+
backToSignInText: t('auth.forgotPassword.backToSignInText'),
32+
rememberPasswordText: t('auth.forgotPassword.rememberPasswordText'),
33+
signInText: t('auth.forgotPassword.signInText'),
34+
}}
35+
/>
36+
</AuthPageLayout>
1237
);
1338
}

apps/console/src/pages/LoginPage.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,40 @@
22
* Login Page for ObjectStack Console
33
*/
44

5-
import { useNavigate } from 'react-router-dom';
6-
import { LoginForm } from '@object-ui/auth';
5+
import { useNavigate, Link } from 'react-router-dom';
6+
import { LoginForm, type AuthLinkComponentProps } from '@object-ui/auth';
7+
import { useObjectTranslation } from '@object-ui/i18n';
8+
import { AuthPageLayout } from '../components/AuthPageLayout';
9+
10+
const RouterLink = ({ href, className, children }: AuthLinkComponentProps) => (
11+
<Link to={href} className={className}>{children}</Link>
12+
);
713

814
export function LoginPage() {
915
const navigate = useNavigate();
16+
const { t } = useObjectTranslation();
1017

1118
return (
12-
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
19+
<AuthPageLayout>
1320
<LoginForm
1421
onSuccess={() => navigate('/')}
1522
registerUrl="/register"
1623
forgotPasswordUrl="/forgot-password"
24+
title={t('auth.login.title')}
25+
description={t('auth.login.description')}
26+
linkComponent={RouterLink}
27+
labels={{
28+
emailLabel: t('auth.login.emailLabel'),
29+
emailPlaceholder: t('auth.login.emailPlaceholder'),
30+
passwordLabel: t('auth.login.passwordLabel'),
31+
passwordPlaceholder: t('auth.login.passwordPlaceholder'),
32+
forgotPasswordText: t('auth.login.forgotPasswordText'),
33+
submitButton: t('auth.login.submitButton'),
34+
submittingButton: t('auth.login.submittingButton'),
35+
noAccountText: t('auth.login.noAccountText'),
36+
signUpText: t('auth.login.signUpText'),
37+
}}
1738
/>
18-
</div>
39+
</AuthPageLayout>
1940
);
2041
}

apps/console/src/pages/RegisterPage.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,44 @@
22
* Register Page for ObjectStack Console
33
*/
44

5-
import { useNavigate } from 'react-router-dom';
6-
import { RegisterForm } from '@object-ui/auth';
5+
import { useNavigate, Link } from 'react-router-dom';
6+
import { RegisterForm, type AuthLinkComponentProps } from '@object-ui/auth';
7+
import { useObjectTranslation } from '@object-ui/i18n';
8+
import { AuthPageLayout } from '../components/AuthPageLayout';
9+
10+
const RouterLink = ({ href, className, children }: AuthLinkComponentProps) => (
11+
<Link to={href} className={className}>{children}</Link>
12+
);
713

814
export function RegisterPage() {
915
const navigate = useNavigate();
16+
const { t } = useObjectTranslation();
1017

1118
return (
12-
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-8">
19+
<AuthPageLayout>
1320
<RegisterForm
1421
onSuccess={() => navigate('/')}
1522
loginUrl="/login"
23+
title={t('auth.register.title')}
24+
description={t('auth.register.description')}
25+
linkComponent={RouterLink}
26+
labels={{
27+
nameLabel: t('auth.register.nameLabel'),
28+
namePlaceholder: t('auth.register.namePlaceholder'),
29+
emailLabel: t('auth.register.emailLabel'),
30+
emailPlaceholder: t('auth.register.emailPlaceholder'),
31+
passwordLabel: t('auth.register.passwordLabel'),
32+
passwordPlaceholder: t('auth.register.passwordPlaceholder'),
33+
confirmPasswordLabel: t('auth.register.confirmPasswordLabel'),
34+
confirmPasswordPlaceholder: t('auth.register.confirmPasswordPlaceholder'),
35+
passwordMismatchError: t('auth.register.passwordMismatchError'),
36+
passwordTooShortError: t('auth.register.passwordTooShortError'),
37+
submitButton: t('auth.register.submitButton'),
38+
submittingButton: t('auth.register.submittingButton'),
39+
hasAccountText: t('auth.register.hasAccountText'),
40+
signInText: t('auth.register.signInText'),
41+
}}
1642
/>
17-
</div>
43+
</AuthPageLayout>
1844
);
1945
}

packages/auth/src/ForgotPasswordForm.tsx

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@
88

99
import React, { useState } from 'react';
1010
import { useAuth } from './useAuth';
11+
import type { AuthLinkComponentProps } from './types';
12+
13+
/** Translatable labels for the ForgotPasswordForm */
14+
export interface ForgotPasswordFormLabels {
15+
emailLabel?: string;
16+
emailPlaceholder?: string;
17+
submitButton?: string;
18+
submittingButton?: string;
19+
successTitle?: string;
20+
successDescription?: string;
21+
backToSignInText?: string;
22+
rememberPasswordText?: string;
23+
signInText?: string;
24+
}
1125

1226
export interface ForgotPasswordFormProps {
1327
/** Callback on successful submission */
@@ -20,8 +34,16 @@ export interface ForgotPasswordFormProps {
2034
title?: string;
2135
/** Custom description */
2236
description?: string;
37+
/** Custom link component for SPA navigation (e.g. React Router's Link) */
38+
linkComponent?: React.ComponentType<AuthLinkComponentProps>;
39+
/** Override default labels for i18n */
40+
labels?: ForgotPasswordFormLabels;
2341
}
2442

43+
const DefaultLink = ({ href, className, children }: AuthLinkComponentProps) => (
44+
<a href={href} className={className}>{children}</a>
45+
);
46+
2547
/**
2648
* Forgot password form component.
2749
* Sends a password reset email to the user.
@@ -40,12 +62,26 @@ export function ForgotPasswordForm({
4062
loginUrl = '/login',
4163
title = 'Reset your password',
4264
description = 'Enter your email address and we\'ll send you a link to reset your password',
65+
linkComponent: LinkComp = DefaultLink,
66+
labels = {},
4367
}: ForgotPasswordFormProps) {
4468
const { forgotPassword, isLoading } = useAuth();
4569
const [email, setEmail] = useState('');
4670
const [error, setError] = useState<string | null>(null);
4771
const [submitted, setSubmitted] = useState(false);
4872

73+
const l = {
74+
emailLabel: labels.emailLabel ?? 'Email',
75+
emailPlaceholder: labels.emailPlaceholder ?? 'name@example.com',
76+
submitButton: labels.submitButton ?? 'Send Reset Link',
77+
submittingButton: labels.submittingButton ?? 'Sending...',
78+
successTitle: labels.successTitle ?? 'Check your email',
79+
successDescription: labels.successDescription ?? "We've sent a password reset link to {{email}}. Please check your inbox.",
80+
backToSignInText: labels.backToSignInText ?? 'Back to sign in',
81+
rememberPasswordText: labels.rememberPasswordText ?? 'Remember your password?',
82+
signInText: labels.signInText ?? 'Sign in',
83+
};
84+
4985
const handleSubmit = async (e: React.FormEvent) => {
5086
e.preventDefault();
5187
setError(null);
@@ -62,28 +98,28 @@ export function ForgotPasswordForm({
6298
};
6399

64100
if (submitted) {
101+
const successMsg = l.successDescription.includes('{{email}}')
102+
? l.successDescription.replace('{{email}}', email)
103+
: `${l.successDescription} ${email}`;
65104
return (
66-
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
105+
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[380px]">
67106
<div className="flex flex-col space-y-2 text-center">
68-
<h1 className="text-2xl font-semibold tracking-tight">Check your email</h1>
69-
<p className="text-sm text-muted-foreground">
70-
We&apos;ve sent a password reset link to <strong>{email}</strong>.
71-
Please check your inbox and follow the instructions.
72-
</p>
107+
<h1 className="text-2xl font-semibold tracking-tight">{l.successTitle}</h1>
108+
<p className="text-sm text-muted-foreground">{successMsg}</p>
73109
</div>
74110
{loginUrl && (
75111
<p className="px-8 text-center text-sm text-muted-foreground">
76-
<a href={loginUrl} className="text-primary underline-offset-4 hover:underline">
77-
Back to sign in
78-
</a>
112+
<LinkComp href={loginUrl} className="text-primary underline-offset-4 hover:underline">
113+
{l.backToSignInText}
114+
</LinkComp>
79115
</p>
80116
)}
81117
</div>
82118
);
83119
}
84120

85121
return (
86-
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
122+
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[380px]">
87123
<div className="flex flex-col space-y-2 text-center">
88124
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
89125
<p className="text-sm text-muted-foreground">{description}</p>
@@ -98,12 +134,12 @@ export function ForgotPasswordForm({
98134

99135
<div className="space-y-2">
100136
<label htmlFor="forgot-email" className="text-sm font-medium leading-none">
101-
Email
137+
{l.emailLabel}
102138
</label>
103139
<input
104140
id="forgot-email"
105141
type="email"
106-
placeholder="name@example.com"
142+
placeholder={l.emailPlaceholder}
107143
value={email}
108144
onChange={(e) => setEmail(e.target.value)}
109145
required
@@ -118,16 +154,16 @@ export function ForgotPasswordForm({
118154
disabled={isLoading}
119155
className="inline-flex h-10 w-full items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
120156
>
121-
{isLoading ? 'Sending...' : 'Send Reset Link'}
157+
{isLoading ? l.submittingButton : l.submitButton}
122158
</button>
123159
</form>
124160

125161
{loginUrl && (
126162
<p className="px-8 text-center text-sm text-muted-foreground">
127-
Remember your password?{' '}
128-
<a href={loginUrl} className="text-primary underline-offset-4 hover:underline">
129-
Sign in
130-
</a>
163+
{l.rememberPasswordText}{' '}
164+
<LinkComp href={loginUrl} className="text-primary underline-offset-4 hover:underline">
165+
{l.signInText}
166+
</LinkComp>
131167
</p>
132168
)}
133169
</div>

0 commit comments

Comments
 (0)