├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── undraw_page_not_found_su7k.svg └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── Blockui.js ├── GlobalStyles.js ├── MockApi.js ├── MockApi.test.js ├── Page.js └── Page.test.js ├── contexts └── AuthContext.js ├── index.css ├── index.js ├── layouts └── DashboardLayout │ ├── NavBar │ ├── NavItem.js │ └── index.js │ ├── TopBar.js │ └── index.js ├── logo.svg ├── serviceWorker.js ├── setupTest.js ├── theme ├── index.js ├── shadows.js └── typography.js └── views ├── Dashboard.js ├── Homepage.js ├── Login.js ├── NotFoundPage.js └── SignIn.js /.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 | ###### Türkçe bilgilendirmeyi aşağıda bulabilirsiniz. 2 | # React Authentication Using Context Api 3 | 4 | Context was came after React version 16.3. In react, data is passed top-down (Parent to child) via props. When you want to pass data from Layout to one component where inside a few layer. You have to need pass props to every layer componentes. Context is primarily used when some data needs to be accessible by many components at different nesting levels. In this expamle, Demonstrates how to use context in authenticate. 5 | 6 | You can learn more information about context in [here](https://reactjs.org/docs/context.html) 7 | 8 | ## Installation 9 | 10 | If you want to clone your local environment. You can use below command 11 | 12 | ```bash 13 | git clone https://github.com/mbozkaya/react-authentication-with-context-api.git 14 | ``` 15 | 16 | ## Live Preview 17 | 18 | You can click [here](https://sad-kalam-2fe6f8.netlify.app/) to look at the live preview. 19 | 20 | ## Usage 21 | 22 | In this example, You able to use these features which login, signup and logout. Also I want to say, I've done login and signup requests use client side. If you want to look at this you can clik [here](https://github.com/mbozkaya/react-authentication-with-context-api/blob/main/src/components/MockApi.js) 23 | 24 | ## Contributing 25 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 26 | 27 | Please make sure to update tests as appropriate. 28 | 29 | ## License 30 | [MIT](https://choosealicense.com/licenses/mit/) 31 | 32 | # 33 | # 34 | 35 | # Context Kullanarak React Authentication Yapılandırması 36 | 37 | Context özelliği React'ın 16.3 versiyonundan sonra gelmiştir. Bu özellik sayesinde artık componentler arası state paylaşımı basit hale gelmiştir. React'ta bilindiği üzere componentler arası veri paylaşımı yukarıdan aşağıya yani parenttan child'a doğrudur ve aksi yönde veri paylaşmak için farklı yöntemler kullanılmaktadır. Ayrıca veriyi paylaşmak için en üst componentten hedef component'e kadar her component veriyi içeri aktarmak zorundadır. Context özelliğiyle birlikte bu zorunluluk ortadan kalkmış ve Oluşturulan bir context, useContext metoduyla her yerden erişilebilir hale gelmiştir. Bu sayede veri paylaşımı basit hale gelmiştir. Bu örnekte Context yaklaşımıyla authentication yapılandırmasını göreceksiniz. 38 | 39 | Context hakkında daha fazla bilgi almak için [burayı](https://reactjs.org/docs/context.html) ziyaret edebilirsiniz. 40 | 41 | ## Canlı Örnek 42 | 43 | [Buraya](https://sad-kalam-2fe6f8.netlify.app/) tıklayarak canlı örneğe göz atabilirsiniz. 44 | 45 | ## Kullanım 46 | 47 | Basit bir şekilde web sitesine giriş yapma, çıkış yapma ve kaydolma özelliklerini kullanabilirsiniz. Tamamıyla client tarafında çalışan bir örnek olduğu için ön tarafta sahte bir api requesti kullanmaktadır. Sahte apiyi incelemek için [buraya](https://github.com/mbozkaya/react-authentication-with-context-api/blob/main/src/components/MockApi.js) tıklayabilirsiniz. 48 | 49 | 50 | ## Katılımcı Olmak 51 | 52 | Bir düzeltme yada geliştirme yapmak için rahatlıkla çekme isteği(PR yada MR) oluşturabilirsiniz. 53 | 54 | ## Lisans 55 | [MIT](https://choosealicense.com/licenses/mit/) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.0", 7 | "@material-ui/icons": "^4.9.1", 8 | "@material-ui/lab": "^4.0.0-alpha.56", 9 | "@material-ui/styles": "^4.10.0", 10 | "@testing-library/jest-dom": "^4.2.4", 11 | "@testing-library/react": "^9.5.0", 12 | "@testing-library/user-event": "^7.2.1", 13 | "clsx": "^1.1.1", 14 | "formik": "^2.1.5", 15 | "history": "^5.0.0", 16 | "prop-types": "^15.7.2", 17 | "react": "^16.14.0", 18 | "react-dom": "^16.14.0", 19 | "react-feather": "^2.0.8", 20 | "react-helmet": "^6.1.0", 21 | "react-router": "^6.0.0-beta.0", 22 | "react-router-dom": "^6.0.0-beta.0", 23 | "react-scripts": "3.4.3", 24 | "yup": "^0.29.3" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "enzyme": "^3.11.0", 49 | "enzyme-adapter-react-16": "^1.15.5", 50 | "enzyme-to-json": "^3.6.1", 51 | "react-test-renderer": "^17.0.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbozkaya/react-authentication-with-context-api/905d20bcea70eef72c6fa16e9d83c2e8466540bf/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbozkaya/react-authentication-with-context-api/905d20bcea70eef72c6fa16e9d83c2e8466540bf/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbozkaya/react-authentication-with-context-api/905d20bcea70eef72c6fa16e9d83c2e8466540bf/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/undraw_page_not_found_su7k.svg: -------------------------------------------------------------------------------- 1 | page not found -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThemeProvider } from '@material-ui/core'; 3 | import './App.css'; 4 | import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; 5 | import { AuthProvider, AuthRoute } from './contexts/AuthContext'; 6 | import HomePage from './views/Homepage'; 7 | import Dashboard from './views/Dashboard'; 8 | import Login from './views/Login'; 9 | import theme from './theme'; 10 | import GlobalStyles from './components/GlobalStyles'; 11 | import Blockui from './components/Blockui'; 12 | import SignIn from './views/SignIn'; 13 | import NotFoundView from './views/NotFoundPage'; 14 | 15 | 16 | function App() { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import App from './App'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import { configure } from 'enzyme'; 6 | configure({ adapter: new Adapter() }); 7 | it("renders without crashing", () => { 8 | shallow(); 9 | }); 10 | 11 | it("renders Account header", () => { 12 | const wrapper = shallow(); 13 | const welcome =

