| @@ -0,0 +1,2 @@ | |||
| REACT_APP_BASE_API_URL=https://portalgatewayapi-q1.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* | |||
| @@ -0,0 +1,70 @@ | |||
| # 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,68 @@ | |||
| { | |||
| "name": "web", | |||
| "version": "0.1.0", | |||
| "private": true, | |||
| "dependencies": { | |||
| "@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", | |||
| "formik": "^2.2.9", | |||
| "i18next": "^20.3.1", | |||
| "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" | |||
| }, | |||
| "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,37 @@ | |||
| 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 HomePage from './pages/HomePage/HomePage'; | |||
| import NotFoundPage from './pages/ErrorPages/NotFoundPage'; | |||
| import ErrorPage from './pages/ErrorPages/ErrorPage'; | |||
| import ForgotPasswordPage from './pages/ForgotPasswordPage/ForgotPasswordPage'; | |||
| 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, { 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,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: true, | |||
| 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,87 @@ | |||
| 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', | |||
| 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', | |||
| 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', | |||
| 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" | |||
| } | |||
| }; | |||
| @@ -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,21 @@ | |||
| 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'; | |||
| ReactDOM.render( | |||
| <HelmetProvider> | |||
| <React.StrictMode> | |||
| <Provider store={store}> | |||
| <App /> | |||
| </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,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; | |||
| @@ -0,0 +1,145 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { Field, Form, Formik } from 'formik'; | |||
| import { useDispatch, useSelector } from 'react-redux'; | |||
| import { NavLink } from 'react-router-dom'; | |||
| import * as Yup from 'yup'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import i18next from 'i18next'; | |||
| import PasswordField from '../../components/InputFields/PasswordField'; | |||
| import Button from '../../components/Button/Button'; | |||
| import TextField from '../../components/InputFields/TextField'; | |||
| import Auth from '../../components/Auth/Auth'; | |||
| import AuthCard from '../../components/AuthCards/AuthCard'; | |||
| import { | |||
| clearLoginErrors, | |||
| fetchUser, | |||
| } from '../../store/actions/login/loginActions'; | |||
| import { | |||
| selectLoginError, | |||
| } from '../../store/selectors/loginSelectors'; | |||
| import { | |||
| FORGOT_PASSWORD_PAGE, HOME_PAGE, | |||
| } from '../../constants/pages'; | |||
| import { selectIsLoadingByActionType } from '../../store/selectors/loadingSelectors'; | |||
| import { LOGIN_USER_LOADING } from '../../store/actions/login/loginActionConstants'; | |||
| const LoginValidationSchema = Yup.object().shape({ | |||
| username: Yup.string().required(i18next.t('login.usernameRequired')), | |||
| password: Yup.string().required(i18next.t('login.passwordRequired')), | |||
| }); | |||
| const LoginPage = ({ history }) => { | |||
| const dispatch = useDispatch(); | |||
| const { t } = useTranslation(); | |||
| const error = useSelector(selectLoginError); | |||
| // When user refreshes page | |||
| // useEffect(() => { | |||
| // function redirectClient() { | |||
| // if (!tokens.RefreshToken && !tokens.JwtToken) { | |||
| // return | |||
| // } | |||
| // } | |||
| // redirectClient(); | |||
| // }, [history, tokens]); | |||
| const isLoading = useSelector( | |||
| selectIsLoadingByActionType(LOGIN_USER_LOADING), | |||
| ); | |||
| const handleApiResponseSuccess =()=>{ | |||
| history.push({ | |||
| pathname: HOME_PAGE, | |||
| state: { | |||
| from: history.location.pathname, | |||
| }, | |||
| }); | |||
| } | |||
| const handleSubmit = (values) => { | |||
| // destructure value as username. | |||
| const { username: Username } = values; | |||
| const { password: Password } = values; | |||
| dispatch(clearLoginErrors()); | |||
| dispatch( | |||
| fetchUser({ | |||
| Username, | |||
| Password, | |||
| handleApiResponseSuccess | |||
| }, | |||
| ), | |||
| ); | |||
| }; | |||
| return ( | |||
| <Auth> | |||
| <AuthCard | |||
| title="Log In" | |||
| isLoading={isLoading} | |||
| > | |||
| <div className="c-login c-login--user"> | |||
| <div className="c-login__form"> | |||
| <Formik | |||
| initialValues={{ | |||
| username: '', | |||
| password: '', | |||
| }} | |||
| onSubmit={handleSubmit} | |||
| validationSchema={LoginValidationSchema} | |||
| validateOnBlur | |||
| enableReinitialize | |||
| > | |||
| {({ values }) => ( | |||
| <Form> | |||
| <Field | |||
| label={t('common.labelUsername')} | |||
| value={values.username.value} | |||
| component={TextField} | |||
| name="username" | |||
| /> | |||
| <Field | |||
| label={ | |||
| <div className="c-login--password__label"> | |||
| {t('common.labelPassword')} | |||
| </div> | |||
| } | |||
| link={ | |||
| <NavLink | |||
| to={FORGOT_PASSWORD_PAGE} | |||
| > | |||
| {t('login.forgotYourPassword')} | |||
| </NavLink> | |||
| } | |||
| name="password" | |||
| component={PasswordField} | |||
| errorMessage={error} | |||
| autoFocus | |||
| /> | |||
| <Button | |||
| className="c-login__button" | |||
| authButton | |||
| variant="primary" | |||
| type="submit" | |||
| > | |||
| {t('common.continue')} | |||
| </Button> | |||
| </Form> | |||
| )} | |||
| </Formik> | |||
| </div> | |||
| </div> | |||
| </AuthCard> | |||
| </Auth> | |||
| ); | |||
| }; | |||
| LoginPage.propTypes = { | |||
| history: PropTypes.shape({ | |||
| replace: PropTypes.func, | |||
| push: PropTypes.func, | |||
| location: PropTypes.shape({ | |||
| pathname: PropTypes.string, | |||
| }), | |||
| }), | |||
| }; | |||
| export default LoginPage; | |||
| @@ -0,0 +1,13 @@ | |||
| const reportWebVitals = onPerfEntry => { | |||
| if (onPerfEntry && onPerfEntry instanceof Function) { | |||
| import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { | |||
| getCLS(onPerfEntry); | |||
| getFID(onPerfEntry); | |||
| getFCP(onPerfEntry); | |||
| getLCP(onPerfEntry); | |||
| getTTFB(onPerfEntry); | |||
| }); | |||
| } | |||
| }; | |||
| export default reportWebVitals; | |||
| @@ -0,0 +1,71 @@ | |||
| import { | |||
| deleteRequest, | |||
| getRequest, | |||
| patchRequest, | |||
| replaceInUrl, | |||
| postRequest, | |||
| } from './index'; | |||
| import apiEndpoints from './apiEndpoints'; | |||
| export const getAccount = (accountUid) => | |||
| getRequest(replaceInUrl(apiEndpoints.accounts.get, { accountUid })); | |||
| export const getAccountUsers = (accountUid) => | |||
| getRequest(replaceInUrl(apiEndpoints.accounts.getUsers, { accountUid })); | |||
| export const getUserPermissions = (currentAccountUid, currentUserUid) => | |||
| getRequest( | |||
| replaceInUrl(apiEndpoints.accounts.getCurrentUserPermissions, { | |||
| currentAccountUid, | |||
| currentUserUid, | |||
| }), | |||
| ); | |||
| export const getAccountAddresses = (accountUid) => | |||
| getRequest(replaceInUrl(apiEndpoints.accounts.getAddresses, { accountUid })); | |||
| export const getAccountSettingsRequest = (accountUid) => | |||
| getRequest(replaceInUrl(apiEndpoints.accounts.getSettings, { accountUid })); | |||
| export const updateAccountAddressRequest = (accountUid, addressUid, data) => | |||
| patchRequest( | |||
| replaceInUrl(apiEndpoints.accounts.updateAddress, { | |||
| accountUid, | |||
| addressUid, | |||
| }), | |||
| data, | |||
| ); | |||
| export const deleteAccountAddressRequest = (accountUid, addressUid) => | |||
| deleteRequest( | |||
| replaceInUrl(apiEndpoints.accounts.deleteAddress, { | |||
| accountUid, | |||
| addressUid, | |||
| }), | |||
| ); | |||
| export const postNewAccountUserRequest = (accountUid, data) => | |||
| postRequest( | |||
| replaceInUrl(apiEndpoints.accounts.createUser, { | |||
| accountUid, | |||
| }), | |||
| data, | |||
| ); | |||
| export const updateAccountUserRequest = ( | |||
| accountUid, | |||
| userUid, | |||
| actionType, | |||
| data, | |||
| ) => | |||
| patchRequest( | |||
| replaceInUrl(apiEndpoints.accounts.updateUser, { | |||
| accountUid, | |||
| userUid, | |||
| actionType, | |||
| }), | |||
| data, | |||
| ); | |||
| export const postAgreementRequest = (data) => | |||
| postRequest(apiEndpoints.accounts.agreement, data); | |||
| @@ -0,0 +1,156 @@ | |||
| export default { | |||
| accounts: { | |||
| get: 'accounts/{accountUid}', | |||
| getCurrentUserPermissions: | |||
| 'accounts/{currentAccountUid}/users/{currentUserUid}/permissions', | |||
| getAddresses: 'accounts/{accountUid}/addresses', | |||
| updateAddress: 'account/{accountUid}/addresses/{addressUid}', | |||
| deleteAddress: 'accounts/{accountUid}/addresses/{addressUid}', | |||
| getUsers: 'accounts/{accountUid}/users', | |||
| createUser: 'accounts/{accountUid}/users', | |||
| updateUser: 'account/{accountUid}/users/{userUid}?actionType={actionType}', | |||
| deleteUser: 'accounts/{accountUid}/users/{userUid}', | |||
| getSettings: 'accounts/{accountUid}/settings', | |||
| getIraSettings: 'accounts/{accountUid}/iraSettings', | |||
| getSettingsRegistration: 'application/settings', | |||
| agreement: 'accounts/agreement', | |||
| }, | |||
| authentications: { | |||
| getUsernames: 'authenticate/usernames', | |||
| login: 'authenticate', | |||
| getUserSecurityQuestion: 'users/username/securityquestion', | |||
| confirmSecurityQuestion: 'authenticate/confirm', | |||
| confirmForgotPassword: 'users/passwords/reset_token', | |||
| resetPassword: 'users/passwords', | |||
| refreshToken: '/authenticate/refresh', | |||
| generateToken: '/authenticate/generate', | |||
| authenticate: | |||
| '/authenticate?fp={fp}&offer={offer}&landingPageUrl={landingPageUrl}®istrationFlowType={registrationFlowType}', | |||
| confirmAuthentication: | |||
| '/authenticate/confirm?fp={fp}&offer={offer}&landingPageUrl={landingPageUrl}®istrationFlowType={registrationFlowType}', | |||
| }, | |||
| bankAccounts: { | |||
| get: 'accounts/{accountUid}/bankaccounts', | |||
| getBankAccount: | |||
| 'accounts/{accountUid}/bankaccounts/{bankAccountUid}?type={type}', | |||
| getBankAccountsByType: | |||
| 'accounts/{accountUid}/bankaccounts?type={type}&active=true', | |||
| getBankDetailsByRoutingNumber: 'banks/{routingNumber}', | |||
| newAccount: 'accounts/{accountUid}/bankaccounts', | |||
| deleteAccount: | |||
| 'accounts/{accountUid}/bankaccounts/{bankAccountUid}?type={type}', | |||
| verify: '/accounts/{accountUid}/bankaccountverification/{bankAccountUid}', | |||
| postBankAccountRegistration: '/accounts/{applicationUid}/bankaccounts', | |||
| getRegistration: 'banks/{applicationUid}/bankaccounts', | |||
| }, | |||
| documents: { | |||
| getDocuments: 'accounts/{accountUid}/documents?year={year}', | |||
| getDocument: 'accounts/{accountUid}/documents/{documentType}', | |||
| }, | |||
| countries: '/countries', | |||
| metalStream: { | |||
| getMetalStreamSettings: 'accounts/{accountUid}/metalstream', | |||
| getMetalStreamFundings: 'applications/{applicationUid}/metalStreamFunding', | |||
| }, | |||
| orders: { | |||
| buyForStorage: '/accounts/{accountUid}/orders/buyForStorage', | |||
| buyForDelivery: '/accounts/{accountUid}/orders/buyForDelivery', | |||
| verifyBuyForDelivery: '/accounts/{accountUid}/orders/buyForDelivery/verify', | |||
| sellFromStorage: '/accounts/{accountUid}/orders/sellFromStorage', | |||
| fractionalConversion: '/accounts/{accountUid}/orders/fractionalConversion', | |||
| deliverFromStorageVerify: | |||
| '/accounts/{accountUid}/orders/deliverFromStorage/verify', | |||
| deliverFromStorage: '/accounts/{accountUid}/orders/deliverFromStorage', | |||
| iraCashDistribution: '/accounts/{accountUid}/orders/iraCashDistribution', | |||
| iraCashTransfer: '/accounts/{accountUid}/orders/iraCashTransfer', | |||
| iraFeeWithdrawal: 'accounts/{accountUid}/orders/iraFeeWithdrawal', | |||
| achDeposit: 'accounts/{accountUid}/orders/achDeposit', | |||
| wireWithdrawal: '/accounts/{accountUid}/orders/wireWithdrawal', | |||
| checkWithdrawal: '/accounts/{accountUid}/orders/checkWithdrawal', | |||
| }, | |||
| portfolio: { | |||
| getPortfolioValuations: 'accounts/{accountUid}/portfolio/valuations', | |||
| getPortfolioMetalPrices: 'marketprices', | |||
| getPortfolioHoldings: | |||
| 'accounts/{accountUid}/portfolio/products?valuation=true', | |||
| getPortfolioProductCodes: '/accounts/{accountUid}/portfolio/productcodes', | |||
| getPortfolioBalances: '/accounts/{accountUid}/portfolio/balances', | |||
| getPortfolioProductBySymbol: | |||
| '/accounts/{accountUid}/portfolio/products/{symbol}', | |||
| getPortfolioTransactions: '/accounts/{accountUid}/transactions', | |||
| getPortfolioSingleTransaction: | |||
| '/accounts/{accountUid}/transactions/{transactionUid}', | |||
| getProductPortoflioTransactions: 'accounts/{accountUid}/transactions', | |||
| getRecentPortfolioTransactions: | |||
| 'accounts/{accountUid}/transactions?content=Recent', | |||
| getFinancialPortfolioTransactions: 'accounts/{accountUid}/transactions', | |||
| getFinancialPortfolioPendingTransactions: | |||
| 'accounts/{accountUid}/transactions/fundinghistory', | |||
| patchFinancialPortfolioPendingTransactions: | |||
| '/accounts/{accountUid}/transactions/fundinghistory/{depositKey}', | |||
| }, | |||
| products: { | |||
| getPrices: '/accounts/{accountUid}/products/prices', | |||
| prices: 'accounts/{accountUid}/products/prices?side={side}', | |||
| tiers: | |||
| '/accounts/{accountUid}/products/prices/{symbol}/tiers?side={side}&location={location}', | |||
| symbolPrices: '/accounts/{accountUid}/products/{symbol}/prices?side={side}', | |||
| getPricesRegistration: 'applications/{applicationUid}/products/prices', | |||
| }, | |||
| settings: { | |||
| get: 'settings', | |||
| }, | |||
| taxForms: { | |||
| getTaxForms: 'settings/taxForms/{applicationType}', | |||
| }, | |||
| users: { | |||
| getAccounts: '/users/{userUid}/accounts', | |||
| getRegistrationAccounts: '/users/{userUid}/accounts', | |||
| updateUser: '/users/{userUid}?updateUserActionType={actionType}', | |||
| updateUserPassword: '/users/{userUid}/passwords', | |||
| logout: '/users/{userUid}/logout', | |||
| getUsernames: '/users/email', | |||
| createUser: | |||
| '/users?fp={fp}&offer={offer}&landingPageUrl={landingPageUrl}®istrationFlowType={registrationFlowType}', | |||
| updateUserRegistration: '/users/{userUid}', | |||
| invite: '/users/invite', | |||
| }, | |||
| applications: { | |||
| application: '/applications/{applicationUid}', | |||
| addPerson: '/applications/{applicationUid}/persons', | |||
| updatePerson: '/applications/{applicationUid}/persons/{personUid}', | |||
| addPersonWithGiftState: | |||
| '/applications/{applicationUid}/UTMA/persons?giftState={giftState}', | |||
| updatePersonWithGiftState: | |||
| '/applications/{applicationUid}/UTMA/persons/{personUid}?giftState={giftState}', | |||
| addPersonWithCompanyName: | |||
| '/applications/{applicationUid}/IRA/persons?companyName={companyName}', | |||
| updatePersonWithCompanyName: | |||
| '/applications/{applicationUid}/IRA/persons/{personUid}?companyName={companyName}', | |||
| submitLegalEntity: '/applications/{applicationUid}/legalEntities', | |||
| updateLegalEntity: | |||
| '/applications/{applicationUid}/legalEntities/{personUid}', | |||
| postNonIraFunding: '/applications/{applicationUid}/funding', | |||
| postIraFunding: '/applications/{applicationUid}/iraFunding', | |||
| postMSFunding: '/applications/{applicationUid}/metalStreamFunding', | |||
| consent: '/applications/{applicationUid}/consents', | |||
| updateConsent: `/applications/{applicationUid}/consents/{agreementConsentUid}`, | |||
| submitMetalStreamRegistration: | |||
| '/applications/{applicationUid}/metalStreamFunding', | |||
| }, | |||
| common: { | |||
| getCountries: '/countries/', | |||
| getTaxForms: '/taxForms/', | |||
| getContributionYears: 'contributionYears', | |||
| getCountryStates: '/countries/{iso3CountryCode}/states/', | |||
| getSecurityQuestions: '/registration/securityQuestions/', | |||
| getPortalSecurityQuestions: '/securityQuestions', | |||
| }, | |||
| plaid: { | |||
| getToken: '/bankaccounts/createplaidlinktoken', | |||
| }, | |||
| affiliate: { | |||
| setCookie: '/affiliate/picture', | |||
| setFingerprint: '/affiliate/fingerprint', | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,63 @@ | |||
| import axios from 'axios'; | |||
| import queryString from 'qs'; | |||
| const request = axios.create({ | |||
| baseURL: process.env.REACT_APP_BASE_API_URL, | |||
| headers: { | |||
| 'Content-Type': 'application/json', | |||
| }, | |||
| withCredentials: true, | |||
| paramsSerializer: (params) => | |||
| queryString.stringify(params, { arrayFormat: 'comma' }), | |||
| }); | |||
| export const getRequest = (url, params = null, options = null) => | |||
| request.get(url, { params, ...options }); | |||
| export const postRequest = (url, data, params = null, options = null) => | |||
| request.post(url, data, { params, ...options }); | |||
| export const putRequest = (url, data, params = null, options = null) => | |||
| request.put(url, data, { params, ...options }); | |||
| export const patchRequest = (url, data, params = null, options = null) => | |||
| request.patch(url, data, { params, ...options }); | |||
| export const deleteRequest = (url, params = null, options = null) => | |||
| request.delete(url, { params, ...options }); | |||
| export const downloadRequest = (url, params = null, options = null) => | |||
| request.get(url, { params, ...options, responseType: 'blob' }); | |||
| export const replaceInUrl = (url, pathVariables = {}) => { | |||
| const keys = Object.keys(pathVariables); | |||
| if (!keys.length) { | |||
| return url; | |||
| } | |||
| return keys.reduce( | |||
| (acc, key) => acc.replace(`{${key}}`, pathVariables[`${key}`]), | |||
| url, | |||
| ); | |||
| }; | |||
| export const addHeaderToken = (token) => { | |||
| request.defaults.headers.Authorization = `Bearer ${token}`; | |||
| }; | |||
| export const addHeaderCookie = (key, value) => { | |||
| request.defaults.headers[`${key}`] = value; | |||
| }; | |||
| export const removeHeaderToken = () => { | |||
| delete request.defaults.headers.Authorization; | |||
| }; | |||
| export const attachPostRequestListener = (postRequestListener) => { | |||
| request.interceptors.response.use( | |||
| (response) => response, | |||
| (response) => postRequestListener(response), | |||
| ); | |||
| }; | |||
| export const apiDefaultUrl = request.defaults.baseURL; | |||
| @@ -0,0 +1,19 @@ | |||
| import {postRequest, replaceInUrl } from './index'; | |||
| import apiEndpoints from './apiEndpoints'; | |||
| export const attemptLogin = (payload) => | |||
| postRequest(apiEndpoints.authentications.login, payload); | |||
| export const refreshTokenRequest = (payload) => | |||
| postRequest(apiEndpoints.authentications.refreshToken, payload); | |||
| export const logoutUserRequest = (userUid) => | |||
| postRequest( | |||
| replaceInUrl(apiEndpoints.users.logout, { | |||
| userUid, | |||
| }), | |||
| ); | |||
| export const generateTokenRequest = (payload) => | |||
| postRequest(apiEndpoints.authentications.generateToken, payload); | |||
| @@ -0,0 +1,5 @@ | |||
| // jest-dom adds custom jest matchers for asserting on DOM nodes. | |||
| // allows you to do things like: | |||
| // expect(element).toHaveTextContent(/react/i) | |||
| // learn more: https://github.com/testing-library/jest-dom | |||
| import '@testing-library/jest-dom'; | |||
| @@ -0,0 +1,21 @@ | |||
| export const FETCH = '[FETCH]'; | |||
| export const UPDATE = '[UPDATE]'; | |||
| export const SUCCESS = '[SUCCESS]'; | |||
| export const SET = '[SET]'; | |||
| export const ERROR = '[ERROR]'; | |||
| export const SUBMIT = '[SUBMIT]'; | |||
| export const CLEAR = '[CLEAR]'; | |||
| export const DELETE = '[DELETE]'; | |||
| export const LOADING = '[LOADING]'; | |||
| const createType = (typeDescription) => (type) => `${typeDescription}${type}`; | |||
| export const createFetchType = createType(FETCH); | |||
| export const createUpdateType = createType(UPDATE); | |||
| export const createSuccessType = createType(SUCCESS); | |||
| export const createSetType = createType(SET); | |||
| export const createErrorType = createType(ERROR); | |||
| export const createSubmitType = createType(SUBMIT); | |||
| export const createClearType = createType(CLEAR); | |||
| export const createDeleteType = createType(DELETE); | |||
| export const createLoadingType = createType(LOADING); | |||
| @@ -0,0 +1,3 @@ | |||
| import { createLoadingType } from '../actionHelpers'; | |||
| export const APP_LOADING = createLoadingType('APP_LOADING'); | |||
| @@ -0,0 +1,5 @@ | |||
| import { APP_LOADING } from './appActionConstants'; | |||
| export const setAppReady = () => ({ | |||
| type: APP_LOADING, | |||
| }); | |||
| @@ -0,0 +1,30 @@ | |||
| import { | |||
| createClearType, | |||
| createErrorType, | |||
| createFetchType, | |||
| createLoadingType, | |||
| createSuccessType, | |||
| createSubmitType, | |||
| } from '../actionHelpers'; | |||
| const LOGIN_USER_SCOPE = 'LOGIN_USER'; | |||
| export const LOGIN_USER_FETCH = createFetchType(LOGIN_USER_SCOPE); | |||
| export const LOGIN_USER_SUCCESS = createSuccessType(LOGIN_USER_SCOPE); | |||
| export const LOGIN_USER_ERROR = createErrorType(LOGIN_USER_SCOPE); | |||
| export const CLEAR_LOGIN_USER_ERROR = createClearType( | |||
| `${LOGIN_USER_SCOPE}_ERROR`, | |||
| ); | |||
| export const LOGIN_USER_LOADING = createLoadingType(LOGIN_USER_SCOPE); | |||
| export const UPDATE_USER_JWT_TOKEN = 'UPDATE_USER_JWT_TOKEN'; | |||
| export const RESET_LOGIN_STATE = 'RESET_LOGIN_STATE'; | |||
| export const AUTHENTICATE_USER = 'AUTHENTICATE_USER'; | |||
| export const LOGOUT_USER = 'LOGOUT_USER'; | |||
| export const REFRESH_TOKEN = 'REFRESH_TOKEN'; | |||
| const GENERATE_TOKEN_SCOPE = 'GENERATE_TOKEN'; | |||
| export const GENERATE_TOKEN = createSubmitType(GENERATE_TOKEN_SCOPE); | |||
| export const GENERATE_TOKEN_SUCCESS = createSuccessType(GENERATE_TOKEN_SCOPE); | |||
| export const GENERATE_TOKEN_ERROR = createErrorType(GENERATE_TOKEN_SCOPE); | |||
| @@ -0,0 +1,70 @@ | |||
| import { | |||
| AUTHENTICATE_USER, | |||
| CLEAR_LOGIN_USER_ERROR, | |||
| LOGIN_USER_ERROR, | |||
| LOGIN_USER_FETCH, | |||
| LOGIN_USER_SUCCESS, | |||
| LOGOUT_USER, | |||
| RESET_LOGIN_STATE, | |||
| UPDATE_USER_JWT_TOKEN, | |||
| REFRESH_TOKEN, | |||
| GENERATE_TOKEN, | |||
| GENERATE_TOKEN_SUCCESS, | |||
| GENERATE_TOKEN_ERROR, | |||
| } from './loginActionConstants'; | |||
| export const fetchUser = (payload) => ({ | |||
| type: LOGIN_USER_FETCH, | |||
| payload, | |||
| }); | |||
| export const fetchUserSuccess = (payload) => ({ | |||
| type: LOGIN_USER_SUCCESS, | |||
| payload, | |||
| }); | |||
| export const fetchUserError = (payload) => ({ | |||
| type: LOGIN_USER_ERROR, | |||
| payload, | |||
| }); | |||
| export const updateUserToken = (payload) => ({ | |||
| type: UPDATE_USER_JWT_TOKEN, | |||
| payload, | |||
| }); | |||
| export const resetLoginState = () => ({ | |||
| type: RESET_LOGIN_STATE, | |||
| }); | |||
| export const clearLoginErrors = () => ({ | |||
| type: CLEAR_LOGIN_USER_ERROR, | |||
| }); | |||
| export const authenticateUser = () => ({ | |||
| type: AUTHENTICATE_USER, | |||
| }); | |||
| export const logoutUser = () => ({ | |||
| type: LOGOUT_USER, | |||
| }); | |||
| export const refreshUserToken = () => ({ | |||
| type: REFRESH_TOKEN, | |||
| }); | |||
| export const generateToken = (payload) => ({ | |||
| type: GENERATE_TOKEN, | |||
| payload, | |||
| }); | |||
| export const generateTokenSuccess = (payload) => ({ | |||
| type: GENERATE_TOKEN_SUCCESS, | |||
| payload, | |||
| }); | |||
| export const generateTokenError = (payload) => ({ | |||
| type: GENERATE_TOKEN_ERROR, | |||
| payload, | |||
| }); | |||
| @@ -0,0 +1,3 @@ | |||
| export const SET_USER = 'SET_USER'; | |||
| export const SET_USER_ERROR = 'SET_USER_ERROR'; | |||
| @@ -0,0 +1,14 @@ | |||
| import { | |||
| SET_USER, | |||
| SET_USER_ERROR, | |||
| } from './userActionConstants'; | |||
| export const setUser = (payload) => ({ | |||
| type: SET_USER, | |||
| payload, | |||
| }); | |||
| export const setUserError = (payload) => ({ | |||
| type: SET_USER_ERROR, | |||
| payload, | |||
| }); | |||
| @@ -0,0 +1,23 @@ | |||
| import { applyMiddleware, compose, createStore } from 'redux'; | |||
| import createSagaMiddleware from 'redux-saga'; | |||
| import rootReducer from './reducers'; | |||
| import rootSaga from './saga'; | |||
| import loadingMiddleware from './middleware/loadingMiddleware'; | |||
| import requestStatusMiddleware from './middleware/requestStatusMiddleware'; | |||
| import internalServerErrorMiddleware from './middleware/internalServerErrorMiddleware'; | |||
| const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; | |||
| const sagaMiddleware = createSagaMiddleware(); | |||
| export default createStore( | |||
| rootReducer, | |||
| composeEnhancers( | |||
| applyMiddleware( | |||
| sagaMiddleware, | |||
| loadingMiddleware, | |||
| requestStatusMiddleware, | |||
| internalServerErrorMiddleware, | |||
| ), | |||
| ), | |||
| ); | |||
| sagaMiddleware.run(rootSaga); | |||
| @@ -0,0 +1,17 @@ | |||
| import { ERROR_PAGE } from '../../constants/pages'; | |||
| import { attachPostRequestListener } from '../../request'; | |||
| import history from '../utils/history'; | |||
| export default () => (next) => (action) => { | |||
| attachPostRequestListener((error) => { | |||
| if (!error.response) { | |||
| return Promise.reject(error); | |||
| } | |||
| if (error.response.status === 500) { | |||
| return history.push(ERROR_PAGE); | |||
| } | |||
| return Promise.reject(error); | |||
| }); | |||
| next(action); | |||
| }; | |||
| @@ -0,0 +1,41 @@ | |||
| import { | |||
| DELETE, | |||
| ERROR, | |||
| FETCH, | |||
| SUCCESS, | |||
| UPDATE, | |||
| SUBMIT, | |||
| } from '../actions/actionHelpers'; | |||
| const promiseTypes = [FETCH, UPDATE, DELETE, SUBMIT]; | |||
| export default ({ dispatch }) => (next) => (action) => { | |||
| const promiseType = promiseTypes.find((promiseType) => | |||
| action.type.includes(promiseType), | |||
| ); | |||
| if (promiseType) { | |||
| dispatch({ | |||
| type: 'UPDATE_LOADER', | |||
| payload: { | |||
| actionType: action.type.replace(promiseType, '[LOADING]'), | |||
| isLoading: true, | |||
| }, | |||
| }); | |||
| return next(action); | |||
| } | |||
| if (action.type.includes(SUCCESS) || action.type.includes(ERROR)) { | |||
| const actionType = action.type.includes(SUCCESS) | |||
| ? action.type.replace(SUCCESS, '[LOADING]') | |||
| : action.type.replace(ERROR, '[LOADING]'); | |||
| dispatch({ | |||
| type: 'UPDATE_LOADER', | |||
| payload: { | |||
| actionType, | |||
| isLoading: false, | |||
| }, | |||
| }); | |||
| return next(action); | |||
| } | |||
| next(action); | |||
| }; | |||
| @@ -0,0 +1,22 @@ | |||
| import { attachPostRequestListener } from '../../request'; | |||
| import apiEndpoints from '../../request/apiEndpoints'; | |||
| import { logoutUser } from '../actions/login/loginActions'; | |||
| export default ({ dispatch }) => (next) => (action) => { | |||
| attachPostRequestListener((error) => { | |||
| if (!error.response) { | |||
| return Promise.reject(error); | |||
| } | |||
| if ( | |||
| error.response.config.url !== apiEndpoints.authentications.login && | |||
| error.response.config.url !== | |||
| apiEndpoints.authentications.confirmSecurityQuestion && | |||
| error.response.status === 401 | |||
| ) { | |||
| return dispatch(logoutUser()); | |||
| } | |||
| return Promise.reject(error); | |||
| }); | |||
| next(action); | |||
| }; | |||