| @@ -0,0 +1,2 @@ | |||
| REACT_APP_BASE_API_URL=https://portalgatewayapi.bullioninternational.info/ | |||
| @@ -0,0 +1,28 @@ | |||
| { | |||
| "extends": [ | |||
| "react-app", | |||
| "airbnb", | |||
| "prettier" | |||
| ], | |||
| "plugins": [ | |||
| "react", | |||
| "react-hooks", | |||
| "security" | |||
| ], | |||
| "rules": { | |||
| "react/jsx-filename-extension": "off", | |||
| "react/jsx-props-no-spreading": "off", | |||
| "react/button-has-type": "off", | |||
| "react/require-default-props": "off", | |||
| "import/no-extraneous-dependencies": "off", | |||
| "import/prefer-default-export": "off", | |||
| "consistent-return": "off", | |||
| "no-shadow": "off", | |||
| "no-use-before-define": "off", | |||
| "no-template-curly-in-string": "off", | |||
| "react-hooks/exhaustive-deps": "warn", | |||
| "prettier/prettier": ["error", { | |||
| "endOfLine":"auto" | |||
| }] | |||
| } | |||
| } | |||
| @@ -0,0 +1,22 @@ | |||
| { | |||
| "env": { | |||
| "browser": true, | |||
| "es2021": true | |||
| }, | |||
| "extends": [ | |||
| "eslint:recommended", | |||
| "plugin:react/recommended" | |||
| ], | |||
| "parserOptions": { | |||
| "ecmaFeatures": { | |||
| "jsx": true | |||
| }, | |||
| "ecmaVersion": 12, | |||
| "sourceType": "module" | |||
| }, | |||
| "plugins": [ | |||
| "react" | |||
| ], | |||
| "rules": { | |||
| } | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | |||
| # dependencies | |||
| /node_modules | |||
| /.pnp | |||
| .pnp.js | |||
| # testing | |||
| /coverage | |||
| # production | |||
| /build | |||
| # misc | |||
| .DS_Store | |||
| .env.local | |||
| .env.development.local | |||
| .env.test.local | |||
| .env.production.local | |||
| npm-debug.log* | |||
| yarn-debug.log* | |||
| yarn-error.log* | |||
| @@ -1,2 +1,70 @@ | |||
| # HRCenter | |||
| # Getting Started with Create React App | |||
| This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). | |||
| ## Available Scripts | |||
| In the project directory, you can run: | |||
| ### `npm start` | |||
| Runs the app in the development mode.\ | |||
| Open [http://localhost:3000](http://localhost:3000) to view it in the browser. | |||
| The page will reload if you make edits.\ | |||
| You will also see any lint errors in the console. | |||
| ### `npm test` | |||
| Launches the test runner in the interactive watch mode.\ | |||
| See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. | |||
| ### `npm run build` | |||
| Builds the app for production to the `build` folder.\ | |||
| It correctly bundles React in production mode and optimizes the build for the best performance. | |||
| The build is minified and the filenames include the hashes.\ | |||
| Your app is ready to be deployed! | |||
| See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. | |||
| ### `npm run eject` | |||
| **Note: this is a one-way operation. Once you `eject`, you can’t go back!** | |||
| If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. | |||
| Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. | |||
| You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. | |||
| ## Learn More | |||
| You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). | |||
| To learn React, check out the [React documentation](https://reactjs.org/). | |||
| ### Code Splitting | |||
| This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) | |||
| ### Analyzing the Bundle Size | |||
| This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) | |||
| ### Making a Progressive Web App | |||
| This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) | |||
| ### Advanced Configuration | |||
| This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) | |||
| ### Deployment | |||
| This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) | |||
| ### `npm run build` fails to minify | |||
| This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) | |||
| @@ -0,0 +1,15 @@ | |||
| const faker = require('faker'); | |||
| module.exports = () => { | |||
| const items = []; | |||
| for (let id = 1; id <= 500; id++) { | |||
| items.push({ | |||
| id: id, | |||
| name: `${faker.commerce.productAdjective()} ${faker.commerce.productMaterial()} ${faker.commerce.product()}`, | |||
| color: faker.commerce.color(), | |||
| price: `$${faker.commerce.price()}`, | |||
| company: faker.company.companyName(), | |||
| }); | |||
| } | |||
| return { items }; | |||
| }; | |||
| @@ -0,0 +1,76 @@ | |||
| { | |||
| "name": "web", | |||
| "version": "0.1.0", | |||
| "private": true, | |||
| "dependencies": { | |||
| "@emotion/react": "^11.5.0", | |||
| "@emotion/styled": "^11.3.0", | |||
| "@mui/icons-material": "^5.0.5", | |||
| "@mui/material": "^5.0.6", | |||
| "@mui/x-data-grid": "^5.0.1", | |||
| "@reduxjs/toolkit": "^1.5.1", | |||
| "@testing-library/jest-dom": "^5.13.0", | |||
| "@testing-library/react": "^11.2.7", | |||
| "@testing-library/user-event": "^12.8.3", | |||
| "axios": "^0.21.1", | |||
| "date-fns": "^2.22.1", | |||
| "eslint-plugin-prettier": "^3.4.0", | |||
| "eslint-plugin-security": "^1.4.0", | |||
| "faker": "^5.5.3", | |||
| "formik": "^2.2.9", | |||
| "i18next": "^20.3.1", | |||
| "json-server": "^0.17.0", | |||
| "jsonwebtoken": "^8.5.1", | |||
| "lodash": "^4.17.21", | |||
| "lodash.isempty": "^4.4.0", | |||
| "owasp-password-strength-test": "^1.3.0", | |||
| "react": "^17.0.2", | |||
| "react-dom": "^17.0.2", | |||
| "react-helmet-async": "^1.0.9", | |||
| "react-i18next": "^11.10.0", | |||
| "react-redux": "^7.2.4", | |||
| "react-router-dom": "^5.2.0", | |||
| "react-scripts": "4.0.3", | |||
| "react-select": "^4.3.1", | |||
| "redux": "^4.1.0", | |||
| "redux-saga": "^1.1.3", | |||
| "sass": "^1.34.1", | |||
| "web-vitals": "^1.1.2", | |||
| "yup": "^0.32.9" | |||
| }, | |||
| "scripts": { | |||
| "start": "react-scripts start", | |||
| "build": "react-scripts build", | |||
| "test": "react-scripts test", | |||
| "eject": "react-scripts eject", | |||
| "json-serve": "json-server ./db/db.js --port=4000" | |||
| }, | |||
| "eslintConfig": { | |||
| "extends": [ | |||
| "react-app", | |||
| "react-app/jest" | |||
| ] | |||
| }, | |||
| "browserslist": { | |||
| "production": [ | |||
| ">0.2%", | |||
| "not dead", | |||
| "not op_mini all" | |||
| ], | |||
| "development": [ | |||
| "last 1 chrome version", | |||
| "last 1 firefox version", | |||
| "last 1 safari version" | |||
| ] | |||
| }, | |||
| "devDependencies": { | |||
| "eslint": "^7.28.0", | |||
| "eslint-config-airbnb": "^18.2.1", | |||
| "eslint-config-prettier": "^8.3.0", | |||
| "eslint-plugin-import": "^2.23.4", | |||
| "eslint-plugin-jsx-a11y": "^6.4.1", | |||
| "eslint-plugin-react": "^7.24.0", | |||
| "eslint-plugin-react-hooks": "^4.2.0", | |||
| "prettier": "2.3.1" | |||
| } | |||
| } | |||
| @@ -0,0 +1,43 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="utf-8" /> | |||
| <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |||
| <meta name="theme-color" content="#000000" /> | |||
| <meta | |||
| name="description" | |||
| content="Web site created using create-react-app" | |||
| /> | |||
| <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> | |||
| <!-- | |||
| manifest.json provides metadata used when your web app is installed on a | |||
| user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ | |||
| --> | |||
| <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> | |||
| <!-- | |||
| Notice the use of %PUBLIC_URL% in the tags above. | |||
| It will be replaced with the URL of the `public` folder during the build. | |||
| Only files inside the `public` folder can be referenced from the HTML. | |||
| Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will | |||
| work correctly both with client-side routing and a non-root public URL. | |||
| Learn how to configure a non-root public URL by running `npm run build`. | |||
| --> | |||
| <title>React App</title> | |||
| </head> | |||
| <body> | |||
| <noscript>You need to enable JavaScript to run this app.</noscript> | |||
| <div id="root"></div> | |||
| <!-- | |||
| This HTML file is a template. | |||
| If you open it directly in the browser, you will see an empty page. | |||
| You can add webfonts, meta tags, or analytics to this file. | |||
| The build step will place the bundled scripts into the <body> tag. | |||
| To begin the development, run `npm start` or `yarn start`. | |||
| To create a production bundle, use `npm run build` or `yarn build`. | |||
| --> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,25 @@ | |||
| { | |||
| "short_name": "React App", | |||
| "name": "Create React App Sample", | |||
| "icons": [ | |||
| { | |||
| "src": "favicon.ico", | |||
| "sizes": "64x64 32x32 24x24 16x16", | |||
| "type": "image/x-icon" | |||
| }, | |||
| { | |||
| "src": "logo192.png", | |||
| "type": "image/png", | |||
| "sizes": "192x192" | |||
| }, | |||
| { | |||
| "src": "logo512.png", | |||
| "type": "image/png", | |||
| "sizes": "512x512" | |||
| } | |||
| ], | |||
| "start_url": ".", | |||
| "display": "standalone", | |||
| "theme_color": "#000000", | |||
| "background_color": "#ffffff" | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| # https://www.robotstxt.org/robotstxt.html | |||
| User-agent: * | |||
| Disallow: | |||
| @@ -0,0 +1,38 @@ | |||
| .App { | |||
| text-align: center; | |||
| } | |||
| .App-logo { | |||
| height: 40vmin; | |||
| pointer-events: none; | |||
| } | |||
| @media (prefers-reduced-motion: no-preference) { | |||
| .App-logo { | |||
| animation: App-logo-spin infinite 20s linear; | |||
| } | |||
| } | |||
| .App-header { | |||
| background-color: #282c34; | |||
| min-height: 100vh; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| justify-content: center; | |||
| font-size: calc(10px + 2vmin); | |||
| color: white; | |||
| } | |||
| .App-link { | |||
| color: #61dafb; | |||
| } | |||
| @keyframes App-logo-spin { | |||
| from { | |||
| transform: rotate(0deg); | |||
| } | |||
| to { | |||
| transform: rotate(360deg); | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| import React from 'react'; | |||
| import { Router } from 'react-router-dom'; | |||
| import { Helmet } from 'react-helmet-async'; | |||
| import i18next from 'i18next'; | |||
| import history from './store/utils/history'; | |||
| import AppRoutes from './AppRoutes'; | |||
| const App = () => ( | |||
| <> | |||
| <Router history={history}> | |||
| <Helmet> | |||
| <title> | |||
| {i18next.t('app.title')} | |||
| </title> | |||
| </Helmet> | |||
| <main className="l-page"> | |||
| <AppRoutes /> | |||
| </main> | |||
| </Router> | |||
| </> | |||
| ); | |||
| export default App; | |||
| @@ -0,0 +1,8 @@ | |||
| import { render, screen } from '@testing-library/react'; | |||
| import App from './App'; | |||
| test('renders learn react link', () => { | |||
| render(<App />); | |||
| const linkElement = screen.getByText(/learn react/i); | |||
| expect(linkElement).toBeInTheDocument(); | |||
| }); | |||
| @@ -0,0 +1,40 @@ | |||
| import React from 'react'; | |||
| import { Redirect, Route, Switch } from 'react-router-dom'; | |||
| import { | |||
| LOGIN_PAGE, | |||
| HOME_PAGE, | |||
| FORGOT_PASSWORD_PAGE, | |||
| NOT_FOUND_PAGE, | |||
| ERROR_PAGE, | |||
| BASE_PAGE, | |||
| } from './constants/pages'; | |||
| // import LoginPage from './pages/LoginPage/LoginPage'; | |||
| import LoginPage from './pages/LoginPage/LoginPageMUI'; | |||
| // import HomePage from './pages/HomePage/HomePage'; | |||
| import HomePage from './pages/HomePage/HomePageMUI'; | |||
| import NotFoundPage from './pages/ErrorPages/NotFoundPage'; | |||
| import ErrorPage from './pages/ErrorPages/ErrorPage'; | |||
| // import ForgotPasswordPage from './pages/ForgotPasswordPage/ForgotPasswordPage'; | |||
| import ForgotPasswordPage from './pages/ForgotPasswordPage/ForgotPasswordPageMUI'; | |||
| import PrivateRoute from './components/Router/PrivateRoute'; | |||
| const AppRoutes = () => ( | |||
| <Switch> | |||
| <Route exact path={BASE_PAGE} component={LoginPage} /> | |||
| <Route exact path={LOGIN_PAGE} component={LoginPage} /> | |||
| <Route path={NOT_FOUND_PAGE} component={NotFoundPage} /> | |||
| <Route path={ERROR_PAGE} component={ErrorPage} /> | |||
| <Route path={FORGOT_PASSWORD_PAGE} component={ForgotPasswordPage} /> | |||
| <PrivateRoute | |||
| exact | |||
| path={HOME_PAGE} | |||
| component={HomePage} | |||
| /> | |||
| <Redirect from="*" to={NOT_FOUND_PAGE} /> | |||
| </Switch> | |||
| ); | |||
| export default AppRoutes; | |||
| @@ -0,0 +1,10 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <g id="Master" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | |||
| <g id="Icons" transform="translate(-40.000000, -151.000000)" stroke="#000000"> | |||
| <g id="Icons-/-Chevron-/-Down" transform="translate(40.000000, 151.000000)"> | |||
| <polyline id="Path" transform="translate(8.000000, 8.000000) rotate(-270.000000) translate(-8.000000, -8.000000) " points="5 2 11 8 5 14" stroke="#e2930a"></polyline> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @@ -0,0 +1,12 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <g id="Master" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g id="Buy-page" transform="translate(-136.000000, -852.000000)" stroke="currentColor"> | |||
| <g id="Filter-item-Copy-16" transform="translate(136.000000, 848.000000)"> | |||
| <g id="Icons-/-Checkbox-/-off" transform="translate(0.000000, 4.000000)"> | |||
| <polyline id="Path" transform="translate(8.000000, 8.000000) rotate(-270.000000) translate(-8.000000, -8.000000) " points="5 2 11 8 5 14"></polyline> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @@ -0,0 +1,6 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <g id="Caps-Lock" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <path d="M17,19 L17,21 L7,21 L7,19 L17,19 Z M12,3 L21,10 L17,10 L17,17 L7,17 L7,10 L3,10 L12,3 Z" id="Combined-Shape" fill="currentColor"></path> | |||
| </g> | |||
| </svg> | |||
| @@ -0,0 +1,10 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <g id="Master" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g id="Icons" transform="translate(-40.000000, -79.000000)" fill="currentColor"> | |||
| <g id="eye-off-outline" transform="translate(40.000000, 79.000000)"> | |||
| <path d="M2.47398116,3.99464566 L2.53252547,4.02549094 L13.8658588,11.3588243 C14.0358755,11.4688351 14.0845198,11.6958421 13.9745091,11.8658588 C13.8767217,12.0169847 13.6864928,12.0722116 13.5260188,12.0053543 L13.4674745,11.9745091 L2.1341412,4.64117572 C1.96412454,4.53116495 1.91548016,4.30415786 2.02549094,4.1341412 C2.1232783,3.98301528 2.31350719,3.92778845 2.47398116,3.99464566 Z M3.46302885,6.35365053 L4.1083929,6.7708723 C3.81029658,7.0585608 3.52305647,7.38077325 3.2494004,7.73654755 L3.04674798,8.0096561 L3.08366782,8.06547524 C4.27326632,9.84797951 6.06094208,10.962963 7.98004454,10.962963 C8.65722341,10.962963 9.32390587,10.8201119 9.95521088,10.5562937 L10.69564,11.0338727 C9.84843225,11.4648639 8.92701525,11.7037037 7.98004454,11.7037037 C5.75860617,11.7037037 3.72666166,10.4226157 2.40821861,8.41164019 C2.24142826,8.1600981 2.24649075,7.83544344 2.42215728,7.58739593 C2.74871545,7.13128497 3.09707817,6.71935421 3.46302885,6.35365053 Z M5.71548132,7.81273581 L6.50413753,8.32383034 C6.65572725,8.9862843 7.26257495,9.48148148 7.98812175,9.48148148 C8.07995333,9.48148148 8.1698834,9.47354859 8.25722954,9.45834774 L9.04753326,9.96841802 C8.73098269,10.1305066 8.37054437,10.2222222 7.98812175,10.2222222 C6.72856811,10.2222222 5.70749814,9.22729944 5.70749814,8 C5.70749814,7.93693045 5.7101946,7.87447455 5.71548132,7.81273581 Z M7.98004454,4.2962963 C9.10605419,4.2962963 10.2063142,4.63943012 11.2089405,5.27021664 C12.1313806,5.85055521 12.9459997,6.66069361 13.5686244,7.59770297 C13.7306028,7.84293225 13.7306028,8.15776219 13.569353,8.40188399 C13.2688682,8.86037696 12.9288642,9.28189795 12.5567195,9.65936669 L11.9125299,9.24126945 C12.2874179,8.87549361 12.6294637,8.45971211 12.9293712,8.00210112 C12.9300086,8.00103291 12.9300086,7.99966153 12.9300086,7.99944169 C11.736763,6.20380333 9.88220431,5.03703704 7.98004454,5.03703704 C7.33011363,5.03703704 6.68048528,5.17600946 6.04999663,5.44731057 L5.31648269,4.97253171 C6.16987895,4.52781301 7.07030912,4.2962963 7.98004454,4.2962963 Z M7.98812175,5.77777778 C9.24767539,5.77777778 10.2687454,6.77270056 10.2687454,8 C10.2687454,8.05834477 10.2664378,8.11616438 10.2619067,8.1733769 L9.4680009,7.65873003 C9.30983274,7.00504358 8.70728744,6.51851852 7.98812175,6.51851852 C7.90245898,6.51851852 7.81845084,6.52542142 7.73665123,6.53868749 L6.94186885,6.02489746 C7.25523458,5.8669741 7.61098796,5.77777778 7.98812175,5.77777778 Z" id="Combined-Shape"></path> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @@ -0,0 +1,4 @@ | |||
| <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path d="M7.98049 4.29626C5.93884 4.29626 3.94406 5.4623 2.4226 7.58736C2.24693 7.83541 2.24187 8.16007 2.40866 8.41161C3.7271 10.4226 5.75905 11.7037 7.98049 11.7037C10.1878 11.7037 12.2564 10.406 13.5698 8.40185C13.731 8.15773 13.731 7.8429 13.5691 7.59767C12.9464 6.66066 12.1318 5.85052 11.2094 5.27018C10.2068 4.6394 9.1065 4.29626 7.98049 4.29626ZM7.98049 5.03701C9.88265 5.03701 11.7372 6.20377 12.9305 7.99941C12.9305 7.99963 12.9305 8.001 12.9298 8.00207C11.746 9.80837 9.90566 10.9629 7.98049 10.9629C6.06138 10.9629 4.27371 9.84795 3.08411 8.06544L3.04719 8.00962C4.43378 6.07295 6.20601 5.03701 7.98049 5.03701Z" fill="currentColor"/> | |||
| <path d="M7.98765 5.77771C6.7281 5.77771 5.70703 6.77263 5.70703 7.99993C5.70703 9.22723 6.7281 10.2222 7.98765 10.2222C9.24721 10.2222 10.2683 9.22723 10.2683 7.99993C10.2683 6.77263 9.24721 5.77771 7.98765 5.77771ZM7.98765 6.51845C8.82736 6.51845 9.50807 7.18173 9.50807 7.99993C9.50807 8.81813 8.82736 9.48141 7.98765 9.48141C7.14795 9.48141 6.46724 8.81813 6.46724 7.99993C6.46724 7.18173 7.14795 6.51845 7.98765 6.51845Z" fill="currentColor"/> | |||
| </svg> | |||
| @@ -0,0 +1,10 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <g id="Master" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g id="Icons" transform="translate(-136.000000, -79.000000)" stroke="currentColor"> | |||
| <g id="Icons/search" transform="translate(136.000000, 79.000000)"> | |||
| <path d="M7,3 C4.790861,3 3,4.790861 3,7 C3,9.209139 4.790861,11 7,11 C9.209139,11 11,9.209139 11,7 C10.9998594,4.79091925 9.20908075,3.00014061 7,3 Z M10,10 L13,13" id="Combined-Shape"></path> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @@ -0,0 +1,46 @@ | |||
| body { | |||
| margin: 0; | |||
| -webkit-font-smoothing: antialiased; | |||
| -moz-osx-font-smoothing: grayscale; | |||
| overflow-anchor: none; | |||
| } | |||
| * { | |||
| box-sizing: border-box; | |||
| } | |||
| html { | |||
| min-height: 100%; | |||
| font-size: 16px; | |||
| @include media-below($bp-xxl) { | |||
| font-size: 14px; | |||
| } | |||
| @include media-below($bp-xs) { | |||
| font-size: 13px; | |||
| } | |||
| @include media-below($bp-xxs) { | |||
| font-size: 10.5px; | |||
| } | |||
| } | |||
| html, | |||
| body, | |||
| #root { | |||
| @include flex-column; | |||
| flex: 1 0 auto; | |||
| } | |||
| input[type='search']::-webkit-search-decoration, | |||
| input[type='search']::-webkit-search-cancel-button, | |||
| input[type='search']::-webkit-search-results-button, | |||
| input[type='search']::-webkit-search-results-decoration { | |||
| -webkit-appearance: none; | |||
| } | |||
| ul { | |||
| list-style: none; | |||
| padding: 0; | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| @function pxToRem($target, $context: $base-font-size) { | |||
| @return ($target / $context) * 1rem; | |||
| } | |||
| @function pxToRemMd($target, $context: $base-font-size-md) { | |||
| @return ($target / $context) * 1rem; | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| .l-page { | |||
| @include flex-column; | |||
| flex: 1 1 auto; | |||
| padding-bottom: 7rem; | |||
| position:relative; | |||
| @include media-below($bp-xl) { | |||
| padding-bottom: 4rem; | |||
| } | |||
| } | |||
| .l-section { | |||
| padding: 0 3.25rem; | |||
| @include media-below($bp-xl) { | |||
| padding: 0; | |||
| } | |||
| } | |||
| @@ -0,0 +1,81 @@ | |||
| @mixin desktop { | |||
| @media (min-width: 1280px) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin desktop-lg { | |||
| @media (min-width: 1480px) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin tablet { | |||
| @media (max-width: 1024px) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin media-up($media) { | |||
| @media only screen and (min-width: $media) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin media-below($media) { | |||
| @media only screen and (max-width: #{$media - 0.02px}) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin media-between($mediaMin, $mediaMax) { | |||
| @media screen and (min-width: $mediaMin) and (max-width: #{$mediaMax - 0.02px}) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin flex-center { | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| } | |||
| @mixin flex-column { | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| @mixin button-clear { | |||
| border: none; | |||
| padding: 0; | |||
| background-color: transparent; | |||
| } | |||
| @mixin outline-none { | |||
| &, | |||
| &:active, | |||
| &:focus { | |||
| outline: none; | |||
| } | |||
| } | |||
| @mixin reset-position { | |||
| position: relative; | |||
| top: initial; | |||
| left: initial; | |||
| right: initial; | |||
| bottom: initial; | |||
| } | |||
| @mixin text-ellipsis { | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| @mixin line-clamp($lines) { | |||
| display: -webkit-box; | |||
| -webkit-line-clamp: $lines; | |||
| -webkit-box-orient: vertical; | |||
| overflow: hidden; | |||
| } | |||
| @@ -0,0 +1,244 @@ | |||
| // Overwrite | |||
| .ais-ClearRefinements-button { | |||
| color: $grey-11; | |||
| font-size: pxToRem(14px); | |||
| letter-spacing: 0; | |||
| line-height: 1.15; | |||
| background-color: transparent; | |||
| border: none; | |||
| text-decoration: underline; | |||
| position: relative; | |||
| transition: all 0.2s; | |||
| outline: none; | |||
| cursor: pointer; | |||
| &[disabled] { | |||
| pointer-events: none; | |||
| opacity: 0.5; | |||
| cursor: auto; | |||
| } | |||
| &:hover { | |||
| color: $color-primary-light; | |||
| } | |||
| &:active { | |||
| color: $color-primary-dark; | |||
| } | |||
| } | |||
| .ais-RefinementList { | |||
| margin-bottom: pxToRem(32px); | |||
| margin-left: pxToRem(16px); | |||
| &.c-filter__refinement--closed { | |||
| display: none; | |||
| } | |||
| } | |||
| .ais-RefinementList.expanded { | |||
| .ais-RefinementList-showMore::before { | |||
| transform: rotate(180deg); | |||
| } | |||
| } | |||
| .ais-RefinementList-showMore { | |||
| color: $color-primary; | |||
| font-size: pxToRem(14px); | |||
| font-weight: 600; | |||
| letter-spacing: 0; | |||
| line-height: 1.56; | |||
| text-align: center; | |||
| background-color: transparent; | |||
| border: none; | |||
| position: relative; | |||
| margin-left: pxToRem(20px); | |||
| outline: none; | |||
| transition: all ease-in-out 0.3s; | |||
| cursor: pointer; | |||
| &[disabled] { | |||
| display: none; | |||
| } | |||
| &:hover { | |||
| color: $color-primary-light; | |||
| } | |||
| &:active { | |||
| color: $color-primary-dark; | |||
| } | |||
| &::before { | |||
| content: ''; | |||
| background-image: url('../images/chevron-down.svg'); | |||
| fill: $color-primary; | |||
| -webkit-text-stroke-color: $color-primary; | |||
| background-position: center; | |||
| width: pxToRem(20px); | |||
| height: pxToRem(20px); | |||
| position: absolute; | |||
| left: pxToRem(-22px); | |||
| transition: all 0.2s; | |||
| } | |||
| } | |||
| .ais-SearchBox { | |||
| display: flex; | |||
| justify-content: flex-end; | |||
| margin-bottom: pxToRem(24px); | |||
| } | |||
| .ais-SearchBox-input { | |||
| border: none; | |||
| color: $blue; | |||
| font-size: pxToRem(16px); | |||
| letter-spacing: 0; | |||
| line-height: 1.5; | |||
| outline: none; | |||
| -moz-appearance: none; | |||
| -webkit-appearance: none; | |||
| flex-grow: 1; | |||
| &::placeholder { | |||
| color: $blue; | |||
| font-size: pxToRem(16px); | |||
| } | |||
| @include media-below($bp-xl) { | |||
| font-size: pxToRemMd(16px); | |||
| &::placeholder { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| } | |||
| .ais-SearchBox-form { | |||
| border: 1px solid $grey-6; | |||
| border-radius: $border-radius; | |||
| overflow: hidden; | |||
| padding: 0 pxToRem(12px); | |||
| height: pxToRem(33px); | |||
| align-items: center; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| min-width: pxToRem(340px); | |||
| } | |||
| .ais-SearchBox-submit, | |||
| .ais-SearchBox-reset { | |||
| border: none; | |||
| background: transparent; | |||
| outline: none; | |||
| height: pxToRem(16px); | |||
| > svg { | |||
| color: $blue-1; | |||
| fill: $blue-1; | |||
| } | |||
| } | |||
| .ais-SearchBox-submitIcon { | |||
| width: pxToRem(14px); | |||
| height: pxToRem(14px); | |||
| color: $blue-1; | |||
| fill: $blue-1; | |||
| } | |||
| .ais-SearchBox-resetIcon { | |||
| width: pxToRem(14px); | |||
| height: pxToRem(14px); | |||
| } | |||
| .ais-SearchBox-reset { | |||
| margin-left: pxToRem(10px); | |||
| cursor: pointer; | |||
| } | |||
| .c-plaid-link { | |||
| padding: 0 !important; | |||
| background: transparent !important; | |||
| border-width: 0 !important; | |||
| border-radius: 0 !important; | |||
| box-shadow: $box-shadow !important; | |||
| &.c-plaid-link--select-card { | |||
| margin-top: pxToRem(40px); | |||
| .c-select-card__button { | |||
| margin-top: 0; | |||
| } | |||
| } | |||
| } | |||
| .ais-InfiniteHitsWrap { | |||
| min-height: pxToRem(200px); | |||
| } | |||
| .ais-Highlight-highlighted { | |||
| background: #fff1d6; | |||
| font-style: normal; | |||
| } | |||
| .acsb-trigger { | |||
| display: none !important; | |||
| visibility: hidden !important; | |||
| width: 0 !important; | |||
| height: 0 !important; | |||
| } | |||
| .ais-CurrentRefinements-list { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| > :not(:last-child) { | |||
| margin-right: pxToRem(16px); | |||
| } | |||
| } | |||
| .ais-CurrentRefinements-item { | |||
| border-radius: $border-radius; | |||
| background-color: $dark-blue; | |||
| padding: pxToRem(4px) pxToRem(8px); | |||
| flex-shrink: 0; | |||
| margin-bottom: pxToRem(16px); | |||
| } | |||
| .ais-CurrentRefinements-item-link { | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| font-weight: 600; | |||
| color: $white; | |||
| display: flex; | |||
| align-items: center; | |||
| text-decoration: none; | |||
| } | |||
| .ais-CurrentRefinements-close { | |||
| color: $white; | |||
| width: pxToRem(24px); | |||
| margin-left: pxToRem(8px); | |||
| } | |||
| .recharts-surface { | |||
| overflow: visible; | |||
| } | |||
| .recharts-cartesian-axis-tick-value { | |||
| color: #9aa1a9; | |||
| font-size: 10px; | |||
| letter-spacing: 0; | |||
| line-height: 20px; | |||
| } | |||
| .recharts-tooltip-wrapper:empty{ | |||
| display: 'none', | |||
| } | |||
| .recharts-text{ | |||
| &.recharts-pie-label-text{ | |||
| font-size: 14px; | |||
| @include media-below($bp-xl) { | |||
| font-size: 12px; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,127 @@ | |||
| /** | |||
| * Reset | |||
| * | |||
| */ | |||
| *, | |||
| *:before, | |||
| *:after { | |||
| box-sizing: border-box; | |||
| } | |||
| *, | |||
| body, | |||
| button, | |||
| input, | |||
| textarea, | |||
| select { | |||
| text-rendering: optimizeLegibility; | |||
| -moz-osx-font-smoothing: grayscale; | |||
| } | |||
| body, | |||
| div, | |||
| dl, | |||
| dt, | |||
| dd, | |||
| ul, | |||
| ol, | |||
| li, | |||
| h1, | |||
| h2, | |||
| h3, | |||
| h4, | |||
| h5, | |||
| h6, | |||
| pre, | |||
| form, | |||
| fieldset, | |||
| button, | |||
| input, | |||
| textarea, | |||
| p, | |||
| blockquote, | |||
| th, | |||
| td { | |||
| margin: 0; | |||
| padding: 0; | |||
| } | |||
| table { | |||
| border-collapse: collapse; | |||
| border-spacing: 0; | |||
| } | |||
| fieldset, | |||
| img { | |||
| border: 0; | |||
| } | |||
| address, | |||
| caption, | |||
| cite, | |||
| code, | |||
| dfn, | |||
| em, | |||
| th, | |||
| var { | |||
| font-style: normal; | |||
| font-weight: normal; | |||
| } | |||
| strong { | |||
| font-weight: 800; | |||
| } | |||
| ol, | |||
| ul { | |||
| list-style: none; | |||
| } | |||
| caption, | |||
| th { | |||
| text-align: left; | |||
| } | |||
| q:before, | |||
| q:after { | |||
| content: ''; | |||
| } | |||
| abbr, | |||
| acronym { | |||
| border: 0; | |||
| } | |||
| svg { | |||
| flex-shrink: 0; | |||
| } | |||
| textarea, | |||
| input:matches([type='email'], [type='number'], [type='password'], [type='search'], [type='tel'], [type='text'], [type='url']) { | |||
| -webkit-appearance: none; | |||
| &::-webkit-autofill, | |||
| &::-webkit-contacts-auto-fill-button, | |||
| &::-webkit-credentials-auto-fill-button { | |||
| visibility: hidden; | |||
| user-select: none; | |||
| display: none !important; | |||
| position: absolute; | |||
| } | |||
| } | |||
| input[type='number']::-webkit-inner-spin-button, | |||
| input[type='number']::-webkit-outer-spin-button { | |||
| -webkit-appearance: none; | |||
| margin: 0; | |||
| &::-webkit-autofill, | |||
| &::-webkit-contacts-auto-fill-button, | |||
| &::-webkit-credentials-auto-fill-button { | |||
| visibility: hidden; | |||
| user-select: none; | |||
| display: none !important; | |||
| position: absolute; | |||
| } | |||
| } | |||
| @@ -0,0 +1,57 @@ | |||
| body, | |||
| div, | |||
| dl, | |||
| dt, | |||
| dd, | |||
| ul, | |||
| ol, | |||
| li, | |||
| h1, | |||
| h2, | |||
| h3, | |||
| h4, | |||
| h5, | |||
| h6, | |||
| pre, | |||
| form, | |||
| fieldset, | |||
| button, | |||
| input, | |||
| textarea, | |||
| p, | |||
| blockquote, | |||
| th, | |||
| td { | |||
| font-family: $font-family; | |||
| } | |||
| p { | |||
| vertical-align: middle; | |||
| display: inline-block; | |||
| word-break: break-word; | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| @include media-below($bp-md) { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| a { | |||
| font-size: inherit; | |||
| line-height: inherit; | |||
| color: inherit; | |||
| } | |||
| strong { | |||
| font-weight: bold; | |||
| } | |||
| h1, | |||
| h2, | |||
| h3, | |||
| h4, | |||
| h5, | |||
| h6 { | |||
| font-weight: 500; | |||
| } | |||
| @@ -0,0 +1,39 @@ | |||
| .u-mr-24 { | |||
| margin-right: 24px; | |||
| } | |||
| .u-ml-32 { | |||
| margin-left: pxToRem(32px); | |||
| } | |||
| .u-position-relative { | |||
| position: relative; | |||
| } | |||
| .u-column { | |||
| @include flex-column; | |||
| } | |||
| .u-display-none { | |||
| display: none; | |||
| } | |||
| .u-superscript { | |||
| font-size: pxToRem(14px); | |||
| font-weight: medium; | |||
| } | |||
| .u-text-align-right { | |||
| text-align: right; | |||
| } | |||
| .u-hide { | |||
| width: 0; | |||
| height: 0; | |||
| visibility: hidden; | |||
| display: none; | |||
| position: fixed; | |||
| top: -20px; | |||
| right: -20px; | |||
| z-index: -1; | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| $base-font-size: 16px; | |||
| $base-font-size-md: 14px; | |||
| $font-family: 'Avenir Next'; | |||
| // Colors | |||
| $color-primary: #024f86; | |||
| $color-primary-light: #024f86; | |||
| $color-primary-dark: #003246; | |||
| $yellow: #ffeac1; | |||
| $white: #ffffff; | |||
| $grey: #f4f4f4; | |||
| $grey-1: #ccced0; | |||
| $grey-2: #fafafa; | |||
| $grey-3: #c2c5c6; | |||
| $grey-4: #d8d8d8; | |||
| $grey-5: #808285; | |||
| $grey-6: #c9d6db; | |||
| $grey-7: rgba(128, 130, 133, 0.5); | |||
| $grey-8: rgba(201, 214, 219, 0.25); | |||
| $grey-9: #ebeff2; | |||
| $grey-10: #f0f5f6; | |||
| $grey-11: #8b8b8b; | |||
| $grey-12: #b0bfc540; | |||
| $grey-13: #9d9ea4; | |||
| $grey-14: #f7fafa; | |||
| $grey-15: #ebf2f2; | |||
| $dark-blue: #003246; | |||
| $blue: #4e7a8c; | |||
| $blue-1: #6e8fae; | |||
| $blue-2: #024f86; | |||
| $blue-3: #0f85ec; | |||
| $blue-4: #5c7e9f; | |||
| $blue-5: #dde5e7; | |||
| $black: #000; | |||
| $black-1: rgba(0, 0, 0, 0.3); | |||
| $black-2: rgba(32, 38, 43, 0.9); | |||
| $black-4: #172029; | |||
| $black-5: #272727; | |||
| $black-6: #1d2731; | |||
| $red: #ff5028; | |||
| $success: #09846b; | |||
| $success-1: #00876a; | |||
| $success-2: #008a68; | |||
| // Shadow | |||
| $button-shadow-hover: 0 5px 6px 0 rgba(112, 120, 135, 0.24); | |||
| $button-shadow-pressed: 0 2px 4px 0 rgba(112, 120, 135, 0.24); | |||
| $box-shadow: 0 3px 8px 0 rgba(112, 120, 135, 0.24); | |||
| $account-dropdown-shadow: 0 6px 38px 0 rgba(112, 120, 135, 0.56); | |||
| // Border Radius | |||
| $border-radius: 4px; | |||
| $border-radius-md: 8px; | |||
| // Breakpoints | |||
| $bp-xxs: 325px; | |||
| $bp-xs: 400px; | |||
| $bp-sm: 576px; | |||
| $bp-md: 768px; | |||
| $bp-lg: 992px; | |||
| $bp-xl: 1200px; | |||
| $bp-xxl: 1350px; | |||
| // z-index | |||
| $index-xxs: 1; | |||
| $index-xs: 2; | |||
| $index-sm: 3; | |||
| $index-md: 4; | |||
| $index-lg: 5; | |||
| $index-xl: 6; | |||
| $index-xxl: 7; | |||
| @@ -0,0 +1,60 @@ | |||
| .c-button { | |||
| display: inline-flex; | |||
| align-items: center; | |||
| border-radius: $border-radius; | |||
| background-color: $color-primary; | |||
| box-shadow: 0 2px 4px 0 rgba(112, 120, 135, 0.24); | |||
| border: transparent; | |||
| padding: 8px 0; | |||
| color: $white; | |||
| width: 100%; | |||
| text-align: center; | |||
| justify-content: center; | |||
| font-family: "Avenir Next"; | |||
| font-size: pxToRem(18px); | |||
| font-weight: 600; | |||
| letter-spacing: 0; | |||
| line-height: 26px; | |||
| outline: none; | |||
| text-transform: uppercase; | |||
| transition: all 0.3s ease-in-out; | |||
| cursor: pointer; | |||
| &.c-button--clean { | |||
| background: transparent; | |||
| border: 1px solid $color-primary; | |||
| color: $color-primary; | |||
| &:hover { | |||
| border-color: $color-primary-light; | |||
| color: $color-primary-light; | |||
| background-color: transparent; | |||
| } | |||
| &:active { | |||
| border-color: $color-primary-dark; | |||
| color: $color-primary-dark; | |||
| } | |||
| } | |||
| &.c-button--dropdown { | |||
| justify-content: flex-end; | |||
| padding: 8px 14px; | |||
| background-image: url("../../images/down.svg"); | |||
| background-repeat: no-repeat; | |||
| background-position: 8% 50%; | |||
| } | |||
| &[disabled] { | |||
| pointer-events: none; | |||
| opacity: 0.5; | |||
| } | |||
| &:hover { | |||
| background-color: $color-primary-light; | |||
| } | |||
| &:active { | |||
| background-color: $color-primary-dark; | |||
| } | |||
| } | |||
| @@ -0,0 +1,45 @@ | |||
| .c-auth-card { | |||
| max-width: pxToRem(624px); | |||
| width: 100%; | |||
| box-shadow: $box-shadow; | |||
| border-radius: $border-radius; | |||
| border: 1px solid $color-primary-light; | |||
| padding: pxToRem(24px) pxToRem(40px) pxToRem(32px); | |||
| @include media-below($bp-md) { | |||
| border: none; | |||
| box-shadow: none; | |||
| padding: 0; | |||
| max-width: 100%; | |||
| .c-auth-card__title { | |||
| text-align: left; | |||
| font-size: pxToRemMd(36px); | |||
| margin-bottom: pxToRemMd(6px); | |||
| } | |||
| .c-auth-card__subtitle { | |||
| font-size: pxToRemMd(16px); | |||
| text-align: left; | |||
| } | |||
| } | |||
| } | |||
| .c-auth-card__title { | |||
| text-align: left; | |||
| font-size: pxToRem(36px); | |||
| line-height: 1.22; | |||
| color: $dark-blue; | |||
| font-weight: 400; | |||
| margin-bottom: pxToRem(16px); | |||
| } | |||
| .c-auth-card__subtitle { | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| letter-spacing: 0; | |||
| color: $color-primary; | |||
| text-align: left; | |||
| width: 100%; | |||
| font-weight: 600; | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| .c-auth { | |||
| @include flex-center; | |||
| flex-direction: column; | |||
| padding-bottom: pxToRem(56px); | |||
| @include media-below($bp-md) { | |||
| padding: 0 pxToRemMd(24px) pxToRemMd(92px); | |||
| .c-auth__title { | |||
| margin: pxToRemMd(48px) auto; | |||
| font-size: pxToRemMd(24px); | |||
| line-height: 1.35; | |||
| } | |||
| } | |||
| } | |||
| .c-auth__title { | |||
| margin: pxToRem(56px) auto pxToRem(80px); | |||
| font-size: pxToRem(36px); | |||
| line-height: 1.22; | |||
| color: $dark-blue; | |||
| font-weight: bold; | |||
| } | |||
| @@ -0,0 +1,173 @@ | |||
| .c-btn { | |||
| @include outline-none; | |||
| @include button-clear; | |||
| @include flex-center; | |||
| font-size: pxToRem(18px); | |||
| line-height: 1.35; | |||
| padding: pxToRem(8px) pxToRem(8px); | |||
| border-radius: $border-radius; | |||
| box-shadow: $button-shadow-pressed; | |||
| color: inherit; | |||
| font-weight: 600; | |||
| letter-spacing: 0; | |||
| text-align: center; | |||
| text-transform: uppercase; | |||
| user-select: none; | |||
| white-space: nowrap; | |||
| min-width: pxToRem(120px); | |||
| flex-shrink: 0; | |||
| cursor: pointer; | |||
| transition: background-color 0.2s, color 0.2s; | |||
| &:disabled { | |||
| opacity: 0.5; | |||
| cursor: auto; | |||
| } | |||
| &.c-btn--primary { | |||
| background-color: $color-primary; | |||
| color: $white; | |||
| border: 1px solid $color-primary; | |||
| &:disabled { | |||
| &:hover { | |||
| background-color: $color-primary; | |||
| box-shadow: none; | |||
| } | |||
| } | |||
| &:hover { | |||
| background-color: $color-primary-light; | |||
| box-shadow: $button-shadow-hover; | |||
| } | |||
| &:focus, | |||
| &:active { | |||
| background-color: $color-primary-dark; | |||
| box-shadow: $button-shadow-pressed; | |||
| } | |||
| } | |||
| &.c-btn--primary-outlined { | |||
| background-color: transparent; | |||
| color: $color-primary; | |||
| border: 1px solid $color-primary; | |||
| &:disabled { | |||
| &:hover { | |||
| color: $color-primary; | |||
| border: 1px solid $color-primary; | |||
| } | |||
| } | |||
| &:hover { | |||
| color: $color-primary; | |||
| border: 1px solid $color-primary; | |||
| } | |||
| &:focus, | |||
| &:active { | |||
| color: $color-primary; | |||
| border: 1px solid $color-primary; | |||
| } | |||
| } | |||
| &.c-btn--blue { | |||
| background-color: $blue-3; | |||
| color: $white; | |||
| background-color: $blue-3; | |||
| } | |||
| &.c-btn--white { | |||
| background-color: $white; | |||
| color: $grey-3; | |||
| border: 1px solid $grey-4; | |||
| box-shadow: $box-shadow; | |||
| &:disabled { | |||
| &:hover { | |||
| background-color: $white; | |||
| color: $grey-3; | |||
| } | |||
| } | |||
| &:hover { | |||
| color: $grey-5; | |||
| } | |||
| &:focus, | |||
| &:active { | |||
| background-color: $grey; | |||
| } | |||
| } | |||
| &.c-btn--primary-clear { | |||
| background-color: transparent; | |||
| color: $color-primary; | |||
| border: none; | |||
| box-shadow: none; | |||
| padding: 0; | |||
| } | |||
| &.c-btn--auto { | |||
| min-width: auto; | |||
| } | |||
| &.c-btn--sm { | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| padding: pxToRem(4px) pxToRem(15px); | |||
| } | |||
| &.c-btn--capitalize { | |||
| text-transform: capitalize; | |||
| } | |||
| &.c-btn--bank-acount-card { | |||
| padding: 0 pxToRem(16px); | |||
| min-height: pxToRem(32px); | |||
| min-width: pxToRem(120px); | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| } | |||
| &.c-btn--hidden { | |||
| visibility: hidden; | |||
| height: 0; | |||
| } | |||
| @include media-below($bp-md) { | |||
| padding: pxToRemMd(4px) pxToRemMd(25px); | |||
| font-size: pxToRemMd(16px); | |||
| line-height: 1.5; | |||
| min-width: pxToRemMd(80px); | |||
| &.c-btn--auth { | |||
| padding: pxToRemMd(12px) pxToRemMd(25px); | |||
| line-height: 1.35; | |||
| font-size: pxToRemMd(18px); | |||
| } | |||
| &.c-btn--sm { | |||
| padding: pxToRemMd(4px) pxToRemMd(15px); | |||
| } | |||
| &.c-btn--bank-acount-card { | |||
| flex-grow: 1; | |||
| min-height: pxToRemMd(40px); | |||
| padding: pxToRemMd(8px) pxToRemMd(16px); | |||
| font-size: pxToRemMd(18px); | |||
| line-height: 1.33; | |||
| } | |||
| &.c-btn--lg { | |||
| padding: pxToRemMd(7.5px) pxToRemMd(15px); | |||
| font-size: pxToRemMd(18px); | |||
| line-height: 1.5; | |||
| } | |||
| } | |||
| @include media-below($bp-xs) { | |||
| white-space: unset; | |||
| } | |||
| } | |||
| @@ -0,0 +1,46 @@ | |||
| .c-error-page { | |||
| margin-top: pxToRem(120px); | |||
| @include media-below($bp-md) { | |||
| margin-top: pxToRemMd(120px); | |||
| .c-error-page__title { | |||
| font-size: pxToRemMd(160px); | |||
| margin-bottom: pxToRemMd(27px); | |||
| } | |||
| .c-error-page__text { | |||
| margin-bottom: pxToRem(24px); | |||
| } | |||
| } | |||
| } | |||
| .c-error-page__content-container { | |||
| @include flex-center; | |||
| } | |||
| .c-error-page__content { | |||
| @include flex-column; | |||
| align-items: center; | |||
| padding: 0 pxToRem(32px); | |||
| } | |||
| .c-error-page__title { | |||
| font-size: pxToRem(160px); | |||
| line-height: 1.35; | |||
| color: $dark-blue; | |||
| margin-bottom: pxToRem(32px); | |||
| color: $color-primary; | |||
| font-weight: bold; | |||
| } | |||
| .c-error-page__text { | |||
| font-weight: 600; | |||
| margin-bottom: pxToRem(24px); | |||
| text-align: center; | |||
| } | |||
| .c-error-page__button { | |||
| margin-bottom: pxToRem(16px); | |||
| min-width: pxToRem(250px); | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| .c-reset-security { | |||
| padding-top: pxToRem(56px); | |||
| @include media-below($bp-md) { | |||
| padding-top: pxToRemMd(40px); | |||
| .c-reset-security__button { | |||
| width: 100%; | |||
| margin-top: pxToRemMd(44px); | |||
| } | |||
| } | |||
| } | |||
| .c-reset-security__question { | |||
| color: $dark-blue; | |||
| font-weight: 600; | |||
| margin-bottom: pxToRem(20px); | |||
| } | |||
| .c-reset-security__button { | |||
| width: 100%; | |||
| margin-top: pxToRem(48px); | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| .c-icon-button { | |||
| @include flex-center; | |||
| @include outline-none; | |||
| @include button-clear; | |||
| user-select: none; | |||
| cursor: pointer; | |||
| } | |||
| @@ -0,0 +1,479 @@ | |||
| .c-input { | |||
| @include flex-column; | |||
| position: relative; | |||
| &.c-input--error { | |||
| .c-input__field, | |||
| .c-select__control, | |||
| .c-select__control:hover { | |||
| border-color: $red; | |||
| } | |||
| } | |||
| &.c-input--password { | |||
| .c-input__icon { | |||
| position: absolute; | |||
| right: 0; | |||
| top: 50%; | |||
| transform: translate(0, -50%); | |||
| margin-right: pxToRem(12px); | |||
| width: pxToRem(24px); | |||
| height: pxToRem(24px); | |||
| display: flex; | |||
| svg { | |||
| width: pxToRem(24px); | |||
| height: pxToRem(24px); | |||
| color: $black; | |||
| } | |||
| } | |||
| .c-input__caps-lock { | |||
| position: absolute; | |||
| right: 0; | |||
| top: 50%; | |||
| transform: translate(0, -50%); | |||
| margin-right: pxToRem(40px); | |||
| width: pxToRem(24px); | |||
| height: pxToRem(24px); | |||
| display: flex; | |||
| width: pxToRem(24px); | |||
| height: pxToRem(24px); | |||
| color: $black; | |||
| } | |||
| .c-input__field { | |||
| padding-right: pxToRem(72px); | |||
| } | |||
| } | |||
| &.c-input--demi-bold { | |||
| .c-input__field { | |||
| font-weight: 600; | |||
| } | |||
| } | |||
| &.c-input--search { | |||
| position: relative; | |||
| width: 100%; | |||
| .c-input__icon { | |||
| width: pxToRem(24px); | |||
| height: pxToRem(24px); | |||
| position: absolute; | |||
| right: 0; | |||
| top: 50%; | |||
| transform: translate(0, -50%); | |||
| color: $blue-1; | |||
| margin-right: pxToRem(12px); | |||
| } | |||
| &.c-input--search-management { | |||
| max-width: pxToRem(344px); | |||
| margin-right: pxToRem(24px); | |||
| .c-input__field { | |||
| height: pxToRem(34px); | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| letter-spacing: 0; | |||
| } | |||
| } | |||
| } | |||
| &.c-input--center-text { | |||
| input { | |||
| text-align: center; | |||
| } | |||
| } | |||
| @include media-below($bp-xl) { | |||
| &.c-input--search { | |||
| &.c-input--search-management { | |||
| max-width: 100%; | |||
| margin-right: pxToRemMd(16px); | |||
| .c-input__field { | |||
| height: pxToRemMd(32px); | |||
| font-size: pxToRemMd(16px); | |||
| line-height: 1.5; | |||
| letter-spacing: 0; | |||
| } | |||
| } | |||
| } | |||
| .c-input__label { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| .c-input__field { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| .c-input__error { | |||
| font-size: pxToRemMd(14px); | |||
| } | |||
| .c-select__control { | |||
| &.c-select__control { | |||
| font-size: pxToRemMd(16px); | |||
| min-height: 0; | |||
| .c-select__input, | |||
| .c-select__placeholder { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| .c-select__indicator { | |||
| > svg { | |||
| width: pxToRemMd(16px); | |||
| height: pxToRemMd(16px); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .c-select__menu { | |||
| .c-select__option, | |||
| .c-select__menu-notice { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| .c-input__link { | |||
| a, | |||
| span { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| //Overide | |||
| .c-password-strength__container { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| .c-phone-number { | |||
| .PhoneInput { | |||
| font-size: pxToRemMd(16px); | |||
| &::placeholder { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| .PhoneInputInput { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| } | |||
| &.c-input--dropdown-full-height { | |||
| .c-select__menu { | |||
| max-height: initial; | |||
| } | |||
| } | |||
| } | |||
| .c-input__label { | |||
| color: $blue; | |||
| font-size: pxToRem(16px); | |||
| font-weight: 600; | |||
| letter-spacing: 0; | |||
| line-height: 1.75; | |||
| margin-bottom: pxToRem(4px); | |||
| } | |||
| .c-input__field-wrap { | |||
| width: 100%; | |||
| position: relative; | |||
| } | |||
| .c-input__field { | |||
| @include outline-none; | |||
| border: 1px solid $grey-6; | |||
| border-radius: $border-radius; | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| height: pxToRem(50px); | |||
| padding: 0 pxToRem(12px); | |||
| color: $blue; | |||
| background-color: $white; | |||
| width: 100%; | |||
| &:disabled { | |||
| background-color: $grey-8; | |||
| border-color: $grey-6; | |||
| } | |||
| &:focus { | |||
| border-color: $color-primary; | |||
| } | |||
| } | |||
| .c-input__error { | |||
| position: absolute; | |||
| top: 100%; | |||
| left: 0; | |||
| right: 0; | |||
| color: $red; | |||
| font-size: pxToRem(14px); | |||
| line-height: 1.35; | |||
| font-weight: 500; | |||
| margin: pxToRem(4px) 0; | |||
| } | |||
| .c-select__control { | |||
| &.c-select__control { | |||
| @include outline-none; | |||
| border: 1px solid $grey-6; | |||
| border-radius: $border-radius; | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| height: pxToRem(50px); | |||
| padding: 0 pxToRem(12px); | |||
| color: $blue; | |||
| background-color: $white; | |||
| box-shadow: none; | |||
| &:hover { | |||
| border-color: $grey-6; | |||
| } | |||
| &.c-select__control--is-focused { | |||
| border-color: $color-primary; | |||
| box-shadow: none; | |||
| &:hover { | |||
| border-color: $color-primary; | |||
| } | |||
| &.c-select__control--menu-is-open{ | |||
| .c-select__indicator { | |||
| svg { | |||
| transform: rotate(-180deg); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .css-1uccc91-singleValue { | |||
| color: $blue; | |||
| margin: 0; | |||
| } | |||
| .css-b8ldur-Input { | |||
| margin: 0; | |||
| } | |||
| .c-select__value-container { | |||
| height: 100%; | |||
| padding: 0; | |||
| padding-right: pxToRem(32px); | |||
| } | |||
| .c-select__input, | |||
| .c-select__placeholder { | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| letter-spacing: 0; | |||
| color: $blue; | |||
| } | |||
| .c-select__indicator-separator { | |||
| display: none; | |||
| } | |||
| .c-select__indicator { | |||
| padding: 0; | |||
| > svg { | |||
| width: pxToRem(16px); | |||
| height: pxToRem(16px); | |||
| color: $blue; | |||
| transform: rotate(0); | |||
| transition: transform 0.2s; | |||
| } | |||
| } | |||
| &.c-select__control--is-disabled { | |||
| background-color: $grey-8; | |||
| } | |||
| } | |||
| } | |||
| .c-select__menu { | |||
| @include flex-column; | |||
| position: absolute; | |||
| top: 100%; | |||
| left: 0; | |||
| right: 0; | |||
| margin-top: pxToRem(4px); | |||
| margin-bottom: pxToRem(4px); | |||
| border: 1px solid $grey-6; | |||
| border-radius: $border-radius; | |||
| box-shadow: $box-shadow; | |||
| max-height: pxToRem(150px); | |||
| overflow: auto; | |||
| .c-select__menu-list { | |||
| @include flex-column; | |||
| padding: 0; | |||
| flex-grow: 1; | |||
| } | |||
| .c-select__option, | |||
| .c-select__menu-notice { | |||
| padding: pxToRem(12px) pxToRem(15px); | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| letter-spacing: 0; | |||
| color: $blue; | |||
| text-align: left; | |||
| &:hover { | |||
| background-color: $grey-2; | |||
| } | |||
| &.c-select__option--is-selected { | |||
| background-color: $grey-2; | |||
| } | |||
| &.c-select__option--is-focused { | |||
| background-color: $grey-2; | |||
| } | |||
| } | |||
| } | |||
| .c-input__link { | |||
| position: absolute; | |||
| top: 0; | |||
| right: 0; | |||
| a, | |||
| span { | |||
| color: $grey-11; | |||
| font-size: pxToRem(16px); | |||
| letter-spacing: 0; | |||
| line-height: 1.15; | |||
| text-decoration: underline; | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| //Overide | |||
| .c-password-strength__container { | |||
| margin-top: pxToRem(8px); | |||
| font-size: pxToRem(16px); | |||
| & .c-password-strength__line--wrapper { | |||
| border-radius: 8px; | |||
| overflow: hidden; | |||
| background-color: $grey; | |||
| height: pxToRem(5px); | |||
| .c-password-strength__line { | |||
| height: pxToRem(5px); | |||
| left: 0; | |||
| top: 0; | |||
| } | |||
| } | |||
| } | |||
| .c-password { | |||
| min-height: pxToRem(110px); | |||
| @include media-below($bp-xl) { | |||
| min-height: pxToRemMd(110px); | |||
| } | |||
| } | |||
| .c-phone-number { | |||
| .PhoneInput { | |||
| @include outline-none; | |||
| box-sizing: border-box; | |||
| border: 1px solid $grey-6; | |||
| border-radius: $border-radius; | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| min-height: pxToRem(50px); | |||
| color: $blue; | |||
| background-color: $white; | |||
| box-shadow: none; | |||
| width: 100%; | |||
| overflow: hidden; | |||
| &::placeholder { | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| } | |||
| &:disabled { | |||
| background-color: $grey-8; | |||
| border-color: $grey-6; | |||
| } | |||
| &.PhoneInput--focus { | |||
| border-color: $color-primary; | |||
| .PhoneInputCountry { | |||
| border-color: $color-primary; | |||
| } | |||
| } | |||
| } | |||
| .PhoneInputCountry { | |||
| @include flex-center; | |||
| width: pxToRem(96px); | |||
| border-right: 1px solid $grey-6; | |||
| } | |||
| .PhoneInputCountryIcon { | |||
| margin-right: pxToRem(16px); | |||
| width: auto; | |||
| height: auto; | |||
| border: none; | |||
| } | |||
| .PhoneInputCountryIconImg { | |||
| width: pxToRem(36px); | |||
| object-fit: contain; | |||
| } | |||
| .PhoneInputCountrySelectArrow { | |||
| border: none; | |||
| width: 0; | |||
| height: 0; | |||
| transform: translate(0); | |||
| border-left: pxToRem(8px) solid transparent; | |||
| border-right: pxToRem(8px) solid transparent; | |||
| border-top: pxToRem(8px) solid $blue; | |||
| } | |||
| .PhoneInputInput { | |||
| @include outline-none; | |||
| border-color: transparent; | |||
| height: 100%; | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| padding: 0; | |||
| color: $blue; | |||
| background-color: $white; | |||
| width: 100%; | |||
| margin: 0; | |||
| padding: 0 pxToRem(26px); | |||
| height: pxToRem(50px); | |||
| } | |||
| .PhoneInputCountry { | |||
| margin-right: 0; | |||
| } | |||
| &.c-input--error { | |||
| .PhoneInput { | |||
| border-color: $red; | |||
| .PhoneInputCountry { | |||
| border-color: $red; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| .c-loader__wrapper { | |||
| @include flex-column; | |||
| flex: 1 1 auto; | |||
| position: relative; | |||
| min-height: 0; | |||
| min-width: 0; | |||
| &.c-loader__wrapper--block { | |||
| box-shadow: $box-shadow; | |||
| .c-loader { | |||
| position: relative; | |||
| top: unset; | |||
| left: unset; | |||
| right: unset; | |||
| bottom: unset; | |||
| } | |||
| } | |||
| &.c-loader__wrapper--full-height { | |||
| height: 100%; | |||
| } | |||
| &.c-loader__wrapper--no-shadow { | |||
| box-shadow: none; | |||
| } | |||
| .c-loader { | |||
| @include flex-center; | |||
| width: 100%; | |||
| height: 100%; | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| bottom: 0; | |||
| padding: pxToRem(15px) 0; | |||
| background-color: rgba(255, 255, 255, 0.4); | |||
| z-index: $index-lg; | |||
| &.c-loader--page { | |||
| position: fixed; | |||
| .c-loader__icon { | |||
| border: 20px solid transparent; | |||
| width: pxToRem(200px); | |||
| height: pxToRem(200px); | |||
| border-bottom-color: $color-primary; | |||
| border-top-color: $color-primary; | |||
| } | |||
| } | |||
| } | |||
| .c-loader__icon { | |||
| border-radius: 50%; | |||
| border: 10px solid transparent; | |||
| border-bottom-color: $color-primary; | |||
| border-top-color: $color-primary; | |||
| animation: 1s loader-animation linear infinite; | |||
| width: pxToRem(100px); | |||
| height: pxToRem(100px); | |||
| } | |||
| @keyframes loader-animation { | |||
| 0% { | |||
| transform: rotate(0deg); | |||
| } | |||
| 100% { | |||
| transform: rotate(360deg); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,31 @@ | |||
| .c-login-card { | |||
| border: 1px solid $color-primary-light; | |||
| border-radius: $border-radius; | |||
| box-shadow: $box-shadow; | |||
| max-width: pxToRem(624px); | |||
| width: 100%; | |||
| margin: pxToRem(28px) auto 0; | |||
| padding: pxToRem(36px) pxToRem(40px) pxToRem(32px); | |||
| } | |||
| .c-login-card__note { | |||
| color: $color-primary; | |||
| font-weight: 600; | |||
| margin-bottom: pxToRem(37px); | |||
| } | |||
| .c-login-card__form { | |||
| display: grid; | |||
| grid-row-gap: pxToRem(24px); | |||
| } | |||
| .c-login-card__submit { | |||
| margin-top: pxToRem(24px); | |||
| width: 100%; | |||
| } | |||
| .c-login-card__question { | |||
| color: $blue; | |||
| font-weight: 600; | |||
| margin-bottom: pxToRem(16px); | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| .c-login { | |||
| &.c-login--user { | |||
| .c-login__form { | |||
| .c-input:first-child { | |||
| margin-bottom: pxToRem(20px); | |||
| } | |||
| } | |||
| } | |||
| @include media-below($bp-xl) { | |||
| .c-login__link { | |||
| margin-top: pxToRemMd(70px); | |||
| } | |||
| } | |||
| @include media-below($bp-md) { | |||
| .c-login__form { | |||
| margin: pxToRemMd(36px) 0 0; | |||
| } | |||
| .c-login__button { | |||
| margin-bottom: pxToRemMd(40px); | |||
| margin-top: pxToRemMd(36px); | |||
| } | |||
| .c-login__link { | |||
| margin-top: pxToRemMd(80px); | |||
| } | |||
| &.c-login--user { | |||
| .c-login__form { | |||
| .c-input:first-child { | |||
| margin-bottom: pxToRemMd(20px); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .c-login__link { | |||
| color: $color-primary; | |||
| font-weight: 600; | |||
| margin-top: pxToRem(40px); | |||
| width: max-content; | |||
| } | |||
| .c-login__form { | |||
| margin: pxToRem(36px) 0 0; | |||
| > form { | |||
| @include flex-column; | |||
| } | |||
| } | |||
| .c-login__button { | |||
| width: 100%; | |||
| margin-top: pxToRem(68px); | |||
| margin-bottom: pxToRem(24px); | |||
| } | |||
| .c-login__text { | |||
| text-align: center; | |||
| width: 100%; | |||
| color: $blue; | |||
| a { | |||
| color: $color-primary; | |||
| font-weight: bold; | |||
| letter-spacing: inherit; | |||
| font-size: inherit; | |||
| line-height: inherit; | |||
| } | |||
| } | |||
| @@ -0,0 +1,169 @@ | |||
| $header-height-desktop: pxToRem(80px); | |||
| $header-height-mobile: pxToRemMd(74px); | |||
| .c-modal-wrap { | |||
| position: fixed; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| bottom: 0; | |||
| z-index: $index-xl; | |||
| background-color: $black-1; | |||
| &.c-modal-wrap--no-bg { | |||
| background-color: transparent; | |||
| } | |||
| &.c-modal-wrap--over-modal { | |||
| background-color: transparent; | |||
| z-index: $index-xxl; | |||
| } | |||
| &.c-modal-wrap--sm { | |||
| .c-modal { | |||
| max-width: pxToRem(390px); | |||
| width: 100%; | |||
| } | |||
| .c-modal__header { | |||
| padding: pxToRem(12px); | |||
| } | |||
| } | |||
| .c-modal__header { | |||
| padding: pxToRem(12px); | |||
| } | |||
| &.c-modal-wrap--md { | |||
| .c-modal { | |||
| max-width: pxToRem(521px); | |||
| width: 100%; | |||
| } | |||
| .c-modal__header { | |||
| padding: pxToRem(12px) pxToRem(20px); | |||
| } | |||
| } | |||
| &.c-modal-wrap--lg { | |||
| .c-modal { | |||
| max-width: pxToRem(782px); | |||
| width: 100%; | |||
| } | |||
| .c-modal__header { | |||
| padding: pxToRem(12px) pxToRem(20px); | |||
| } | |||
| } | |||
| &.c-modal-wrap--close { | |||
| display: none; | |||
| } | |||
| @include media-below($bp-xl) { | |||
| &, | |||
| &.c-modal-wrap--sm, | |||
| &.c-modal-wrap--md { | |||
| .c-modal { | |||
| margin: $header-height-mobile auto $header-height-mobile; | |||
| max-height: calc(100vh - #{2 * $header-height-mobile}); | |||
| } | |||
| } | |||
| } | |||
| @include media-below($bp-md) { | |||
| &, | |||
| &.c-modal-wrap--sm, | |||
| &.c-modal-wrap--md { | |||
| .c-modal__header { | |||
| padding: pxToRemMd(16px); | |||
| } | |||
| .c-modal__title { | |||
| font-size: pxToRemMd(16px); | |||
| line-height: 1.4; | |||
| } | |||
| .c-modal__close { | |||
| width: pxToRemMd(24px); | |||
| height: pxToRemMd(24px); | |||
| } | |||
| .c-modal__back { | |||
| width: pxToRemMd(24px); | |||
| height: pxToRemMd(24px); | |||
| margin-right: pxToRemMd(8px); | |||
| } | |||
| .c-modal__back-button { | |||
| margin-left: -#{pxToRemMd(8px)}; | |||
| } | |||
| .c-modal, | |||
| &.c-modal-wrap--lg .c-modal { | |||
| max-width: 100%; | |||
| max-height: 100vh; | |||
| height: 100%; | |||
| margin: 0; | |||
| border-radius: 0; | |||
| } | |||
| &.c-modal-wrap--mobile-modal { | |||
| display: flex; | |||
| padding: 0 pxToRemMd(20px); | |||
| .c-modal { | |||
| height: auto; | |||
| margin: auto; | |||
| border-radius: 2px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .c-modal { | |||
| @include flex-column; | |||
| box-shadow: $box-shadow; | |||
| border-radius: $border-radius; | |||
| background-color: $white; | |||
| margin: $header-height-desktop auto $header-height-desktop; | |||
| max-height: calc(100vh - #{2 * $header-height-desktop}); | |||
| position: relative; | |||
| } | |||
| .c-modal__header { | |||
| display: flex; | |||
| align-items: center; | |||
| box-shadow: $box-shadow; | |||
| z-index: $index-xxs; | |||
| } | |||
| .c-modal__title { | |||
| @include text-ellipsis; | |||
| font-size: pxToRem(16px); | |||
| font-weight: 600; | |||
| line-height: 1.5; | |||
| color: $dark-blue; | |||
| padding-right: pxToRem(10px); | |||
| margin-right: auto; | |||
| } | |||
| .c-modal__close { | |||
| width: pxToRem(16px); | |||
| height: pxToRem(16px); | |||
| color: $dark-blue; | |||
| } | |||
| .c-modal__back { | |||
| width: pxToRem(16px); | |||
| height: pxToRem(16px); | |||
| color: $dark-blue; | |||
| margin-right: pxToRem(10px); | |||
| } | |||
| .c-modal__body { | |||
| @include flex-column; | |||
| flex: 1 1 auto; | |||
| overflow: auto; | |||
| } | |||
| @@ -0,0 +1,29 @@ | |||
| .c-radio { | |||
| display: flex; | |||
| cursor: pointer; | |||
| &.c-radio--selected { | |||
| border-color: $dark-blue; | |||
| } | |||
| } | |||
| .c-radio__field { | |||
| display: none; | |||
| } | |||
| .c-radio__indicator { | |||
| margin-top: pxToRem(4px); | |||
| margin-right: pxToRem(16px); | |||
| } | |||
| .c-radio__icon { | |||
| width: pxToRem(16px); | |||
| height: pxToRem(16px); | |||
| } | |||
| .c-radio__text { | |||
| font-size: pxToRem(14px); | |||
| line-height: 1.15; | |||
| color: $blue; | |||
| user-select: none; | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| const Auth = ({ children }) => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <div className="c-auth"> | |||
| <h1 className="c-auth__title">{t(`login.welcome`)}</h1> | |||
| {children} | |||
| </div> | |||
| ); | |||
| }; | |||
| Auth.propTypes = { | |||
| children: PropTypes.node, | |||
| }; | |||
| export default Auth; | |||
| @@ -0,0 +1,24 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import SectionLoader from '../Loader/SectionLoader'; | |||
| const AuthCard = ({ children, title, subtitle, isLoading }) => { | |||
| return ( | |||
| <div className="c-auth-card"> | |||
| <SectionLoader isLoading={isLoading}> | |||
| <h1 className="c-auth-card__title">{title}</h1> | |||
| <h2 className="c-auth-card__subtitle">{subtitle}</h2> | |||
| {children} | |||
| </SectionLoader> | |||
| </div> | |||
| ); | |||
| }; | |||
| AuthCard.propTypes = { | |||
| children: PropTypes.node, | |||
| title: PropTypes.string, | |||
| subtitle: PropTypes.string, | |||
| isLoading: PropTypes.bool, | |||
| }; | |||
| export default AuthCard; | |||
| @@ -0,0 +1,93 @@ | |||
| import React, { useRef } from 'react'; | |||
| import PropType from 'prop-types'; | |||
| const Button = ({ | |||
| variant, | |||
| size, | |||
| children, | |||
| authButton, | |||
| type, | |||
| onClick, | |||
| textTransform, | |||
| className, | |||
| disabled, | |||
| hidden, | |||
| minWidth, | |||
| ...restProps | |||
| }) => { | |||
| const buttonRef = useRef(null); | |||
| function styles() { | |||
| let style = 'c-btn'; | |||
| if (variant) { | |||
| style += ` c-btn--${variant}`; | |||
| } | |||
| if (size) { | |||
| style += ` c-btn--${size}`; | |||
| } | |||
| if (textTransform) { | |||
| style += ` c-btn--${textTransform}`; | |||
| } | |||
| if (authButton) { | |||
| style += ` c-btn--auth`; | |||
| } | |||
| if (minWidth) { | |||
| style += ` c-btn--${minWidth}`; | |||
| } | |||
| if (hidden) { | |||
| style += ` c-btn--hidden`; | |||
| } | |||
| if (className) { | |||
| style += ` ${className}`; | |||
| } | |||
| return style; | |||
| } | |||
| function handleClick() { | |||
| buttonRef.current.blur(); | |||
| if (typeof onClick === 'function') { | |||
| onClick(); | |||
| } | |||
| } | |||
| return ( | |||
| <button | |||
| ref={buttonRef} | |||
| className={styles()} | |||
| onClick={handleClick} | |||
| type={type} | |||
| disabled={disabled} | |||
| {...restProps} | |||
| > | |||
| {children} | |||
| </button> | |||
| ); | |||
| }; | |||
| Button.propTypes = { | |||
| children: PropType.node, | |||
| textTransform: PropType.oneOf(['uppercase', 'capitalize']), | |||
| size: PropType.oneOf(['sm', 'md', 'lg', 'xl']), | |||
| authButton: PropType.bool, | |||
| variant: PropType.string, | |||
| type: PropType.oneOf(['button', 'submit', 'reset']), | |||
| onClick: PropType.func, | |||
| className: PropType.string, | |||
| disabled: PropType.bool, | |||
| minWidth: PropType.oneOf(['auto']), | |||
| hidden: PropType.bool, | |||
| }; | |||
| Button.defaultProps = { | |||
| type: 'button', | |||
| }; | |||
| export default Button; | |||
| @@ -0,0 +1,32 @@ | |||
| import React, { useRef } from 'react'; | |||
| import PropType from 'prop-types'; | |||
| const IconButton = ({ children, onClick, className }) => { | |||
| const buttonRef = useRef(null); | |||
| function handleClick() { | |||
| buttonRef.current.blur(); | |||
| if (typeof onClick === 'function') { | |||
| onClick(); | |||
| } | |||
| } | |||
| return ( | |||
| <button | |||
| type="button" | |||
| ref={buttonRef} | |||
| onClick={handleClick} | |||
| className={`c-icon-button ${className && className}`} | |||
| > | |||
| {children} | |||
| </button> | |||
| ); | |||
| }; | |||
| IconButton.propTypes = { | |||
| children: PropType.node, | |||
| onClick: PropType.func, | |||
| className: PropType.string, | |||
| }; | |||
| export default IconButton; | |||
| @@ -0,0 +1,187 @@ | |||
| import React, { useEffect, useState, useRef } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ErrorMessage } from 'formik'; | |||
| import IconButton from '../IconButton/IconButton'; | |||
| import { ReactComponent as Search } from '../../assets/images/svg/search.svg'; | |||
| import { ReactComponent as EyeOn } from '../../assets/images/svg/eye-on.svg'; | |||
| import { ReactComponent as EyeOff } from '../../assets/images/svg/eye-off.svg'; | |||
| import { ReactComponent as CapsLock } from '../../assets/images/svg/caps-lock.svg'; | |||
| const BaseInputField = ({ | |||
| type, | |||
| label, | |||
| field, | |||
| form, | |||
| placeholder, | |||
| clearPlaceholderOnFocus = true, | |||
| isSearch, | |||
| className, | |||
| disabled, | |||
| centerText, | |||
| link, | |||
| errorMessage, | |||
| autoFocus, | |||
| isCapsLockOn, | |||
| ...props | |||
| }) => { | |||
| const [inputPlaceholder, setPlaceholder] = useState(placeholder); | |||
| const inputField = useRef(null); | |||
| useEffect(() => { | |||
| if (autoFocus) { | |||
| inputField.current.focus(); | |||
| } | |||
| }, [autoFocus, inputField]); | |||
| useEffect(() => { | |||
| if (errorMessage) { | |||
| form.setFieldError(field.name, errorMessage); | |||
| } | |||
| }, [errorMessage]); // eslint-disable-line | |||
| useEffect(() => { | |||
| setPlaceholder(placeholder); | |||
| }, [placeholder]); | |||
| const [inputType, setInputType] = useState('password'); | |||
| const passwordInput = type === 'password' ? ' c-input--password' : ''; | |||
| const showPassword = () => { | |||
| if (inputType === 'password') { | |||
| setInputType('text'); | |||
| } else { | |||
| setInputType('password'); | |||
| } | |||
| }; | |||
| // Nester Formik Field Names get bugged because of Undefined values, so i had to fix it like this | |||
| // If you ask why 0 and 1? I dont see a need for forms to be nested more then 2 levels? | |||
| const fieldName = field.name.split('.'); | |||
| const formError = | |||
| fieldName[0] && fieldName[1] | |||
| ? form.errors[fieldName[0]] && form.errors[fieldName[0]][fieldName[1]] | |||
| : form.errors[fieldName[0]]; | |||
| const formTouched = | |||
| fieldName[0] && fieldName[1] | |||
| ? form.touched[fieldName[0]] && form.touched[fieldName[0]][fieldName[1]] | |||
| : form.touched[fieldName[0]]; | |||
| function styles() { | |||
| let style = 'c-input'; | |||
| if (formError && formTouched) { | |||
| style += ` c-input--error`; | |||
| } | |||
| if (type === 'password') { | |||
| style += ` c-input--password`; | |||
| } | |||
| if (isSearch) { | |||
| style += ` c-input--search`; | |||
| } | |||
| if (centerText) { | |||
| style += ` c-input--center-text`; | |||
| } | |||
| if (type === 'number') { | |||
| style += ` c-input--demi-bold`; | |||
| } | |||
| if (className) { | |||
| style += ` ${className}`; | |||
| } | |||
| return style; | |||
| } | |||
| const additionalActions = () => { | |||
| if (!clearPlaceholderOnFocus) { | |||
| return null; | |||
| } | |||
| return { | |||
| onFocus: () => { | |||
| setPlaceholder(''); | |||
| }, | |||
| onBlur: (e) => { | |||
| setPlaceholder(placeholder); | |||
| field.onBlur(e); | |||
| }, | |||
| }; | |||
| }; | |||
| return ( | |||
| <div className={styles()}> | |||
| {!!label && ( | |||
| <label className="c-input__label" htmlFor={field.name}> | |||
| {label} | |||
| </label> | |||
| )} | |||
| {link && <div className="c-input__link">{link}</div>} | |||
| <div className="c-input__field-wrap"> | |||
| <input | |||
| ref={inputField} | |||
| type={type === 'password' ? inputType : type} | |||
| placeholder={inputPlaceholder} | |||
| disabled={disabled} | |||
| {...field} | |||
| {...props} | |||
| {...additionalActions()} | |||
| className="c-input__field" | |||
| /> | |||
| {!!isSearch && <Search className="c-input__icon" />} | |||
| {!!passwordInput && ( | |||
| <> | |||
| {isCapsLockOn && <CapsLock className="c-input__caps-lock" />} | |||
| <IconButton | |||
| onClick={() => { | |||
| showPassword(); | |||
| }} | |||
| className="c-input__icon" | |||
| > | |||
| {inputType === 'password' ? <EyeOff /> : <EyeOn />} | |||
| </IconButton> | |||
| </> | |||
| )} | |||
| </div> | |||
| <ErrorMessage name={field.name}> | |||
| {(errorMessage) => ( | |||
| <span className="c-input__error">{errorMessage}</span> | |||
| )} | |||
| </ErrorMessage> | |||
| </div> | |||
| ); | |||
| }; | |||
| BaseInputField.propTypes = { | |||
| type: PropTypes.string, | |||
| field: PropTypes.shape({ | |||
| name: PropTypes.string, | |||
| onFocus: PropTypes.func, | |||
| onBlur: PropTypes.func, | |||
| }), | |||
| form: PropTypes.shape({ | |||
| errors: PropTypes.shape({}), | |||
| setFieldError: PropTypes.func, | |||
| touched: PropTypes.shape({}), | |||
| }), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
| disabled: PropTypes.bool, | |||
| isSearch: PropTypes.bool, | |||
| className: PropTypes.string, | |||
| link: PropTypes.node, | |||
| errorMessage: PropTypes.string, | |||
| centerText: PropTypes.bool, | |||
| clearPlaceholderOnFocus: PropTypes.bool, | |||
| demiBold: PropTypes.bool, | |||
| touched: PropTypes.bool, | |||
| autoFocus: PropTypes.bool, | |||
| isCapsLockOn: PropTypes.bool, | |||
| }; | |||
| export default BaseInputField; | |||
| @@ -0,0 +1,40 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ReactComponent as Checked } from '../../assets/images/svg/checked.svg'; | |||
| import { ReactComponent as Unchecked } from '../../assets/images/svg/unchecked.svg'; | |||
| const Checkbox = ({ className, children, name, onChange, checked, field }) => ( | |||
| <label htmlFor={name} className={`c-checkbox ${className || ''}`}> | |||
| <input | |||
| name={name} | |||
| id={name} | |||
| className="c-checkbox__field" | |||
| type="checkbox" | |||
| checked={checked} | |||
| {...field} | |||
| onChange={onChange || field.onChange} | |||
| /> | |||
| <div className="c-checkbox__indicator"> | |||
| {checked ? ( | |||
| <Checked className="c-checkbox__icon" /> | |||
| ) : ( | |||
| <Unchecked className="c-checkbox__icon" /> | |||
| )} | |||
| </div> | |||
| <div className="c-checkbox__text">{children}</div> | |||
| </label> | |||
| ); | |||
| Checkbox.propTypes = { | |||
| children: PropTypes.node, | |||
| onChange: PropTypes.func, | |||
| checked: PropTypes.bool, | |||
| name: PropTypes.string, | |||
| field: PropTypes.shape({ | |||
| onChange: PropTypes.func, | |||
| }), | |||
| className: PropTypes.string, | |||
| }; | |||
| export default Checkbox; | |||
| @@ -0,0 +1,123 @@ | |||
| import React, { useEffect, useRef } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ErrorMessage, useField } from 'formik'; | |||
| import CurrencyInput from 'react-currency-input-field'; | |||
| import { formatMoneyNumeral } from '../../util/helpers/numeralHelpers'; | |||
| import { | |||
| PLUS_SYMBOL, | |||
| MINUS_SYMBOL, | |||
| NUMPAD_MINUS_SYMBOL, | |||
| NUMPAD_PLUS_SYMBOL, | |||
| K_KEYCODE, | |||
| } from '../../constants/keyCodeConstants'; | |||
| const CurrencyField = ({ | |||
| autoFocus, | |||
| notCentered, | |||
| notBold, | |||
| label, | |||
| onChange, | |||
| value, | |||
| ...props | |||
| }) => { | |||
| const [field, meta] = useField(props); | |||
| const inputField = useRef(null); | |||
| function styles() { | |||
| let style = 'c-currency-field'; | |||
| if (meta.error && meta.touched) { | |||
| style += ` c-currency-field--error`; | |||
| } | |||
| if (notCentered) { | |||
| style += ` c-currency-field--not-centered`; | |||
| } | |||
| if (notBold) { | |||
| style += ` c-currency-field--not-bold`; | |||
| } | |||
| return style; | |||
| } | |||
| useEffect(() => { | |||
| if (autoFocus) { | |||
| inputField.current.focus(); | |||
| } | |||
| }, [autoFocus, inputField]); | |||
| const onKeydownHandler = (event) => { | |||
| if ( | |||
| event.keyCode === MINUS_SYMBOL || | |||
| event.keyCode === PLUS_SYMBOL || | |||
| event.keyCode === NUMPAD_MINUS_SYMBOL || | |||
| event.keyCode === NUMPAD_PLUS_SYMBOL || | |||
| event.keyCode === K_KEYCODE | |||
| ) { | |||
| event.preventDefault(); | |||
| } | |||
| }; | |||
| const prefix = formatMoneyNumeral(0); | |||
| const prefixSymbol = () => { | |||
| if (prefix.includes('CAD')) { | |||
| return 'CAD '; | |||
| } | |||
| return '$'; | |||
| }; | |||
| return ( | |||
| <div className={styles()}> | |||
| {!!label && ( | |||
| <label className="c-currency-field__label" htmlFor={field.name}> | |||
| {label} | |||
| </label> | |||
| )} | |||
| {value ? ( | |||
| <CurrencyInput | |||
| {...props} | |||
| prefix={prefixSymbol()} | |||
| onValueChange={(value) => { | |||
| onChange(value ? Number(value) : ''); | |||
| }} | |||
| onKeyDown={(event) => onKeydownHandler(event)} | |||
| ref={inputField} | |||
| defaultValue={0} | |||
| value={value} | |||
| /> | |||
| ) : ( | |||
| <CurrencyInput | |||
| {...props} | |||
| prefix={prefixSymbol()} | |||
| onValueChange={(value) => { | |||
| onChange(value ? Number(value) : ''); | |||
| }} | |||
| onKeyDown={(event) => onKeydownHandler(event)} | |||
| ref={inputField} | |||
| /> | |||
| )} | |||
| <ErrorMessage name={field.name}> | |||
| {(errorMessage) => ( | |||
| <span className="c-currency-field__error">{errorMessage}</span> | |||
| )} | |||
| </ErrorMessage> | |||
| </div> | |||
| ); | |||
| }; | |||
| CurrencyField.propTypes = { | |||
| field: PropTypes.shape({ | |||
| name: PropTypes.string, | |||
| }), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| disabled: PropTypes.bool, | |||
| onChange: PropTypes.func, | |||
| autoFocus: PropTypes.bool, | |||
| notCentered: PropTypes.bool, | |||
| notBold: PropTypes.bool, | |||
| value: PropTypes.number, | |||
| }; | |||
| export default CurrencyField; | |||
| @@ -0,0 +1,33 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| const EmailField = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| ...props | |||
| }) => ( | |||
| <BaseInputField | |||
| type="email" | |||
| label={label} | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| {...props} | |||
| /> | |||
| ); | |||
| EmailField.propTypes = { | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.string, | |||
| disabled: PropTypes.bool, | |||
| }; | |||
| export default EmailField; | |||
| @@ -0,0 +1,74 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| import { | |||
| PERIOD_SYMBOL, | |||
| COMMA_SYMBOL, | |||
| PLUS_SYMBOL, | |||
| MINUS_SYMBOL, | |||
| NUMPAD_PERIOD_SYMBOL, | |||
| NUMPAD_MINUS_SYMBOL, | |||
| NUMPAD_PLUS_SYMBOL, | |||
| DOWN_ARROW_KEYCODE, | |||
| UP_ARROW_KEYCODE, | |||
| } from '../../constants/keyCodeConstants'; | |||
| const NumberField = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| preventAllExceptNumbers, | |||
| ...props | |||
| }) => { | |||
| const onKeydownHandler = (event) => { | |||
| if (preventAllExceptNumbers) { | |||
| if ( | |||
| event.keyCode === PERIOD_SYMBOL || | |||
| event.keyCode === COMMA_SYMBOL || | |||
| event.keyCode === NUMPAD_PERIOD_SYMBOL || | |||
| event.keyCode === DOWN_ARROW_KEYCODE || | |||
| event.keyCode === UP_ARROW_KEYCODE | |||
| ) { | |||
| event.preventDefault(); | |||
| } | |||
| } | |||
| if ( | |||
| event.keyCode === PLUS_SYMBOL || | |||
| event.keyCode === MINUS_SYMBOL || | |||
| event.keyCode === NUMPAD_MINUS_SYMBOL || | |||
| event.keyCode === NUMPAD_PLUS_SYMBOL || | |||
| event.keyCode === DOWN_ARROW_KEYCODE || | |||
| event.keyCode === UP_ARROW_KEYCODE | |||
| ) { | |||
| event.preventDefault(); | |||
| } | |||
| }; | |||
| return ( | |||
| <BaseInputField | |||
| type="number" | |||
| label={label} | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| {...props} | |||
| onKeyDown={(event) => onKeydownHandler(event)} | |||
| /> | |||
| ); | |||
| }; | |||
| NumberField.propTypes = { | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), | |||
| disabled: PropTypes.bool, | |||
| preventAllExceptNumbers: PropTypes.bool, | |||
| }; | |||
| export default NumberField; | |||
| @@ -0,0 +1,74 @@ | |||
| import React, { useState } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| import PasswordStrength from './PasswordStrength'; | |||
| const PasswordField = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| shouldTestPasswordStrength, | |||
| autoFocus, | |||
| ...props | |||
| }) => { | |||
| const [passwordValue, setPasswordValue] = useState(''); | |||
| const [isCapsLockOn, setIsCapsLockOn] = useState(false); | |||
| const onChange = (e) => { | |||
| if (shouldTestPasswordStrength) { | |||
| const { value } = e.target; | |||
| setPasswordValue(value); | |||
| } | |||
| field.onChange(e); | |||
| }; | |||
| const onKeyDown = (keyEvent) => { | |||
| if (keyEvent.getModifierState('CapsLock')) { | |||
| setIsCapsLockOn(true); | |||
| } else { | |||
| setIsCapsLockOn(false); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className="c-password"> | |||
| <BaseInputField | |||
| type="password" | |||
| label={label} | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| {...props} | |||
| onChange={onChange} | |||
| autoFocus={autoFocus} | |||
| onKeyDown={onKeyDown} | |||
| isCapsLockOn={isCapsLockOn} | |||
| /> | |||
| {shouldTestPasswordStrength && ( | |||
| <PasswordStrength | |||
| passwordValue={passwordValue} | |||
| shouldTestPasswordStrength | |||
| /> | |||
| )} | |||
| </div> | |||
| ); | |||
| }; | |||
| PasswordField.propTypes = { | |||
| field: PropTypes.shape({ | |||
| onChange: PropTypes.func, | |||
| }), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.string, | |||
| disabled: PropTypes.bool, | |||
| shouldTestPasswordStrength: PropTypes.bool, | |||
| autoFocus: PropTypes.bool, | |||
| }; | |||
| export default PasswordField; | |||
| @@ -0,0 +1,130 @@ | |||
| import React, { useEffect, useRef, useState } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import owasp from 'owasp-password-strength-test'; | |||
| import i18next from 'i18next'; | |||
| owasp.config({ | |||
| minOptionalTestsToPass: 3, | |||
| }); | |||
| const passwordStrengthOptions = [ | |||
| { | |||
| strength: 'weak', | |||
| color: '#FF5028', | |||
| }, | |||
| { | |||
| strength: 'average', | |||
| color: '#FDB942', | |||
| }, | |||
| { | |||
| strength: 'good', | |||
| color: '#06BEE7', | |||
| }, | |||
| { | |||
| strength: 'strong', | |||
| color: '#00876A', | |||
| }, | |||
| ]; | |||
| /** | |||
| * User must pass a required test and at least 3 optional. | |||
| * @param result - owasp result | |||
| * @returns {number} - index of password strength 0-3 | |||
| */ | |||
| function getPasswordStrengthIndex(result) { | |||
| // requirement for strong password is required test passed and at least 3 optional tests | |||
| if (result.strong) { | |||
| return 3; | |||
| } | |||
| if (!result.strong && result.optionalTestsPassed >= 3) { | |||
| return 2; | |||
| } | |||
| if (result.optionalTestsPassed <= 0) { | |||
| return 0; | |||
| } | |||
| return result.optionalTestsPassed - 1; | |||
| } | |||
| const PasswordStrength = ({ | |||
| shouldTestPasswordStrength, | |||
| passwordValue, | |||
| passwordStrengthTestsRequired, | |||
| }) => { | |||
| const strengthContainer = useRef(null); | |||
| const [passwordStrength, setPasswordStrength] = useState({ | |||
| width: 0, | |||
| color: 'red', | |||
| }); | |||
| const [error, setError] = useState(''); | |||
| useEffect(() => { | |||
| if (shouldTestPasswordStrength && passwordValue) { | |||
| const bBox = strengthContainer.current.getBoundingClientRect(); | |||
| const result = owasp.test(passwordValue); | |||
| const passwordStrengthIndex = getPasswordStrengthIndex(result); | |||
| const passwordOption = passwordStrengthOptions[passwordStrengthIndex]; | |||
| const width = !passwordValue | |||
| ? 0 | |||
| : (bBox.width * (passwordStrengthIndex + 1)) / | |||
| passwordStrengthTestsRequired; | |||
| setPasswordStrength({ width, color: passwordOption.color }); | |||
| const strength = i18next.t(`password.${passwordOption.strength}`); | |||
| setError(i18next.t('login.passwordStrength', { strength })); | |||
| } | |||
| }, [ | |||
| passwordValue, | |||
| shouldTestPasswordStrength, | |||
| passwordStrengthTestsRequired, | |||
| ]); | |||
| if (!shouldTestPasswordStrength || !passwordValue) { | |||
| return null; | |||
| } | |||
| const renderError = () => { | |||
| if (!error) { | |||
| return null; | |||
| } | |||
| return ( | |||
| <div | |||
| className="c-input--error" | |||
| style={{ | |||
| color: passwordStrength.color, | |||
| }} | |||
| > | |||
| {error} | |||
| </div> | |||
| ); | |||
| }; | |||
| return ( | |||
| <div ref={strengthContainer} className="c-password-strength__container"> | |||
| <div className="c-password-strength__line--wrapper"> | |||
| <div | |||
| className="c-password-strength__line" | |||
| style={{ | |||
| backgroundColor: passwordStrength.color, | |||
| width: passwordStrength.width, | |||
| }} | |||
| /> | |||
| </div> | |||
| {renderError()} | |||
| </div> | |||
| ); | |||
| }; | |||
| PasswordStrength.propTypes = { | |||
| shouldTestPasswordStrength: PropTypes.bool, | |||
| passwordValue: PropTypes.string, | |||
| passwordStrengthTestsRequired: PropTypes.number, | |||
| }; | |||
| PasswordStrength.defaultProps = { | |||
| passwordStrengthTestsRequired: 4, | |||
| }; | |||
| export default PasswordStrength; | |||
| @@ -0,0 +1,45 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import NumberFormat from 'react-number-format'; | |||
| import TextField from './TextField'; | |||
| const PercentageField = ({ field, ...props }) => { | |||
| const handleOnChange = (percentageField) => { | |||
| const { floatValue } = percentageField; | |||
| if (!props.onChange) { | |||
| throw Error('Provide an onChange handler'); | |||
| } | |||
| if (floatValue > 100) { | |||
| return props.onChange('100'); | |||
| } | |||
| if (floatValue <= 0 || !floatValue) { | |||
| return props.onChange('0'); | |||
| } | |||
| return props.onChange(floatValue.toString()); | |||
| }; | |||
| return ( | |||
| <NumberFormat | |||
| format="###%" | |||
| value={field.value} | |||
| customInput={TextField} | |||
| field={field} | |||
| {...props} | |||
| onValueChange={handleOnChange} | |||
| onChange={() => {}} | |||
| /> | |||
| ); | |||
| }; | |||
| PercentageField.propTypes = { | |||
| onChange: PropTypes.func, | |||
| field: PropTypes.shape({ | |||
| value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
| }), | |||
| }; | |||
| export default PercentageField; | |||
| @@ -0,0 +1,49 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ErrorMessage, useField } from 'formik'; | |||
| import PhoneInput from 'react-phone-number-input'; | |||
| import 'react-phone-number-input/style.css'; | |||
| const PhoneNumberField = ({ label, ...props }) => { | |||
| const [field, meta] = useField(props); | |||
| const inputErrorClassName = | |||
| meta.error && meta.touched ? 'c-input--error' : ''; | |||
| return ( | |||
| <div className={`c-input c-phone-number ${inputErrorClassName}`}> | |||
| {!!label && ( | |||
| <label className="c-input__label" htmlFor={field.name}> | |||
| {label} | |||
| </label> | |||
| )} | |||
| <PhoneInput | |||
| international | |||
| defaultCountry="US" | |||
| {...field} | |||
| {...props} | |||
| onChange={(value) => { | |||
| props.onPhoneChange(value); | |||
| }} | |||
| countryOptionsOrder={['US']} | |||
| /> | |||
| <ErrorMessage name={field.name}> | |||
| {(errorMessage) => ( | |||
| <span className="c-input__error">{errorMessage}</span> | |||
| )} | |||
| </ErrorMessage> | |||
| </div> | |||
| ); | |||
| }; | |||
| PhoneNumberField.propTypes = { | |||
| field: PropTypes.shape({ | |||
| name: PropTypes.string, | |||
| }), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| disabled: PropTypes.bool, | |||
| onChange: PropTypes.func, | |||
| onPhoneChange: PropTypes.func, | |||
| }; | |||
| export default PhoneNumberField; | |||
| @@ -0,0 +1,54 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ReactComponent as RadioOn } from '../../assets/images/svg/radio-on.svg'; | |||
| import { ReactComponent as RadioOff } from '../../assets/images/svg/radio-off.svg'; | |||
| const Checkbox = ({ | |||
| className, | |||
| children, | |||
| name, | |||
| checked, | |||
| field, | |||
| value, | |||
| selected, | |||
| id, | |||
| }) => ( | |||
| <label | |||
| htmlFor={name} | |||
| className={`c-radio ${selected ? 'c-radio--selected' : ''} ${ | |||
| className || '' | |||
| }`} | |||
| > | |||
| <input | |||
| name={name} | |||
| id={id} | |||
| className="c-radio__field" | |||
| type="radio" | |||
| checked={checked} | |||
| value={value} | |||
| {...field} | |||
| /> | |||
| <div className="c-radio__indicator"> | |||
| {selected ? ( | |||
| <RadioOn className="c-radio__icon" /> | |||
| ) : ( | |||
| <RadioOff className="c-radio__icon" /> | |||
| )} | |||
| </div> | |||
| <div className="c-radio__text">{children}</div> | |||
| </label> | |||
| ); | |||
| Checkbox.propTypes = { | |||
| children: PropTypes.node, | |||
| checked: PropTypes.bool, | |||
| name: PropTypes.string, | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| className: PropTypes.string, | |||
| value: PropTypes.string, | |||
| selected: PropTypes.bool, | |||
| id: PropTypes.string, | |||
| }; | |||
| export default Checkbox; | |||
| @@ -0,0 +1,37 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| const Search = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| className, | |||
| ...props | |||
| }) => ( | |||
| <BaseInputField | |||
| type="text" | |||
| label="" | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| isSearch | |||
| className={className} | |||
| {...props} | |||
| /> | |||
| ); | |||
| Search.propTypes = { | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.string, | |||
| disabled: PropTypes.bool, | |||
| className: PropTypes.string, | |||
| }; | |||
| export default Search; | |||
| @@ -0,0 +1,122 @@ | |||
| import React, { useEffect } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import Select, { components, createFilter } from 'react-select'; | |||
| import { ErrorMessage, useField } from 'formik'; | |||
| import { ReactComponent as FilledChevronDown } from '../../assets/images/svg/filled-chevron-down.svg'; | |||
| const SelectField = ({ | |||
| label, | |||
| disabled, | |||
| options, | |||
| link, | |||
| defaultSelected = null, | |||
| dropdownFullHeight, | |||
| selectOption, | |||
| ...props | |||
| }) => { | |||
| const [field, meta, helpers] = useField(props); | |||
| const filterConfig = { | |||
| ignoreCase: true, | |||
| ignoreAccents: true, | |||
| trim: true, | |||
| matchFrom: 'start', | |||
| }; | |||
| useEffect(() => { | |||
| if (defaultSelected) { | |||
| helpers.setValue(defaultSelected); | |||
| } | |||
| }, [defaultSelected]); // eslint-disable-line | |||
| const DropdownIndicator = (props) => | |||
| components.DropdownIndicator && ( | |||
| <components.DropdownIndicator {...props}> | |||
| <FilledChevronDown /> | |||
| </components.DropdownIndicator> | |||
| ); | |||
| function styles() { | |||
| let style = 'c-input'; | |||
| if (meta.error && meta.touched) { | |||
| style += ` c-input--error`; | |||
| } | |||
| if (dropdownFullHeight) { | |||
| style += ` c-input--dropdown-full-height`; | |||
| } | |||
| return style; | |||
| } | |||
| return ( | |||
| <div className={styles()}> | |||
| {!!label && ( | |||
| <label className="c-input__label" htmlFor={field.name}> | |||
| {label} | |||
| </label> | |||
| )} | |||
| {!!link && <div className="c-input__link">{link}</div>} | |||
| <Select | |||
| defaultValue={defaultSelected || options[0]} | |||
| components={{ DropdownIndicator }} | |||
| isSearchable={false} | |||
| classNamePrefix="c-select" | |||
| options={options} | |||
| isDisabled={disabled} | |||
| {...field} | |||
| {...props} | |||
| onBlur={(e) => { | |||
| helpers.setTouched(true); | |||
| field.onBlur(e); | |||
| }} | |||
| onChange={(selectedOption) => { | |||
| helpers.setValue(selectedOption); | |||
| if (props.onChange) { | |||
| props.onChange(); | |||
| } | |||
| if (selectOption) { | |||
| selectOption(selectedOption); | |||
| } | |||
| }} | |||
| filterOption={createFilter(filterConfig)} | |||
| /> | |||
| <ErrorMessage name={field.name}> | |||
| {(errorMessage) => { | |||
| if (typeof errorMessage === 'string') { | |||
| return <span className="c-input__error">{errorMessage}</span>; | |||
| } | |||
| return <span className="c-input__error">{errorMessage.value}</span>; | |||
| }} | |||
| </ErrorMessage> | |||
| </div> | |||
| ); | |||
| }; | |||
| SelectField.propTypes = { | |||
| field: PropTypes.shape({ | |||
| name: PropTypes.string, | |||
| }), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| disabled: PropTypes.bool, | |||
| options: PropTypes.arrayOf( | |||
| PropTypes.shape({ | |||
| label: PropTypes.string, | |||
| value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
| }), | |||
| ), | |||
| onChange: PropTypes.func, | |||
| link: PropTypes.node, | |||
| defaultSelected: PropTypes.shape({ | |||
| label: PropTypes.string, | |||
| value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
| }), | |||
| dropdownFullHeight: PropTypes.bool, | |||
| selectOption: PropTypes.func, | |||
| }; | |||
| export default SelectField; | |||
| @@ -0,0 +1,72 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| import { | |||
| BACKSPACE_KEYCODE, | |||
| TAB_KEYCODE, | |||
| RIGHT_ARROW_KEYCODE, | |||
| LEFT_ARROW_KEYCODE, | |||
| } from '../../constants/keyCodeConstants'; | |||
| const TextField = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| centerText, | |||
| autoFocus, | |||
| preventAllExceptNumbers, | |||
| ...props | |||
| }) => { | |||
| const onKeydownHandler = (event) => { | |||
| if (preventAllExceptNumbers) { | |||
| if ( | |||
| event.keyCode === BACKSPACE_KEYCODE || | |||
| event.keyCode === TAB_KEYCODE || | |||
| event.keyCode === RIGHT_ARROW_KEYCODE || | |||
| event.keyCode === LEFT_ARROW_KEYCODE | |||
| ) { | |||
| return; | |||
| } | |||
| if ( | |||
| (event.keyCode < 58 && event.keyCode > 47) || | |||
| (event.keyCode < 106 && event.keyCode > 95) | |||
| ) { | |||
| return; | |||
| } | |||
| event.preventDefault(); | |||
| } | |||
| }; | |||
| return ( | |||
| <BaseInputField | |||
| autoFocus={autoFocus} | |||
| type="text" | |||
| label={label} | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| centerText={centerText} | |||
| {...props} | |||
| onKeyDown={(event) => onKeydownHandler(event)} | |||
| /> | |||
| ); | |||
| }; | |||
| TextField.propTypes = { | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.string, | |||
| placeholder: PropTypes.string, | |||
| disabled: PropTypes.bool, | |||
| centerText: PropTypes.bool, | |||
| autoFocus: PropTypes.bool, | |||
| preventAllExceptNumbers: PropTypes.bool, | |||
| }; | |||
| export default TextField; | |||
| @@ -0,0 +1,26 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| const BlockSectionLoader = ({ children, isLoading, fullHeight, noShadow }) => ( | |||
| <div | |||
| className={`c-loader__wrapper c-loader__wrapper--block ${ | |||
| fullHeight ? 'c-loader__wrapper--full-height' : '' | |||
| } ${noShadow ? 'c-loader__wrapper--no-shadow' : ''}`} | |||
| > | |||
| {children} | |||
| {isLoading && ( | |||
| <div className="c-loader"> | |||
| <div className="c-loader__icon" /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| BlockSectionLoader.propTypes = { | |||
| children: PropTypes.node, | |||
| isLoading: PropTypes.bool, | |||
| fullHeight: PropTypes.bool, | |||
| noShadow: PropTypes.bool, | |||
| }; | |||
| export default BlockSectionLoader; | |||
| @@ -0,0 +1,11 @@ | |||
| import React from 'react'; | |||
| const FullPageLoader = () => { | |||
| return ( | |||
| <div className="c-loader c-loader--page"> | |||
| <div className="c-loader__icon" /> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default FullPageLoader; | |||
| @@ -0,0 +1,20 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| const SectionLoader = ({ children, isLoading }) => ( | |||
| <div className="c-loader__wrapper"> | |||
| {children} | |||
| {isLoading && ( | |||
| <div className="c-loader"> | |||
| <div className="c-loader__icon" /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| SectionLoader.propTypes = { | |||
| children: PropTypes.node, | |||
| isLoading: PropTypes.bool, | |||
| }; | |||
| export default SectionLoader; | |||
| @@ -0,0 +1,26 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { Backdrop, CircularProgress } from '@mui/material'; | |||
| import { alpha } from '@mui/system'; | |||
| const BackdropComponent = ({ position = 'fixed', isLoading }) => ( | |||
| <Backdrop | |||
| sx={{ | |||
| // 'fixed' takes whole page, 'absolute' takes whole space of the parent element which needs to have 'relative' position | |||
| position, | |||
| backgroundColor: ({ palette }) => | |||
| alpha(palette.background.default, palette.action.disabledOpacity), | |||
| zIndex: ({ zIndex }) => zIndex.drawer + 1, | |||
| }} | |||
| open={isLoading} | |||
| > | |||
| <CircularProgress /> | |||
| </Backdrop> | |||
| ); | |||
| BackdropComponent.propTypes = { | |||
| position: PropTypes.oneOf(['fixed', 'absolute']), | |||
| isLoading: PropTypes.bool.isRequired, | |||
| }; | |||
| export default BackdropComponent; | |||
| @@ -0,0 +1,57 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { | |||
| Dialog, | |||
| DialogContent, | |||
| DialogTitle, | |||
| DialogActions, | |||
| Button, | |||
| useMediaQuery, | |||
| useTheme, | |||
| } from '@mui/material'; | |||
| const DialogComponent = ({ | |||
| title, | |||
| content, | |||
| onClose, | |||
| open, | |||
| maxWidth, | |||
| fullWidth, | |||
| responsive, | |||
| }) => { | |||
| const theme = useTheme(); | |||
| const fullScreen = useMediaQuery(theme.breakpoints.down('md')); | |||
| const handleClose = () => { | |||
| onClose(); | |||
| }; | |||
| return ( | |||
| <Dialog | |||
| maxWidth={maxWidth} | |||
| fullWidth={fullWidth} | |||
| fullScreen={responsive && fullScreen} | |||
| onClose={handleClose} | |||
| open={open} | |||
| > | |||
| <DialogTitle>{title}</DialogTitle> | |||
| {content && <DialogContent>{content}</DialogContent>} | |||
| <DialogActions> | |||
| <Button onClick={handleClose}>OK</Button> | |||
| <Button onClick={handleClose}>Cancel</Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| ); | |||
| }; | |||
| DialogComponent.propTypes = { | |||
| title: PropTypes.string, | |||
| open: PropTypes.bool.isRequired, | |||
| content: PropTypes.any, | |||
| onClose: PropTypes.func.isRequired, | |||
| maxWidth: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']), | |||
| fullWidth: PropTypes.bool, | |||
| responsive: PropTypes.bool, | |||
| }; | |||
| export default DialogComponent; | |||
| @@ -0,0 +1,28 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { Drawer } from '@mui/material'; | |||
| const DrawerComponent = ({ open, toggleOpen, content, anchor = 'right' }) => ( | |||
| <Drawer | |||
| sx={{ | |||
| minWidth: 250, | |||
| '& .MuiDrawer-paper': { | |||
| minWidth: 250, | |||
| }, | |||
| }} | |||
| anchor={anchor} | |||
| open={open} | |||
| onClose={toggleOpen} | |||
| > | |||
| {content ? content : null} | |||
| </Drawer> | |||
| ); | |||
| DrawerComponent.propTypes = { | |||
| open: PropTypes.bool, | |||
| toggleOpen: PropTypes.func, | |||
| content: PropTypes.any, | |||
| anchor: PropTypes.oneOf(['top', 'right', 'left', 'bottom']), | |||
| }; | |||
| export default DrawerComponent; | |||
| @@ -0,0 +1,15 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { Typography } from '@mui/material'; | |||
| const ErrorMessageComponent = ({ error }) => ( | |||
| <Typography variant="body1" color="error" my={2}> | |||
| {error} | |||
| </Typography> | |||
| ); | |||
| ErrorMessageComponent.propTypes = { | |||
| error: PropTypes.string.isRequired, | |||
| }; | |||
| export default ErrorMessageComponent; | |||
| @@ -0,0 +1,29 @@ | |||
| import React from 'react'; | |||
| import { Paper, Typography } from '@mui/material'; | |||
| import { DataGrid } from '@mui/x-data-grid'; | |||
| // Use these values from REDUX? | |||
| const rows = [ | |||
| { id: 1, col1: 'Example', col2: 'Row', col3: '1' }, | |||
| { id: 2, col1: 'Row', col2: 'Example', col3: '2' }, | |||
| { id: 3, col1: '3', col2: 'Row', col3: 'Example' }, | |||
| ]; | |||
| const columns = [ | |||
| { field: 'col1', headerName: 'Column 1', flex: 1 }, | |||
| { field: 'col2', headerName: 'Column 2', flex: 1 }, | |||
| { field: 'col3', headerName: 'Column 2', flex: 1 }, | |||
| ]; | |||
| const DataGridExample = () => { | |||
| return ( | |||
| <Paper sx={{ p: 2 }} elevation={5}> | |||
| <Typography variant="h4" gutterBottom align="center"> | |||
| DataGrid Example | |||
| </Typography> | |||
| <DataGrid autoHeight rows={rows} columns={columns} /> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default DataGridExample; | |||
| @@ -0,0 +1,64 @@ | |||
| import React, { useState } from 'react'; | |||
| import { Button, Divider, Paper, Typography } from '@mui/material'; | |||
| import DialogComponent from '../DialogComponent'; | |||
| import DrawerComponent from '../DrawerComponent'; | |||
| import PopoverComponent from '../PopoverComponent'; | |||
| const Modals = () => { | |||
| const [dialogOpen, setDialogOpen] = useState(false); | |||
| const [drawerOpen, setDrawerOpen] = useState(false); | |||
| const [popoverOpen, setPopoverOpen] = useState(false); | |||
| const [anchorEl, setAnchorEl] = useState(null); | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| p: 2, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| }} | |||
| elevation={5} | |||
| > | |||
| <Typography variant="h4" gutterBottom align="center"> | |||
| Modals Example | |||
| </Typography> | |||
| <Divider /> | |||
| <Button onClick={() => setDialogOpen(true)}>Open Dialog</Button> | |||
| <Button onClick={() => setDrawerOpen(true)}>Open Drawer</Button> | |||
| <Button | |||
| onClick={(e) => { | |||
| setPopoverOpen(true); | |||
| setAnchorEl(e.currentTarget); | |||
| }} | |||
| > | |||
| Open Popover | |||
| </Button> | |||
| <DialogComponent | |||
| title="Dialog Title" | |||
| content={<Typography>Dialog Content</Typography>} | |||
| open={dialogOpen} | |||
| onClose={() => setDialogOpen(false)} | |||
| maxWidth="md" | |||
| fullWidth | |||
| responsive | |||
| /> | |||
| <DrawerComponent | |||
| anchor="left" | |||
| content={<Typography sx={{ p: 2 }}>Drawer Content</Typography>} | |||
| open={drawerOpen} | |||
| toggleOpen={() => setDrawerOpen(!drawerOpen)} | |||
| /> | |||
| <PopoverComponent | |||
| anchorEl={anchorEl} | |||
| open={popoverOpen} | |||
| onClose={() => { | |||
| setPopoverOpen(false); | |||
| setAnchorEl(null); | |||
| }} | |||
| content={<Typography sx={{ p: 2 }}>Popover Content</Typography>} | |||
| /> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default Modals; | |||
| @@ -0,0 +1,183 @@ | |||
| import React, { useEffect, useState } from 'react'; | |||
| import { | |||
| Paper, | |||
| Box, | |||
| Grid, | |||
| Typography, | |||
| Divider, | |||
| TablePagination, | |||
| TextField, | |||
| FormControl, | |||
| InputLabel, | |||
| Select, | |||
| MenuItem, | |||
| } from '@mui/material'; | |||
| // import { useTranslation } from 'react-i18next'; | |||
| import { useDispatch, useSelector, batch } from 'react-redux'; | |||
| import useDebounce from '../../../hooks/useDebounceHook'; | |||
| import { | |||
| itemsSelector, | |||
| pageSelector, | |||
| itemsPerPageSelector, | |||
| countSelector, | |||
| sortSelector, | |||
| } from '../../../store/selectors/randomDataSelectors'; | |||
| import { | |||
| loadData, | |||
| updatePage, | |||
| updateItemsPerPage, | |||
| updateFilter, | |||
| updateSort, | |||
| } from '../../../store/actions/randomData/randomDataActions'; | |||
| const PagingSortingFilteringExample = () => { | |||
| const [filterText, setFilterText] = useState(''); | |||
| const dispatch = useDispatch(); | |||
| // const { t } = useTranslation(); | |||
| const items = useSelector(itemsSelector); | |||
| const currentPage = useSelector(pageSelector); | |||
| const itemsPerPage = useSelector(itemsPerPageSelector); | |||
| const totalCount = useSelector(countSelector); | |||
| const sort = useSelector(sortSelector) || 'name-asc'; | |||
| // Use debounce to prevent too many rerenders | |||
| const debouncedFilterText = useDebounce(filterText, 500); | |||
| useEffect(() => { | |||
| dispatch(loadData(30)); | |||
| dispatch(updateSort(sort)); | |||
| }, []); | |||
| useEffect(() => { | |||
| batch(() => { | |||
| dispatch(updateFilter(filterText)); | |||
| currentPage > 0 && dispatch(updatePage(0)); | |||
| }); | |||
| }, [debouncedFilterText]); | |||
| const handleFilterTextChange = (event) => { | |||
| const filterText = event.target.value; | |||
| setFilterText(filterText); | |||
| }; | |||
| const handleSortChange = (event) => { | |||
| const sort = event.target.value; | |||
| dispatch(updateSort(sort)); | |||
| }; | |||
| const handlePageChange = (event, newPage) => { | |||
| dispatch(updatePage(newPage)); | |||
| }; | |||
| const handleItemsPerPageChange = (event) => { | |||
| const itemsPerPage = parseInt(event.target.value); | |||
| batch(() => { | |||
| dispatch(updateItemsPerPage(itemsPerPage)); | |||
| dispatch(updatePage(0)); | |||
| }); | |||
| }; | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| justifyContent: 'start', | |||
| py: 2, | |||
| minHeight: 500, | |||
| }} | |||
| elevation={5} | |||
| > | |||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | |||
| Pagination, Filtering and Sorting Example Client Side | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| flexWrap: 'wrap', | |||
| mx: 2, | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| {/* TODO Separate into SelectComponent */} | |||
| <FormControl sx={{ flexGrow: 1 }}> | |||
| <InputLabel id="sort-label">Sort</InputLabel> | |||
| <Select | |||
| label="Sort" | |||
| labelId="sort-label" | |||
| id="sort-select-helper" | |||
| value={sort} | |||
| onChange={handleSortChange} | |||
| > | |||
| <MenuItem value="name-asc">Name - A-Z</MenuItem> | |||
| <MenuItem value="name-desc">Name - Z-A</MenuItem> | |||
| <MenuItem value="price-asc">Price - Lowest to Highest</MenuItem> | |||
| <MenuItem value="price-desc">Price - Highest to Lowest</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <TextField | |||
| sx={{ flexGrow: 1 }} | |||
| variant="outlined" | |||
| label="Filter" | |||
| placeholder="Filter" | |||
| value={filterText} | |||
| onChange={handleFilterTextChange} | |||
| /> | |||
| </Box> | |||
| </Box> | |||
| <Grid container> | |||
| {items && | |||
| items.length > 0 && | |||
| items | |||
| .slice( | |||
| currentPage * itemsPerPage, | |||
| currentPage * itemsPerPage + itemsPerPage | |||
| ) | |||
| .map((product, index) => ( | |||
| // ! DON'T USE index for key, this is for example only | |||
| <Grid item sx={{ p: 2 }} xs={12} sm={6} md={4} lg={3} key={index}> | |||
| {/* TODO separate into component */} | |||
| <Paper sx={{ p: 3, height: '100%' }} elevation={3}> | |||
| <Typography sx={{ fontWeight: 600 }}>Name: </Typography> | |||
| <Typography display="inline"> {product.name}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Designer: </Typography> | |||
| <Typography display="inline"> {product.designer}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Type: </Typography> | |||
| <Typography display="inline"> {product.type}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Price: </Typography> | |||
| <Typography display="inline"> ${product.price}</Typography> | |||
| </Paper> | |||
| </Grid> | |||
| ))} | |||
| </Grid> | |||
| <Box sx={{ width: '100%' }}> | |||
| <TablePagination | |||
| component="div" | |||
| count={totalCount} | |||
| page={currentPage} | |||
| onPageChange={handlePageChange} | |||
| rowsPerPage={itemsPerPage} | |||
| onRowsPerPageChange={handleItemsPerPageChange} | |||
| rowsPerPageOptions={[12, 24, 48, 96]} | |||
| labelRowsPerPage="Items per page" | |||
| showFirstButton | |||
| showLastButton | |||
| /> | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default PagingSortingFilteringExample; | |||
| @@ -0,0 +1,159 @@ | |||
| import React, { useEffect, useState } from 'react'; | |||
| import { | |||
| Paper, | |||
| Box, | |||
| Grid, | |||
| Typography, | |||
| Divider, | |||
| TablePagination, | |||
| TextField, | |||
| FormControl, | |||
| InputLabel, | |||
| Select, | |||
| MenuItem, | |||
| } from '@mui/material'; | |||
| // import { useTranslation } from 'react-i18next'; | |||
| import Backdrop from '../BackdropComponent'; | |||
| import useDebounce from '../../../hooks/useDebounceHook'; | |||
| import { useRandomData } from '../../../context/RandomDataContext'; | |||
| const PagingSortingFilteringExampleServerSide = () => { | |||
| const [filterText, setFilterText] = useState(''); | |||
| const { state, data } = useRandomData(); | |||
| const { items, loading, totalCount, currentPage, itemsPerPage, sort } = data; | |||
| const { setPage, setItemsPerPage, setSort, setFilter } = state; | |||
| // const { t } = useTranslation(); | |||
| // Use debounce to prevent too many rerenders | |||
| const debouncedFilterText = useDebounce(filterText, 500); | |||
| useEffect(() => { | |||
| setFilter(filterText); | |||
| }, [debouncedFilterText]); | |||
| const handleFilterTextChange = (event) => { | |||
| const filterText = event.target.value; | |||
| setFilterText(filterText); | |||
| }; | |||
| const handleSortChange = (event) => { | |||
| const sort = event.target.value; | |||
| setSort(sort); | |||
| }; | |||
| const handlePageChange = (event, newPage) => { | |||
| setPage(newPage); | |||
| }; | |||
| const handleItemsPerPageChange = (event) => { | |||
| const itemsPerPage = parseInt(event.target.value); | |||
| setItemsPerPage(itemsPerPage); | |||
| setPage(0); | |||
| }; | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| justifyContent: 'start', | |||
| py: 2, | |||
| minHeight: 500, | |||
| position: 'relative', | |||
| }} | |||
| elevation={5} | |||
| > | |||
| {loading && <Backdrop isLoading position="absolute" />} | |||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | |||
| Pagination, Filtering and Sorting Example Server Side | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| flexWrap: 'wrap', | |||
| mx: 2, | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| <FormControl sx={{ flexGrow: 1 }}> | |||
| <InputLabel id="sort-label">Sort</InputLabel> | |||
| <Select | |||
| label="Sort" | |||
| labelId="sort-label" | |||
| id="sort-select-helper" | |||
| value={sort || ''} | |||
| onChange={handleSortChange} | |||
| > | |||
| <MenuItem value="">None</MenuItem> | |||
| <MenuItem value="name-asc">Name - A-Z</MenuItem> | |||
| <MenuItem value="name-desc">Name - Z-A</MenuItem> | |||
| <MenuItem value="price-asc">Price - Lowest to Highest</MenuItem> | |||
| <MenuItem value="price-desc">Price - Highest to Lowest</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <TextField | |||
| sx={{ flexGrow: 1 }} | |||
| variant="outlined" | |||
| label="Filter" | |||
| placeholder="Filter" | |||
| value={filterText} | |||
| onChange={handleFilterTextChange} | |||
| /> | |||
| </Box> | |||
| <Grid container sx={{ position: 'relative' }}> | |||
| {items && | |||
| items.length > 0 && | |||
| items.map((item) => ( | |||
| <Grid | |||
| item | |||
| sx={{ p: 2 }} | |||
| xs={12} | |||
| sm={6} | |||
| md={4} | |||
| lg={3} | |||
| key={item.id} | |||
| > | |||
| {/* TODO separate into component */} | |||
| <Paper sx={{ p: 3, height: '100%' }} elevation={3}> | |||
| <Typography sx={{ fontWeight: 600 }}>Name: </Typography> | |||
| <Typography display="inline"> {item.name}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Company: </Typography> | |||
| <Typography display="inline"> {item.company}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Color: </Typography> | |||
| <Typography display="inline"> {item.color}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Price: </Typography> | |||
| <Typography display="inline"> {item.price}</Typography> | |||
| </Paper> | |||
| </Grid> | |||
| ))} | |||
| </Grid> | |||
| <Box sx={{ width: '100%' }}> | |||
| <TablePagination | |||
| component="div" | |||
| count={totalCount} | |||
| page={currentPage} | |||
| onPageChange={handlePageChange} | |||
| rowsPerPage={itemsPerPage} | |||
| onRowsPerPageChange={handleItemsPerPageChange} | |||
| rowsPerPageOptions={[12, 24, 48, 96]} | |||
| labelRowsPerPage="Items per page" | |||
| showFirstButton | |||
| showLastButton | |||
| /> | |||
| </Box> | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default PagingSortingFilteringExampleServerSide; | |||
| @@ -0,0 +1,26 @@ | |||
| import React, { useState } from 'react'; | |||
| import { Button, Menu, MenuItem } from '@mui/material'; | |||
| const MenuListComponent = () => { | |||
| const [anchorEl, setAnchorEl] = useState(null); | |||
| const open = Boolean(anchorEl); | |||
| const handleClick = (event) => { | |||
| setAnchorEl(event.currentTarget); | |||
| }; | |||
| const handleClose = () => { | |||
| setAnchorEl(null); | |||
| }; | |||
| return ( | |||
| <div> | |||
| <Button onClick={handleClick}>Menu List</Button> | |||
| <Menu id="menu-list" anchorEl={anchorEl} open={open} onClose={handleClose}> | |||
| <MenuItem onClick={handleClose}>Menu Item 1</MenuItem> | |||
| <MenuItem onClick={handleClose}>Menu Item 2</MenuItem> | |||
| <MenuItem onClick={handleClose}>Menu Item 3</MenuItem> | |||
| </Menu> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default MenuListComponent; | |||
| @@ -0,0 +1,160 @@ | |||
| import React, { useState, useMemo, useContext } from 'react'; | |||
| import { | |||
| AppBar, | |||
| Badge, | |||
| Box, | |||
| IconButton, | |||
| Toolbar, | |||
| Typography, | |||
| List, | |||
| ListItem, | |||
| ListItemButton, | |||
| ListItemIcon, | |||
| ListItemText, | |||
| useMediaQuery, | |||
| } from '@mui/material'; | |||
| import { useTheme } from '@mui/system'; | |||
| import MenuOutlinedIcon from '@mui/icons-material/MenuOutlined'; | |||
| import ShoppingBasketIcon from '@mui/icons-material/ShoppingBasket'; | |||
| import Brightness4Icon from '@mui/icons-material/Brightness4'; | |||
| import Brightness7Icon from '@mui/icons-material/Brightness7'; | |||
| import MenuList from './MenuListComponent'; | |||
| import Drawer from './DrawerComponent'; | |||
| import { ColorModeContext } from '../../context/ColorModeContext'; | |||
| const NavbarComponent = () => { | |||
| const [openDrawer, setOpenDrawer] = useState(false); | |||
| const theme = useTheme(); | |||
| const matches = useMediaQuery(theme.breakpoints.down('sm')); | |||
| const toggleColorMode = useContext(ColorModeContext); | |||
| const handleToggleDrawer = () => { | |||
| setOpenDrawer(!openDrawer); | |||
| }; | |||
| const drawerContent = useMemo( | |||
| () => ( | |||
| <List> | |||
| <ListItemButton divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 1</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItemButton> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 2</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItem> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemText>Link 3</ListItemText> | |||
| </ListItem> | |||
| <ListItem divider> | |||
| <IconButton onClick={toggleColorMode}> | |||
| <ListItemText>Toggle {theme.palette.mode} mode</ListItemText> | |||
| {theme.palette.mode === 'dark' ? ( | |||
| <Brightness7Icon /> | |||
| ) : ( | |||
| <Brightness4Icon /> | |||
| )} | |||
| </IconButton> | |||
| </ListItem> | |||
| </List> | |||
| ), | |||
| [handleToggleDrawer] | |||
| ); | |||
| return ( | |||
| <AppBar | |||
| elevation={2} | |||
| sx={{ backgroundColor: 'background.default', position: 'relative' }} | |||
| > | |||
| <Toolbar> | |||
| <Box | |||
| component="div" | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| alignItems: 'center', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| {matches ? ( | |||
| <Drawer | |||
| open={openDrawer} | |||
| toggleOpen={handleToggleDrawer} | |||
| content={drawerContent} | |||
| /> | |||
| ) : ( | |||
| <Box sx={{ display: 'flex' }}> | |||
| <Typography | |||
| variant="h6" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: 'pointer', | |||
| color: 'text.primary', | |||
| }} | |||
| > | |||
| Link 1 | |||
| </Typography> | |||
| <Typography | |||
| variant="body1" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: 'pointer', | |||
| color: 'text.primary', | |||
| }} | |||
| > | |||
| Link 2 | |||
| </Typography> | |||
| <Typography | |||
| variant="subtitle1" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: 'pointer', | |||
| color: 'text.primary', | |||
| }} | |||
| > | |||
| Link 3 | |||
| </Typography> | |||
| </Box> | |||
| )} | |||
| <Box> | |||
| <MenuList /> | |||
| </Box> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'center', | |||
| alignItems: 'center', | |||
| }} | |||
| > | |||
| {matches ? ( | |||
| <Box> | |||
| <IconButton onClick={handleToggleDrawer}> | |||
| <MenuOutlinedIcon /> | |||
| </IconButton> | |||
| </Box> | |||
| ) : ( | |||
| <Box> | |||
| <IconButton> | |||
| <Badge badgeContent={3} color="primary"> | |||
| <ShoppingBasketIcon color="action" /> | |||
| </Badge> | |||
| </IconButton> | |||
| <IconButton sx={{ ml: 1 }} onClick={toggleColorMode}> | |||
| {theme.palette.mode === 'dark' ? ( | |||
| <Brightness7Icon /> | |||
| ) : ( | |||
| <Brightness4Icon /> | |||
| )} | |||
| </IconButton> | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| </Box> | |||
| </Toolbar> | |||
| </AppBar> | |||
| ); | |||
| }; | |||
| export default NavbarComponent; | |||
| @@ -0,0 +1,35 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { Box, Popover } from '@mui/material'; | |||
| const PopoverComponent = ({ open, anchorEl, onClose, content }) => { | |||
| const handleClose = () => { | |||
| onClose(); | |||
| }; | |||
| return ( | |||
| <Box component="div"> | |||
| <Popover | |||
| sx={{ p: 5 }} | |||
| open={open} | |||
| anchorEl={anchorEl} | |||
| onClose={handleClose} | |||
| anchorOrigin={{ | |||
| vertical: 'bottom', | |||
| horizontal: 'left', | |||
| }} | |||
| > | |||
| {content} | |||
| </Popover> | |||
| </Box> | |||
| ); | |||
| }; | |||
| PopoverComponent.propTypes = { | |||
| anchorEl: PropTypes.object, | |||
| open: PropTypes.bool.isRequired, | |||
| onClose: PropTypes.func.isRequired, | |||
| content: PropTypes.any, | |||
| }; | |||
| export default PopoverComponent; | |||
| @@ -0,0 +1,26 @@ | |||
| import React, { useEffect } from 'react'; | |||
| import { Redirect, Route } from 'react-router'; | |||
| import { useDispatch } from 'react-redux'; | |||
| import { authenticateUser } from '../../store/actions/login/loginActions'; | |||
| // import { selectIsUserAuthenticated } from '../../store/selectors/userSelectors'; | |||
| import { LOGIN_PAGE } from '../../constants/pages'; | |||
| const PrivateRoute = ({ ...props }) => { | |||
| const dispatch = useDispatch(); | |||
| // const isUserAuthenticated = useSelector(selectIsUserAuthenticated); | |||
| const isUserAuthenticated = true; | |||
| useEffect(() => { | |||
| if (!isUserAuthenticated) { | |||
| dispatch(authenticateUser()); | |||
| } | |||
| }, [isUserAuthenticated]); // eslint-disable-line | |||
| return isUserAuthenticated ? ( | |||
| <Route {...props} /> | |||
| ) : ( | |||
| <Redirect to={LOGIN_PAGE} /> | |||
| ); | |||
| }; | |||
| export default PrivateRoute; | |||
| @@ -0,0 +1,13 @@ | |||
| import React from 'react'; | |||
| import PropType from 'prop-types'; | |||
| const Section = ({ children, className }) => ( | |||
| <section className={`l-section ${className || ''}`}>{children}</section> | |||
| ); | |||
| Section.propTypes = { | |||
| children: PropType.node, | |||
| className: PropType.string, | |||
| }; | |||
| export default Section; | |||
| @@ -0,0 +1,14 @@ | |||
| export const PERIOD_SYMBOL = 190; | |||
| export const COMMA_SYMBOL = 188; | |||
| export const PLUS_SYMBOL = 187; | |||
| export const MINUS_SYMBOL = 189; | |||
| export const NUMPAD_PERIOD_SYMBOL = 110; | |||
| export const NUMPAD_MINUS_SYMBOL = 109; | |||
| export const NUMPAD_PLUS_SYMBOL = 107; | |||
| export const K_KEYCODE = 75; | |||
| export const DOWN_ARROW_KEYCODE = 38; | |||
| export const UP_ARROW_KEYCODE = 40; | |||
| export const RIGHT_ARROW_KEYCODE = 39; | |||
| export const LEFT_ARROW_KEYCODE = 37; | |||
| export const BACKSPACE_KEYCODE = 8; | |||
| export const TAB_KEYCODE = 9; | |||
| @@ -0,0 +1,3 @@ | |||
| export const JWT_TOKEN = 'JwtToken'; | |||
| export const JWT_REFRESH_TOKEN = 'JwtRefreshToken'; | |||
| export const REFRESH_TOKEN_CONST = 'RefreshToken'; | |||
| @@ -0,0 +1,6 @@ | |||
| export const BASE_PAGE = '/'; | |||
| export const LOGIN_PAGE = '/login'; | |||
| export const FORGOT_PASSWORD_PAGE = '/forgot-password'; | |||
| export const HOME_PAGE = '/home'; | |||
| export const ERROR_PAGE = '/error-page'; | |||
| export const NOT_FOUND_PAGE = '/not-found'; | |||
| @@ -0,0 +1,21 @@ | |||
| import React, { createContext } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ThemeProvider } from '@mui/material/styles'; | |||
| import useToggleColorMode from '../hooks/useToggleColorMode'; | |||
| export const ColorModeContext = createContext(); | |||
| const ColorModeProvider = ({ children }) => { | |||
| const [toggleColorMode, theme] = useToggleColorMode(); | |||
| return ( | |||
| <ColorModeContext.Provider value={toggleColorMode}> | |||
| <ThemeProvider theme={theme}>{children}</ThemeProvider> | |||
| </ColorModeContext.Provider> | |||
| ); | |||
| }; | |||
| ColorModeProvider.propTypes = { | |||
| children: PropTypes.node, | |||
| }; | |||
| export default ColorModeProvider; | |||
| @@ -0,0 +1,63 @@ | |||
| import React, { createContext, useContext, useState } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import usePagingHook from '../hooks/usePagingHook'; | |||
| import { getRequest } from '../request/jsonServerRequest'; | |||
| const apiCall = (page, itemsPerPage, sort, sortDirection, filter) => | |||
| getRequest('/items', { | |||
| _page: page, | |||
| _limit: itemsPerPage, | |||
| // Conditionally add to params object if keys exist | |||
| ...(sort && { _sort: sort }), | |||
| ...(sortDirection && { _order: sortDirection }), | |||
| ...(filter && { q: filter }), | |||
| }); | |||
| const Context = createContext(); | |||
| export const useRandomData = () => useContext(Context); | |||
| const RandomDataProvider = ({ children }) => { | |||
| const setPage = (page) => { | |||
| setState({ ...state, page }); | |||
| }; | |||
| const setItemsPerPage = (itemsPerPage) => { | |||
| setState({ ...state, itemsPerPage }); | |||
| }; | |||
| const setSort = (sort) => { | |||
| setState({ ...state, sort }); | |||
| }; | |||
| const setFilter = (filter) => { | |||
| setState({ ...state, filter }); | |||
| }; | |||
| const [state, setState] = useState({ | |||
| page: 0, | |||
| setPage, | |||
| itemsPerPage: 12, | |||
| setItemsPerPage, | |||
| sort: '', | |||
| setSort, | |||
| filter: '', | |||
| setFilter, | |||
| }); | |||
| const data = usePagingHook( | |||
| state.page, | |||
| state.itemsPerPage, | |||
| state.sort, | |||
| state.filter, | |||
| apiCall | |||
| ); | |||
| return ( | |||
| <Context.Provider value={{ state, data }}>{children}</Context.Provider> | |||
| ); | |||
| }; | |||
| RandomDataProvider.propTypes = { | |||
| children: PropTypes.node, | |||
| }; | |||
| export default RandomDataProvider; | |||
| @@ -0,0 +1,17 @@ | |||
| import { useEffect, useState } from 'react'; | |||
| const useDebounce = (value, delay) => { | |||
| const [debouncedValue, setDebouncedValue] = useState(value); | |||
| useEffect(() => { | |||
| const timer = setTimeout(() => setDebouncedValue(value), delay || 500); | |||
| return () => { | |||
| clearTimeout(timer); | |||
| }; | |||
| }, [value, delay]); | |||
| return debouncedValue; | |||
| }; | |||
| export default useDebounce; | |||
| @@ -0,0 +1,66 @@ | |||
| import { useState, useCallback, useEffect } from 'react'; | |||
| import { unstable_batchedUpdates } from 'react-dom'; | |||
| const usePagingHook = (page, itemsPerPage, sort, filter, apiCallback) => { | |||
| const [items, setItems] = useState([]); | |||
| const [totalPages, setTotalPages] = useState(0); | |||
| const [currentPage, setCurrentPage] = useState(0); | |||
| const [loading, setLoading] = useState(false); | |||
| const [totalCount, setTotalCount] = useState(0); | |||
| const reload = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| const [sortColumn, sortDirection] = sort.split('-'); | |||
| const response = await apiCallback( | |||
| page, | |||
| itemsPerPage, | |||
| sortColumn, | |||
| sortDirection, | |||
| filter | |||
| ); | |||
| if (response.status === 200) { | |||
| // Prevents multiple rerenders | |||
| unstable_batchedUpdates(() => { | |||
| setItems(response.data); | |||
| setTotalCount(parseInt(response.headers['x-total-count'])); | |||
| setTotalPages( | |||
| Math.ceil(response.headers['x-total-count'] / itemsPerPage) | |||
| ); | |||
| setCurrentPage(page); | |||
| }); | |||
| } | |||
| } catch (e) { | |||
| console.error(e); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, [ | |||
| setItems, | |||
| setLoading, | |||
| setTotalPages, | |||
| setCurrentPage, | |||
| apiCallback, | |||
| page, | |||
| itemsPerPage, | |||
| sort, | |||
| filter, | |||
| ]); | |||
| useEffect(() => { | |||
| reload(); | |||
| }, [reload]); | |||
| return { | |||
| items, | |||
| loading, | |||
| reload, | |||
| totalCount, | |||
| totalPages, | |||
| currentPage, | |||
| itemsPerPage, | |||
| sort, | |||
| }; | |||
| }; | |||
| export default usePagingHook; | |||
| @@ -0,0 +1,31 @@ | |||
| import { useState, useMemo } from 'react'; | |||
| import { createTheme } from '@mui/material/styles'; | |||
| import { | |||
| authScopeSetHelper, | |||
| authScopeStringGetHelper, | |||
| } from '../util/helpers/authScopeHelpers'; | |||
| const useToggleColorMode = () => { | |||
| const currentColorMode = authScopeStringGetHelper('colorMode') || 'light'; | |||
| const [mode, setMode] = useState(currentColorMode); | |||
| const toggleColorMode = () => { | |||
| const nextMode = mode === 'light' ? 'dark' : 'light'; | |||
| setMode(nextMode); | |||
| authScopeSetHelper('colorMode', nextMode); | |||
| }; | |||
| const theme = useMemo( | |||
| () => | |||
| createTheme({ | |||
| palette: { | |||
| mode, | |||
| }, | |||
| }), | |||
| [mode] | |||
| ); | |||
| return [toggleColorMode, theme]; | |||
| }; | |||
| export default useToggleColorMode; | |||
| @@ -0,0 +1,28 @@ | |||
| import { format as formatDate } from 'date-fns'; | |||
| import i18n from 'i18next'; | |||
| import { initReactI18next } from 'react-i18next'; | |||
| import enTranslations from './resources/en'; | |||
| i18n.use(initReactI18next).init({ | |||
| lng: 'en', | |||
| fallbackLng: 'en', | |||
| debug: false, | |||
| supportedLngs: ['en'], | |||
| resources: { | |||
| en: { | |||
| translation: enTranslations, | |||
| }, | |||
| }, | |||
| interpolation: { | |||
| format: (value, format) => { | |||
| if (value instanceof Date) { | |||
| return formatDate(value, format); | |||
| } | |||
| return value; | |||
| }, | |||
| }, | |||
| }); | |||
| export default i18n; | |||
| @@ -0,0 +1,93 @@ | |||
| export default { | |||
| app: { | |||
| title: 'React template' | |||
| }, | |||
| refresh: { | |||
| title: 'Are you active?', | |||
| cta: | |||
| "You were registered as not active, please confirm that you are active in the next minute, if you don't you will be logged out.", | |||
| }, | |||
| common: { | |||
| close: 'Close', | |||
| trademark: 'TM', | |||
| search: 'Search', | |||
| error: 'Error', | |||
| continue: 'Continue', | |||
| labelUsername: 'Username', | |||
| labelPassword: 'Password', | |||
| next: 'Next', | |||
| nextPage: 'Next page', | |||
| previousPage: 'Previous page', | |||
| back: 'Back', | |||
| goBack: 'Go Back', | |||
| ok: 'Ok', | |||
| done: 'Done', | |||
| confirm: 'Confirm', | |||
| printDownload: 'Print/Download', | |||
| cancel: 'Cancel', | |||
| remove: 'Remove', | |||
| invite: 'Invite', | |||
| save: 'Save', | |||
| complete: 'Complete', | |||
| download: 'Download', | |||
| yes: 'Yes', | |||
| no: 'No', | |||
| to: 'to', | |||
| select: 'Select...', | |||
| none: 'None', | |||
| date: { | |||
| range: '{{start}} to {{end}}', | |||
| }, | |||
| }, | |||
| login: { | |||
| welcome: 'React template', | |||
| dontHaveAccount: "Don't have an account? ", | |||
| emailFormat: 'Invalid email address format.', | |||
| emailRequired: 'An email or username is required.', | |||
| noUsers: 'There are no users with that email.', | |||
| passwordStrength: 'Your password is {{strength}}.', | |||
| passwordLength: 'Your password contain between 8 and 50 characters.', | |||
| signUpRecommendation: 'Sign up', | |||
| email: 'Please enter your email address or username to log in:', | |||
| logInTitle: 'Log In', | |||
| logIn: 'Log In', | |||
| signUp: 'Sign Up', | |||
| usernameRequired: 'Username is required.', | |||
| passwordRequired: 'A Password is required.', | |||
| forgotYourPassword: 'Forgot your password?', | |||
| forgotPasswordEmail:'Email', | |||
| useDifferentEmail: 'Use different email address or username', | |||
| }, | |||
| password: { | |||
| weak: 'weak', | |||
| average: 'average', | |||
| good: 'good', | |||
| strong: 'strong', | |||
| }, | |||
| forgotPassword: { | |||
| title: 'Forgot Password', | |||
| label: 'Send email', | |||
| emailRequired: 'An email is required.', | |||
| emailFormat: 'Invalid email address format.', | |||
| forgotPassword: { | |||
| title: 'Forgot Password', | |||
| subtitle: | |||
| 'Please answer the security question to gain access to your account:', | |||
| label: 'Reset Password', | |||
| }, | |||
| }, | |||
| notFound: { | |||
| text: "We're sorry but we couldn't find the page you were looking for.", | |||
| goBack: 'Go back to homepage', | |||
| }, | |||
| errorPage: { | |||
| text: | |||
| "We're sorry, an internal server error came up. Please be patient or try again later.", | |||
| goBack: 'Go back to homepage', | |||
| logout: 'Logout', | |||
| }, | |||
| apiErrors:{ | |||
| ClientIpAddressIsNullOrEmpty:"Client Ip address is null or empty", | |||
| UsernameDoesNotExist: "Username does not exist" | |||
| } | |||
| }; | |||
| @@ -0,0 +1,13 @@ | |||
| body { | |||
| margin: 0; | |||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', | |||
| 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', | |||
| sans-serif; | |||
| -webkit-font-smoothing: antialiased; | |||
| -moz-osx-font-smoothing: grayscale; | |||
| } | |||
| code { | |||
| font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', | |||
| monospace; | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| import React from 'react'; | |||
| import ReactDOM from 'react-dom'; | |||
| import { Provider } from 'react-redux'; | |||
| import { HelmetProvider } from 'react-helmet-async'; | |||
| import './main.scss'; | |||
| import App from './App'; | |||
| import store from './store'; | |||
| import './i18n'; | |||
| import ColorModeProvider from './context/ColorModeContext'; | |||
| ReactDOM.render( | |||
| <HelmetProvider> | |||
| <React.StrictMode> | |||
| <Provider store={store}> | |||
| <ColorModeProvider> | |||
| <App /> | |||
| </ColorModeProvider> | |||
| </Provider> | |||
| </React.StrictMode> | |||
| </HelmetProvider>, | |||
| document.getElementById('root'), | |||
| ); | |||
| @@ -0,0 +1 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg> | |||
| @@ -0,0 +1,22 @@ | |||
| @import './assets/styles/variables'; | |||
| @import './assets/styles/mixins'; | |||
| @import './assets/styles/functions'; | |||
| @import './assets/styles/typography'; | |||
| @import './assets/styles/base'; | |||
| @import './assets/styles/reset'; | |||
| @import './assets/styles/components/button'; | |||
| @import './assets/styles/components/icon-button'; | |||
| @import './assets/styles/components/app-button'; | |||
| @import './assets/styles/components/loader'; | |||
| @import './assets/styles/components/radio'; | |||
| @import './assets/styles/components/modal'; | |||
| @import './assets/styles/components/auth-card'; | |||
| @import './assets/styles/components/auth'; | |||
| @import './assets/styles/components/login'; | |||
| @import './assets/styles/components/login-card'; | |||
| @import './assets/styles/components/forgot-password'; | |||
| @import './assets/styles/components/input'; | |||
| @import './assets/styles/components/error-page'; | |||
| @import './assets/styles/layout'; | |||
| @import './assets/styles/overwrite'; | |||
| @import './assets/styles/utility'; | |||
| @@ -0,0 +1,19 @@ | |||
| import React from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| const ErrorPage = () => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <div className="c-error-page"> | |||
| <div className="c-error-page__content"> | |||
| <h1 className="c-error-page__title">500</h1> | |||
| <p className="c-error-page__text">{t('errorPage.text')}</p> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| ErrorPage.propTypes = {}; | |||
| export default ErrorPage; | |||
| @@ -0,0 +1,29 @@ | |||
| import React from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import Button from '../../components/Button/Button'; | |||
| import Section from '../../components/Section/Section'; | |||
| const NotFoundPage = () => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <div className="c-error-page"> | |||
| <Section className="c-error-page__content-container"> | |||
| <div className="c-error-page__content"> | |||
| <h1 className="c-error-page__title">404</h1> | |||
| <p className="c-error-page__text">{t('notFound.text')}</p> | |||
| <Button | |||
| className="c-error-page__button" | |||
| variant="primary-outlined" | |||
| > | |||
| {t('notFound.goBack')} | |||
| </Button> | |||
| </div> | |||
| </Section> | |||
| </div> | |||
| ); | |||
| }; | |||
| NotFoundPage.propTypes = {}; | |||
| export default NotFoundPage; | |||
| @@ -0,0 +1,63 @@ | |||
| import React from 'react'; | |||
| import { Formik, Form, Field } from 'formik'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import * as Yup from 'yup'; | |||
| import i18next from 'i18next'; | |||
| import Auth from '../../components/Auth/Auth'; | |||
| import AuthCard from '../../components/AuthCards/AuthCard'; | |||
| import TextField from '../../components/InputFields/TextField'; | |||
| import Button from '../../components/Button/Button'; | |||
| import Section from '../../components/Section/Section'; | |||
| const forgotPasswordValidationSchema = Yup.object().shape({ | |||
| email: Yup.string().required( | |||
| i18next.t('login.securityQuestion.answerRequired'), | |||
| ), | |||
| }); | |||
| const ForgotPasswordPage = () => { | |||
| const { t } = useTranslation(); | |||
| const handleSubmit = (values) => { | |||
| console.log("Values",values) | |||
| }; | |||
| return ( | |||
| <Auth> | |||
| <AuthCard | |||
| title={t('forgotPassword.title')} | |||
| > | |||
| <Section> | |||
| <div className="c-reset-security"> | |||
| <div className="c-reset-security__form"> | |||
| <Formik | |||
| onSubmit={handleSubmit} | |||
| initialValues={{ email: '' }} | |||
| validationSchema={forgotPasswordValidationSchema} | |||
| > | |||
| <Form> | |||
| <Field | |||
| label={t('login.forgotPasswordEmail')} | |||
| name="email" | |||
| component={TextField} | |||
| /> | |||
| <Button | |||
| className="c-reset-security__button" | |||
| authButton | |||
| variant="primary" | |||
| type="submit" | |||
| > | |||
| {t('forgotPassword.label')} | |||
| </Button> | |||
| </Form> | |||
| </Formik> | |||
| </div> | |||
| </div> | |||
| </Section> | |||
| </AuthCard> | |||
| </Auth> | |||
| ); | |||
| }; | |||
| export default ForgotPasswordPage; | |||
| @@ -0,0 +1,96 @@ | |||
| import React from 'react'; | |||
| import { useFormik } from 'formik'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import * as Yup from 'yup'; | |||
| import i18next from 'i18next'; | |||
| import { | |||
| Box, | |||
| Container, | |||
| Typography, | |||
| Button, | |||
| TextField, | |||
| Link, | |||
| Grid, | |||
| } from '@mui/material'; | |||
| import Backdrop from '../../components/MUI/BackdropComponent'; | |||
| import { LOGIN_PAGE } from '../../constants/pages'; | |||
| import { NavLink } from 'react-router-dom'; | |||
| const forgotPasswordValidationSchema = Yup.object().shape({ | |||
| email: Yup.string() | |||
| .required(i18next.t('forgotPassword.emailRequired')) | |||
| .email(i18next.t('forgotPassword.emailFormat')), | |||
| }); | |||
| const ForgotPasswordPage = () => { | |||
| const { t } = useTranslation(); | |||
| const handleSubmit = (values) => { | |||
| console.log('Values', values); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| email: '', | |||
| }, | |||
| validationSchema: forgotPasswordValidationSchema, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <Container component="main" maxWidth="md"> | |||
| <Box | |||
| sx={{ | |||
| marginTop: 32, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| alignItems: 'center', | |||
| }} | |||
| > | |||
| <Typography component="h1" variant="h5"> | |||
| {t('forgotPassword.title')} | |||
| </Typography> | |||
| <Box | |||
| component="form" | |||
| onSubmit={formik.handleSubmit} | |||
| sx={{ position: 'relative', mt: 1, p: 1 }} | |||
| > | |||
| <Backdrop position="absolute" isLoading={false} /> | |||
| <TextField | |||
| name="email" | |||
| label={t('login.forgotPasswordEmail')} | |||
| margin="normal" | |||
| value={formik.values.email} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.email && Boolean(formik.errors.email)} | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ mt: 3, mb: 2 }} | |||
| fullWidth | |||
| > | |||
| {t('forgotPassword.label')} | |||
| </Button> | |||
| <Grid container justifyContent="center"> | |||
| <Link | |||
| to={LOGIN_PAGE} | |||
| component={NavLink} | |||
| variant="body2" | |||
| underline="hover" | |||
| > | |||
| {t('common.back')} | |||
| </Link> | |||
| </Grid> | |||
| </Box> | |||
| </Box> | |||
| </Container> | |||
| ); | |||
| }; | |||
| export default ForgotPasswordPage; | |||
| @@ -0,0 +1,15 @@ | |||
| import React from 'react'; | |||
| const HomePage = () => { | |||
| return ( | |||
| <div className="c-error-page"> | |||
| <div className="c-error-page__content"> | |||
| <h1 className="c-error-page__title">Home page</h1> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| HomePage.propTypes = {}; | |||
| export default HomePage; | |||