diff --git a/package-lock.json b/package-lock.json
index 21d1eb0..d570213 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1447,37 +1447,51 @@
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
},
"@hapi/address": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
- "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ=="
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.0.1.tgz",
+ "integrity": "sha512-0oEP5UiyV4f3d6cBL8F3Z5S7iWSX39Knnl0lY8i+6gfmmIBj44JCBNtcMgwyS+5v7j3VYavNay0NFHDS+UGQcw==",
+ "requires": {
+ "@hapi/hoek": "^9.0.0"
+ }
},
"@hapi/bourne": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz",
"integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA=="
},
+ "@hapi/formula": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-2.0.0.tgz",
+ "integrity": "sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A=="
+ },
"@hapi/hoek": {
- "version": "8.5.1",
- "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz",
- "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow=="
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.0.4.tgz",
+ "integrity": "sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw=="
},
"@hapi/joi": {
- "version": "15.1.1",
- "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz",
- "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==",
+ "version": "17.1.1",
+ "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-17.1.1.tgz",
+ "integrity": "sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg==",
"requires": {
- "@hapi/address": "2.x.x",
- "@hapi/bourne": "1.x.x",
- "@hapi/hoek": "8.x.x",
- "@hapi/topo": "3.x.x"
+ "@hapi/address": "^4.0.1",
+ "@hapi/formula": "^2.0.0",
+ "@hapi/hoek": "^9.0.0",
+ "@hapi/pinpoint": "^2.0.0",
+ "@hapi/topo": "^5.0.0"
}
},
+ "@hapi/pinpoint": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.0.tgz",
+ "integrity": "sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw=="
+ },
"@hapi/topo": {
- "version": "3.1.6",
- "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz",
- "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz",
+ "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==",
"requires": {
- "@hapi/hoek": "^8.3.0"
+ "@hapi/hoek": "^9.0.0"
}
},
"@jest/console": {
@@ -22137,6 +22151,11 @@
"shallowequal": "^1.1.0"
}
},
+ "react-hook-form": {
+ "version": "5.5.1",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-5.5.1.tgz",
+ "integrity": "sha512-OicuetMsvuWaTMZqmOAszsqky56hZIirOA6YSVUGyL4IFDkubgqFoWmdEHFRjv3hly8KHEaPFkhZrRrIkUN4ww=="
+ },
"react-hotkeys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz",
@@ -27156,6 +27175,35 @@
"workbox-window": "^4.3.1"
},
"dependencies": {
+ "@hapi/address": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
+ "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ=="
+ },
+ "@hapi/hoek": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz",
+ "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow=="
+ },
+ "@hapi/joi": {
+ "version": "15.1.1",
+ "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz",
+ "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==",
+ "requires": {
+ "@hapi/address": "2.x.x",
+ "@hapi/bourne": "1.x.x",
+ "@hapi/hoek": "8.x.x",
+ "@hapi/topo": "3.x.x"
+ }
+ },
+ "@hapi/topo": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz",
+ "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==",
+ "requires": {
+ "@hapi/hoek": "^8.3.0"
+ }
+ },
"fs-extra": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz",
diff --git a/package.json b/package.json
index 8732cb2..eead9f8 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
},
"proxy": "http://localhost:8000",
"dependencies": {
+ "@hapi/joi": "17.1.1",
"@material-ui/core": "4.5.1",
"@material-ui/icons": "4.5.1",
"@material-ui/lab": "^4.0.0-alpha.29",
@@ -27,6 +28,7 @@
"prop-types": "^15.7.2",
"react": "16.11.0",
"react-dom": "16.11.0",
+ "react-hook-form": "5.5.1",
"react-router-dom": "5.1.2",
"react-scripts": "^3.4.0",
"react-select": "3.0.8",
diff --git a/src/components/Auth/AuthForm.spec.js b/src/components/Auth/AuthForm.spec.js
index 11a1d5a..01783db 100644
--- a/src/components/Auth/AuthForm.spec.js
+++ b/src/components/Auth/AuthForm.spec.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { fireEvent, render } from '@testing-library/react';
+import { fireEvent, render, act } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { BrowserRouter } from 'react-router-dom';
import AuthForm from './AuthForm';
@@ -81,6 +81,38 @@ describe('Signup', () => {
expect(mockRegisterResponse).toHaveBeenCalledTimes(1);
});
+
+ it('Show required field validation error', async () => {
+ const { getByText, getByTestId } = render(
+
+
+
+ );
+
+ await act(async () => fireEvent.click(getByTestId('submitButton')));
+ expect(getByText('Username*').className).toContain('Mui-error');
+ expect(getByText('Email*').className).toContain('Mui-error');
+ expect(getByText('Password*').className).toContain('Mui-error');
+ expect(getByText('First Name*').className).toContain('Mui-error');
+ });
+
+ it('Show username length validation error', async () => {
+ const { getByText, getByTestId, getByLabelText } = render(
+
+
+
+ );
+
+ fireEvent.change(getByLabelText(/username/i), {
+ target: { value: 'ga' },
+ });
+
+ await act(async () => fireEvent.click(getByTestId('submitButton')));
+
+ expect(
+ getByText(/"Username" length must be at least 3 characters long/i)
+ ).toBeInTheDocument();
+ });
});
describe('Login', () => {
@@ -98,16 +130,22 @@ describe('Login', () => {
username: 'Carolyne.Carter',
},
});
- fireEvent.change(getByLabelText(/username/i), {
- target: { value: 'Carolyne.Carter' },
- });
- fireEvent.change(getByLabelText(/password/i), {
- target: { value: 'password' },
- });
+ await act(async () =>
+ fireEvent.change(getByLabelText(/username/i), {
+ target: { value: 'Carolyne.Carter' },
+ })
+ );
+
+ await act(async () =>
+ fireEvent.change(getByLabelText(/password/i), {
+ target: { value: 'password' },
+ })
+ );
+
const submit = getByRole('button');
- fireEvent.click(submit);
- await mockLoginResponse();
+ await act(async () => fireEvent.click(submit));
+ await act(async () => mockLoginResponse());
expect(mockLoginResponse).toHaveBeenCalledTimes(1);
});
});
diff --git a/src/components/Auth/SignUpForm.js b/src/components/Auth/SignUpForm.js
index 0101b51..10e4108 100644
--- a/src/components/Auth/SignUpForm.js
+++ b/src/components/Auth/SignUpForm.js
@@ -4,26 +4,22 @@ import { Link, Redirect } from 'react-router-dom';
import { Box, Button, TextField } from '@material-ui/core/';
import axios from 'axios';
import { useAuth } from './AuthContext';
+import { validationResolver, defaultValues } from './SignUpForm.schema';
+import { Form, Field } from '../form';
const SignUpForm = ({ toggleActiveForm }) => {
- const [firstName, setFirstName] = useState(null);
- const [lastName, setLastName] = useState(null);
- const [username, setUsername] = useState(null);
- const [email, setEmail] = useState(null);
- const [password, setPassword] = useState(null);
const [errorMessage, setErrorMessage] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const referer = '/profile';
const auth = useAuth();
- const handleSignup = e => {
- e.preventDefault();
+ const onSubmit = ({ username, password, firstName, lastName, email }) => {
const data = {
- username: username,
- password: password,
+ username,
+ password,
+ email,
first_name: firstName,
last_name: lastName,
- email: email,
};
axios
.post('/auth/users/', data)
@@ -36,102 +32,111 @@ const SignUpForm = ({ toggleActiveForm }) => {
setErrorMessage(Object.values(err.response.data).join(''));
});
};
+
if (isLoggedIn) {
return ;
}
+
+ if (auth && auth.authTokens) {
+ return
Welcome!
;
+ }
+
return (
- <>
- {(auth && auth.authTokens) || isLoggedIn ? (
- Welcome!
- ) : (
- <>
+
+
+ Create an account
+
+
+
+
+
+
+
+
+ {errorMessage && {errorMessage}}
+
+
+
+
+
+ Already have an account?
+ {toggleActiveForm ? (
-
- Create an account
-
- setFirstName(e.target.value)}
- />
- setLastName(e.target.value)}
- />
- setUsername(e.target.value)}
- />
- setEmail(e.target.value)}
- />
- setPassword(e.target.value)}
- />
- {errorMessage && {errorMessage}}
-
-
-
-
- Already have an account?
- {toggleActiveForm ? (
-
- Log in
-
- ) : (
- Log in
- )}
- .
-
+ Log in
- >
- )}
- >
+ ) : (
+ Log in
+ )}
+ .
+
+
);
};
diff --git a/src/components/Auth/SignUpForm.schema.js b/src/components/Auth/SignUpForm.schema.js
new file mode 100644
index 0000000..d836de3
--- /dev/null
+++ b/src/components/Auth/SignUpForm.schema.js
@@ -0,0 +1,39 @@
+import Joi from '@hapi/joi';
+import { createValidationResolver } from '../form';
+
+const schema = Joi.object({
+ firstName: Joi.string()
+ .required()
+ .trim()
+ .label('First Name'),
+ lastName: Joi.string()
+ .allow('')
+ .trim()
+ .label('Last Name'),
+ username: Joi.string()
+ .alphanum()
+ .min(3)
+ .max(30)
+ .required()
+ .trim()
+ .label('Username'),
+ email: Joi.string()
+ .email({ tlds: { allow: false } })
+ .required()
+ .trim()
+ .label('Email'),
+ password: Joi.string()
+ .required()
+ .label('Password'),
+});
+
+const defaultValues = {
+ firstName: '',
+ lastName: '',
+ username: '',
+ email: '',
+ password: '',
+};
+
+const validationResolver = createValidationResolver(schema);
+export { validationResolver, schema, defaultValues };
diff --git a/src/components/Auth/__snapshots__/AuthForm.spec.js.snap b/src/components/Auth/__snapshots__/AuthForm.spec.js.snap
index daffbad..e23d6a7 100644
--- a/src/components/Auth/__snapshots__/AuthForm.spec.js.snap
+++ b/src/components/Auth/__snapshots__/AuthForm.spec.js.snap
@@ -24,7 +24,7 @@ exports[`AuthForm should render accordingly 1`] = `
data-shrink="false"
for="first-name"
>
- First Name
+ First Name*
@@ -69,6 +70,7 @@ exports[`AuthForm should render accordingly 1`] = `
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMarginDense MuiOutlinedInput-inputMarginDense"
id="last-name"
+ name="lastName"
type="text"
value=""
/>
@@ -92,16 +94,11 @@ exports[`AuthForm should render accordingly 1`] = `
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth"
>
@@ -134,16 +131,11 @@ exports[`AuthForm should render accordingly 1`] = `
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginDense MuiFormControl-fullWidth"
>