Skip to content

Commit

Permalink
Merge pull request #1 from theforbiddenpool/login-signup-pages
Browse files Browse the repository at this point in the history
Login & Sign Up pages
  • Loading branch information
theforbiddenpool committed Jul 15, 2022
2 parents 7dafaf0 + cd0cec8 commit 0066f5f
Show file tree
Hide file tree
Showing 17 changed files with 1,572 additions and 18 deletions.
6 changes: 3 additions & 3 deletions ui/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ module.exports = {
],
rules: {
'react/react-in-jsx-scope': 'off',
'react/jsx-props-no-spreading': [1, {
exceptions: ['Component'],
}],
'react/jsx-props-no-spreading': 'off',
'react/prop-types': 'off',
'react/require-default-props': 'off',
'jsx-a11y/label-has-associated-control': [2, {
assert: 'either',
}],
Expand Down
41 changes: 41 additions & 0 deletions ui/components/LoginForm/LoginForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';

describe('Login Form', () => {
test('renders and submits', async () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
const user = userEvent.setup();

await user.type(screen.getByLabelText(/username/i), 'john');
await user.type(screen.getByLabelText(/password/i), '$Password123');

await user.click(screen.getByRole('button', { name: /log in/i }));

await waitFor(() => expect(handleSubmit).toHaveBeenCalledWith({
username: 'john',
password: '$Password123',
}));
});

test('validation schema is correct', async () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
const user = userEvent.setup();

await user.click(screen.getByRole('button', { name: /log in/i }));

await waitFor(() => {
expect(handleSubmit).not.toHaveBeenCalled();

// required fields
expect(screen.getByText(/username is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});

// no password strength verification
await user.type(screen.getByLabelText(/password/i), 'password');
expect(screen.queryByText(/password is too weak/i)).not.toBeInTheDocument();
});
});
57 changes: 57 additions & 0 deletions ui/components/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Formik, FormikValues } from 'formik';
import { Button } from '@chakra-ui/react';
import { MdLogin } from 'react-icons/md';
import { loginSchema } from '../../utils/validation/schemas';
import { FormikInput } from '../layout';

interface LoginFormProps {
onSubmit?: (values: FormikValues) => void
}

function LoginForm({ onSubmit }: LoginFormProps) {
return (
<Formik
initialValues={{
username: '', password: '',
}}
validationSchema={loginSchema}
onSubmit={(values, { setSubmitting }) => {
onSubmit?.(values);
console.log('submitted', values);
setTimeout(() => { setSubmitting(false); }, 400);
}}
>
{ (formik) => (
<form onSubmit={formik.handleSubmit}>
<FormikInput
id="username"
name="username"
label="Username"
type="text"
isRequired
/>
<FormikInput
id="password"
name="password"
label="Password"
type="password"
isRequired
/>

<Button
type="submit"
variant="solid"
leftIcon={<MdLogin />}
isLoading={formik.isSubmitting}
>
Log In
</Button>

</form>
)}

</Formik>
);
}

export default LoginForm;
60 changes: 60 additions & 0 deletions ui/components/SignUpForm/SignUpForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SignUpForm from './SignUpForm';

describe('Sign Up Form', () => {
test('renders and submits', async () => {
const handleSubmit = jest.fn();
render(<SignUpForm onSubmit={handleSubmit} />);
const user = userEvent.setup();

await user.type(screen.getByLabelText(/^name/i), 'John');
await user.type(screen.getByLabelText(/username/i), 'john');
await user.type(screen.getByLabelText(/email/i), 'john@example.com');
await user.type(screen.getByLabelText(/password/i), '$Password123');

await user.click(screen.getByRole('button', { name: /sign up/i }));

await waitFor(() => expect(handleSubmit).toHaveBeenCalledWith({
name: 'John',
username: 'john',
email: 'john@example.com',
password: '$Password123',
}));
});

test('validation schema is correct', async () => {
const handleSubmit = jest.fn();
render(<SignUpForm onSubmit={handleSubmit} />);
const user = userEvent.setup();

// submit the form
await user.click(screen.getByRole('button', { name: /sign up/i }));

await waitFor(() => {
expect(handleSubmit).not.toHaveBeenCalled();

// required fields
expect(screen.queryByText(/^name is required/i)).toBeInTheDocument();
expect(screen.queryByText(/username is required/i)).toBeInTheDocument();
expect(screen.queryByText(/email is required/i)).toBeInTheDocument();
expect(screen.queryByText(/password is required/i)).toBeInTheDocument();
});

// valid email
const emailInput = screen.getByLabelText(/email/i);
await user.type(emailInput, 'jj');
expect(screen.queryByText(/email must be valid/i)).toBeInTheDocument();
await user.type(emailInput, 'ohn@email.com');
expect(screen.queryByText(/email must be valid/i)).not.toBeInTheDocument();

// valid password
const passwordInput = screen.getByLabelText(/password/i);
await user.type(passwordInput, 'pass');
expect(screen.queryByText(/password must contain at least 8 characters/i)).toBeInTheDocument();
await user.type(passwordInput, 'word');
expect(screen.queryByText(/password is too weak/i)).toBeInTheDocument();
await user.type(passwordInput, 'D123$');
expect(screen.queryByText(/password is too weak/i)).not.toBeInTheDocument();
});
});
70 changes: 70 additions & 0 deletions ui/components/SignUpForm/SignUpForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Formik, FormikValues } from 'formik';
import { Button } from '@chakra-ui/react';
import { MdLogin } from 'react-icons/md';
import { signUpSchema } from '../../utils/validation/schemas';
import { FormikInput } from '../layout';

