import type { ActionArgs, LoaderArgs, MetaFunction } from '@remix-run/node'; import { json, redirect } from '@remix-run/node'; import { Form, Link, useActionData, useCatch, useNavigate, useSearchParams } from '@remix-run/react'; import * as React from 'react'; import { TextInput, Box, Group, Button, PasswordInput, Text, Drawer, Popover, Progress, useMantineTheme, Alert } from '@mantine/core'; import { AlertTriangle, AtSign, Check, Lock, X } from 'react-feather'; import { requireUser } from '~/session.server'; import { createUser, getUserByEmail } from '~/models/user.server'; import { safeRedirect, validateEmail } from '~/utils'; export async function loader({ request }: LoaderArgs) { const loggedUser = await requireUser(request); if (!loggedUser || !loggedUser.admin) return redirect('/login'); return json({}); } export async function action({ request }: ActionArgs) { const formData = await request.formData(); const email = formData.get('email'); const password = formData.get('password'); const confirmPassword = formData.get('confirmPassword'); if (!validateEmail(email)) { return json( { errors: { email: 'Email is invalid', password: null, confirmPassword: null } }, { status: 400 } ); } if (typeof password !== 'string' || password.length === 0) { return json( { errors: { email: null, password: 'Password is required', confirmPassword: null } }, { status: 400 } ); } if (password.length < 8) { return json( { errors: { email: null, password: 'Password is too short', confirmPassword: null } }, { status: 400 } ); } if (password !== confirmPassword) { return json( { errors: { email: null, password: 'Passwords do not match', confirmPassword: 'Passwords do not match' } }, { status: 400 } ); } const existingUser = await getUserByEmail(email); if (existingUser) { return json( { errors: { email: 'A user already exists with this email', password: null, confirmPassword: null } }, { status: 400 } ); } await createUser(email, password); return redirect('/users'); } function PasswordRequirement({ meets, label }: { meets: boolean; label: string; }) { return ( {meets ? : } {label} ); } const requirements = [ { re: /[0-9]/, label: 'Includes number' }, { re: /[a-z]/, label: 'Includes lowercase letter' }, { re: /[A-Z]/, label: 'Includes uppercase letter' }, { re: /[$&+,:;=?@#|'<>.^*()%!-]/, label: 'Includes special symbol' } ]; function getStrength(password: string) { let multiplier = password.length > 7 ? 0 : 1; requirements.forEach((requirement) => { if (!requirement.re.test(password)) { multiplier += 1; } }); return Math.max(100 - (100 / (requirements.length + 1)) * multiplier, 10); } const LayoutWrapper = ({ children }: React.PropsWithChildren<{}>) => { const theme = useMantineTheme(); const navigate = useNavigate(); return ( { navigate('/users'); }} > {children} ); }; export default function SignUpPage() { const [searchParams] = useSearchParams(); const redirectTo = searchParams.get('redirectTo') ?? undefined; const actionData = useActionData(); const emailRef = React.useRef(null); const passwordRef = React.useRef(null); const [popoverOpened, setPopoverOpened] = React.useState(false); const [password, setPassword] = React.useState(''); const checks = requirements.map((requirement, index) => ( )); const strength = getStrength(password); const color = strength === 100 ? 'teal' : strength > 50 ? 'yellow' : 'red'; React.useEffect(() => { if (actionData?.errors?.email) { emailRef.current?.focus(); } else if (actionData?.errors?.password) { passwordRef.current?.focus(); } }, [actionData]); return (
} id="email" ref={emailRef} required autoFocus={true} name="email" type="email" autoComplete="email" aria-invalid={actionData?.errors?.email ? true : undefined} error={actionData?.errors?.email} errorProps={{ children: actionData?.errors?.email }} />
setPopoverOpened(true)} onBlurCapture={() => setPopoverOpened(false)} > } name="password" type="password" autoComplete="new-password" value={password} onChange={(event) => setPassword(event.target.value)} aria-invalid={actionData?.errors?.password ? true : undefined} error={actionData?.errors?.password ? true : undefined} /> {actionData?.errors?.password && ( {actionData?.errors?.password} )}
7} /> {checks}
} name="confirmPassword" type="password" autoComplete="new-password" aria-invalid={actionData?.errors?.confirmPassword ? true : undefined} error={actionData?.errors?.confirmPassword ? true : undefined} /> {actionData?.errors?.confirmPassword && ( {actionData?.errors?.confirmPassword} )}
); } export function ErrorBoundary({ error }: { error: Error }) { console.error(error); return ( } title="Error" color="red"> An unexpected error occurred: {error.message} ); } export function CatchBoundary() { const caught = useCatch(); if (caught.status === 404) { return ( } title="Error" color="red"> Not found ); } throw new Error(`Unexpected caught response with status: ${caught.status}`); }