Nenad Stojanovic 3 年之前
父節點
當前提交
440899652b
共有 100 個檔案被更改,包括 21855 行新增1 行删除
  1. 2
    0
      .env
  2. 28
    0
      .eslintrc
  3. 22
    0
      .eslintrc.json
  4. 23
    0
      .gitignore
  5. 69
    1
      README.md
  6. 15
    0
      db/db.js
  7. 16757
    0
      package-lock.json
  8. 76
    0
      package.json
  9. 二進制
      public/favicon.ico
  10. 43
    0
      public/index.html
  11. 二進制
      public/logo192.png
  12. 二進制
      public/logo512.png
  13. 25
    0
      public/manifest.json
  14. 3
    0
      public/robots.txt
  15. 38
    0
      src/App.css
  16. 24
    0
      src/App.js
  17. 8
    0
      src/App.test.js
  18. 40
    0
      src/AppRoutes.js
  19. 10
    0
      src/assets/images/chevron-down.svg
  20. 12
    0
      src/assets/images/down.svg
  21. 6
    0
      src/assets/images/svg/caps-lock.svg
  22. 10
    0
      src/assets/images/svg/eye-off.svg
  23. 4
    0
      src/assets/images/svg/eye-on.svg
  24. 10
    0
      src/assets/images/svg/search.svg
  25. 46
    0
      src/assets/styles/_base.scss
  26. 7
    0
      src/assets/styles/_functions.scss
  27. 17
    0
      src/assets/styles/_layout.scss
  28. 81
    0
      src/assets/styles/_mixins.scss
  29. 244
    0
      src/assets/styles/_overwrite.scss
  30. 127
    0
      src/assets/styles/_reset.scss
  31. 57
    0
      src/assets/styles/_typography.scss
  32. 39
    0
      src/assets/styles/_utility.scss
  33. 72
    0
      src/assets/styles/_variables.scss
  34. 60
    0
      src/assets/styles/components/_app-button.scss
  35. 45
    0
      src/assets/styles/components/_auth-card.scss
  36. 23
    0
      src/assets/styles/components/_auth.scss
  37. 173
    0
      src/assets/styles/components/_button.scss
  38. 46
    0
      src/assets/styles/components/_error-page.scss
  39. 23
    0
      src/assets/styles/components/_forgot-password.scss
  40. 7
    0
      src/assets/styles/components/_icon-button.scss
  41. 479
    0
      src/assets/styles/components/_input.scss
  42. 72
    0
      src/assets/styles/components/_loader.scss
  43. 31
    0
      src/assets/styles/components/_login-card.scss
  44. 72
    0
      src/assets/styles/components/_login.scss
  45. 169
    0
      src/assets/styles/components/_modal.scss
  46. 29
    0
      src/assets/styles/components/_radio.scss
  47. 20
    0
      src/components/Auth/Auth.js
  48. 24
    0
      src/components/AuthCards/AuthCard.js
  49. 93
    0
      src/components/Button/Button.js
  50. 32
    0
      src/components/IconButton/IconButton.js
  51. 187
    0
      src/components/InputFields/BaseInputField.js
  52. 40
    0
      src/components/InputFields/Checkbox.js
  53. 123
    0
      src/components/InputFields/CurrencyField.js
  54. 33
    0
      src/components/InputFields/EmailField.js
  55. 74
    0
      src/components/InputFields/NumberField.js
  56. 74
    0
      src/components/InputFields/PasswordField.js
  57. 130
    0
      src/components/InputFields/PasswordStrength.js
  58. 45
    0
      src/components/InputFields/PercentageField.js
  59. 49
    0
      src/components/InputFields/PhoneNumberField.js
  60. 54
    0
      src/components/InputFields/Radio.js
  61. 37
    0
      src/components/InputFields/Search.js
  62. 122
    0
      src/components/InputFields/SelectField.js
  63. 72
    0
      src/components/InputFields/TextField.js
  64. 26
    0
      src/components/Loader/BlockSectionLoader.js
  65. 11
    0
      src/components/Loader/FullPageLoader.js
  66. 20
    0
      src/components/Loader/SectionLoader.js
  67. 26
    0
      src/components/MUI/BackdropComponent.js
  68. 57
    0
      src/components/MUI/DialogComponent.js
  69. 28
    0
      src/components/MUI/DrawerComponent.js
  70. 15
    0
      src/components/MUI/ErrorMessageComponent.js
  71. 29
    0
      src/components/MUI/Examples/DataGridExample.js
  72. 64
    0
      src/components/MUI/Examples/ModalsExample.js
  73. 183
    0
      src/components/MUI/Examples/PagingSortingFilteringExample.js
  74. 159
    0
      src/components/MUI/Examples/PagingSortingFilteringExampleServerSide.js
  75. 26
    0
      src/components/MUI/MenuListComponent.js
  76. 160
    0
      src/components/MUI/NavbarComponent.js
  77. 35
    0
      src/components/MUI/PopoverComponent.js
  78. 26
    0
      src/components/Router/PrivateRoute.js
  79. 13
    0
      src/components/Section/Section.js
  80. 14
    0
      src/constants/keyCodeConstants.js
  81. 3
    0
      src/constants/localStorage.js
  82. 6
    0
      src/constants/pages.js
  83. 0
    0
      src/constants/sessionStorage.js
  84. 21
    0
      src/context/ColorModeContext.js
  85. 63
    0
      src/context/RandomDataContext.js
  86. 17
    0
      src/hooks/useDebounceHook.js
  87. 66
    0
      src/hooks/usePagingHook.js
  88. 31
    0
      src/hooks/useToggleColorMode.js
  89. 28
    0
      src/i18n/index.js
  90. 93
    0
      src/i18n/resources/en.js
  91. 13
    0
      src/index.css
  92. 24
    0
      src/index.js
  93. 1
    0
      src/logo.svg
  94. 22
    0
      src/main.scss
  95. 19
    0
      src/pages/ErrorPages/ErrorPage.js
  96. 29
    0
      src/pages/ErrorPages/NotFoundPage.js
  97. 63
    0
      src/pages/ForgotPasswordPage/ForgotPasswordPage.js
  98. 96
    0
      src/pages/ForgotPasswordPage/ForgotPasswordPageMUI.js
  99. 15
    0
      src/pages/HomePage/HomePage.js
  100. 0
    0
      src/pages/HomePage/HomePageMUI.js

