Files
gridpilot.gg/apps/website/templates/auth/SignupTemplate.tsx
2026-01-17 15:46:55 +01:00

215 lines
8.4 KiB
TypeScript

'use client';
import React from 'react';
import { UserPlus, Mail, User, Check, X, AlertCircle } from 'lucide-react';
import { Button } from '@/ui/Button';
import { Input } from '@/ui/Input';
import { Text } from '@/ui/Text';
import { Link } from '@/ui/Link';
import { Icon } from '@/ui/Icon';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { PasswordField } from '@/ui/PasswordField';
import { AuthCard } from '@/components/auth/AuthCard';
import { AuthForm } from '@/components/auth/AuthForm';
import { AuthFooterLinks } from '@/components/auth/AuthFooterLinks';
import { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData';
import { checkPasswordStrength } from '@/lib/utils/validation';
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' },
];
return (
<AuthCard
title="Create Account"
description="Join the GridPilot racing community"
>
<AuthForm onSubmit={formActions.handleSubmit}>
<Stack gap={6}>
<Stack gap={4}>
<Text size="xs" weight="bold" color="text-low" uppercase letterSpacing="wide" block>Personal Information</Text>
<Box display="grid" gridCols={{ 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} />}
/>
</Box>
<Box p={3} bg="warning-amber/5" border borderColor="warning-amber/20" rounded="md">
<Stack direction="row" align="start" gap={2}>
<Icon icon={AlertCircle} size={3.5} color="var(--color-warning)" mt={0.5} />
<Text size="xs" color="text-med">
<Text weight="bold" color="text-warning-amber">Note:</Text> Your name cannot be changed after signup.
</Text>
</Stack>
</Box>
<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} />}
/>
</Stack>
<Stack gap={4}>
<Text size="xs" weight="bold" color="text-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 && (
<Stack gap={3}>
<Stack direction="row" align="center" gap={2}>
<Box flexGrow={1} h="1px" bg="outline-steel" rounded="full" overflow="hidden">
<Box
h="full"
transition
w={`${(passwordStrength.score / 5) * 100}%`}
bg={
passwordStrength.score <= 2 ? 'critical-red' :
passwordStrength.score <= 4 ? 'warning-amber' : 'success-green'
}
/>
</Box>
<Text size="xs" weight="bold" color="text-low" uppercase>
{passwordStrength.label}
</Text>
</Stack>
<Box display="grid" gridCols={2} gap={2}>
{passwordRequirements.map((req, index) => (
<Stack key={index} direction="row" align="center" gap={1.5}>
<Icon icon={req.met ? Check : X} size={3} color={req.met ? 'var(--color-success)' : 'var(--color-text-low)'} />
<Text size="xs" color={req.met ? 'text-med' : 'text-low'}>
{req.label}
</Text>
</Stack>
))}
</Box>
</Stack>
)}
<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)}
/>
</Stack>
</Stack>
{mutationState.error && (
<Box p={4} bg="critical-red/10" border borderColor="critical-red/30" rounded="md">
<Stack direction="row" align="start" gap={3}>
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
</Stack>
</Box>
)}
<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" color="text-gray-400">
Already have an account?{' '}
<Link
href={viewData.returnTo && viewData.returnTo !== '/onboarding' ? `/auth/login?returnTo=${encodeURIComponent(viewData.returnTo)}` : '/auth/login'}
>
<Text color="text-primary-accent" weight="bold">Sign in</Text>
</Link>
</Text>
<Box mt={2}>
<Text size="xs" color="text-gray-600">
By creating an account, you agree to our{' '}
<Link href="/terms">Terms</Link>
{' '}and{' '}
<Link href="/privacy">Privacy</Link>
</Text>
</Box>
</AuthFooterLinks>
</AuthCard>
);
}