Share the login/register form between Auth and Join
CredentialsForm owns the mode tabs, the name/email/password fields, the error and the submit button; the two pages keep only their own wiring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
|
import { CredentialsForm, type CredentialsFormMode } from './CredentialsForm'
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
loginTab: 'Log in',
|
||||||
|
registerTab: 'Create account',
|
||||||
|
nameLabel: 'Name',
|
||||||
|
namePlaceholder: 'Your name',
|
||||||
|
emailLabel: 'Email',
|
||||||
|
emailPlaceholder: 'Email address',
|
||||||
|
passwordLabel: 'Password',
|
||||||
|
passwordPlaceholder: 'Password',
|
||||||
|
submitLabel: 'Submit',
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderForm(mode: CredentialsFormMode, overrides: { error?: string; disabled?: boolean } = {}) {
|
||||||
|
const onModeChange = vi.fn()
|
||||||
|
const onSubmit = vi.fn()
|
||||||
|
render(
|
||||||
|
<CredentialsForm
|
||||||
|
mode={mode}
|
||||||
|
onModeChange={onModeChange}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
error={overrides.error}
|
||||||
|
disabled={overrides.disabled}
|
||||||
|
idPrefix="cf"
|
||||||
|
classPrefix="cf-page"
|
||||||
|
labels={labels}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
return { onModeChange, onSubmit }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CredentialsForm', () => {
|
||||||
|
it('renders the login mode without a name field', () => {
|
||||||
|
renderForm('login')
|
||||||
|
expect(screen.getByTestId('cf-tab-login')).toHaveAttribute('aria-pressed', 'true')
|
||||||
|
expect(screen.getByTestId('cf-tab-register')).toHaveAttribute('aria-pressed', 'false')
|
||||||
|
expect(screen.queryByPlaceholderText('Your name')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('cf-email')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('cf-password')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the register mode with a name field', () => {
|
||||||
|
renderForm('register')
|
||||||
|
expect(screen.getByTestId('cf-tab-register')).toHaveAttribute('aria-pressed', 'true')
|
||||||
|
expect(screen.getByTestId('cf-tab-login')).toHaveAttribute('aria-pressed', 'false')
|
||||||
|
expect(screen.getByPlaceholderText('Your name')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls onModeChange when a tab is clicked', () => {
|
||||||
|
const { onModeChange } = renderForm('login')
|
||||||
|
fireEvent.click(screen.getByTestId('cf-tab-register'))
|
||||||
|
expect(onModeChange).toHaveBeenCalledWith('register')
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId('cf-tab-login'))
|
||||||
|
expect(onModeChange).toHaveBeenCalledWith('login')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the error alert when an error is given', () => {
|
||||||
|
renderForm('login', { error: 'Something went wrong' })
|
||||||
|
expect(screen.getByRole('alert')).toHaveTextContent('Something went wrong')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders no alert when there is no error', () => {
|
||||||
|
renderForm('login')
|
||||||
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('submits the entered name, email and password for register mode', () => {
|
||||||
|
const { onSubmit } = renderForm('register')
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Your name'), { target: { value: 'Alex' } })
|
||||||
|
fireEvent.change(screen.getByTestId('cf-email'), { target: { value: 'a@b.com' } })
|
||||||
|
fireEvent.change(screen.getByTestId('cf-password'), { target: { value: 'secret' } })
|
||||||
|
fireEvent.click(screen.getByTestId('cf-submit'))
|
||||||
|
|
||||||
|
expect(onSubmit).toHaveBeenCalledWith({ displayName: 'Alex', email: 'a@b.com', password: 'secret' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('submits an empty displayName for login mode, where the field is hidden', () => {
|
||||||
|
const { onSubmit } = renderForm('login')
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByTestId('cf-email'), { target: { value: 'a@b.com' } })
|
||||||
|
fireEvent.change(screen.getByTestId('cf-password'), { target: { value: 'secret' } })
|
||||||
|
fireEvent.click(screen.getByTestId('cf-submit'))
|
||||||
|
|
||||||
|
expect(onSubmit).toHaveBeenCalledWith({ displayName: '', email: 'a@b.com', password: 'secret' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disables the submit button when disabled is true', () => {
|
||||||
|
renderForm('login', { disabled: true })
|
||||||
|
expect(screen.getByTestId('cf-submit')).toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not disable the submit button when disabled is not given', () => {
|
||||||
|
renderForm('login')
|
||||||
|
expect(screen.getByTestId('cf-submit')).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { useState, type ReactNode } from 'react'
|
||||||
|
import { FormField } from '@/components/common/FormField'
|
||||||
|
import { Alert } from '@/components/common/Alert'
|
||||||
|
|
||||||
|
export type CredentialsFormMode = 'login' | 'register'
|
||||||
|
|
||||||
|
export interface CredentialsFormValues {
|
||||||
|
displayName: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All copy the form needs, so the two callers (`Auth`, `Join`) can supply
|
||||||
|
* their own translated strings without this component knowing their i18n
|
||||||
|
* keys. */
|
||||||
|
export interface CredentialsFormLabels {
|
||||||
|
loginTab: ReactNode
|
||||||
|
registerTab: ReactNode
|
||||||
|
nameLabel: ReactNode
|
||||||
|
namePlaceholder: string
|
||||||
|
emailLabel: ReactNode
|
||||||
|
emailPlaceholder: string
|
||||||
|
passwordLabel: ReactNode
|
||||||
|
passwordPlaceholder: string
|
||||||
|
submitLabel: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CredentialsFormProps {
|
||||||
|
mode: CredentialsFormMode
|
||||||
|
onModeChange: (mode: CredentialsFormMode) => void
|
||||||
|
onSubmit: (values: CredentialsFormValues) => void
|
||||||
|
error?: ReactNode
|
||||||
|
disabled?: boolean | undefined
|
||||||
|
/** Prefixes every id/data-testid, e.g. `auth` → `auth-email`. */
|
||||||
|
idPrefix: string
|
||||||
|
/** Prefixes the BEM classes for the tabs/form, e.g. `auth-page` →
|
||||||
|
* `auth-page__tabs`. */
|
||||||
|
classPrefix: string
|
||||||
|
labels: CredentialsFormLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The login/register tab pair + credentials form shared by `Auth` (the
|
||||||
|
* app's front door) and `Join` (the partner-invite landing page) — until
|
||||||
|
* now hand-duplicated between them. Owns the field values internally;
|
||||||
|
* callers only see the submitted `{ displayName, email, password }` tuple
|
||||||
|
* and the mode toggle, and stay responsible for the error banner's content,
|
||||||
|
* the submit button's disabled state and label, and what happens on submit.
|
||||||
|
*/
|
||||||
|
export function CredentialsForm({
|
||||||
|
mode,
|
||||||
|
onModeChange,
|
||||||
|
onSubmit,
|
||||||
|
error,
|
||||||
|
disabled,
|
||||||
|
idPrefix,
|
||||||
|
classPrefix,
|
||||||
|
labels,
|
||||||
|
}: CredentialsFormProps): ReactNode {
|
||||||
|
const [displayName, setDisplayName] = useState('')
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const isRegister = mode === 'register'
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit({ displayName, email, password })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={`segmented ${classPrefix}__tabs`} role="group">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`segmented__option ${classPrefix}__tab`}
|
||||||
|
aria-pressed={!isRegister}
|
||||||
|
onClick={() => onModeChange('login')}
|
||||||
|
data-testid={`${idPrefix}-tab-login`}
|
||||||
|
>
|
||||||
|
{labels.loginTab}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`segmented__option ${classPrefix}__tab`}
|
||||||
|
aria-pressed={isRegister}
|
||||||
|
onClick={() => onModeChange('register')}
|
||||||
|
data-testid={`${idPrefix}-tab-register`}
|
||||||
|
>
|
||||||
|
{labels.registerTab}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form className={`${classPrefix}__form`} onSubmit={handleSubmit}>
|
||||||
|
{isRegister && (
|
||||||
|
<FormField id={`${idPrefix}-name`} label={labels.nameLabel}>
|
||||||
|
<input
|
||||||
|
className="field__control"
|
||||||
|
placeholder={labels.namePlaceholder}
|
||||||
|
value={displayName}
|
||||||
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
<FormField id={`${idPrefix}-email`} label={labels.emailLabel}>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
data-testid={`${idPrefix}-email`}
|
||||||
|
className="field__control"
|
||||||
|
placeholder={labels.emailPlaceholder}
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField id={`${idPrefix}-password`} label={labels.passwordLabel}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
data-testid={`${idPrefix}-password`}
|
||||||
|
className="field__control"
|
||||||
|
placeholder={labels.passwordPlaceholder}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
{error && <Alert variant="error">{error}</Alert>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn--primary btn--block"
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid={`${idPrefix}-submit`}
|
||||||
|
>
|
||||||
|
{labels.submitLabel}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
+30
-68
@@ -4,8 +4,11 @@ import { useAuth } from '@/context/AuthContext'
|
|||||||
import { useWorkspace } from '@/context/WorkspaceContext'
|
import { useWorkspace } from '@/context/WorkspaceContext'
|
||||||
import { useTranslation } from '@/i18n'
|
import { useTranslation } from '@/i18n'
|
||||||
import { LanguageToggle } from '@/components/layout/LanguageToggle'
|
import { LanguageToggle } from '@/components/layout/LanguageToggle'
|
||||||
import { FormField } from '@/components/common/FormField'
|
import {
|
||||||
import { Alert } from '@/components/common/Alert'
|
CredentialsForm,
|
||||||
|
type CredentialsFormMode,
|
||||||
|
type CredentialsFormValues,
|
||||||
|
} from '@/components/auth/CredentialsForm'
|
||||||
import './auth.css'
|
import './auth.css'
|
||||||
|
|
||||||
export function Auth() {
|
export function Auth() {
|
||||||
@@ -13,11 +16,9 @@ export function Auth() {
|
|||||||
const { isLoading: wsLoading } = useWorkspace()
|
const { isLoading: wsLoading } = useWorkspace()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [isRegister, setIsRegister] = useState(false)
|
const [mode, setMode] = useState<CredentialsFormMode>('login')
|
||||||
const [email, setEmail] = useState('')
|
|
||||||
const [password, setPassword] = useState('')
|
|
||||||
const [displayName, setDisplayName] = useState('')
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const isRegister = mode === 'register'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated && !wsLoading) {
|
if (isAuthenticated && !wsLoading) {
|
||||||
@@ -25,17 +26,15 @@ export function Auth() {
|
|||||||
}
|
}
|
||||||
}, [isAuthenticated, wsLoading, navigate])
|
}, [isAuthenticated, wsLoading, navigate])
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async ({ displayName, email, password }: CredentialsFormValues) => {
|
||||||
e.preventDefault()
|
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
if (isRegister) {
|
if (isRegister) {
|
||||||
await register({ email, password, displayName })
|
await register({ email, password, displayName })
|
||||||
navigate('/', { replace: true })
|
|
||||||
} else {
|
} else {
|
||||||
await login({ email, password })
|
await login({ email, password })
|
||||||
navigate('/', { replace: true })
|
|
||||||
}
|
}
|
||||||
|
navigate('/', { replace: true })
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setError(err instanceof Error ? err.message : t('auth.genericError'))
|
setError(err instanceof Error ? err.message : t('auth.genericError'))
|
||||||
}
|
}
|
||||||
@@ -44,8 +43,8 @@ export function Auth() {
|
|||||||
// Switching tabs clears any error from the previous tab's attempt — a
|
// Switching tabs clears any error from the previous tab's attempt — a
|
||||||
// failed login's error text should not linger, confusingly, once the
|
// failed login's error text should not linger, confusingly, once the
|
||||||
// person has moved on to creating an account (or back).
|
// person has moved on to creating an account (or back).
|
||||||
const switchTab = (register: boolean) => {
|
const switchMode = (next: CredentialsFormMode) => {
|
||||||
setIsRegister(register)
|
setMode(next)
|
||||||
setError(null)
|
setError(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,62 +56,25 @@ export function Auth() {
|
|||||||
<div className="auth-page__column">
|
<div className="auth-page__column">
|
||||||
<img className="auth-page__mark" src="/img/icon-64.png" alt="" width={32} height={32} />
|
<img className="auth-page__mark" src="/img/icon-64.png" alt="" width={32} height={32} />
|
||||||
<h1 className="auth-page__title">{isRegister ? t('auth.createAccountTitle') : t('auth.loginTitle')}</h1>
|
<h1 className="auth-page__title">{isRegister ? t('auth.createAccountTitle') : t('auth.loginTitle')}</h1>
|
||||||
<div className="segmented auth-page__tabs" role="group">
|
<CredentialsForm
|
||||||
<button
|
mode={mode}
|
||||||
type="button"
|
onModeChange={switchMode}
|
||||||
className="segmented__option auth-page__tab"
|
onSubmit={(values) => void handleSubmit(values)}
|
||||||
aria-pressed={!isRegister}
|
error={error}
|
||||||
onClick={() => switchTab(false)}
|
idPrefix="auth"
|
||||||
data-testid="auth-tab-login"
|
classPrefix="auth-page"
|
||||||
>
|
labels={{
|
||||||
{t('auth.loginButton')}
|
loginTab: t('auth.loginButton'),
|
||||||
</button>
|
registerTab: t('auth.createAccountButton'),
|
||||||
<button
|
nameLabel: t('auth.nameLabel'),
|
||||||
type="button"
|
namePlaceholder: t('auth.namePlaceholder'),
|
||||||
className="segmented__option auth-page__tab"
|
emailLabel: t('auth.emailLabel'),
|
||||||
aria-pressed={isRegister}
|
emailPlaceholder: t('auth.emailPlaceholder'),
|
||||||
onClick={() => switchTab(true)}
|
passwordLabel: t('auth.passwordLabel'),
|
||||||
data-testid="auth-tab-register"
|
passwordPlaceholder: t('auth.passwordPlaceholder'),
|
||||||
>
|
submitLabel: isRegister ? t('auth.registerButton') : t('auth.loginButton'),
|
||||||
{t('auth.createAccountButton')}
|
}}
|
||||||
</button>
|
/>
|
||||||
</div>
|
|
||||||
<form className="auth-page__form" onSubmit={(e) => void handleSubmit(e)}>
|
|
||||||
{isRegister && (
|
|
||||||
<FormField id="auth-name" label={t('auth.nameLabel')}>
|
|
||||||
<input
|
|
||||||
className="field__control"
|
|
||||||
placeholder={t('auth.namePlaceholder')}
|
|
||||||
value={displayName}
|
|
||||||
onChange={(e) => setDisplayName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
)}
|
|
||||||
<FormField id="auth-email" label={t('auth.emailLabel')}>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
data-testid="auth-email"
|
|
||||||
className="field__control"
|
|
||||||
placeholder={t('auth.emailPlaceholder')}
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
<FormField id="auth-password" label={t('auth.passwordLabel')}>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
data-testid="auth-password"
|
|
||||||
className="field__control"
|
|
||||||
placeholder={t('auth.passwordPlaceholder')}
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
{error && <Alert variant="error">{error}</Alert>}
|
|
||||||
<button type="submit" className="btn btn--primary btn--block" data-testid="auth-submit">
|
|
||||||
{isRegister ? t('auth.registerButton') : t('auth.loginButton')}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|||||||
+34
-78
@@ -5,12 +5,15 @@ import { useWorkspace } from '@/context/WorkspaceContext'
|
|||||||
import { redeemInvitation } from '@/services/api'
|
import { redeemInvitation } from '@/services/api'
|
||||||
import { useTranslation, type TranslationKey } from '@/i18n'
|
import { useTranslation, type TranslationKey } from '@/i18n'
|
||||||
import { LanguageToggle } from '@/components/layout/LanguageToggle'
|
import { LanguageToggle } from '@/components/layout/LanguageToggle'
|
||||||
import { FormField } from '@/components/common/FormField'
|
|
||||||
import { Alert } from '@/components/common/Alert'
|
import { Alert } from '@/components/common/Alert'
|
||||||
|
import {
|
||||||
|
CredentialsForm,
|
||||||
|
type CredentialsFormMode,
|
||||||
|
type CredentialsFormValues,
|
||||||
|
} from '@/components/auth/CredentialsForm'
|
||||||
import './join.css'
|
import './join.css'
|
||||||
|
|
||||||
type Phase = 'checking' | 'form' | 'redeeming' | 'error' | 'done'
|
type Phase = 'checking' | 'form' | 'redeeming' | 'error' | 'done'
|
||||||
type Mode = 'login' | 'register'
|
|
||||||
|
|
||||||
/** Maps a redeem/register failure's backend message to a translated,
|
/** Maps a redeem/register failure's backend message to a translated,
|
||||||
* distinct reason. The backend (see `InvitationError` call sites in
|
* distinct reason. The backend (see `InvitationError` call sites in
|
||||||
@@ -43,10 +46,7 @@ export function Join() {
|
|||||||
|
|
||||||
const [phase, setPhase] = useState<Phase>('checking')
|
const [phase, setPhase] = useState<Phase>('checking')
|
||||||
const [errorKey, setErrorKey] = useState<TranslationKey | null>(null)
|
const [errorKey, setErrorKey] = useState<TranslationKey | null>(null)
|
||||||
const [mode, setMode] = useState<Mode>('register')
|
const [mode, setMode] = useState<CredentialsFormMode>('register')
|
||||||
const [email, setEmail] = useState('')
|
|
||||||
const [password, setPassword] = useState('')
|
|
||||||
const [displayName, setDisplayName] = useState('')
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
// Guards against redeeming twice: once from a submit handler and again
|
// Guards against redeeming twice: once from a submit handler and again
|
||||||
@@ -84,8 +84,7 @@ export function Join() {
|
|||||||
}
|
}
|
||||||
}, [authLoading, isAuthenticated, token, redeemAndGo])
|
}, [authLoading, isAuthenticated, token, redeemAndGo])
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async ({ displayName, email, password }: CredentialsFormValues) => {
|
||||||
e.preventDefault()
|
|
||||||
if (!token) return
|
if (!token) return
|
||||||
setErrorKey(null)
|
setErrorKey(null)
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
@@ -113,7 +112,7 @@ export function Join() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const switchMode = (next: Mode) => {
|
const switchMode = (next: CredentialsFormMode) => {
|
||||||
setMode(next)
|
setMode(next)
|
||||||
setErrorKey(null)
|
setErrorKey(null)
|
||||||
}
|
}
|
||||||
@@ -152,75 +151,32 @@ export function Join() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{phase === 'form' && (
|
{phase === 'form' && (
|
||||||
<>
|
<CredentialsForm
|
||||||
<div className="segmented join-page__tabs" role="group">
|
mode={mode}
|
||||||
<button
|
onModeChange={switchMode}
|
||||||
type="button"
|
onSubmit={(values) => void handleSubmit(values)}
|
||||||
className="segmented__option join-page__tab"
|
error={errorKey ? t(errorKey) : null}
|
||||||
aria-pressed={mode === 'login'}
|
disabled={isSubmitting}
|
||||||
onClick={() => switchMode('login')}
|
idPrefix="join"
|
||||||
data-testid="join-tab-login"
|
classPrefix="join-page"
|
||||||
>
|
labels={{
|
||||||
{t('invite.joinLoginTab')}
|
loginTab: t('invite.joinLoginTab'),
|
||||||
</button>
|
registerTab: t('invite.joinRegisterTab'),
|
||||||
<button
|
nameLabel: t('invite.joinNameLabel'),
|
||||||
type="button"
|
namePlaceholder: t('invite.joinNamePlaceholder'),
|
||||||
className="segmented__option join-page__tab"
|
emailLabel: t('invite.joinEmailLabel'),
|
||||||
aria-pressed={mode === 'register'}
|
emailPlaceholder: t('invite.joinEmailPlaceholder'),
|
||||||
onClick={() => switchMode('register')}
|
passwordLabel: t('invite.joinPasswordLabel'),
|
||||||
data-testid="join-tab-register"
|
passwordPlaceholder: t('invite.joinPasswordPlaceholder'),
|
||||||
>
|
submitLabel: isSubmitting
|
||||||
{t('invite.joinRegisterTab')}
|
? mode === 'register'
|
||||||
</button>
|
? t('invite.joinSubmittingRegister')
|
||||||
</div>
|
: t('invite.joinSubmittingLogin')
|
||||||
<form className="join-page__form" onSubmit={(e) => void handleSubmit(e)}>
|
: mode === 'register'
|
||||||
{mode === 'register' && (
|
? t('invite.joinSubmitRegister')
|
||||||
<FormField id="join-name" label={t('invite.joinNameLabel')}>
|
: t('invite.joinSubmitLogin'),
|
||||||
<input
|
}}
|
||||||
className="field__control"
|
/>
|
||||||
placeholder={t('invite.joinNamePlaceholder')}
|
|
||||||
value={displayName}
|
|
||||||
onChange={(e) => setDisplayName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
)}
|
|
||||||
<FormField id="join-email" label={t('invite.joinEmailLabel')}>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
data-testid="join-email"
|
|
||||||
className="field__control"
|
|
||||||
placeholder={t('invite.joinEmailPlaceholder')}
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
<FormField id="join-password" label={t('invite.joinPasswordLabel')}>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
data-testid="join-password"
|
|
||||||
className="field__control"
|
|
||||||
placeholder={t('invite.joinPasswordPlaceholder')}
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
{errorKey && <Alert variant="error">{t(errorKey)}</Alert>}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn--primary btn--block"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
data-testid="join-submit"
|
|
||||||
>
|
|
||||||
{isSubmitting
|
|
||||||
? mode === 'register'
|
|
||||||
? t('invite.joinSubmittingRegister')
|
|
||||||
: t('invite.joinSubmittingLogin')
|
|
||||||
: mode === 'register'
|
|
||||||
? t('invite.joinSubmitRegister')
|
|
||||||
: t('invite.joinSubmitLogin')}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user