+ 2
- 0
.env 查看文件

@@ -0,0 +1,2 @@

REACT_APP_BASE_API_URL=https://portalgatewayapi.bullioninternational.info/

+ 28
- 0
.eslintrc 查看文件

@@ -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"
}]
}
}

+ 22
- 0
.eslintrc.json 查看文件

@@ -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": {
}
}

+ 23
- 0
.gitignore 查看文件

@@ -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*

+ 69
- 1
README.md 查看文件

@@ -1,2 +1,70 @@
# HRCenter
# Getting Started with Create React App

This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).

## Available Scripts

In the project directory, you can run:

### `npm start`

Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.

The page will reload if you make edits.\
You will also see any lint errors in the console.

### `npm test`

Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.

### `npm run build`

Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!

See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.

### `npm run eject`

**Note: this is a one-way operation. Once you `eject`, you can’t go back!**

If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.

Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.

You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.

## Learn More

You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).

To learn React, check out the [React documentation](https://reactjs.org/).

### Code Splitting

This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)

### Analyzing the Bundle Size

This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)

### Making a Progressive Web App

This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)

### Advanced Configuration

This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)

### Deployment

This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)

### `npm run build` fails to minify

This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

+ 15
- 0
db/db.js 查看文件

@@ -0,0 +1,15 @@
const faker = require('faker');

module.exports = () => {
const items = [];
for (let id = 1; id <= 500; id++) {
items.push({
id: id,
name: `${faker.commerce.productAdjective()} ${faker.commerce.productMaterial()} ${faker.commerce.product()}`,
color: faker.commerce.color(),
price: `$${faker.commerce.price()}`,
company: faker.company.companyName(),
});
}
return { items };
};

+ 16757
- 0
package-lock.json
文件差異過大導致無法顯示
查看文件


+ 76
- 0
package.json 查看文件

@@ -0,0 +1,76 @@
{
"name": "web",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/icons-material": "^5.0.5",
"@mui/material": "^5.0.6",
"@mui/x-data-grid": "^5.0.1",
"@reduxjs/toolkit": "^1.5.1",
"@testing-library/jest-dom": "^5.13.0",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"axios": "^0.21.1",
"date-fns": "^2.22.1",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-security": "^1.4.0",
"faker": "^5.5.3",
"formik": "^2.2.9",
"i18next": "^20.3.1",
"json-server": "^0.17.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"lodash.isempty": "^4.4.0",
"owasp-password-strength-test": "^1.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet-async": "^1.0.9",
"react-i18next": "^11.10.0",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-select": "^4.3.1",
"redux": "^4.1.0",
"redux-saga": "^1.1.3",
"sass": "^1.34.1",
"web-vitals": "^1.1.2",
"yup": "^0.32.9"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"json-serve": "json-server ./db/db.js --port=4000"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"eslint": "^7.28.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"prettier": "2.3.1"
}
}

二進制
public/favicon.ico 查看文件


+ 43
- 0
public/index.html 查看文件

@@ -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>

二進制
public/logo192.png 查看文件


二進制
public/logo512.png 查看文件


+ 25
- 0
public/manifest.json 查看文件

@@ -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"
}

+ 3
- 0
public/robots.txt 查看文件

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

+ 38
- 0
src/App.css 查看文件

@@ -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);
}
}

+ 24
- 0
src/App.js 查看文件

@@ -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;

+ 8
- 0
src/App.test.js 查看文件

@@ -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();
});

+ 40
- 0
src/AppRoutes.js 查看文件

@@ -0,0 +1,40 @@
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';

import {
LOGIN_PAGE,
HOME_PAGE,
FORGOT_PASSWORD_PAGE,
NOT_FOUND_PAGE,
ERROR_PAGE,
BASE_PAGE,
} from './constants/pages';

// import LoginPage from './pages/LoginPage/LoginPage';
import LoginPage from './pages/LoginPage/LoginPageMUI';
// import HomePage from './pages/HomePage/HomePage';
import HomePage from './pages/HomePage/HomePageMUI';
import NotFoundPage from './pages/ErrorPages/NotFoundPage';
import ErrorPage from './pages/ErrorPages/ErrorPage';
// import ForgotPasswordPage from './pages/ForgotPasswordPage/ForgotPasswordPage';
import ForgotPasswordPage from './pages/ForgotPasswordPage/ForgotPasswordPageMUI';
import PrivateRoute from './components/Router/PrivateRoute';

const AppRoutes = () => (
<Switch>
<Route exact path={BASE_PAGE} component={LoginPage} />
<Route exact path={LOGIN_PAGE} component={LoginPage} />
<Route path={NOT_FOUND_PAGE} component={NotFoundPage} />
<Route path={ERROR_PAGE} component={ErrorPage} />
<Route path={FORGOT_PASSWORD_PAGE} component={ForgotPasswordPage} />
<PrivateRoute
exact
path={HOME_PAGE}
component={HomePage}
/>
<Redirect from="*" to={NOT_FOUND_PAGE} />
</Switch>
);


export default AppRoutes;

+ 10
- 0
src/assets/images/chevron-down.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" 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>

+ 12
- 0
src/assets/images/down.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>

+ 6
- 0
src/assets/images/svg/caps-lock.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>

