feat: add login page
This commit is contained in:
parent
3946d0f05c
commit
b717c55643
|
|
@ -1,41 +1,62 @@
|
||||||
import type { ActionArgs, LoaderArgs, MetaFunction } from '@remix-run/node'
|
import type { ActionArgs, LoaderArgs, MetaFunction } from '@remix-run/node';
|
||||||
import { json, redirect } from '@remix-run/node'
|
import { json, redirect } from '@remix-run/node';
|
||||||
import { Form, Link, useActionData, useSearchParams } from '@remix-run/react'
|
import { Form, useActionData, useSearchParams } from '@remix-run/react';
|
||||||
import * as React from 'react'
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
TextInput,
|
||||||
|
Box,
|
||||||
|
Checkbox,
|
||||||
|
Group,
|
||||||
|
Button,
|
||||||
|
PasswordInput
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { AtSign, Lock } from 'react-feather';
|
||||||
|
|
||||||
import { verifyLogin } from '~/models/user.server'
|
import { verifyLogin } from '~/models/user.server';
|
||||||
import { createUserSession, getUserId } from '~/session.server'
|
import { createUserSession, getUserId } from '~/session.server';
|
||||||
import { safeRedirect, validateEmail } from '~/utils'
|
import { safeRedirect, validateEmail } from '~/utils';
|
||||||
|
|
||||||
export async function loader({ request }: LoaderArgs) {
|
export async function loader({ request }: LoaderArgs) {
|
||||||
const userId = await getUserId(request)
|
const userId = await getUserId(request);
|
||||||
if (userId) return redirect('/')
|
if (userId) return redirect('/time-entries');
|
||||||
return json({})
|
return json({});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function action({ request }: ActionArgs) {
|
export async function action({ request }: ActionArgs) {
|
||||||
const formData = await request.formData()
|
const formData = await request.formData();
|
||||||
const email = formData.get('email')
|
const email = formData.get('email');
|
||||||
const password = formData.get('password')
|
const password = formData.get('password');
|
||||||
const redirectTo = safeRedirect(formData.get('redirectTo'), '/')
|
const redirectTo = safeRedirect(formData.get('redirectTo'), '/');
|
||||||
const remember = formData.get('remember')
|
const remember = formData.get('remember');
|
||||||
|
|
||||||
if (!validateEmail(email)) {
|
if (!validateEmail(email)) {
|
||||||
return json({ errors: { email: 'Email is invalid', password: null } }, { status: 400 })
|
return json(
|
||||||
|
{ errors: { email: 'Email is invalid', password: null } },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof password !== 'string' || password.length === 0) {
|
if (typeof password !== 'string' || password.length === 0) {
|
||||||
return json({ errors: { password: 'Password is required', email: null } }, { status: 400 })
|
return json(
|
||||||
|
{ errors: { password: 'Password is required', email: null } },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
return json({ errors: { password: 'Password is too short', email: null } }, { status: 400 })
|
return json(
|
||||||
|
{ errors: { password: 'Password is too short', email: null } },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await verifyLogin(email, password)
|
const user = await verifyLogin(email, password);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return json({ errors: { email: 'Invalid email or password', password: null } }, { status: 400 })
|
return json(
|
||||||
|
{ errors: { email: 'Invalid email or password', password: null } },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return createUserSession({
|
return createUserSession({
|
||||||
|
|
@ -43,104 +64,75 @@ export async function action({ request }: ActionArgs) {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
remember: remember === 'on' ? true : false,
|
remember: remember === 'on' ? true : false,
|
||||||
redirectTo
|
redirectTo
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return {
|
return {
|
||||||
title: 'Login'
|
title: 'Login | WorkTimer',
|
||||||
}
|
description:
|
||||||
}
|
'WorkTimer is a time tracking app. Helps you track your time spent on projects.'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams();
|
||||||
const redirectTo = searchParams.get('redirectTo') || '/notes'
|
const redirectTo = searchParams.get('redirectTo') || '/time-entries';
|
||||||
const actionData = useActionData<typeof action>()
|
const actionData = useActionData<typeof action>();
|
||||||
const emailRef = React.useRef<HTMLInputElement>(null)
|
const emailRef = React.useRef<HTMLInputElement>(null);
|
||||||
const passwordRef = React.useRef<HTMLInputElement>(null)
|
const passwordRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (actionData?.errors?.email) {
|
if (actionData?.errors?.email) {
|
||||||
emailRef.current?.focus()
|
emailRef.current?.focus();
|
||||||
} else if (actionData?.errors?.password) {
|
} else if (actionData?.errors?.password) {
|
||||||
passwordRef.current?.focus()
|
passwordRef.current?.focus();
|
||||||
}
|
}
|
||||||
}, [actionData])
|
}, [actionData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-full flex-col justify-center">
|
<Box sx={{ maxWidth: 300 }} mx="auto">
|
||||||
<div className="mx-auto w-full max-w-md px-8">
|
<Form method="post" noValidate>
|
||||||
<Form method="post" className="space-y-6" noValidate>
|
<TextInput
|
||||||
<div>
|
mb={12}
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
withAsterisk
|
||||||
Email address
|
label="Email address"
|
||||||
</label>
|
placeholder="your@email.com"
|
||||||
<div className="mt-1">
|
icon={<AtSign size={16} />}
|
||||||
<input
|
id="email"
|
||||||
ref={emailRef}
|
ref={emailRef}
|
||||||
id="email"
|
required
|
||||||
required
|
autoFocus={true}
|
||||||
autoFocus={true}
|
name="email"
|
||||||
name="email"
|
type="email"
|
||||||
type="email"
|
autoComplete="email"
|
||||||
autoComplete="email"
|
aria-invalid={actionData?.errors?.email ? true : undefined}
|
||||||
aria-invalid={actionData?.errors?.email ? true : undefined}
|
error={actionData?.errors?.email}
|
||||||
aria-describedby="email-error"
|
errorProps={{ children: actionData?.errors?.email }}
|
||||||
className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
|
/>
|
||||||
/>
|
|
||||||
{actionData?.errors?.email && (
|
|
||||||
<div className="pt-1 text-red-700" id="email-error">
|
|
||||||
{actionData.errors.email}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<PasswordInput
|
||||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
mb={12}
|
||||||
Password
|
withAsterisk
|
||||||
</label>
|
label="Password"
|
||||||
<div className="mt-1">
|
id="password"
|
||||||
<input
|
ref={passwordRef}
|
||||||
id="password"
|
placeholder="********"
|
||||||
ref={passwordRef}
|
icon={<Lock size={16} />}
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
aria-invalid={actionData?.errors?.password ? true : undefined}
|
aria-invalid={actionData?.errors?.password ? true : undefined}
|
||||||
aria-describedby="password-error"
|
error={actionData?.errors?.password ? true : undefined}
|
||||||
className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
|
errorProps={{ children: actionData?.errors?.password }}
|
||||||
/>
|
/>
|
||||||
{actionData?.errors?.password && (
|
|
||||||
<div className="pt-1 text-red-700" id="password-error">
|
|
||||||
{actionData.errors.password}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="hidden" name="redirectTo" value={redirectTo} />
|
<input type="hidden" name="redirectTo" value={redirectTo} />
|
||||||
<button
|
|
||||||
type="submit"
|
<Group position="center" mt="md">
|
||||||
className="w-full rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400"
|
<Button type="submit">Log In</Button>
|
||||||
>
|
</Group>
|
||||||
Log in
|
</Form>
|
||||||
</button>
|
</Box>
|
||||||
<div className="flex items-center justify-between">
|
);
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
id="remember"
|
|
||||||
name="remember"
|
|
||||||
type="checkbox"
|
|
||||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<label htmlFor="remember" className="ml-2 block text-sm text-gray-900">
|
|
||||||
Remember me
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue