Files
gridpilot.gg/apps/website/templates/auth/SignupTemplate.tsx
2026-01-26 17:56:11 +01:00

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>
);
}