+ 10
- 0
src/assets/images/svg/eye-off.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>

+ 4
- 0
src/assets/images/svg/eye-on.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>

+ 10
- 0
src/assets/images/svg/search.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>

+ 46
- 0
src/assets/styles/_base.scss 查看文件

@@ -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;
}

+ 7
- 0
src/assets/styles/_functions.scss 查看文件

@@ -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;
}

+ 17
- 0
src/assets/styles/_layout.scss 查看文件

@@ -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;
}
}

+ 81
- 0
src/assets/styles/_mixins.scss 查看文件

@@ -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;
}

+ 244
- 0
src/assets/styles/_overwrite.scss 查看文件

@@ -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;
}
}
}

+ 127
- 0
src/assets/styles/_reset.scss 查看文件

@@ -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;
}
}

+ 57
- 0
src/assets/styles/_typography.scss 查看文件

@@ -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;
}

+ 39
- 0
src/assets/styles/_utility.scss 查看文件

@@ -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;
}

+ 72
- 0
src/assets/styles/_variables.scss 查看文件

@@ -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;

+ 60
- 0
src/assets/styles/components/_app-button.scss 查看文件

@@ -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;
}
}

+ 45
- 0
src/assets/styles/components/_auth-card.scss 查看文件

@@ -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;
}

+ 23
- 0
src/assets/styles/components/_auth.scss 查看文件

@@ -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;
}

+ 173
- 0
src/assets/styles/components/_button.scss 查看文件

@@ -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;
}
}

+ 46
- 0
src/assets/styles/components/_error-page.scss 查看文件

@@ -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);
}

+ 23
- 0
src/assets/styles/components/_forgot-password.scss 查看文件

@@ -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);
}

+ 7
- 0
src/assets/styles/components/_icon-button.scss 查看文件

@@ -0,0 +1,7 @@
.c-icon-button {
@include flex-center;
@include outline-none;
@include button-clear;
user-select: none;
cursor: pointer;
}

+ 479
- 0
src/assets/styles/components/_input.scss 查看文件

@@ -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;
}
}
}
}

+ 72
- 0
src/assets/styles/components/_loader.scss 查看文件

@@ -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);
}
}
}

+ 31
- 0
src/assets/styles/components/_login-card.scss 查看文件

@@ -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);
}

+ 72
- 0
src/assets/styles/components/_login.scss 查看文件

@@ -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;
}
}

+ 169
- 0
src/assets/styles/components/_modal.scss 查看文件

@@ -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;
}

+ 29
- 0
src/assets/styles/components/_radio.scss 查看文件

@@ -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;
}

+ 20
- 0
src/components/Auth/Auth.js 查看文件

@@ -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;

+ 24
- 0
src/components/AuthCards/AuthCard.js 查看文件

@@ -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;

+ 93
- 0
src/components/Button/Button.js 查看文件

@@ -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;

+ 32
- 0
src/components/IconButton/IconButton.js 查看文件

@@ -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;

+ 187
- 0
src/components/InputFields/BaseInputField.js 查看文件

@@ -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;

+ 40
- 0
src/components/InputFields/Checkbox.js 查看文件

@@ -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;

+ 123
- 0
src/components/InputFields/CurrencyField.js 查看文件

@@ -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;

+ 33
- 0
src/components/InputFields/EmailField.js 查看文件

@@ -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;

+ 74
- 0
src/components/InputFields/NumberField.js 查看文件

@@ -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;

+ 74
- 0
src/components/InputFields/PasswordField.js 查看文件

@@ -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;

+ 130
- 0
src/components/InputFields/PasswordStrength.js 查看文件

@@ -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;

+ 45
- 0
src/components/InputFields/PercentageField.js 查看文件

@@ -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;

+ 49
- 0
src/components/InputFields/PhoneNumberField.js 查看文件

@@ -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;

+ 54
- 0
src/components/InputFields/Radio.js 查看文件

@@ -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;

+ 37
- 0
src/components/InputFields/Search.js 查看文件

@@ -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;

+ 122
- 0
src/components/InputFields/SelectField.js 查看文件

@@ -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;

+ 72
- 0
src/components/InputFields/TextField.js 查看文件

@@ -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;

+ 26
- 0
src/components/Loader/BlockSectionLoader.js 查看文件

@@ -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;

+ 11
- 0
src/components/Loader/FullPageLoader.js 查看文件

@@ -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;

+ 20
- 0
src/components/Loader/SectionLoader.js 查看文件

@@ -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;

+ 26
- 0
src/components/MUI/BackdropComponent.js 查看文件

@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Backdrop, CircularProgress } from '@mui/material';
import { alpha } from '@mui/system';

const BackdropComponent = ({ position = 'fixed', isLoading }) => (
<Backdrop
sx={{
// 'fixed' takes whole page, 'absolute' takes whole space of the parent element which needs to have 'relative' position
position,
backgroundColor: ({ palette }) =>
alpha(palette.background.default, palette.action.disabledOpacity),
zIndex: ({ zIndex }) => zIndex.drawer + 1,
}}
open={isLoading}
>
<CircularProgress />
</Backdrop>
);

BackdropComponent.propTypes = {
position: PropTypes.oneOf(['fixed', 'absolute']),
isLoading: PropTypes.bool.isRequired,
};

export default BackdropComponent;

+ 57
- 0
src/components/MUI/DialogComponent.js 查看文件

@@ -0,0 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Dialog,
DialogContent,
DialogTitle,
DialogActions,
Button,
useMediaQuery,
useTheme,
} from '@mui/material';

const DialogComponent = ({
title,
content,
onClose,
open,
maxWidth,
fullWidth,
responsive,
}) => {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('md'));

const handleClose = () => {
onClose();
};

