164 lines
5.8 KiB
TypeScript
164 lines
5.8 KiB
TypeScript
'use client';
|
|
|
|
import { AuthCard } from '@/components/auth/AuthCard';
|
|
import { AuthFooterLinks } from '@/components/auth/AuthFooterLinks';
|
|
import { AuthForm } from '@/components/auth/AuthForm';
|
|
import { EnhancedFormError } from '@/components/errors/EnhancedFormError';
|
|
import { FormState } from '@/lib/builders/view-data/types/FormState';
|
|
import { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData';
|
|
import { routes } from '@/lib/routing/RouteConfig';
|
|
import { Box } from '@/ui/Box';
|
|
import { Button } from '@/ui/Button';
|
|
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 { Stack } from '@/ui/Stack';
|
|
import { Text } from '@/ui/Text';
|
|
import { AlertCircle, LogIn, Mail } from 'lucide-react';
|
|
import React from 'react';
|
|
|
|
interface LoginTemplateProps {
|
|
viewData: LoginViewData;
|
|
formActions: {
|
|
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => Promise<void>;
|
|
setFormState: React.Dispatch<React.SetStateAction<FormState>>;
|
|
setShowPassword: (show: boolean) => void;
|
|
setShowErrorDetails: (show: boolean) => void;
|
|
};
|
|
mutationState: {
|
|
isPending: boolean;
|
|
error: string | null;
|
|
};
|
|
}
|
|
|
|
export function LoginTemplate({ viewData, formActions, mutationState }: LoginTemplateProps) {
|
|
const isSubmitting = viewData.formState.isSubmitting || mutationState.isPending;
|
|
|
|
return (
|
|
<AuthCard
|
|
title="Welcome Back"
|
|
description="Sign in to access your racing dashboard"
|
|
>
|
|
<AuthForm onSubmit={formActions.handleSubmit}>
|
|
<Stack gap={4}>
|
|
<Input
|
|
label="Email Address"
|
|
id="email"
|
|
name="email"
|
|
type="email"
|
|
value={viewData.formState.fields.email.value as string}
|
|
onChange={formActions.handleChange}
|
|
errorMessage={viewData.formState.fields.email.error}
|
|
placeholder="you@example.com"
|
|
disabled={isSubmitting}
|
|
autoComplete="email"
|
|
icon={<Mail size={16} />}
|
|
/>
|
|
|
|
<Stack gap={1.5}>
|
|
<PasswordField
|
|
label="Password"
|
|
id="password"
|
|
name="password"
|
|
value={viewData.formState.fields.password.value as string}
|
|
onChange={formActions.handleChange}
|
|
errorMessage={viewData.formState.fields.password.error}
|
|
placeholder="••••••••"
|
|
disabled={isSubmitting}
|
|
autoComplete="current-password"
|
|
showPassword={viewData.showPassword}
|
|
onTogglePassword={() => formActions.setShowPassword(!viewData.showPassword)}
|
|
/>
|
|
<Box textAlign="right">
|
|
<Link href={routes.auth.forgotPassword}>
|
|
<Text size="xs" color="text-primary-accent">
|
|
Forgot password?
|
|
</Text>
|
|
</Link>
|
|
</Box>
|
|
</Stack>
|
|
|
|
<Stack direction="row" align="center" gap={2}>
|
|
<Box
|
|
as="input"
|
|
id="rememberMe"
|
|
name="rememberMe"
|
|
type="checkbox"
|
|
rounded="sm"
|
|
borderColor="outline-steel"
|
|
bg="surface-charcoal"
|
|
color="text-primary-accent"
|
|
ring="focus:ring-primary-accent/50"
|
|
w="1rem"
|
|
h="1rem"
|
|
checked={viewData.formState.fields.rememberMe.value as boolean}
|
|
onChange={formActions.handleChange}
|
|
disabled={isSubmitting}
|
|
/>
|
|
<Text as="label" htmlFor="rememberMe" size="sm" color="text-med" cursor="pointer">
|
|
Keep me signed in
|
|
</Text>
|
|
</Stack>
|
|
</Stack>
|
|
|
|
{viewData.hasInsufficientPermissions && (
|
|
<Box p={4} bg="warning-amber/10" border borderColor="warning-amber/30" rounded="md">
|
|
<Stack direction="row" align="start" gap={3}>
|
|
<Icon icon={AlertCircle} size={5} color="var(--color-warning)" />
|
|
<Box>
|
|
<Text weight="bold" color="text-warning-amber" block size="sm">Insufficient Permissions</Text>
|
|
<Text size="xs" color="text-gray-400" block mt={1}>
|
|
Please log in with an account that has the required role.
|
|
</Text>
|
|
</Box>
|
|
</Stack>
|
|
</Box>
|
|
)}
|
|
|
|
{viewData.submitError && (
|
|
<EnhancedFormError
|
|
error={new Error(viewData.submitError)}
|
|
onDismiss={() => {
|
|
formActions.setFormState((prev: FormState) => ({ ...prev, submitError: undefined }));
|
|
}}
|
|
showDeveloperDetails={viewData.showErrorDetails}
|
|
/>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
disabled={isSubmitting}
|
|
fullWidth
|
|
icon={isSubmitting ? <LoadingSpinner size={4} /> : <LogIn size={16} />}
|
|
>
|
|
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
|
</Button>
|
|
</AuthForm>
|
|
|
|
<AuthFooterLinks>
|
|
<Text size="sm" color="text-gray-400">
|
|
Don't have an account?{' '}
|
|
<Link
|
|
href={viewData.returnTo && viewData.returnTo !== '/dashboard' ? `/auth/signup?returnTo=${encodeURIComponent(viewData.returnTo)}` : '/auth/signup'}
|
|
>
|
|
<Text as="span" color="text-primary-accent" weight="bold">Create one</Text>
|
|
</Link>
|
|
</Text>
|
|
|
|
<Box mt={2}>
|
|
<Text size="xs" color="text-gray-600">
|
|
By signing in, you agree to our{' '}
|
|
<Link href="/terms">Terms</Link>
|
|
{' '}and{' '}
|
|
<Link href="/privacy">Privacy</Link>
|
|
</Text>
|
|
</Box>
|
|
</AuthFooterLinks>
|
|
</AuthCard>
|
|
);
|
|
}
|