interface SignUpFormProps {
onSubmit?: (values: FormikValues) => void
}

function SignUpForm({ onSubmit }: SignUpFormProps) {
return (
<Formik
initialValues={{
name: '', username: '', email: '', password: '',
}}
validationSchema={signUpSchema}
onSubmit={(values, { setSubmitting }) => {
onSubmit?.(values);
console.log('submitted', values);
setTimeout(() => { setSubmitting(false); }, 400);
}}
>
{ (formik) => (
<form onSubmit={formik.handleSubmit}>
<FormikInput
id="name"
name="name"
label="Name"
type="text"
isRequired
/>
<FormikInput
id="username"
name="username"
label="Username"
type="text"
isRequired
/>
<FormikInput
id="email"
name="email"
label="Email"
type="email"
isRequired
/>
<FormikInput
id="password"
name="password"
label="Password"
type="password"
isRequired
/>

<Button
type="submit"
leftIcon={<MdLogin />}
isLoading={formik.isSubmitting}
>
Sign Up
</Button>

</form>
)}

</Formik>
);
}

export default SignUpForm;
4 changes: 3 additions & 1 deletion ui/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable import/prefer-default-export */
import Main from './Main/Main';
import SignUpForm from './SignUpForm/SignUpForm';
import LoginForm from './LoginForm/LoginForm';

export { Main };
export { Main, SignUpForm, LoginForm };
39 changes: 39 additions & 0 deletions ui/components/layout/FormikInput/FormikInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { FieldHookConfig, useField } from 'formik';
import {
FormControl, FormErrorMessage, FormLabel, Input, InputProps,
} from '@chakra-ui/react';

interface FormControlProps {
isDisabled?: boolean;
isReadOnly?: boolean;
isRequired?: boolean;
}

type FormikInputProps =
FieldHookConfig<string> & FormControlProps & InputProps & {
label?: string
};

function FormikInput({ label, id, ...props }: FormikInputProps) {
const [field, meta] = useField(props);

const fmp: FormControlProps = {};
let inputProps: InputProps = {};

({
isDisabled: fmp.isDisabled,
isReadOnly: fmp.isReadOnly,
isRequired: fmp.isRequired,
...inputProps
} = props);

return (
<FormControl isInvalid={meta.touched && Boolean(meta.error)} {...fmp}>
{ label && <FormLabel htmlFor={id}>{label}</FormLabel> }
<Input id={id} {...field} {...inputProps} />
<FormErrorMessage>{meta.error}</FormErrorMessage>
</FormControl>
);
}

export default FormikInput;
91 changes: 91 additions & 0 deletions ui/components/layout/Link/Link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import NextLink, { LinkProps as NextLinkProps } from 'next/link';
import {
Button as ChakraButton,
ButtonProps as ChakraButtonProps,
Link as ChakraLink,
LinkProps as ChakraLinkProps,
} from '@chakra-ui/react';
import { FiExternalLink } from 'react-icons/fi';

type ButtonLink =
Exclude<ChakraButtonProps, React.ButtonHTMLAttributes<HTMLButtonElement>>
& React.AnchorHTMLAttributes<HTMLAnchorElement>;

export type LinkProps =
Omit<NextLinkProps, 'as' | 'passHref'>
& ((Omit<ChakraLinkProps, 'href'> & { type?: 'link' }) | (ButtonLink & { type: 'button' }));

/**
* There are two @ts-ignore because the ChakraButton, even though having the 'as' property, it
* expects to receive certain props as HTMLButtonElement. I cannot be arsed to try to fix this,
* nor I know how to.
*/
function Link({ type = 'link', children, ...props }: LinkProps) {
const {
href, prefetch, replace, scroll, shallow, locale, role, ...chakraProps
} = props;

const nextjsProps = {
href, prefetch, replace, scroll, shallow, locale,
};

const isExternal = typeof href === 'string' && (href.startsWith('http') || href.startsWith('mailto:'));

switch (type) {
case 'button':
if (isExternal) {
return (
// @ts-ignore
<ChakraButton
as="a"
href={href}
target="_blank"
rel="noopener"
{...chakraProps}
>
{children}
{' '}
<FiExternalLink style={{ marginLeft: '0.33em' }} />
</ChakraButton>
);
}

return (
<NextLink {...nextjsProps} passHref>
{ /** @ts-ignore */}
<ChakraButton
as="a"
{...chakraProps}
>
{children}
</ChakraButton>
</NextLink>
);
case 'link':
if (isExternal) {
return (
<ChakraLink
href={href}
isExternal
{...chakraProps}
>
{children}
{' '}
<FiExternalLink style={{ display: 'inline', verticalAlign: 'middle' }} />
</ChakraLink>
);
}

return (
<NextLink {...nextjsProps} passHref>
<ChakraLink {...chakraProps}>{children}</ChakraLink>
</NextLink>

);
default:
return null;
}
}

export default Link;
4 changes: 3 additions & 1 deletion ui/components/layout/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable import/prefer-default-export */
import HeadTitle from './HeadTitle/HeadTitle';
import Link from './Link/Link';
import FormikInput from './FormikInput/FormikInput';

export { HeadTitle };
export { HeadTitle, Link, FormikInput };

0 comments on commit 0066f5f

Please sign in to comment.