return (
<Dialog
maxWidth={maxWidth}
fullWidth={fullWidth}
fullScreen={responsive && fullScreen}
onClose={handleClose}
open={open}
>
<DialogTitle>{title}</DialogTitle>
{content && <DialogContent>{content}</DialogContent>}
<DialogActions>
<Button onClick={handleClose}>OK</Button>
<Button onClick={handleClose}>Cancel</Button>
</DialogActions>
</Dialog>
);
};

DialogComponent.propTypes = {
title: PropTypes.string,
open: PropTypes.bool.isRequired,
content: PropTypes.any,
onClose: PropTypes.func.isRequired,
maxWidth: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
fullWidth: PropTypes.bool,
responsive: PropTypes.bool,
};

export default DialogComponent;

+ 28
- 0
src/components/MUI/DrawerComponent.js 查看文件

@@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Drawer } from '@mui/material';

const DrawerComponent = ({ open, toggleOpen, content, anchor = 'right' }) => (
<Drawer
sx={{
minWidth: 250,
'& .MuiDrawer-paper': {
minWidth: 250,
},
}}
anchor={anchor}
open={open}
onClose={toggleOpen}
>
{content ? content : null}
</Drawer>
);

DrawerComponent.propTypes = {
open: PropTypes.bool,
toggleOpen: PropTypes.func,
content: PropTypes.any,
anchor: PropTypes.oneOf(['top', 'right', 'left', 'bottom']),
};

export default DrawerComponent;

+ 15
- 0
src/components/MUI/ErrorMessageComponent.js 查看文件

@@ -0,0 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Typography } from '@mui/material';

const ErrorMessageComponent = ({ error }) => (
<Typography variant="body1" color="error" my={2}>
{error}
</Typography>
);

ErrorMessageComponent.propTypes = {
error: PropTypes.string.isRequired,
};

export default ErrorMessageComponent;

+ 29
- 0
src/components/MUI/Examples/DataGridExample.js 查看文件

@@ -0,0 +1,29 @@
import React from 'react';
import { Paper, Typography } from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';

// Use these values from REDUX?
const rows = [
{ id: 1, col1: 'Example', col2: 'Row', col3: '1' },
{ id: 2, col1: 'Row', col2: 'Example', col3: '2' },
{ id: 3, col1: '3', col2: 'Row', col3: 'Example' },
];

const columns = [
{ field: 'col1', headerName: 'Column 1', flex: 1 },
{ field: 'col2', headerName: 'Column 2', flex: 1 },
{ field: 'col3', headerName: 'Column 2', flex: 1 },
];

const DataGridExample = () => {
return (
<Paper sx={{ p: 2 }} elevation={5}>
<Typography variant="h4" gutterBottom align="center">
DataGrid Example
</Typography>
<DataGrid autoHeight rows={rows} columns={columns} />
</Paper>
);
};

export default DataGridExample;

+ 64
- 0
src/components/MUI/Examples/ModalsExample.js 查看文件

@@ -0,0 +1,64 @@
import React, { useState } from 'react';
import { Button, Divider, Paper, Typography } from '@mui/material';
import DialogComponent from '../DialogComponent';
import DrawerComponent from '../DrawerComponent';
import PopoverComponent from '../PopoverComponent';

const Modals = () => {
const [dialogOpen, setDialogOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const [popoverOpen, setPopoverOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);

return (
<Paper
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
}}
elevation={5}
>
<Typography variant="h4" gutterBottom align="center">
Modals Example
</Typography>
<Divider />
<Button onClick={() => setDialogOpen(true)}>Open Dialog</Button>
<Button onClick={() => setDrawerOpen(true)}>Open Drawer</Button>
<Button
onClick={(e) => {
setPopoverOpen(true);
setAnchorEl(e.currentTarget);
}}
>
Open Popover
</Button>
<DialogComponent
title="Dialog Title"
content={<Typography>Dialog Content</Typography>}
open={dialogOpen}
onClose={() => setDialogOpen(false)}
maxWidth="md"
fullWidth
responsive
/>
<DrawerComponent
anchor="left"
content={<Typography sx={{ p: 2 }}>Drawer Content</Typography>}
open={drawerOpen}
toggleOpen={() => setDrawerOpen(!drawerOpen)}
/>
<PopoverComponent
anchorEl={anchorEl}
open={popoverOpen}
onClose={() => {
setPopoverOpen(false);
setAnchorEl(null);
}}
content={<Typography sx={{ p: 2 }}>Popover Content</Typography>}
/>
</Paper>
);
};

export default Modals;

+ 183
- 0
src/components/MUI/Examples/PagingSortingFilteringExample.js 查看文件

@@ -0,0 +1,183 @@
import React, { useEffect, useState } from 'react';
import {
Paper,
Box,
Grid,
Typography,
Divider,
TablePagination,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
} from '@mui/material';
// import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector, batch } from 'react-redux';
import useDebounce from '../../../hooks/useDebounceHook';
import {
itemsSelector,
pageSelector,
itemsPerPageSelector,
countSelector,
sortSelector,
} from '../../../store/selectors/randomDataSelectors';
import {
loadData,
updatePage,
updateItemsPerPage,
updateFilter,
updateSort,
} from '../../../store/actions/randomData/randomDataActions';

