├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
└── index.html
├── src
├── App.tsx
├── assets
│ ├── github.svg
│ └── google.svg
├── components
│ └── FormInput.tsx
├── index.tsx
├── pages
│ ├── Signup.page.tsx
│ └── login.page.tsx
└── react-app-env.d.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Login and Signup Forms with React, React Hook Form, TypeScript and Material UI
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mui-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.9.0",
7 | "@emotion/styled": "^11.8.1",
8 | "@hookform/resolvers": "^2.8.8",
9 | "@mui/lab": "^5.0.0-alpha.77",
10 | "@mui/material": "^5.6.1",
11 | "@types/node": "^16.11.27",
12 | "@types/react": "^18.0.5",
13 | "@types/react-dom": "^18.0.1",
14 | "react": "^18.0.0",
15 | "react-dom": "^18.0.0",
16 | "react-hook-form": "^7.30.0",
17 | "react-router-dom": "^6.3.0",
18 | "react-scripts": "5.0.1",
19 | "typescript": "^4.6.3",
20 | "zod": "^3.14.4"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | },
46 | "devDependencies": {}
47 | }
48 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wpcodevo/Blog_MUI_React-hook-form/9c4472b468fe4060f598d17f96dcbf774bcf9202/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
18 | React App
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { CssBaseline } from '@mui/material';
2 | import { Route, Routes } from 'react-router-dom';
3 | import LoginPage from './pages/login.page';
4 | import SignupPage from './pages/Signup.page';
5 |
6 | function App() {
7 | return (
8 | <>
9 |
10 |
11 | } />
12 | } />
13 |
14 | >
15 | );
16 | }
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/src/assets/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/google.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/FormInput.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { useFormContext, Controller } from 'react-hook-form';
3 | import { TextField, TextFieldProps } from '@mui/material';
4 | import { styled } from '@mui/material/styles';
5 |
6 | // 👇 Styled Material UI TextField Component
7 | const CssTextField = styled(TextField)({
8 | '& label.Mui-focused': {
9 | color: '#5e5b5d',
10 | fontWeight: 400,
11 | },
12 | '& .MuiInputBase-input': {
13 | borderColor: '#c8d0d4',
14 | },
15 | '& .MuiInput-underline:after': {
16 | border: 'none',
17 | },
18 | '& .MuiOutlinedInput-root': {
19 | '&.Mui-error': {
20 | '& .MuiOutlinedInput-notchedOutline': {
21 | borderColor: '#d32f2f',
22 | },
23 | },
24 | '& fieldset': {
25 | borderColor: '#c8d0d4',
26 | borderRadius: 0,
27 | },
28 | '&:hover fieldset': {
29 | border: '1px solid #c8d0d4',
30 | },
31 | '&.Mui-focused fieldset': {
32 | border: '1px solid #c8d0d4',
33 | },
34 | },
35 | });
36 |
37 | // 👇 Type of Props the FormInput will receive
38 | type FormInputProps = {
39 | name: string;
40 | } & TextFieldProps;
41 |
42 | const FormInput: FC = ({ name, ...otherProps }) => {
43 | // 👇 Utilizing useFormContext to have access to the form Context
44 | const {
45 | control,
46 | formState: { errors },
47 | } = useFormContext();
48 |
49 | return (
50 | (
55 |
65 | )}
66 | />
67 | );
68 | };
69 |
70 | export default FormInput;
71 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import App from './App';
5 |
6 | const root = ReactDOM.createRoot(
7 | document.getElementById('root') as HTMLElement
8 | );
9 | root.render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/pages/Signup.page.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Grid, Box, Typography, Stack } from '@mui/material';
2 | import LoadingButton from '@mui/lab/LoadingButton';
3 | import { FC } from 'react';
4 | import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
5 | import { object, string, TypeOf } from 'zod';
6 | import { zodResolver } from '@hookform/resolvers/zod';
7 | import FormInput from '../components/FormInput';
8 | import { ReactComponent as GoogleLogo } from '../assets/google.svg';
9 | import { ReactComponent as GitHubLogo } from '../assets/github.svg';
10 | import { LinkItem, OauthMuiLink } from './login.page';
11 |
12 | // 👇 SignUp Schema with Zod
13 | const signupSchema = object({
14 | name: string().min(1, 'Name is required').max(70),
15 | email: string().min(1, 'Email is required').email('Email is invalid'),
16 | password: string()
17 | .min(1, 'Password is required')
18 | .min(8, 'Password must be more than 8 characters')
19 | .max(32, 'Password must be less than 32 characters'),
20 | passwordConfirm: string().min(1, 'Please confirm your password'),
21 | }).refine((data) => data.password === data.passwordConfirm, {
22 | path: ['passwordConfirm'],
23 | message: 'Passwords do not match',
24 | });
25 |
26 | // 👇 Infer the Schema to get TypeScript Type
27 | type ISignUp = TypeOf;
28 |
29 | const SignupPage: FC = () => {
30 | // 👇 Default Values
31 | const defaultValues: ISignUp = {
32 | name: '',
33 | email: '',
34 | password: '',
35 | passwordConfirm: '',
36 | };
37 |
38 | // 👇 Object containing all the methods returned by useForm
39 | const methods = useForm({
40 | resolver: zodResolver(signupSchema),
41 | defaultValues,
42 | });
43 |
44 | // 👇 Form Handler
45 | const onSubmitHandler: SubmitHandler = (values: ISignUp) => {
46 | console.log(JSON.stringify(values, null, 4));
47 | };
48 |
49 | // 👇 Returned JSX
50 | return (
51 |
55 |
61 |
65 |
73 |
74 |
84 | Welcome To Loop True!
85 |
86 |
96 |
102 |
111 |
116 | Create new your account
117 |
118 |
119 |
126 |
133 |
140 |
147 |
148 |
159 | Sign Up
160 |
161 |
162 |
163 |
164 |
173 | Sign up using another provider:
174 |
175 |
180 |
181 |
182 | Google
183 |
184 |
185 |
186 | GitHub
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | Already have an account? Login
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 | );
204 | };
205 |
206 | export default SignupPage;
207 |
--------------------------------------------------------------------------------
/src/pages/login.page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Container,
3 | Grid,
4 | Box,
5 | Typography,
6 | Stack,
7 | Link as MuiLink,
8 | FormControlLabel,
9 | Checkbox,
10 | } from '@mui/material';
11 | import LoadingButton from '@mui/lab/LoadingButton';
12 | import { FC } from 'react';
13 | import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
14 | import { Link } from 'react-router-dom';
15 | import { literal, object, string, TypeOf } from 'zod';
16 | import { zodResolver } from '@hookform/resolvers/zod';
17 | import FormInput from '../components/FormInput';
18 | import { ReactComponent as GoogleLogo } from '../assets/google.svg';
19 | import { ReactComponent as GitHubLogo } from '../assets/github.svg';
20 | import styled from '@emotion/styled';
21 |
22 | // 👇 Styled React Route Dom Link Component
23 | export const LinkItem = styled(Link)`
24 | text-decoration: none;
25 | color: #3683dc;
26 | &:hover {
27 | text-decoration: underline;
28 | color: #5ea1b6;
29 | }
30 | `;
31 |
32 | // 👇 Styled Material UI Link Component
33 | export const OauthMuiLink = styled(MuiLink)`
34 | display: flex;
35 | justify-content: center;
36 | align-items: center;
37 | background-color: #f5f6f7;
38 | border-radius: 1;
39 | padding: 0.6rem 0;
40 | column-gap: 1rem;
41 | text-decoration: none;
42 | color: #393e45;
43 | font-weight: 500;
44 | cursor: pointer;
45 |
46 | &:hover {
47 | background-color: #fff;
48 | box-shadow: 0 1px 13px 0 rgb(0 0 0 / 15%);
49 | }
50 | `;
51 |
52 | // 👇 Login Schema with Zod
53 | const loginSchema = object({
54 | email: string().min(1, 'Email is required').email('Email is invalid'),
55 | password: string()
56 | .min(1, 'Password is required')
57 | .min(8, 'Password must be more than 8 characters')
58 | .max(32, 'Password must be less than 32 characters'),
59 | persistUser: literal(true).optional(),
60 | });
61 |
62 | // 👇 Infer the Schema to get the TS Type
63 | type ILogin = TypeOf;
64 |
65 | const LoginPage: FC = () => {
66 | // 👇 Default Values
67 | const defaultValues: ILogin = {
68 | email: '',
69 | password: '',
70 | };
71 |
72 | // 👇 The object returned from useForm Hook
73 | const methods = useForm({
74 | resolver: zodResolver(loginSchema),
75 | defaultValues,
76 | });
77 |
78 | // 👇 Submit Handler
79 | const onSubmitHandler: SubmitHandler = (values: ILogin) => {
80 | console.log(values);
81 | };
82 |
83 | // 👇 JSX to be rendered
84 | return (
85 |
89 |
95 |
99 |
100 |
108 |
118 |
124 |
133 |
138 | Log into your account
139 |
140 |
141 |
148 |
155 |
156 |
164 | }
165 | label={
166 |
174 | Trust this device
175 |
176 | }
177 | />
178 |
179 |
190 | Login
191 |
192 |
193 |
194 |
195 |
204 | Log in with another provider:
205 |
206 |
211 |
212 |
213 | Google
214 |
215 |
216 |
217 | GitHub
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 | Need an account?{' '}
226 | Sign up here
227 |
228 |
229 | Forgot your{' '}
230 | password?
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 | );
240 | };
241 |
242 | export default LoginPage;
243 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------