-
Notifications
You must be signed in to change notification settings - Fork 3
feat(auth): Add linkComponent/labels props to auth forms, i18n support, widescreen layout #990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5e9d00f
6ace5f8
f39db8f
fcd4dcc
53aba85
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| /** | ||
| * Shared layout for authentication pages (login, register, forgot password). | ||
| * Provides a widescreen-optimized split-panel design with branding on the left | ||
| * and form content on the right, inspired by enterprise platforms like Airtable and Salesforce. | ||
| */ | ||
|
|
||
| import type React from 'react'; | ||
|
|
||
| export function AuthPageLayout({ children }: { children: React.ReactNode }) { | ||
| return ( | ||
| <div className="flex min-h-screen"> | ||
| {/* Left branding panel - hidden on mobile, shown on lg+ */} | ||
| <div className="hidden lg:flex lg:w-1/2 items-center justify-center bg-primary p-12"> | ||
| <div className="max-w-md space-y-6 text-primary-foreground"> | ||
| <div className="flex items-center gap-3"> | ||
| <svg | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| className="h-10 w-10" | ||
| > | ||
| <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" /> | ||
| </svg> | ||
| <span className="text-2xl font-bold">ObjectStack</span> | ||
| </div> | ||
| <h2 className="text-3xl font-bold leading-tight"> | ||
| Build powerful business applications, faster. | ||
| </h2> | ||
| <p className="text-lg opacity-90"> | ||
| The universal platform for enterprise data management, workflows, and analytics. | ||
| </p> | ||
|
Comment on lines
+28
to
+35
|
||
| </div> | ||
| </div> | ||
|
|
||
| {/* Right form panel */} | ||
| <div className="flex w-full lg:w-1/2 items-center justify-center bg-background px-6 py-12"> | ||
| {children} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,19 +2,40 @@ | |
| * Login Page for ObjectStack Console | ||
| */ | ||
|
|
||
| import { useNavigate } from 'react-router-dom'; | ||
| import { LoginForm } from '@object-ui/auth'; | ||
| import { useNavigate, Link } from 'react-router-dom'; | ||
| import { LoginForm, type AuthLinkComponentProps } from '@object-ui/auth'; | ||
| import { useObjectTranslation } from '@object-ui/i18n'; | ||
| import { AuthPageLayout } from '../components/AuthPageLayout'; | ||
|
|
||
| const RouterLink = ({ href, className, children }: AuthLinkComponentProps) => ( | ||
| <Link to={href} className={className}>{children}</Link> | ||
| ); | ||
|
Comment on lines
+10
to
+12
|
||
|
|
||
| export function LoginPage() { | ||
| const navigate = useNavigate(); | ||
| const { t } = useObjectTranslation(); | ||
|
|
||
| return ( | ||
| <div className="flex min-h-screen items-center justify-center bg-background px-4 py-8"> | ||
| <AuthPageLayout> | ||
| <LoginForm | ||
| onSuccess={() => navigate('/')} | ||
| registerUrl="/register" | ||
| forgotPasswordUrl="/forgot-password" | ||
| title={t('auth.login.title')} | ||
| description={t('auth.login.description')} | ||
| linkComponent={RouterLink} | ||
| labels={{ | ||
| emailLabel: t('auth.login.emailLabel'), | ||
| emailPlaceholder: t('auth.login.emailPlaceholder'), | ||
| passwordLabel: t('auth.login.passwordLabel'), | ||
| passwordPlaceholder: t('auth.login.passwordPlaceholder'), | ||
| forgotPasswordText: t('auth.login.forgotPasswordText'), | ||
| submitButton: t('auth.login.submitButton'), | ||
| submittingButton: t('auth.login.submittingButton'), | ||
| noAccountText: t('auth.login.noAccountText'), | ||
| signUpText: t('auth.login.signUpText'), | ||
| }} | ||
| /> | ||
| </div> | ||
| </AuthPageLayout> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,20 @@ | |||||||||
|
|
||||||||||
| import React, { useState } from 'react'; | ||||||||||
| import { useAuth } from './useAuth'; | ||||||||||
| import type { AuthLinkComponentProps } from './types'; | ||||||||||
|
|
||||||||||
| /** Translatable labels for the ForgotPasswordForm */ | ||||||||||
| export interface ForgotPasswordFormLabels { | ||||||||||
| emailLabel?: string; | ||||||||||
| emailPlaceholder?: string; | ||||||||||
| submitButton?: string; | ||||||||||
| submittingButton?: string; | ||||||||||
| successTitle?: string; | ||||||||||
| successDescription?: string; | ||||||||||
| backToSignInText?: string; | ||||||||||
| rememberPasswordText?: string; | ||||||||||
| signInText?: string; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| export interface ForgotPasswordFormProps { | ||||||||||
| /** Callback on successful submission */ | ||||||||||
|
|
@@ -20,8 +34,16 @@ export interface ForgotPasswordFormProps { | |||||||||
| title?: string; | ||||||||||
| /** Custom description */ | ||||||||||
| description?: string; | ||||||||||
| /** Custom link component for SPA navigation (e.g. React Router's Link) */ | ||||||||||
| linkComponent?: React.ComponentType<AuthLinkComponentProps>; | ||||||||||
| /** Override default labels for i18n */ | ||||||||||
| labels?: ForgotPasswordFormLabels; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const DefaultLink = ({ href, className, children }: AuthLinkComponentProps) => ( | ||||||||||
| <a href={href} className={className}>{children}</a> | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Forgot password form component. | ||||||||||
| * Sends a password reset email to the user. | ||||||||||
|
|
@@ -40,12 +62,26 @@ export function ForgotPasswordForm({ | |||||||||
| loginUrl = '/login', | ||||||||||
| title = 'Reset your password', | ||||||||||
| description = 'Enter your email address and we\'ll send you a link to reset your password', | ||||||||||
| linkComponent: LinkComp = DefaultLink, | ||||||||||
| labels = {}, | ||||||||||
| }: ForgotPasswordFormProps) { | ||||||||||
| const { forgotPassword, isLoading } = useAuth(); | ||||||||||
| const [email, setEmail] = useState(''); | ||||||||||
| const [error, setError] = useState<string | null>(null); | ||||||||||
| const [submitted, setSubmitted] = useState(false); | ||||||||||
|
|
||||||||||
| const l = { | ||||||||||
| emailLabel: labels.emailLabel ?? 'Email', | ||||||||||
| emailPlaceholder: labels.emailPlaceholder ?? 'name@example.com', | ||||||||||
| submitButton: labels.submitButton ?? 'Send Reset Link', | ||||||||||
| submittingButton: labels.submittingButton ?? 'Sending...', | ||||||||||
| successTitle: labels.successTitle ?? 'Check your email', | ||||||||||
| successDescription: labels.successDescription ?? "We've sent a password reset link to {{email}}. Please check your inbox.", | ||||||||||
| backToSignInText: labels.backToSignInText ?? 'Back to sign in', | ||||||||||
| rememberPasswordText: labels.rememberPasswordText ?? 'Remember your password?', | ||||||||||
| signInText: labels.signInText ?? 'Sign in', | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const handleSubmit = async (e: React.FormEvent) => { | ||||||||||
| e.preventDefault(); | ||||||||||
| setError(null); | ||||||||||
|
|
@@ -62,28 +98,28 @@ export function ForgotPasswordForm({ | |||||||||
| }; | ||||||||||
|
|
||||||||||
| if (submitted) { | ||||||||||
| const successMsg = l.successDescription.includes('{{email}}') | ||||||||||
| ? l.successDescription.replace('{{email}}', email) | ||||||||||
| : `${l.successDescription} ${email}`; | ||||||||||
|
Comment on lines
+101
to
+103
|
||||||||||
| const successMsg = l.successDescription.includes('{{email}}') | |
| ? l.successDescription.replace('{{email}}', email) | |
| : `${l.successDescription} ${email}`; | |
| const successMsg = l.successDescription.replace(/{{email}}/g, email); |
Copilot
AI
Mar 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new successDescription interpolation logic ({{email}} replacement / fallback append) isn’t covered by tests. Consider extending the existing “shows success message after submission” test to assert that the rendered success message includes the submitted email and handles the {{email}} placeholder as expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The branding panel uses an unlabelled SVG and a semantic
<h2>that appears before the form’s<h1>in DOM order. For accessibility, mark decorative SVGs asaria-hidden(or provide an accessible name), and avoid heading-level jumps by using non-heading elements for marketing copy (or ensure a logical heading order).