const PagingSortingFilteringExample = () => {
const [filterText, setFilterText] = useState('');

const dispatch = useDispatch();
// const { t } = useTranslation();
const items = useSelector(itemsSelector);
const currentPage = useSelector(pageSelector);
const itemsPerPage = useSelector(itemsPerPageSelector);
const totalCount = useSelector(countSelector);
const sort = useSelector(sortSelector) || 'name-asc';

// Use debounce to prevent too many rerenders
const debouncedFilterText = useDebounce(filterText, 500);

useEffect(() => {
dispatch(loadData(30));
dispatch(updateSort(sort));
}, []);

useEffect(() => {
batch(() => {
dispatch(updateFilter(filterText));
currentPage > 0 && dispatch(updatePage(0));
});
}, [debouncedFilterText]);

const handleFilterTextChange = (event) => {
const filterText = event.target.value;
setFilterText(filterText);
};

const handleSortChange = (event) => {
const sort = event.target.value;
dispatch(updateSort(sort));
};

const handlePageChange = (event, newPage) => {
dispatch(updatePage(newPage));
};

const handleItemsPerPageChange = (event) => {
const itemsPerPage = parseInt(event.target.value);
batch(() => {
dispatch(updateItemsPerPage(itemsPerPage));
dispatch(updatePage(0));
});
};


return (
<Paper
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'start',
py: 2,
minHeight: 500,
}}
elevation={5}
>
<Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center">
Pagination, Filtering and Sorting Example Client Side
</Typography>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'wrap',
mx: 2,
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
width: '100%',
}}
>
{/* TODO Separate into SelectComponent */}
<FormControl sx={{ flexGrow: 1 }}>
<InputLabel id="sort-label">Sort</InputLabel>
<Select
label="Sort"
labelId="sort-label"
id="sort-select-helper"
value={sort}
onChange={handleSortChange}
>
<MenuItem value="name-asc">Name - A-Z</MenuItem>
<MenuItem value="name-desc">Name - Z-A</MenuItem>
<MenuItem value="price-asc">Price - Lowest to Highest</MenuItem>
<MenuItem value="price-desc">Price - Highest to Lowest</MenuItem>
</Select>
</FormControl>
<TextField
sx={{ flexGrow: 1 }}
variant="outlined"
label="Filter"
placeholder="Filter"
value={filterText}
onChange={handleFilterTextChange}
/>
</Box>
</Box>
<Grid container>
{items &&
items.length > 0 &&
items
.slice(
currentPage * itemsPerPage,
currentPage * itemsPerPage + itemsPerPage
)
.map((product, index) => (
// ! DON'T USE index for key, this is for example only
<Grid item sx={{ p: 2 }} xs={12} sm={6} md={4} lg={3} key={index}>
{/* TODO separate into component */}
<Paper sx={{ p: 3, height: '100%' }} elevation={3}>
<Typography sx={{ fontWeight: 600 }}>Name: </Typography>
<Typography display="inline"> {product.name}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Designer: </Typography>
<Typography display="inline"> {product.designer}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Type: </Typography>
<Typography display="inline"> {product.type}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Price: </Typography>
<Typography display="inline"> ${product.price}</Typography>
</Paper>
</Grid>
))}
</Grid>
<Box sx={{ width: '100%' }}>
<TablePagination
component="div"
count={totalCount}
page={currentPage}
onPageChange={handlePageChange}
rowsPerPage={itemsPerPage}
onRowsPerPageChange={handleItemsPerPageChange}
rowsPerPageOptions={[12, 24, 48, 96]}
labelRowsPerPage="Items per page"
showFirstButton
showLastButton
/>
</Box>
</Paper>
);
};

export default PagingSortingFilteringExample;

+ 159
- 0
src/components/MUI/Examples/PagingSortingFilteringExampleServerSide.js 查看文件

@@ -0,0 +1,159 @@
import React, { useEffect, useState } from 'react';
import {
Paper,
Box,
Grid,
Typography,
Divider,
TablePagination,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
} from '@mui/material';
// import { useTranslation } from 'react-i18next';
import Backdrop from '../BackdropComponent';
import useDebounce from '../../../hooks/useDebounceHook';
import { useRandomData } from '../../../context/RandomDataContext';

const PagingSortingFilteringExampleServerSide = () => {
const [filterText, setFilterText] = useState('');
const { state, data } = useRandomData();
const { items, loading, totalCount, currentPage, itemsPerPage, sort } = data;
const { setPage, setItemsPerPage, setSort, setFilter } = state;
// const { t } = useTranslation();

// Use debounce to prevent too many rerenders
const debouncedFilterText = useDebounce(filterText, 500);

useEffect(() => {
setFilter(filterText);
}, [debouncedFilterText]);

const handleFilterTextChange = (event) => {
const filterText = event.target.value;
setFilterText(filterText);
};

const handleSortChange = (event) => {
const sort = event.target.value;
setSort(sort);
};

const handlePageChange = (event, newPage) => {
setPage(newPage);
};

const handleItemsPerPageChange = (event) => {
const itemsPerPage = parseInt(event.target.value);
setItemsPerPage(itemsPerPage);
setPage(0);
};

return (
<Paper
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'start',
py: 2,
minHeight: 500,
position: 'relative',
}}
elevation={5}
>
{loading && <Backdrop isLoading position="absolute" />}
<Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center">
Pagination, Filtering and Sorting Example Server Side
</Typography>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'wrap',
mx: 2,
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
width: '100%',
}}
>
<FormControl sx={{ flexGrow: 1 }}>
<InputLabel id="sort-label">Sort</InputLabel>
<Select
label="Sort"
labelId="sort-label"
id="sort-select-helper"
value={sort || ''}
onChange={handleSortChange}
>
<MenuItem value="">None</MenuItem>
<MenuItem value="name-asc">Name - A-Z</MenuItem>
<MenuItem value="name-desc">Name - Z-A</MenuItem>
<MenuItem value="price-asc">Price - Lowest to Highest</MenuItem>
<MenuItem value="price-desc">Price - Highest to Lowest</MenuItem>
</Select>
</FormControl>
<TextField
sx={{ flexGrow: 1 }}
variant="outlined"
label="Filter"
placeholder="Filter"
value={filterText}
onChange={handleFilterTextChange}
/>
</Box>
<Grid container sx={{ position: 'relative' }}>
{items &&
items.length > 0 &&
items.map((item) => (
<Grid
item
sx={{ p: 2 }}
xs={12}
sm={6}
md={4}
lg={3}
key={item.id}
>
{/* TODO separate into component */}
<Paper sx={{ p: 3, height: '100%' }} elevation={3}>
<Typography sx={{ fontWeight: 600 }}>Name: </Typography>
<Typography display="inline"> {item.name}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Company: </Typography>
<Typography display="inline"> {item.company}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Color: </Typography>
<Typography display="inline"> {item.color}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Price: </Typography>
<Typography display="inline"> {item.price}</Typography>
</Paper>
</Grid>
))}
</Grid>
<Box sx={{ width: '100%' }}>
<TablePagination
component="div"
count={totalCount}
page={currentPage}
onPageChange={handlePageChange}
rowsPerPage={itemsPerPage}
onRowsPerPageChange={handleItemsPerPageChange}
rowsPerPageOptions={[12, 24, 48, 96]}
labelRowsPerPage="Items per page"
showFirstButton
showLastButton
/>
</Box>
</Box>
</Paper>
);
};