; 14 | expect(wrapper.contains(welcome)).toEqual(false); 15 | }); -------------------------------------------------------------------------------- /src/components/Blockui.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react'; 2 | import Backdrop from '@material-ui/core/Backdrop'; 3 | import CircularProgress from '@material-ui/core/CircularProgress'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import { AuthContext } from '../contexts/AuthContext'; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | backdrop: { 9 | zIndex: theme.zIndex.drawer + 1, 10 | color: '#fff', 11 | }, 12 | })); 13 | 14 | const Blockui = () => { 15 | const authProvider = useContext(AuthContext); 16 | const classes = useStyles(); 17 | const [open, setOpen] = useState(false); 18 | useEffect(() => { 19 | setOpen(authProvider.backdrop); 20 | }, [authProvider.backdrop]) 21 | return ( 22 |
23 | 24 | 25 | 26 |
27 | ); 28 | } 29 | 30 | export default Blockui; 31 | 32 | -------------------------------------------------------------------------------- /src/components/GlobalStyles.js: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles } from '@material-ui/core'; 2 | 3 | const useStyles = makeStyles(() => createStyles({ 4 | '@global': { 5 | '*': { 6 | boxSizing: 'border-box', 7 | margin: 0, 8 | padding: 0, 9 | }, 10 | html: { 11 | '-webkit-font-smoothing': 'antialiased', 12 | '-moz-osx-font-smoothing': 'grayscale', 13 | height: '100%', 14 | width: '100%' 15 | }, 16 | body: { 17 | backgroundColor: '#f4f6f8', 18 | height: '100%', 19 | width: '100%' 20 | }, 21 | a: { 22 | textDecoration: 'none' 23 | }, 24 | '#root': { 25 | height: '100%', 26 | width: '100%' 27 | } 28 | } 29 | })); 30 | 31 | const GlobalStyles = () => { 32 | useStyles(); 33 | 34 | return null; 35 | }; 36 | 37 | export default GlobalStyles; 38 | -------------------------------------------------------------------------------- /src/components/MockApi.js: -------------------------------------------------------------------------------- 1 | export default class MockApi { 2 | constructor(defaultUsers = [], latency = 500) { 3 | this.defaultUsers = defaultUsers; 4 | this.latency = latency; 5 | this.setUsers(defaultUsers); 6 | } 7 | 8 | getAllUsers() { 9 | let users = JSON.parse(localStorage.getItem('mockApiUsers')); 10 | if (!Array.isArray(users)) { 11 | users = Array(users); 12 | } 13 | return users; 14 | } 15 | 16 | setUsers(users) { 17 | if (!Array.isArray(users)) { 18 | users = Array(users); 19 | } 20 | return localStorage.setItem('mockApiUsers', JSON.stringify(users)); 21 | } 22 | 23 | signInUser(username, password) { 24 | return new Promise(resolve => { 25 | setTimeout(() => { 26 | let apiUsers = []; 27 | apiUsers = this.getAllUsers(); 28 | let user = apiUsers.find((user) => user.username === username); 29 | if (user) { 30 | resolve({ error: true, message: 'There is a user who have same credentials' }); 31 | } else { 32 | apiUsers.push({ username, password }); 33 | this.setUsers(apiUsers); 34 | resolve({ error: false }); 35 | } 36 | }, this.latency); 37 | }); 38 | } 39 | 40 | authenticate(username, password) { 41 | let users = this.getAllUsers(); 42 | if (users.length === 0) { 43 | throw new Error('Empty User Store'); 44 | } 45 | return new Promise((resolve, reject) => { 46 | setTimeout(() => { 47 | users = this.getAllUsers(); 48 | let user = users.find((user) => user.username === username && user.password === password); 49 | if (user) { 50 | resolve({ error: false, data: users }); 51 | } else { 52 | resolve({ error: true, data: 'Invalid User Credentials' }); 53 | } 54 | }, this.latency); 55 | }); 56 | } 57 | 58 | checkAuthenticate() { 59 | const email = localStorage.getItem('email'); 60 | const password = localStorage.getItem('password'); 61 | const users = this.getAllUsers(); 62 | let user = users.find((user) => user.username === email && user.password === password); 63 | 64 | if (user) { 65 | return true; 66 | } 67 | 68 | return false; 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/components/MockApi.test.js: -------------------------------------------------------------------------------- 1 | import Adapter from 'enzyme-adapter-react-16'; 2 | import { configure, shallow } from 'enzyme'; 3 | import { MockApi } from './MockApi'; 4 | configure({ adapter: new Adapter() }); 5 | 6 | jest.mock('./MockApi', () => { 7 | // Works and lets you check for constructor calls: 8 | return { 9 | MockApi: jest.fn().mockImplementation(() => { 10 | return { 11 | getAllUSers: () => [], 12 | setUsers: () => undefined, 13 | }; 14 | }), 15 | }; 16 | }); 17 | 18 | beforeEach(() => { 19 | MockApi.mockClear(); 20 | }); 21 | 22 | 23 | 24 | it('requires be an array to get all users method result', () => { 25 | const mockApi = new MockApi(); 26 | expect(mockApi.getAllUSers()).toEqual([]); 27 | }); 28 | 29 | it('requires set users as an array', () => { 30 | const mockApi = new MockApi(); 31 | expect(mockApi.setUsers()).toEqual(undefined); 32 | }); 33 | 34 | -------------------------------------------------------------------------------- /src/components/Page.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const Page = forwardRef(({ 6 | children, 7 | title = '', 8 | ...rest 9 | }, ref) => { 10 | return ( 11 |
15 | 16 | {title} 17 | 18 | {children} 19 |
20 | ); 21 | }); 22 | 23 | Page.propTypes = { 24 | children: PropTypes.node.isRequired, 25 | title: PropTypes.string 26 | }; 27 | 28 | export default Page; -------------------------------------------------------------------------------- /src/components/Page.test.js: -------------------------------------------------------------------------------- 1 | import { mount, shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { configure } from 'enzyme'; 5 | import Page from './Page'; 6 | configure({ adapter: new Adapter() }); 7 | 8 | it('renders without crashing', () => { 9 | shallow(
) 10 | }); 11 | 12 | it('requires render with children', () => { 13 | const children =

; 14 | const wrapper = shallow({children}); 15 | expect(wrapper.contains(children)).toEqual(true); 16 | }); 17 | 18 | it('requires contain children props', () => { 19 | const children =

this is children

; 20 | const wrapper = mount({children}); 21 | expect(wrapper.props().children).toEqual(children); 22 | }); -------------------------------------------------------------------------------- /src/contexts/AuthContext.js: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Navigate, Route } from 'react-router'; 4 | import MockApi from '../components/MockApi'; 5 | 6 | 7 | const AuthContext = React.createContext(); 8 | 9 | const AuthProvider = props => { 10 | const [contextState, setContextState] = useState({ 11 | authorize: false, 12 | checkAuth: false, 13 | backdrop: false, 14 | error: false, 15 | success: false, 16 | }); 17 | 18 | const { children } = props; 19 | const mockApi = new MockApi(JSON.parse(localStorage.getItem('mockApiUsers')) || { username: 'test@test.com', password: 'test123' }, 1000); 20 | 21 | const onLogin = model => { 22 | setContextState({ 23 | ...contextState, 24 | backdrop: true, 25 | }); 26 | mockApi.authenticate(model.email, model.password) 27 | .then(response => { 28 | if (response.error === false) { 29 | localStorage.setItem('email', model.email); 30 | localStorage.setItem('password', model.password); 31 | setContextState({ 32 | authorize: true, 33 | checkAuth: true, 34 | backdrop: false, 35 | error: false, 36 | }); 37 | console.clear(); 38 | } else { 39 | setContextState({ 40 | authorize: false, 41 | checkAuth: true, 42 | backdrop: false, 43 | error: true, 44 | }); 45 | } 46 | }); 47 | }; 48 | 49 | const onLogout = () => { 50 | localStorage.removeItem('email'); 51 | localStorage.removeItem('password'); 52 | setContextState({ 53 | ...contextState, 54 | authorize: false, 55 | checkAuth: true, 56 | }); 57 | }; 58 | 59 | const signIn = model => { 60 | setContextState({ 61 | ...contextState, 62 | backdrop: true, 63 | }); 64 | mockApi.signInUser(model.email, model.password).then(response => { 65 | if (response && response.error === false) { 66 | setContextState({ 67 | ...contextState, 68 | backdrop: false, 69 | success: true, 70 | error:false, 71 | }); 72 | } else { 73 | setContextState({ 74 | ...contextState, 75 | backdrop: false, 76 | error: true, 77 | success: false, 78 | }); 79 | } 80 | }); 81 | } 82 | 83 | useLayoutEffect(() => { 84 | const auth = mockApi.checkAuthenticate(); 85 | if (auth !== contextState.authorize) { 86 | setContextState({ 87 | ...contextState, 88 | authorize: auth, 89 | checkAuth: true, 90 | }); 91 | } 92 | // eslint-disable-next-line react-hooks/exhaustive-deps 93 | }, []); 94 | 95 | return ( 96 | 108 | {children} 109 | 110 | ); 111 | } 112 | 113 | const AuthRoute = ({ component: Component, ...rest }) => ( 114 | 115 | {({ authorize, checkAuth }) => { 116 | let content = ''; 117 | 118 | if (authorize) { 119 | content = ( 120 | ( 122 | 123 | )} 124 | {...rest} 125 | /> 126 | ); 127 | } else if (checkAuth && !authorize) { 128 | console.log('You must be login') 129 | content = ; 130 | } 131 | return content; 132 | }} 133 | 134 | ); 135 | 136 | AuthProvider.propTypes = { 137 | children: PropTypes.node.isRequired, 138 | }; 139 | 140 | export { AuthContext, AuthProvider, AuthRoute }; 141 | 142 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | , 9 | document.getElementById('root') 10 | ); 11 | 12 | // If you want your app to work offline and load faster, you can change 13 | // unregister() to register() below. Note this comes with some pitfalls. 14 | // Learn more about service workers: https://bit.ly/CRA-PWA 15 | serviceWorker.unregister(); 16 | -------------------------------------------------------------------------------- /src/layouts/DashboardLayout/NavBar/NavItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink as RouterLink } from 'react-router-dom'; 3 | import clsx from 'clsx'; 4 | import PropTypes from 'prop-types'; 5 | import { 6 | Button, 7 | ListItem, 8 | makeStyles 9 | } from '@material-ui/core'; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | item: { 13 | display: 'flex', 14 | paddingTop: 0, 15 | paddingBottom: 0 16 | }, 17 | button: { 18 | color: theme.palette.text.secondary, 19 | fontWeight: theme.typography.fontWeightMedium, 20 | justifyContent: 'flex-start', 21 | letterSpacing: 0, 22 | padding: '10px 8px', 23 | textTransform: 'none', 24 | width: '100%' 25 | }, 26 | icon: { 27 | marginRight: theme.spacing(1) 28 | }, 29 | title: { 30 | marginRight: 'auto' 31 | }, 32 | active: { 33 | color: theme.palette.primary.main, 34 | '& $title': { 35 | fontWeight: theme.typography.fontWeightMedium 36 | }, 37 | '& $icon': { 38 | color: theme.palette.primary.main 39 | } 40 | } 41 | })); 42 | 43 | const NavItem = ({ 44 | className, 45 | href, 46 | icon: Icon, 47 | title, 48 | ...rest 49 | }) => { 50 | const classes = useStyles(); 51 | 52 | return ( 53 | 58 | 74 | 75 | ); 76 | }; 77 | 78 | NavItem.propTypes = { 79 | className: PropTypes.string, 80 | href: PropTypes.string, 81 | icon: PropTypes.elementType, 82 | title: PropTypes.string 83 | }; 84 | 85 | export default NavItem; 86 | -------------------------------------------------------------------------------- /src/layouts/DashboardLayout/NavBar/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Link as RouterLink, useLocation } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { 5 | Avatar, 6 | Box, 7 | Divider, 8 | Drawer, 9 | Hidden, 10 | Typography, 11 | makeStyles 12 | } from '@material-ui/core'; 13 | 14 | const user = { 15 | avatar: '/static/images/avatars/avatar_6.png', 16 | jobTitle: 'Developer', 17 | name: localStorage.getItem('email'), 18 | }; 19 | 20 | 21 | const useStyles = makeStyles(() => ({ 22 | mobileDrawer: { 23 | width: 256 24 | }, 25 | desktopDrawer: { 26 | width: 256, 27 | top: 64, 28 | height: 'calc(100% - 64px)' 29 | }, 30 | avatar: { 31 | cursor: 'pointer', 32 | width: 64, 33 | height: 64 34 | } 35 | })); 36 | 37 | const NavBar = ({ onMobileClose, openMobile }) => { 38 | const classes = useStyles(); 39 | const location = useLocation(); 40 | 41 | useEffect(() => { 42 | if (openMobile && onMobileClose) { 43 | onMobileClose(); 44 | } 45 | // eslint-disable-next-line react-hooks/exhaustive-deps 46 | }, [location.pathname]); 47 | 48 | const content = ( 49 | 54 | 60 | 66 | 71 | {user.name} 72 | 73 | 77 | {user.jobTitle} 78 | 79 | 80 | 81 | 82 | ); 83 | 84 | return ( 85 | <> 86 | 87 | 94 | {content} 95 | 96 | 97 | 98 | 104 | {content} 105 | 106 | 107 | 108 | ); 109 | }; 110 | 111 | NavBar.propTypes = { 112 | onMobileClose: PropTypes.func, 113 | openMobile: PropTypes.bool 114 | }; 115 | 116 | NavBar.defaultProps = { 117 | onMobileClose: () => {}, 118 | openMobile: false 119 | }; 120 | 121 | export default NavBar; 122 | -------------------------------------------------------------------------------- /src/layouts/DashboardLayout/TopBar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link as RouterLink } from 'react-router-dom'; 3 | import clsx from 'clsx'; 4 | import PropTypes from 'prop-types'; 5 | import { 6 | AppBar, 7 | Badge, 8 | Box, 9 | Hidden, 10 | IconButton, 11 | Toolbar, 12 | makeStyles, 13 | Dialog, 14 | DialogActions, 15 | DialogContent, 16 | DialogContentText, 17 | DialogTitle, 18 | Button, 19 | } from '@material-ui/core'; 20 | import MenuIcon from '@material-ui/icons/Menu'; 21 | import NotificationsIcon from '@material-ui/icons/NotificationsOutlined'; 22 | import InputIcon from '@material-ui/icons/Input'; 23 | import logo from '../../logo.svg'; 24 | import { AuthContext } from '../../contexts/AuthContext'; 25 | 26 | const useStyles = makeStyles(() => ({ 27 | root: {}, 28 | avatar: { 29 | width: 60, 30 | height: 60 31 | } 32 | })); 33 | 34 | const TopBar = ({ 35 | className, 36 | onMobileNavOpen, 37 | ...rest 38 | }) => { 39 | const classes = useStyles(); 40 | const [notifications] = useState([]); 41 | const [dialogOpen, setDialogOpen] = useState(false); 42 | 43 | return ( 44 | 45 | { 46 | ({ authorize, onLogin, onLogout }) => ( 47 | 52 | 53 | logo 54 | 55 | 56 | 57 | 62 | 63 | 64 | 65 | setDialogOpen(true)}> 66 | 67 | 68 | 69 | 70 | 74 | 75 | 76 | 77 | 78 | setDialogOpen(false)}> 79 | Are you sure? 80 | 81 | Are you want to quit? 82 | 83 | 84 | 87 | 93 | 94 | 95 | 96 | ) 97 | } 98 | 99 | ); 100 | }; 101 | 102 | TopBar.propTypes = { 103 | className: PropTypes.string, 104 | onMobileNavOpen: PropTypes.func 105 | }; 106 | 107 | export default TopBar; 108 | -------------------------------------------------------------------------------- /src/layouts/DashboardLayout/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { makeStyles } from '@material-ui/core'; 3 | import NavBar from './NavBar'; 4 | import TopBar from './TopBar'; 5 | import propsTypes from 'prop-types'; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | root: { 9 | backgroundColor: theme.palette.background.dark, 10 | display: 'flex', 11 | height: '100%', 12 | overflow: 'hidden', 13 | width: '100%' 14 | }, 15 | wrapper: { 16 | display: 'flex', 17 | flex: '1 1 auto', 18 | overflow: 'hidden', 19 | paddingTop: 64, 20 | [theme.breakpoints.up('lg')]: { 21 | paddingLeft: 256 22 | } 23 | }, 24 | contentContainer: { 25 | display: 'flex', 26 | flex: '1 1 auto', 27 | overflow: 'hidden' 28 | }, 29 | content: { 30 | flex: '1 1 auto', 31 | height: '100%', 32 | overflow: 'auto' 33 | } 34 | })); 35 | 36 | const DashboardLayout = props => { 37 | const classes = useStyles(); 38 | const [isMobileNavOpen, setMobileNavOpen] = useState(false); 39 | 40 | return ( 41 |
42 | setMobileNavOpen(true)} /> 43 | setMobileNavOpen(false)} 45 | openMobile={isMobileNavOpen} 46 | /> 47 |
48 |
49 |
50 | {props.children} 51 |
52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default DashboardLayout; 59 | 60 | DashboardLayout.propsType = { 61 | children: propsTypes.node.isRequired, 62 | }; 63 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/setupTest.js: -------------------------------------------------------------------------------- 1 | import { configure } from "enzyme"; 2 | import Adapter from "enzyme-adapter-react-16"; 3 | configure({ adapter: new Adapter() }); 4 | 5 | -------------------------------------------------------------------------------- /src/theme/index.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme, colors } from '@material-ui/core'; 2 | import shadows from './shadows'; 3 | import typography from './typography'; 4 | 5 | const theme = createMuiTheme({ 6 | palette: { 7 | background: { 8 | dark: '#F4F6F8', 9 | default: colors.common.white, 10 | paper: colors.common.white 11 | }, 12 | primary: { 13 | main: colors.indigo[500] 14 | }, 15 | secondary: { 16 | main: colors.indigo[500] 17 | }, 18 | text: { 19 | primary: colors.blueGrey[900], 20 | secondary: colors.blueGrey[600] 21 | } 22 | }, 23 | shadows, 24 | typography 25 | }); 26 | 27 | export default theme; 28 | -------------------------------------------------------------------------------- /src/theme/shadows.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'none', 3 | '0 0 0 1px rgba(63,63,68,0.05), 0 1px 2px 0 rgba(63,63,68,0.15)', 4 | '0 0 1px 0 rgba(0,0,0,0.31), 0 2px 2px -2px rgba(0,0,0,0.25)', 5 | '0 0 1px 0 rgba(0,0,0,0.31), 0 3px 4px -2px rgba(0,0,0,0.25)', 6 | '0 0 1px 0 rgba(0,0,0,0.31), 0 3px 4px -2px rgba(0,0,0,0.25)', 7 | '0 0 1px 0 rgba(0,0,0,0.31), 0 4px 6px -2px rgba(0,0,0,0.25)', 8 | '0 0 1px 0 rgba(0,0,0,0.31), 0 4px 6px -2px rgba(0,0,0,0.25)', 9 | '0 0 1px 0 rgba(0,0,0,0.31), 0 4px 8px -2px rgba(0,0,0,0.25)', 10 | '0 0 1px 0 rgba(0,0,0,0.31), 0 5px 8px -2px rgba(0,0,0,0.25)', 11 | '0 0 1px 0 rgba(0,0,0,0.31), 0 6px 12px -4px rgba(0,0,0,0.25)', 12 | '0 0 1px 0 rgba(0,0,0,0.31), 0 7px 12px -4px rgba(0,0,0,0.25)', 13 | '0 0 1px 0 rgba(0,0,0,0.31), 0 6px 16px -4px rgba(0,0,0,0.25)', 14 | '0 0 1px 0 rgba(0,0,0,0.31), 0 7px 16px -4px rgba(0,0,0,0.25)', 15 | '0 0 1px 0 rgba(0,0,0,0.31), 0 8px 18px -8px rgba(0,0,0,0.25)', 16 | '0 0 1px 0 rgba(0,0,0,0.31), 0 9px 18px -8px rgba(0,0,0,0.25)', 17 | '0 0 1px 0 rgba(0,0,0,0.31), 0 10px 20px -8px rgba(0,0,0,0.25)', 18 | '0 0 1px 0 rgba(0,0,0,0.31), 0 11px 20px -8px rgba(0,0,0,0.25)', 19 | '0 0 1px 0 rgba(0,0,0,0.31), 0 12px 22px -8px rgba(0,0,0,0.25)', 20 | '0 0 1px 0 rgba(0,0,0,0.31), 0 13px 22px -8px rgba(0,0,0,0.25)', 21 | '0 0 1px 0 rgba(0,0,0,0.31), 0 14px 24px -8px rgba(0,0,0,0.25)', 22 | '0 0 1px 0 rgba(0,0,0,0.31), 0 16px 28px -8px rgba(0,0,0,0.25)', 23 | '0 0 1px 0 rgba(0,0,0,0.31), 0 18px 30px -8px rgba(0,0,0,0.25)', 24 | '0 0 1px 0 rgba(0,0,0,0.31), 0 20px 32px -8px rgba(0,0,0,0.25)', 25 | '0 0 1px 0 rgba(0,0,0,0.31), 0 22px 34px -8px rgba(0,0,0,0.25)', 26 | '0 0 1px 0 rgba(0,0,0,0.31), 0 24px 36px -8px rgba(0,0,0,0.25)' 27 | ]; 28 | -------------------------------------------------------------------------------- /src/theme/typography.js: -------------------------------------------------------------------------------- 1 | export default { 2 | h1: { 3 | fontWeight: 500, 4 | fontSize: 35, 5 | letterSpacing: '-0.24px' 6 | }, 7 | h2: { 8 | fontWeight: 500, 9 | fontSize: 29, 10 | letterSpacing: '-0.24px' 11 | }, 12 | h3: { 13 | fontWeight: 500, 14 | fontSize: 24, 15 | letterSpacing: '-0.06px' 16 | }, 17 | h4: { 18 | fontWeight: 500, 19 | fontSize: 20, 20 | letterSpacing: '-0.06px' 21 | }, 22 | h5: { 23 | fontWeight: 500, 24 | fontSize: 16, 25 | letterSpacing: '-0.05px' 26 | }, 27 | h6: { 28 | fontWeight: 500, 29 | fontSize: 14, 30 | letterSpacing: '-0.05px' 31 | }, 32 | overline: { 33 | fontWeight: 500 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/views/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Container, 4 | Grid, 5 | makeStyles 6 | } from '@material-ui/core'; 7 | import Page from '../components/Page'; 8 | import DashboardLayout from '../layouts/DashboardLayout/index'; 9 | 10 | const useStyles = makeStyles((theme) => ({ 11 | root: { 12 | backgroundColor: theme.palette.background.dark, 13 | minHeight: '100%', 14 | paddingBottom: theme.spacing(3), 15 | paddingTop: theme.spacing(3) 16 | } 17 | })); 18 | 19 | const Dashboard = () => { 20 | 21 | const classes = useStyles(); 22 | 23 | return ( 24 | 25 | 29 | 30 | 34 | 41 |

Welcome to Dashboard

42 |
43 |
44 |
45 |
46 |
47 | ); 48 | } 49 | 50 | export default Dashboard; -------------------------------------------------------------------------------- /src/views/Homepage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from '../logo.svg'; 3 | import '../App.css'; 4 | import { Link } from 'react-router-dom'; 5 | import { AuthContext } from '../contexts/AuthContext'; 6 | import { makeStyles } from '@material-ui/core'; 7 | 8 | const useStyles = makeStyles((theme) => ({ 9 | wrapper: { 10 | paddingLeft:theme.spacing(3), 11 | } 12 | })); 13 | 14 | function HomePage() { 15 | const styles = useStyles(); 16 | 17 | return ( 18 | 19 | { 20 | ({ authorize }) => ( 21 |
22 |
23 | logo 24 |

Welcome to Home HomePage

25 | {authorize ? 26 | Dashboard 27 | : (
28 | Login 29 | Signin 30 |
)} 31 |
32 |
33 | ) 34 | } 35 |
36 | 37 | ); 38 | } 39 | 40 | export default HomePage; -------------------------------------------------------------------------------- /src/views/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navigate } from 'react-router'; 3 | import { AuthContext } from '../contexts/AuthContext'; 4 | import * as Yup from 'yup'; 5 | import { Formik } from 'formik'; 6 | import { 7 | Box, 8 | Button, 9 | Container, 10 | TextField, 11 | makeStyles 12 | } from '@material-ui/core'; 13 | import { Alert, AlertTitle } from '@material-ui/lab'; 14 | import Page from '../components/Page'; 15 | 16 | const useStyles = makeStyles((theme) => ({ 17 | root: { 18 | backgroundColor: theme.palette.background.dark, 19 | height: '100%', 20 | paddingBottom: theme.spacing(3), 21 | paddingTop: theme.spacing(3), 22 | }, 23 | alert: { 24 | width: '100%', 25 | '& > * + *': { 26 | marginTop: theme.spacing(2), 27 | }, 28 | } 29 | })); 30 | 31 | 32 | 33 | const Login = () => { 34 | const classes = useStyles(); 35 | return ( 36 | 37 | {({ authorize, onLogin, error }) => ( 38 | (authorize ? : 39 | 43 | 49 | 50 | { 60 | onLogin(model); 61 | }} 62 | > 63 | {({ 64 | errors, 65 | handleBlur, 66 | handleChange, 67 | handleSubmit, 68 | touched, 69 | values 70 | }) => ( 71 |
72 | { 73 | error && (
74 | ErrorYour username or your password is incorrect! 75 |
) 76 | } 77 | 90 | 103 | 104 | 113 | 114 | 115 | )} 116 |
117 |
118 |
119 |
120 | ) 121 | )} 122 |
123 | ); 124 | } 125 | 126 | export default Login; -------------------------------------------------------------------------------- /src/views/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Box, 4 | Container, 5 | Typography, 6 | makeStyles 7 | } from '@material-ui/core'; 8 | import Page from '../components/Page'; 9 | 10 | const useStyles = makeStyles((theme) => ({ 11 | root: { 12 | backgroundColor: theme.palette.background.dark, 13 | height: '100%', 14 | paddingBottom: theme.spacing(3), 15 | paddingTop: theme.spacing(3) 16 | }, 17 | image: { 18 | marginTop: 50, 19 | display: 'inline-block', 20 | maxWidth: '100%', 21 | width: 560 22 | } 23 | })); 24 | 25 | const NotFoundView = () => { 26 | const classes = useStyles(); 27 | 28 | return ( 29 | 33 | 39 | 40 | 45 | 404: The page you are looking for isn’t here 46 | 47 | 52 | You either tried some shady route or you came here by mistake. 53 | Whichever it is, try using the navigation 54 | 55 | 56 | Under development 61 | 62 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default NotFoundView; 69 | -------------------------------------------------------------------------------- /src/views/SignIn.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link as RouterLink, Navigate, useNavigate } from 'react-router-dom'; 3 | import { AuthContext } from '../contexts/AuthContext'; 4 | import * as Yup from 'yup'; 5 | import { Formik } from 'formik'; 6 | import { 7 | Box, 8 | Button, 9 | Container, 10 | TextField, 11 | makeStyles, 12 | Typography, 13 | Checkbox, 14 | FormHelperText, 15 | Link 16 | } from '@material-ui/core'; 17 | import { Alert, AlertTitle } from '@material-ui/lab'; 18 | import Snackbar from '@material-ui/core/Snackbar'; 19 | import Page from '../components/Page'; 20 | 21 | const useStyles = makeStyles((theme) => ({ 22 | root: { 23 | backgroundColor: theme.palette.background.dark, 24 | height: '100%', 25 | paddingBottom: theme.spacing(3), 26 | paddingTop: theme.spacing(3) 27 | } 28 | })); 29 | 30 | const SignIn = () => { 31 | const classes = useStyles(); 32 | const navigate = useNavigate(); 33 | const [snackbar, setSnackbar] = useState(false); 34 | 35 | const handleNavigate = () => { 36 | setSnackbar(true); 37 | setTimeout(() => { 38 | navigate('/login', { replace: true }); 39 | }, 2000); 40 | } 41 | 42 | return ( 43 | 44 | { 45 | ({ authorize, signIn, error, success }) => 46 | (authorize ? () : ( 47 | 51 | { 52 | success && handleNavigate() 53 | } 54 | 60 | 61 | { 79 | const { email, password } = model; 80 | signIn({ email, password }); 81 | }} 82 | > 83 | {({ 84 | errors, 85 | handleBlur, 86 | handleChange, 87 | handleSubmit, 88 | isSubmitting, 89 | touched, 90 | values 91 | }) => ( 92 |
93 | { 94 | error && (
95 | ErrorThere is a user who have same credentials! 96 |
) 97 | } 98 | 99 | 103 | Create new account 104 | 105 | 110 | Use your email to create new account 111 | 112 | 113 | 125 | 137 | 150 | 163 | 168 | 173 | 177 | I have read the 178 | {' '} 179 | 186 | Terms and Conditions 187 | 188 | 189 | 190 | {Boolean(touched.policy && errors.policy) && ( 191 | 192 | {errors.policy} 193 | 194 | )} 195 | 196 | 206 | 207 | 208 | )} 209 |
210 |
211 |
212 |
213 | setSnackbar(false)}> 214 | setSnackbar(false)} severity="success"> 215 | You will redirect for 2 seconds 216 | 217 | 218 |
219 |
220 | )) 221 | } 222 |
223 | ); 224 | }; 225 | 226 | export default SignIn; --------------------------------------------------------------------------------