├── .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 | --------------------------------------------------------------------------------