export default PagingSortingFilteringExampleServerSide;

+ 26
- 0
src/components/MUI/MenuListComponent.js 查看文件

@@ -0,0 +1,26 @@
import React, { useState } from 'react';
import { Button, Menu, MenuItem } from '@mui/material';

const MenuListComponent = () => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};

return (
<div>
<Button onClick={handleClick}>Menu List</Button>
<Menu id="menu-list" anchorEl={anchorEl} open={open} onClose={handleClose}>
<MenuItem onClick={handleClose}>Menu Item 1</MenuItem>
<MenuItem onClick={handleClose}>Menu Item 2</MenuItem>
<MenuItem onClick={handleClose}>Menu Item 3</MenuItem>
</Menu>
</div>
);
};

export default MenuListComponent;

+ 160
- 0
src/components/MUI/NavbarComponent.js 查看文件

@@ -0,0 +1,160 @@
import React, { useState, useMemo, useContext } from 'react';
import {
AppBar,
Badge,
Box,
IconButton,
Toolbar,
Typography,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
useMediaQuery,
} from '@mui/material';
import { useTheme } from '@mui/system';
import MenuOutlinedIcon from '@mui/icons-material/MenuOutlined';
import ShoppingBasketIcon from '@mui/icons-material/ShoppingBasket';
import Brightness4Icon from '@mui/icons-material/Brightness4';
import Brightness7Icon from '@mui/icons-material/Brightness7';
import MenuList from './MenuListComponent';
import Drawer from './DrawerComponent';
import { ColorModeContext } from '../../context/ColorModeContext';

const NavbarComponent = () => {
const [openDrawer, setOpenDrawer] = useState(false);
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down('sm'));
const toggleColorMode = useContext(ColorModeContext);

const handleToggleDrawer = () => {
setOpenDrawer(!openDrawer);
};

const drawerContent = useMemo(
() => (
<List>
<ListItemButton divider onClick={handleToggleDrawer}>
<ListItemIcon>
<ListItemText>Link 1</ListItemText>
</ListItemIcon>
</ListItemButton>
<ListItem divider onClick={handleToggleDrawer}>
<ListItemIcon>
<ListItemText>Link 2</ListItemText>
</ListItemIcon>
</ListItem>
<ListItem divider onClick={handleToggleDrawer}>
<ListItemText>Link 3</ListItemText>
</ListItem>
<ListItem divider>
<IconButton onClick={toggleColorMode}>
<ListItemText>Toggle {theme.palette.mode} mode</ListItemText>
{theme.palette.mode === 'dark' ? (
<Brightness7Icon />
) : (
<Brightness4Icon />
)}
</IconButton>
</ListItem>
</List>
),
[handleToggleDrawer]
);

return (
<AppBar
elevation={2}
sx={{ backgroundColor: 'background.default', position: 'relative' }}
>
<Toolbar>
<Box
component="div"
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
}}
>
{matches ? (
<Drawer
open={openDrawer}
toggleOpen={handleToggleDrawer}
content={drawerContent}
/>
) : (
<Box sx={{ display: 'flex' }}>
<Typography
variant="h6"
sx={{
marginRight: 3,
cursor: 'pointer',
color: 'text.primary',
}}
>
Link 1
</Typography>
<Typography
variant="body1"
sx={{
marginRight: 3,
cursor: 'pointer',
color: 'text.primary',
}}
>
Link 2
</Typography>
<Typography
variant="subtitle1"
sx={{
marginRight: 3,
cursor: 'pointer',
color: 'text.primary',
}}
>
Link 3
</Typography>
</Box>
)}
<Box>
<MenuList />
</Box>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
{matches ? (
<Box>
<IconButton onClick={handleToggleDrawer}>
<MenuOutlinedIcon />
</IconButton>
</Box>
) : (
<Box>
<IconButton>
<Badge badgeContent={3} color="primary">
<ShoppingBasketIcon color="action" />
</Badge>
</IconButton>
<IconButton sx={{ ml: 1 }} onClick={toggleColorMode}>
{theme.palette.mode === 'dark' ? (
<Brightness7Icon />
) : (
<Brightness4Icon />
)}
</IconButton>
</Box>
)}
</Box>
</Box>
</Toolbar>
</AppBar>
);
};

export default NavbarComponent;

+ 35
- 0
src/components/MUI/PopoverComponent.js 查看文件

@@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Box, Popover } from '@mui/material';

const PopoverComponent = ({ open, anchorEl, onClose, content }) => {
const handleClose = () => {
onClose();
};

return (
<Box component="div">
<Popover
sx={{ p: 5 }}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
{content}
</Popover>
</Box>
);
};

PopoverComponent.propTypes = {
anchorEl: PropTypes.object,
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
content: PropTypes.any,
};

export default PopoverComponent;

+ 26
- 0
src/components/Router/PrivateRoute.js 查看文件

