214 lines
8.2 KiB
TypeScript
214 lines
8.2 KiB
TypeScript
|
|
|
|
import { AuthCard } from '@/components/auth/AuthCard';
|
|
import { AuthFooterLinks } from '@/components/auth/AuthFooterLinks';
|
|
import { AuthForm } from '@/components/auth/AuthForm';
|
|
import { checkPasswordStrength } from '@/lib/utils/validation';
|
|
import { SignupViewData } from '@/lib/view-data/SignupViewData';
|
|
import { Button } from '@/ui/Button';
|
|
import { Grid } from '@/ui/Grid';
|
|
import { Group } from '@/ui/Group';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { Input } from '@/ui/Input';
|
|
import { Link } from '@/ui/Link';
|
|
import { LoadingSpinner } from '@/ui/LoadingSpinner';
|
|
import { PasswordField } from '@/ui/PasswordField';
|
|
import { ProgressBar } from '@/ui/ProgressBar';
|
|
import { Text } from '@/ui/Text';
|
|
import { AlertCircle, Check, Mail, User, UserPlus, X } from 'lucide-react';
|
|
import React from 'react';
|
|
|
|
interface SignupTemplateProps {
|
|
viewData: SignupViewData;
|
|
formActions: {
|
|
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => Promise<void>;
|
|
setShowPassword: (show: boolean) => void;
|
|
setShowConfirmPassword: (show: boolean) => void;
|
|
};
|
|
uiState: {
|
|
showPassword: boolean;
|
|
showConfirmPassword: boolean;
|
|
};
|
|
mutationState: {
|
|
isPending: boolean;
|
|
error: string | null;
|
|
};
|
|
}
|
|
|
|
export function SignupTemplate({ viewData, formActions, uiState, mutationState }: SignupTemplateProps) {
|
|
const isSubmitting = mutationState.isPending;
|
|
const passwordValue = viewData.formState.fields.password.value || '';
|
|
const passwordStrength = checkPasswordStrength(passwordValue);
|
|
|
|
const passwordRequirements = [
|
|
{ met: passwordValue.length >= 8, label: '8+ characters' },
|
|
{ met: /[a-z]/.test(passwordValue) && /[A-Z]/.test(passwordValue), label: 'Case mix' },
|
|
{ met: /\d/.test(passwordValue), label: 'Number' },
|
|
{ met: /[^a-zA-Z\d]/.test(passwordValue), label: 'Special' },
|
|
];
|
|
|
|
const getStrengthIntent = () => {
|
|
if (passwordStrength.score <= 2) return 'critical';
|
|
if (passwordStrength.score <= 4) return 'warning';
|
|
return 'success';
|
|
};
|
|
|
|
return (
|
|
<AuthCard
|
|
title="Create Account"
|
|
description="Join the GridPilot racing community"
|
|
>
|
|
<AuthForm onSubmit={formActions.handleSubmit}>
|
|
<Group direction="column" gap={6} fullWidth>
|
|
<Group direction="column" gap={4} fullWidth>
|
|
<Text size="xs" weight="bold" variant="low" uppercase letterSpacing="wide" block>Personal Information</Text>
|
|
<Grid cols={{ base: 1, md: 2 }} gap={4}>
|
|
<Input
|
|
label="First Name"
|
|
id="firstName"
|
|
name="firstName"
|
|
value={viewData.formState.fields.firstName.value}
|
|
onChange={formActions.handleChange}
|
|
errorMessage={viewData.formState.fields.firstName.error}
|
|
placeholder="John"
|
|
disabled={isSubmitting}
|
|
autoComplete="given-name"
|
|
icon={<User size={16} />}
|
|
/>
|
|
<Input
|
|
label="Last Name"
|
|
id="lastName"
|
|
name="lastName"
|
|
value={viewData.formState.fields.lastName.value}
|
|
onChange={formActions.handleChange}
|
|
errorMessage={viewData.formState.fields.lastName.error}
|
|
placeholder="Smith"
|
|
disabled={isSubmitting}
|
|
autoComplete="family-name"
|
|
icon={<User size={16} />}
|
|
/>
|
|
</Grid>
|
|
|
|
<Group direction="row" align="start" gap={2} fullWidth>
|
|
<Icon icon={AlertCircle} size={3.5} color="var(--ui-color-intent-warning)" />
|
|
<Text size="xs" variant="low">
|
|
<Text weight="bold" variant="warning">Note:</Text> Your name cannot be changed after signup.
|
|
</Text>
|
|
</Group>
|
|
|
|
<Input
|
|
label="Email Address"
|
|
id="email"
|
|
name="email"
|
|
type="email"
|
|
value={viewData.formState.fields.email.value}
|
|
onChange={formActions.handleChange}
|
|
errorMessage={viewData.formState.fields.email.error}
|
|
placeholder="you@example.com"
|
|
disabled={isSubmitting}
|
|
autoComplete="email"
|
|
icon={<Mail size={16} />}
|
|
/>
|
|
</Group>
|
|
|
|
<Group direction="column" gap={4} fullWidth>
|
|
<Text size="xs" weight="bold" variant="low" uppercase letterSpacing="wide" block>Security</Text>
|
|
<PasswordField
|
|
label="Password"
|
|
id="password"
|
|
name="password"
|
|
value={viewData.formState.fields.password.value}
|
|
onChange={formActions.handleChange}
|
|
errorMessage={viewData.formState.fields.password.error}
|
|
placeholder="••••••••"
|
|
disabled={isSubmitting}
|
|
autoComplete="new-password"
|
|
showPassword={uiState.showPassword}
|
|
onTogglePassword={() => formActions.setShowPassword(!uiState.showPassword)}
|
|
/>
|
|
|
|
{passwordValue && (
|
|
<Group direction="column" gap={3} fullWidth>
|
|
<Group direction="row" align="center" gap={2} fullWidth>
|
|
<Group fullWidth>
|
|
<ProgressBar
|
|
value={(passwordStrength.score / 5) * 100}
|
|
intent={getStrengthIntent()}
|
|
size="sm"
|
|
/>
|
|
</Group>
|
|
<Text size="xs" weight="bold" variant="low" uppercase>
|
|
{passwordStrength.label}
|
|
</Text>
|
|
</Group>
|
|
<Grid cols={2} gap={2}>
|
|
{passwordRequirements.map((req, index) => (
|
|
<Group key={index} direction="row" align="center" gap={1.5}>
|
|
<Icon icon={req.met ? Check : X} size={3} color={req.met ? 'var(--ui-color-intent-success)' : 'var(--ui-color-text-low)'} />
|
|
<Text size="xs" variant={req.met ? 'med' : 'low'}>
|
|
{req.label}
|
|
</Text>
|
|
</Group>
|
|
))}
|
|
</Grid>
|
|
</Group>
|
|
)}
|
|
|
|
<PasswordField
|
|
label="Confirm Password"
|
|
id="confirmPassword"
|
|
name="confirmPassword"
|
|
value={viewData.formState.fields.confirmPassword.value}
|
|
onChange={formActions.handleChange}
|
|
errorMessage={viewData.formState.fields.confirmPassword.error}
|
|
placeholder="••••••••"
|
|
disabled={isSubmitting}
|
|
autoComplete="new-password"
|
|
showPassword={uiState.showConfirmPassword}
|
|
onTogglePassword={() => formActions.setShowConfirmPassword(!uiState.showConfirmPassword)}
|
|
/>
|
|
</Group>
|
|
</Group>
|
|
|
|
{mutationState.error && (
|
|
<Group direction="row" align="start" gap={3} fullWidth>
|
|
<Icon icon={AlertCircle} size={4.5} color="var(--ui-color-intent-critical)" />
|
|
<Text size="sm" variant="critical">{mutationState.error}</Text>
|
|
</Group>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
disabled={isSubmitting}
|
|
fullWidth
|
|
icon={isSubmitting ? <LoadingSpinner size={4} /> : <UserPlus size={16} />}
|
|
>
|
|
{isSubmitting ? 'Creating account...' : 'Create Account'}
|
|
</Button>
|
|
</AuthForm>
|
|
|
|
<AuthFooterLinks>
|
|
<Text size="sm" variant="low">
|
|
Already have an account?{' '}
|
|
<Link
|
|
href={viewData.returnTo && viewData.returnTo !== '/onboarding' ? `/auth/login?returnTo=${encodeURIComponent(viewData.returnTo)}` : '/auth/login'}
|
|
>
|
|
<Text variant="primary" weight="bold">Sign in</Text>
|
|
</Link>
|
|
</Text>
|
|
|
|
<Group direction="column" gap={1} align="center" fullWidth>
|
|
<Text size="xs" variant="low">
|
|
By creating an account, you agree to our{' '}
|
|
<Link href="/terms">Terms</Link>
|
|
{' '}and{' '}
|
|
<Link href="/privacy">Privacy</Link>
|
|
</Text>
|
|
</Group>
|
|
</AuthFooterLinks>
|
|
</AuthCard>
|
|
);
|
|
}
|