@@ -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;

+ 13
- 0
src/components/Section/Section.js 查看文件

@@ -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;

+ 14
- 0
src/constants/keyCodeConstants.js 查看文件

@@ -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;

+ 3
- 0
src/constants/localStorage.js 查看文件

@@ -0,0 +1,3 @@
export const JWT_TOKEN = 'JwtToken';
export const JWT_REFRESH_TOKEN = 'JwtRefreshToken';
export const REFRESH_TOKEN_CONST = 'RefreshToken';

+ 6
- 0
src/constants/pages.js 查看文件

@@ -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
src/constants/sessionStorage.js 查看文件


+ 21
- 0
src/context/ColorModeContext.js 查看文件

@@ -0,0 +1,21 @@
import React, { createContext } from 'react';
import PropTypes from 'prop-types';
import { ThemeProvider } from '@mui/material/styles';
import useToggleColorMode from '../hooks/useToggleColorMode';

export const ColorModeContext = createContext();

const ColorModeProvider = ({ children }) => {
const [toggleColorMode, theme] = useToggleColorMode();
return (
<ColorModeContext.Provider value={toggleColorMode}>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</ColorModeContext.Provider>
);
};

ColorModeProvider.propTypes = {
children: PropTypes.node,
};

export default ColorModeProvider;

+ 63
- 0
src/context/RandomDataContext.js 查看文件

@@ -0,0 +1,63 @@
import React, { createContext, useContext, useState } from 'react';
import PropTypes from 'prop-types';
import usePagingHook from '../hooks/usePagingHook';
import { getRequest } from '../request/jsonServerRequest';

const apiCall = (page, itemsPerPage, sort, sortDirection, filter) =>
getRequest('/items', {
_page: page,
_limit: itemsPerPage,
// Conditionally add to params object if keys exist
...(sort && { _sort: sort }),
...(sortDirection && { _order: sortDirection }),
...(filter && { q: filter }),
});

const Context = createContext();
export const useRandomData = () => useContext(Context);

const RandomDataProvider = ({ children }) => {
const setPage = (page) => {
setState({ ...state, page });
};

const setItemsPerPage = (itemsPerPage) => {
setState({ ...state, itemsPerPage });
};

const setSort = (sort) => {
setState({ ...state, sort });
};

const setFilter = (filter) => {
setState({ ...state, filter });
};

const [state, setState] = useState({
page: 0,
setPage,
itemsPerPage: 12,
setItemsPerPage,
sort: '',
setSort,
filter: '',
setFilter,
});

const data = usePagingHook(
state.page,
state.itemsPerPage,
state.sort,
state.filter,
apiCall
);
return (
<Context.Provider value={{ state, data }}>{children}</Context.Provider>
);
};

RandomDataProvider.propTypes = {
children: PropTypes.node,
};

export default RandomDataProvider;

+ 17
- 0
src/hooks/useDebounceHook.js 查看文件

@@ -0,0 +1,17 @@
import { useEffect, useState } from 'react';

const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);

return () => {
clearTimeout(timer);
};
}, [value, delay]);

return debouncedValue;
};

export default useDebounce;

+ 66
- 0
src/hooks/usePagingHook.js 查看文件

@@ -0,0 +1,66 @@
import { useState, useCallback, useEffect } from 'react';
import { unstable_batchedUpdates } from 'react-dom';

const usePagingHook = (page, itemsPerPage, sort, filter, apiCallback) => {
const [items, setItems] = useState([]);
const [totalPages, setTotalPages] = useState(0);
const [currentPage, setCurrentPage] = useState(0);
const [loading, setLoading] = useState(false);
const [totalCount, setTotalCount] = useState(0);

const reload = useCallback(async () => {
setLoading(true);
try {
const [sortColumn, sortDirection] = sort.split('-');
const response = await apiCallback(
page,
itemsPerPage,
sortColumn,
sortDirection,
filter
);
if (response.status === 200) {
// Prevents multiple rerenders
unstable_batchedUpdates(() => {
setItems(response.data);
setTotalCount(parseInt(response.headers['x-total-count']));
setTotalPages(
Math.ceil(response.headers['x-total-count'] / itemsPerPage)
);
setCurrentPage(page);
});
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}, [
setItems,
setLoading,
setTotalPages,
setCurrentPage,
apiCallback,
page,
itemsPerPage,
sort,
filter,
]);

useEffect(() => {
reload();
}, [reload]);

return {
items,
loading,
reload,
totalCount,
totalPages,
currentPage,
itemsPerPage,
sort,
};
};

export default usePagingHook;

+ 31
- 0
src/hooks/useToggleColorMode.js 查看文件

@@ -0,0 +1,31 @@
import { useState, useMemo } from 'react';
import { createTheme } from '@mui/material/styles';
import {
authScopeSetHelper,
authScopeStringGetHelper,
} from '../util/helpers/authScopeHelpers';

const useToggleColorMode = () => {
const currentColorMode = authScopeStringGetHelper('colorMode') || 'light';
const [mode, setMode] = useState(currentColorMode);

const toggleColorMode = () => {
const nextMode = mode === 'light' ? 'dark' : 'light';
setMode(nextMode);
authScopeSetHelper('colorMode', nextMode);
};

const theme = useMemo(
() =>
createTheme({
palette: {
mode,
},
}),
[mode]
);

return [toggleColorMode, theme];
};

export default useToggleColorMode;

+ 28
- 0
src/i18n/index.js 查看文件

@@ -0,0 +1,28 @@
import { format as formatDate } from 'date-fns';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

import enTranslations from './resources/en';

i18n.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
debug: false,
supportedLngs: ['en'],
resources: {
en: {
translation: enTranslations,
},
},
interpolation: {
format: (value, format) => {
if (value instanceof Date) {
return formatDate(value, format);
}

return value;
},
},
});

export default i18n;

+ 93
- 0
src/i18n/resources/en.js 查看文件

@@ -0,0 +1,93 @@
export default {
app: {
title: 'React template'
},
refresh: {
title: 'Are you active?',
cta:
"You were registered as not active, please confirm that you are active in the next minute, if you don't you will be logged out.",
},
common: {
close: 'Close',
trademark: 'TM',
search: 'Search',
error: 'Error',
continue: 'Continue',
labelUsername: 'Username',
labelPassword: 'Password',
next: 'Next',
nextPage: 'Next page',
previousPage: 'Previous page',
back: 'Back',
goBack: 'Go Back',
ok: 'Ok',
done: 'Done',
confirm: 'Confirm',
printDownload: 'Print/Download',
cancel: 'Cancel',
remove: 'Remove',
invite: 'Invite',
save: 'Save',
complete: 'Complete',
download: 'Download',
yes: 'Yes',
no: 'No',
to: 'to',
select: 'Select...',
none: 'None',
date: {
range: '{{start}} to {{end}}',
},
},
login: {
welcome: 'React template',
dontHaveAccount: "Don't have an account? ",
emailFormat: 'Invalid email address format.',
emailRequired: 'An email or username is required.',
noUsers: 'There are no users with that email.',
passwordStrength: 'Your password is {{strength}}.',
passwordLength: 'Your password contain between 8 and 50 characters.',
signUpRecommendation: 'Sign up',
email: 'Please enter your email address or username to log in:',
logInTitle: 'Log In',
logIn: 'Log In',
signUp: 'Sign Up',
usernameRequired: 'Username is required.',
passwordRequired: 'A Password is required.',
forgotYourPassword: 'Forgot your password?',
forgotPasswordEmail:'Email',
useDifferentEmail: 'Use different email address or username',
},
password: {
weak: 'weak',
average: 'average',
good: 'good',
strong: 'strong',
},
forgotPassword: {
title: 'Forgot Password',
label: 'Send email',
emailRequired: 'An email is required.',
emailFormat: 'Invalid email address format.',
forgotPassword: {
title: 'Forgot Password',
subtitle:
'Please answer the security question to gain access to your account:',
label: 'Reset Password',
},
},
notFound: {
text: "We're sorry but we couldn't find the page you were looking for.",
goBack: 'Go back to homepage',
},
errorPage: {
text:
"We're sorry, an internal server error came up. Please be patient or try again later.",
goBack: 'Go back to homepage',
logout: 'Logout',
},
apiErrors:{
ClientIpAddressIsNullOrEmpty:"Client Ip address is null or empty",
UsernameDoesNotExist: "Username does not exist"
}
};

+ 13
- 0
src/index.css 查看文件

@@ -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;
}

+ 24
- 0
src/index.js 查看文件

@@ -0,0 +1,24 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { HelmetProvider } from 'react-helmet-async';

import './main.scss';
import App from './App';
import store from './store';

import './i18n';
import ColorModeProvider from './context/ColorModeContext';

ReactDOM.render(
<HelmetProvider>
<React.StrictMode>
<Provider store={store}>
<ColorModeProvider>
<App />
</ColorModeProvider>
</Provider>
</React.StrictMode>
</HelmetProvider>,
document.getElementById('root'),
);

+ 1
- 0
src/logo.svg 查看文件

@@ -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>

+ 22
- 0
src/main.scss 查看文件

@@ -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';

+ 19
- 0
src/pages/ErrorPages/ErrorPage.js 查看文件

@@ -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;

+ 29
- 0
src/pages/ErrorPages/NotFoundPage.js 查看文件

@@ -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;

+ 63
- 0
src/pages/ForgotPasswordPage/ForgotPasswordPage.js 查看文件

@@ -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;

+ 96
- 0
src/pages/ForgotPasswordPage/ForgotPasswordPageMUI.js 查看文件

@@ -0,0 +1,96 @@
import React from 'react';
import { useFormik } from 'formik';
import { useTranslation } from 'react-i18next';
import * as Yup from 'yup';
import i18next from 'i18next';
import {
Box,
Container,
Typography,
Button,
TextField,
Link,
Grid,
} from '@mui/material';
import Backdrop from '../../components/MUI/BackdropComponent';
import { LOGIN_PAGE } from '../../constants/pages';
import { NavLink } from 'react-router-dom';

const forgotPasswordValidationSchema = Yup.object().shape({
email: Yup.string()
.required(i18next.t('forgotPassword.emailRequired'))
.email(i18next.t('forgotPassword.emailFormat')),
});

const ForgotPasswordPage = () => {
const { t } = useTranslation();

const handleSubmit = (values) => {
console.log('Values', values);
};

const formik = useFormik({
initialValues: {
email: '',
},
validationSchema: forgotPasswordValidationSchema,
onSubmit: handleSubmit,
validateOnBlur: true,
enableReinitialize: true,
});

return (
<Container component="main" maxWidth="md">
<Box
sx={{
marginTop: 32,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography component="h1" variant="h5">
{t('forgotPassword.title')}
</Typography>
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<Backdrop position="absolute" isLoading={false} />
<TextField
name="email"
label={t('login.forgotPasswordEmail')}
margin="normal"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
autoFocus
fullWidth
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
{t('forgotPassword.label')}
</Button>
<Grid container justifyContent="center">
<Link
to={LOGIN_PAGE}
component={NavLink}
variant="body2"
underline="hover"
>
{t('common.back')}
</Link>
</Grid>
</Box>
</Box>
</Container>
);
};

export default ForgotPasswordPage;

+ 15
- 0
src/pages/HomePage/HomePage.js 查看文件

@@ -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
src/pages/HomePage/HomePageMUI.js 查看文件


部分文件因文件數量過多而無法顯示

Loading…
取消
儲存