瀏覽代碼

refactor: typescript

master
ntasicc 3 年之前
當前提交
d224937db3
共有 100 個文件被更改,包括 4198 次插入0 次删除
  1. 23
    0
      .eslintrc.json
  2. 33
    0
      .gitignore
  3. 4
    0
      .husky/commit-msg
  4. 4
    0
      .husky/pre-commit
  5. 4
    0
      .husky/pre-push
  6. 1
    0
      .npmrc
  7. 1
    0
      .nvmrc
  8. 4
    0
      .prettierignore
  9. 6
    0
      .prettierrc
  10. 13
    0
      .storybook/main.js
  11. 45
    0
      .storybook/preview.js
  12. 30
    0
      .vscode/launch.json
  13. 8
    0
      .vscode/settings.json
  14. 34
    0
      README.md
  15. 10
    0
      assets/images/chevron-down.svg
  16. 12
    0
      assets/images/down.svg
  17. 6
    0
      assets/images/svg/caps-lock.svg
  18. 10
    0
      assets/images/svg/eye-off.svg
  19. 4
    0
      assets/images/svg/eye-on.svg
  20. 10
    0
      assets/images/svg/search.svg
  21. 46
    0
      assets/styles/_base.scss
  22. 7
    0
      assets/styles/_functions.scss
  23. 17
    0
      assets/styles/_layout.scss
  24. 81
    0
      assets/styles/_mixins.scss
  25. 244
    0
      assets/styles/_overwrite.scss
  26. 127
    0
      assets/styles/_reset.scss
  27. 57
    0
      assets/styles/_typography.scss
  28. 39
    0
      assets/styles/_utility.scss
  29. 72
    0
      assets/styles/_variables.scss
  30. 60
    0
      assets/styles/components/_app-button.scss
  31. 45
    0
      assets/styles/components/_auth-card.scss
  32. 23
    0
      assets/styles/components/_auth.scss
  33. 173
    0
      assets/styles/components/_button.scss
  34. 46
    0
      assets/styles/components/_error-page.scss
  35. 23
    0
      assets/styles/components/_forgot-password.scss
  36. 7
    0
      assets/styles/components/_icon-button.scss
  37. 479
    0
      assets/styles/components/_input.scss
  38. 72
    0
      assets/styles/components/_loader.scss
  39. 31
    0
      assets/styles/components/_login-card.scss
  40. 72
    0
      assets/styles/components/_login.scss
  41. 169
    0
      assets/styles/components/_modal.scss
  42. 29
    0
      assets/styles/components/_radio.scss
  43. 50
    0
      commitlint.config.js
  44. 10
    0
      components/cards/data-card/DataCard.mock.ts
  45. 20
    0
      components/cards/data-card/DataCard.stories.jsx
  46. 28
    0
      components/cards/data-card/DataCard.tsx
  47. 7
    0
      components/cards/data-details-card/DataDetailsCard.mock.ts
  48. 20
    0
      components/cards/data-details-card/DataDetailsCard.stories.jsx
  49. 53
    0
      components/cards/data-details-card/DataDetailsCard.tsx
  50. 5
    0
      components/cards/hover-image-card/HoverImageCard.mock.ts
  51. 20
    0
      components/cards/hover-image-card/HoverImageCard.stories.jsx
  52. 37
    0
      components/cards/hover-image-card/HoverImageCard.tsx
  53. 75
    0
      components/cards/hover-image-card/hover-image-card.module.css
  54. 7
    0
      components/cards/profile-card/ProfileCard.mock.ts
  55. 20
    0
      components/cards/profile-card/ProfileCard.stories.jsx
  56. 35
    0
      components/cards/profile-card/ProfileCard.tsx
  57. 5
    0
      components/forms/contact/ContactForm.mcok.ts
  58. 20
    0
      components/forms/contact/ContactForm.stories.jsx
  59. 123
    0
      components/forms/contact/ContactForm.tsx
  60. 5
    0
      components/forms/forgot-password/ForgotPasswordForm.mock.ts
  61. 20
    0
      components/forms/forgot-password/ForgotPasswordForm.stories.jsx
  62. 82
    0
      components/forms/forgot-password/ForgotPasswordForm.tsx
  63. 5
    0
      components/forms/login/LoginForm.mock.ts
  64. 20
    0
      components/forms/login/LoginForm.stories.jsx
  65. 148
    0
      components/forms/login/LoginForm.tsx
  66. 5
    0
      components/forms/register/RegisterForm.mock.ts
  67. 20
    0
      components/forms/register/RegisterForm.stories.jsx
  68. 204
    0
      components/forms/register/RegisterForm.tsx
  69. 7
    0
      components/layout/base-layout/Layout.mock.js
  70. 20
    0
      components/layout/base-layout/Layout.stories.jsx
  71. 16
    0
      components/layout/base-layout/Layout.tsx
  72. 5
    0
      components/layout/navbar/Navbar.mock.js
  73. 20
    0
      components/layout/navbar/Navbar.stories.jsx
  74. 208
    0
      components/layout/navbar/Navbar.tsx
  75. 5
    0
      components/loader/route-loader/CircularIndeterminate.mock.ts
  76. 20
    0
      components/loader/route-loader/CircularIndeterminate.stories.jsx
  77. 57
    0
      components/loader/route-loader/CircularIndeterminate.tsx
  78. 14
    0
      components/mui/ErrorMessageComponent.tsx
  79. 50
    0
      components/pagination/filter-sort/FilterSortComponent.jsx
  80. 10
    0
      components/pagination/filter-sort/FilterSortComponent.mock.js
  81. 20
    0
      components/pagination/filter-sort/FilterSortComponent.stories.jsx
  82. 5
    0
      components/pagination/react-query/PaginationComponentRQ.mock.ts
  83. 20
    0
      components/pagination/react-query/PaginationComponentRQ.stories.jsx
  84. 127
    0
      components/pagination/react-query/PaginationComponentRQ.tsx
  85. 5
    0
      components/pagination/swr/PaginationComponentSWR.mock.ts
  86. 20
    0
      components/pagination/swr/PaginationComponentSWR.stories.jsx
  87. 116
    0
      components/pagination/swr/PaginationComponentSWR.tsx
  88. 7
    0
      components/templates/base/BaseTemplate.mock.js
  89. 2
    0
      components/templates/base/BaseTemplate.module.css
  90. 20
    0
      components/templates/base/BaseTemplate.stories.jsx
  91. 11
    0
      components/templates/base/BaseTemplate.tsx
  92. 6
    0
      constants/pages.ts
  93. 17
    0
      hooks/use-debounce.ts
  94. 10
    0
      hooks/use-pagination.ts
  95. 23
    0
      hooks/use-swr-with-initial-data.ts
  96. 34
    0
      models/person.ts
  97. 87
    0
      models/user.ts
  98. 5
    0
      next-env.d.ts
  99. 6
    0
      next-i18next.config.js
  100. 0
    0
      next.config.js

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

@@ -0,0 +1,23 @@
{
"extends": [
"plugin:storybook/recommended",
"next",
"next/core-web-vitals",
"eslint:recommended"
],
"globals": {
"React": "readonly"
},
"overrides": [
{
"files": ["*.stories.@(ts|tsx|js|jsx|mjs|cjs)"],
"rules": {
// example of overriding a rule
"storybook/hierarchy-separator": "error"
}
}
],
"rules": {
"no-unused-vars": [1, { "args": "after-used", "argsIgnorePattern": "^_" }]
}
}

+ 33
- 0
.gitignore 查看文件

@@ -0,0 +1,33 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local
.env

# vercel
.vercel

+ 4
- 0
.husky/commit-msg 查看文件

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no -- commitlint --edit $1

+ 4
- 0
.husky/pre-commit 查看文件

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn lint

+ 4
- 0
.husky/pre-push 查看文件

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn build

+ 1
- 0
.npmrc 查看文件

@@ -0,0 +1 @@
engine-strict=true

+ 1
- 0
.nvmrc 查看文件

@@ -0,0 +1 @@
lts/fermium

+ 4
- 0
.prettierignore 查看文件

@@ -0,0 +1,4 @@
.yarn
.next
dist
node_modules

+ 6
- 0
.prettierrc 查看文件

@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}

+ 13
- 0
.storybook/main.js 查看文件

@@ -0,0 +1,13 @@
module.exports = {
stories: ['../**/*.stories.mdx', '../**/*.stories.@(js|jsx|ts|tsx)'],
staticDirs: ['../public'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: '@storybook/react',
core: {
builder: '@storybook/builder-webpack5',
},
};

+ 45
- 0
.storybook/preview.js 查看文件

@@ -0,0 +1,45 @@
import * as NextImage from 'next/image';
import '../styles/globals.css';

const BREAKPOINTS_INT = {
xs: 375,
sm: 600,
md: 900,
lg: 1200,
xl: 1536,
};

const customViewports = Object.fromEntries(
Object.entries(BREAKPOINTS_INT).map(([key, val], idx) => {
console.log(val);
return [
key,
{
name: key,
styles: {
width: `${val}px`,
height: `${(idx + 5) * 10}vh`,
},
},
];
})
);

// Allow Storybook to handle Next's <Image> component
const OriginalNextImage = NextImage.default;

Object.defineProperty(NextImage, 'default', {
configurable: true,
value: (props) => <OriginalNextImage {...props} unoptimized />,
});

export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
viewport: { viewports: customViewports },
};

+ 30
- 0
.vscode/launch.json 查看文件

@@ -0,0 +1,30 @@
{
"version": "0.1.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "yarn dev"
},
{
"name": "Next.js: debug client-side",
"type": "pwa-chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "yarn dev",
"console": "integratedTerminal",
"serverReadyAction": {
"pattern": "started server on .+, url: (https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
}
}
],
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"]
}

+ 8
- 0
.vscode/settings.json 查看文件

@@ -0,0 +1,8 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": true
}
}

+ 34
- 0
README.md 查看文件

@@ -0,0 +1,34 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.

[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.

The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

+ 10
- 0
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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;
}

+ 50
- 0
commitlint.config.js 查看文件

@@ -0,0 +1,50 @@
// build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
// ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
// docs: Documentation only changes
// feat: A new feature
// fix: A bug fix
// perf: A code change that improves performance
// refactor: A code change that neither fixes a bug nor adds a feature
// style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
// test: Adding missing tests or correcting existing tests

module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100],
'header-max-length': [2, 'always', 100],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [
2,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',
'translation',
'security',
'changeset',
],
],
},
};

+ 10
- 0
components/cards/data-card/DataCard.mock.ts 查看文件

@@ -0,0 +1,10 @@
const base = {
data: { name: 'John Doe', age: 30, gender: 'male' },
t: (text: string) => {
return text;
},
};

export const mockDataCardProps = {
base,
};

+ 20
- 0
components/cards/data-card/DataCard.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import DataCard from './DataCard';
import { mockDataCardProps } from './DataCard.mock';

const obj = {
title: 'cards/DataCard',
component: DataCard,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <DataCard {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockDataCardProps.base,
};

+ 28
- 0
components/cards/data-card/DataCard.tsx 查看文件

@@ -0,0 +1,28 @@
import { Divider, Paper, Typography } from '@mui/material';

interface IProps {
data: {
name: string;
gender: string;
age: number;
};
t: (x: string) => string;
}

const DataCard: React.FC<IProps> = ({ data, t }) => {
return (
<Paper sx={{ p: 3, height: '100%' }} elevation={3}>
<Typography sx={{ fontWeight: 600 }}>{t('Name')}</Typography>
<Typography display="inline"> {data.name}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>{t('Age')}</Typography>
<Typography display="inline"> {data.age}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>{t('Gender')}</Typography>
<Typography display="inline"> {data.gender}</Typography>
<Divider />
</Paper>
);
};

export default DataCard;

+ 7
- 0
components/cards/data-details-card/DataDetailsCard.mock.ts 查看文件

@@ -0,0 +1,7 @@
const base = {
profileData: { name: 'John Doe' },
};

export const mockDataDetailsCardProps = {
base,
};

+ 20
- 0
components/cards/data-details-card/DataDetailsCard.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import DataDetailsCard from './DataDetailsCard';
import { mockDataDetailsCardProps } from './ProfileCard.mock';

const obj = {
title: 'cards/DataDetailsCard',
component: DataDetailsCard,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <DataDetailsCard {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockDataDetailsCardProps.base,
};

+ 53
- 0
components/cards/data-details-card/DataDetailsCard.tsx 查看文件

@@ -0,0 +1,53 @@
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
import Image from 'next/image';

interface IProps {
data: {
name: string;
age: number;
};
}
const DataDetailsCard: React.FC<IProps> = ({ data }) => {
return (
<Card
sx={{
maxWidth: 600,
height: 200,
marginX: 'auto',
marginY: 20,
boxShadow: 10,
display: 'flex',
}}
>
<Image
src="https://www.business2community.com/wp-content/uploads/2017/08/blank-profile-picture-973460_640.png"
alt="profile picture"
width={600}
height={500}
/>
<CardContent>
<Typography
gutterBottom
variant="h5"
component="div"
sx={{
textAlign: 'center',
marginTop: 1,
marginBottom: 3,
}}
>
{data.name}, {data.age}
</Typography>
<Typography variant="body2" color="text.secondary">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur
quis odio in libero fringilla pellentesque aliquet et mi. Quisque
maximus lectus a neque luctus, tempus auctor ipsum ultrices.
</Typography>
</CardContent>
</Card>
);
};

export default DataDetailsCard;

+ 5
- 0
components/cards/hover-image-card/HoverImageCard.mock.ts 查看文件

@@ -0,0 +1,5 @@
const base = {};

export const mockHoverImageCardProps = {
base,
};

+ 20
- 0
components/cards/hover-image-card/HoverImageCard.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import HoverImageCard from './HoverImageCard';
import { mockHoverImageCardProps } from './ProfileCard.mock';

const obj = {
title: 'cards/HoverImageCard',
component: HoverImageCard,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <HoverImageCard {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockHoverImageCardProps.base,
};

+ 37
- 0
components/cards/hover-image-card/HoverImageCard.tsx 查看文件

@@ -0,0 +1,37 @@
import styles from './hover-image-card.module.css';

const HoverImageCard = () => {
return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.content}>
<p>Next JS Path</p>
<p>18-8-2022</p>
<button className={styles.btn}>More Details</button>
</div>
{/*Change with Next Image*/}
<img src="/images/image-one.jpg" alt="text" />
</div>
<div className={styles.card}>
<div className={styles.content}>
<p>Text 1</p>
<p>Text 2</p>
<button className={styles.btn}>Button Text</button>
</div>
{/*Change with Next Image*/}
<img src="/images/image-one.jpg" alt="text" />
</div>
<div className={styles.card}>
<div className={styles.content}>
<p>Text 1</p>
<p>Text 2</p>
<button className={styles.btn}>Button Text</button>
</div>
{/*Change with Next Image*/}
<img src="/images/image-one.jpg" alt="text" />
</div>
</div>
);
};

export default HoverImageCard;

+ 75
- 0
components/cards/hover-image-card/hover-image-card.module.css 查看文件

@@ -0,0 +1,75 @@
.container {
display: flex;
justify-content: center;
margin-top: 30px;
}

.card {
position: relative;
width: 230px;
height: 260px;
margin: 0 5px;
background-color: red;
transition: 0.3s;
overflow: hidden;
cursor: pointer;
}

.card img {
width: 100%;
height: 100%;
object-fit: cover;
transition: 0.3s;
}

.card::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: 0.3s;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1));
}

.content {
position: absolute;
bottom: 0;
width: 100%;
padding: 1rem;
z-index: 1;
color: #fff;
transition: 0.3s;
opacity: 0;
}

.btn {
padding: 0.3rem 0.8rem;
font-size: 0.6rem;
border: none;
cursor: pointer;
outline: none;
color: #fff;
background: transparent;
border: 2px solid #fff;
}

.content p {
font-size: 0.6rem;
margin: 0.5rem 0;
}

.card:hover {
width: 350px;
}

.card:hover img {
transform: scale(1.1);
}

.card:hover:after,
.card:hover .content {
opacity: 1;
}

+ 7
- 0
components/cards/profile-card/ProfileCard.mock.ts 查看文件

@@ -0,0 +1,7 @@
const base = {
profileData: { name: 'John Doe' },
};

export const mockProfilePageProps = {
base,
};

+ 20
- 0
components/cards/profile-card/ProfileCard.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import ProfileCard from './ProfileCard';
import { mockProfilePageProps } from './ProfileCard.mock';

const obj = {
title: 'cards/ProfileCard',
component: ProfileCard,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <ProfileCard {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockProfilePageProps.base,
};

+ 35
- 0
components/cards/profile-card/ProfileCard.tsx 查看文件

@@ -0,0 +1,35 @@
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
import Image from 'next/image';

interface IProps {
profileData: {
name: string;
};
}

const ProfileCard: React.FC<IProps> = ({ profileData }) => {
return (
<Card sx={{ maxWidth: 345, marginX: 'auto', marginY: 10, boxShadow: 10 }}>
<Image
src="https://www.business2community.com/wp-content/uploads/2017/08/blank-profile-picture-973460_640.png"
alt="profile picture"
width={600}
height={500}
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{profileData.name}
</Typography>
<Typography variant="body2" color="text.secondary">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur
quis odio in libero fringilla pellentesque aliquet et mi. Quisque
maximus lectus a neque luctus, tempus auctor ipsum ultrices.
</Typography>
</CardContent>
</Card>
);
};

export default ProfileCard;

+ 5
- 0
components/forms/contact/ContactForm.mcok.ts 查看文件

@@ -0,0 +1,5 @@
const base = {};

export const mockContactFormProps = {
base,
};

+ 20
- 0
components/forms/contact/ContactForm.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import ContactForm from './ContactForm';
import { mockContactFormProps } from './ContactForm.mock';

const obj = {
title: 'forms/ContactForm',
component: ContactForm,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <ContactForm {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockContactFormProps.base,
};

+ 123
- 0
components/forms/contact/ContactForm.tsx 查看文件

@@ -0,0 +1,123 @@
import {
Box,
Button,
Container,
Grid,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import { BASE_PAGE } from '../../../constants/pages';
import { contactSchema } from '../../../schemas/contactSchema';

interface FormValues {
firstName: string;
lastName: string;
email: string;
message: string;
}

const ContactForm = () => {
const { t } = useTranslation(['forms', 'contact', 'common']);

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

const formik = useFormik({
initialValues: {
firstName: '',
lastName: '',
email: '',
message: '',
},
validationSchema: contactSchema,
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('contact:Title')}
</Typography>
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="firstName"
label={t('forms:FirstName')}
margin="normal"
value={formik.values.firstName}
onChange={formik.handleChange}
error={formik.touched.firstName && Boolean(formik.errors.firstName)}
helperText={formik.touched.firstName && formik.errors.firstName}
autoFocus
fullWidth
/>
<TextField
name="lastName"
label={t('forms:LastName')}
margin="normal"
value={formik.values.lastName}
onChange={formik.handleChange}
error={formik.touched.lastName && Boolean(formik.errors.lastName)}
helperText={formik.touched.lastName && formik.errors.lastName}
autoFocus
fullWidth
/>
<TextField
name="email"
label={t('forms:Email')}
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
/>
<TextField
name="message"
label={t('forms:Message')}
multiline
margin="normal"
value={formik.values.message}
onChange={formik.handleChange}
error={formik.touched.message && Boolean(formik.errors.message)}
helperText={formik.touched.message && formik.errors.message}
rows={4}
autoFocus
fullWidth
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
{t('contact:SendBtn')}
</Button>
<Grid container justifyContent="center">
<Link href={BASE_PAGE}>{t('common:Back')}</Link>
</Grid>
</Box>
</Box>
</Container>
);
};

export default ContactForm;

+ 5
- 0
components/forms/forgot-password/ForgotPasswordForm.mock.ts 查看文件

@@ -0,0 +1,5 @@
const base = {};

export const mockForgotPasswordFormProps = {
base,
};

+ 20
- 0
components/forms/forgot-password/ForgotPasswordForm.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import ForgotPasswordForm from './ForgotPasswordForm';
import { mockForgotPasswordFormProps } from './ForgotPasswordForm.mock';

const obj = {
title: 'forms/ForgotPasswordForm',
component: ForgotPasswordForm,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <ForgotPasswordForm {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockForgotPasswordFormProps.base,
};

+ 82
- 0
components/forms/forgot-password/ForgotPasswordForm.tsx 查看文件

@@ -0,0 +1,82 @@
import {
Box,
Button,
Container,
Grid,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import { LOGIN_PAGE } from '../../../constants/pages';
import { forgotPasswordSchema } from '../../../schemas/forgotPasswordSchema';

interface FormValues {
email: string;
}

const ForgotPasswordForm = () => {
const { t } = useTranslation(['forms', 'forgotPass', 'common']);

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

const formik = useFormik({
initialValues: {
email: '',
},
validationSchema: forgotPasswordSchema,
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('forgotPass:Title')}
</Typography>
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="email"
label={t('forms:Email')}
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('forgotPass:SendBtn')}
</Button>
<Grid container justifyContent="center">
<Link href={LOGIN_PAGE}>{t('common:Back')}</Link>
</Grid>
</Box>
</Box>
</Container>
);
};

export default ForgotPasswordForm;

+ 5
- 0
components/forms/login/LoginForm.mock.ts 查看文件

@@ -0,0 +1,5 @@
const base = {};

export const mockLoginFormProps = {
base,
};

+ 20
- 0
components/forms/login/LoginForm.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import LoginForm from './LoginForm';
import { mockLoginFormProps } from './LoginForm.mock';

const obj = {
title: 'forms/LoginForm',
component: LoginForm,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <LoginForm {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockLoginFormProps.base,
};

+ 148
- 0
components/forms/login/LoginForm.tsx 查看文件

@@ -0,0 +1,148 @@
import {
Box,
Button,
Container,
Grid,
IconButton,
InputAdornment,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import { signIn } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import {
BASE_PAGE,
FORGOT_PASSWORD_PAGE,
REGISTER_PAGE,
} from '../../../constants/pages';
import { loginSchema } from '../../../schemas/loginSchema';
import ErrorMessageComponent from '../../mui/ErrorMessageComponent';

interface FormValues {
username: string;
password: string;
}

const LoginForm = () => {
const { t } = useTranslation(['forms', 'login']);
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword(!showPassword);
const handleMouseDownPassword = () => setShowPassword(!showPassword);

const router = useRouter();
const [error, setError] = useState({ hasError: false, errorMessage: '' });

const submitHandler = async (values: FormValues) => {
const result = await signIn('credentials', {
redirect: false,
username: values.username,
password: values.password,
});
if (!result?.error) {
router.replace(BASE_PAGE);
} else {
setError({ hasError: true, errorMessage: result.error });
}
};

const formik = useFormik({
initialValues: {
username: '',
password: '',
},
validationSchema: loginSchema,
onSubmit: submitHandler,
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('login:Title')}
</Typography>
{error.hasError && <ErrorMessageComponent error={error.errorMessage} />}
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="username"
label={t('forms:Username')}
margin="normal"
value={formik.values.username}
onChange={formik.handleChange}
error={formik.touched.username && Boolean(formik.errors.username)}
helperText={formik.touched.username && formik.errors.username}
autoFocus
fullWidth
/>
<TextField
name="password"
label={t('forms:Password')}
margin="normal"
type={showPassword ? 'text' : 'password'}
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
></IconButton>
</InputAdornment>
),
}}
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
{t('login:LoginBtn')}
</Button>
<Grid container>
<Grid
item
xs={12}
md={6}
sx={{ textAlign: { xs: 'center', md: 'left' } }}
>
<Link href={FORGOT_PASSWORD_PAGE}>
{t('login:ForgotPassword')}
</Link>
</Grid>
<Grid
item
xs={12}
md={6}
sx={{ textAlign: { xs: 'center', md: 'right' } }}
>
<Link href={REGISTER_PAGE}>{t('login:NoAccount')}</Link>
</Grid>
</Grid>
</Box>
</Box>
</Container>
);
};

export default LoginForm;

+ 5
- 0
components/forms/register/RegisterForm.mock.ts 查看文件

@@ -0,0 +1,5 @@
const base = {};

export const mockRegisterFormProps = {
base,
};

+ 20
- 0
components/forms/register/RegisterForm.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import RegisterForm from './RegisterForm';
import { mockRegisterFormProps } from './RegisterForm.mock';

const obj = {
title: 'forms/RegisterForm',
component: RegisterForm,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <RegisterForm {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockRegisterFormProps.base,
};

+ 204
- 0
components/forms/register/RegisterForm.tsx 查看文件

@@ -0,0 +1,204 @@
import {
Box,
Button,
Container,
Grid,
IconButton,
InputAdornment,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import { useState } from 'react';

import { FORGOT_PASSWORD_PAGE, LOGIN_PAGE } from '../../../constants/pages';
import { createUser } from '../../../requests/accountRequests';
import { registerSchema } from '../../../schemas/registerSchema';
import ErrorMessageComponent from '../../mui/ErrorMessageComponent';

interface FormValues {
fullName: string;
username: string;
email: string;
password: string;
confirmPassword: string;
}

const RegisterForm = () => {
const { t } = useTranslation(['forms', 'register']);

const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword(!showPassword);
const handleMouseDownPassword = () => setShowPassword(!showPassword);

const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const handleClickShowConfirmPassword = () =>
setShowConfirmPassword(!showConfirmPassword);
const handleMouseDownConfirmPassword = () =>
setShowConfirmPassword(!showConfirmPassword);

const [error, setError] = useState({ hasError: false, errorMessage: '' });

const submitHandler = async (values: FormValues) => {
try {
const result = await createUser(
values.fullName,
values.username,
values.email,
values.password
);
console.log(result);
} catch (error) {
if (error instanceof Error)
setError({ hasError: true, errorMessage: error.message });
}
};

const formik = useFormik({
initialValues: {
fullName: '',
username: '',
email: '',
password: '',
confirmPassword: '',
},
validationSchema: registerSchema,
onSubmit: submitHandler,
validateOnBlur: true,
enableReinitialize: true,
});

return (
<Container component="main" maxWidth="md">
<Box
sx={{
marginTop: 10,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography component="h1" variant="h5">
{t('register:Title')}
</Typography>
{error.hasError && <ErrorMessageComponent error={error.errorMessage} />}
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="fullName"
label={t('forms:FullName')}
margin="normal"
value={formik.values.fullName}
onChange={formik.handleChange}
error={formik.touched.fullName && Boolean(formik.errors.fullName)}
helperText={formik.touched.fullName && formik.errors.fullName}
autoFocus
fullWidth
/>
<TextField
name="username"
label={t('forms:Username')}
margin="normal"
value={formik.values.username}
onChange={formik.handleChange}
error={formik.touched.username && Boolean(formik.errors.username)}
helperText={formik.touched.username && formik.errors.username}
fullWidth
/>
<TextField
name="email"
label={t('forms:Email')}
margin="normal"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
fullWidth
/>
<TextField
name="password"
label={t('forms:Password')}
margin="normal"
type={showPassword ? 'text' : 'password'}
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
></IconButton>
</InputAdornment>
),
}}
/>
<TextField
name="confirmPassword"
label={t('forms:ConfirmPassword')}
margin="normal"
type={showPassword ? 'text' : 'password'}
value={formik.values.confirmPassword}
onChange={formik.handleChange}
error={
formik.touched.confirmPassword &&
Boolean(formik.errors.confirmPassword)
}
helperText={
formik.touched.confirmPassword && formik.errors.confirmPassword
}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowConfirmPassword}
onMouseDown={handleMouseDownConfirmPassword}
></IconButton>
</InputAdornment>
),
}}
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
{t('register:RegisterBtn')}
</Button>
<Grid container>
<Grid
item
xs={12}
md={6}
sx={{ textAlign: { xs: 'center', md: 'left' } }}
>
<Link href={FORGOT_PASSWORD_PAGE}>
{t('register:ForgotPassword')}
</Link>
</Grid>
<Grid
item
xs={12}
md={6}
sx={{ textAlign: { xs: 'center', md: 'right' } }}
>
<Link href={LOGIN_PAGE}>{t('register:HaveAccount')}</Link>
</Grid>
</Grid>
</Box>
</Box>
</Container>
);
};

export default RegisterForm;

+ 7
- 0
components/layout/base-layout/Layout.mock.js 查看文件

@@ -0,0 +1,7 @@
const base = {
children: <h1>Test</h1>,
};

export const mockLayoutProps = {
base,
};

+ 20
- 0
components/layout/base-layout/Layout.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import Layout from './Layout';
import { mockLayoutProps } from './Layout.mock';

const obj = {
title: 'layout/Navbar',
component: Layout,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <Layout {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockLayoutProps.base,
};

+ 16
- 0
components/layout/base-layout/Layout.tsx 查看文件

@@ -0,0 +1,16 @@
import Navbar from '../navbar/Navbar';

interface IProps {
children: React.ReactNode;
}

const Layout: React.FC<IProps> = (props) => {
return (
<>
<Navbar />
<main>{props.children}</main>
</>
);
};

export default Layout;

+ 5
- 0
components/layout/navbar/Navbar.mock.js 查看文件

@@ -0,0 +1,5 @@
const base = {};

export const mockNavbarProps = {
base,
};

+ 20
- 0
components/layout/navbar/Navbar.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import Navbar from './Navbar';
import { mockNavbarProps } from './Navbar.mock';

const obj = {
title: 'layout/Navbar',
component: Navbar,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <Navbar {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockNavbarProps.base,
};

+ 208
- 0
components/layout/navbar/Navbar.tsx 查看文件

@@ -0,0 +1,208 @@
import AdbIcon from '@mui/icons-material/Adb';
import MenuIcon from '@mui/icons-material/Menu';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Container from '@mui/material/Container';
import IconButton from '@mui/material/IconButton';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Toolbar from '@mui/material/Toolbar';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import { signOut, useSession } from 'next-auth/react';
import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';
import { LOGIN_PAGE, PROFILE_PAGE } from '../../../constants/pages';

const pages = ['Link 1', 'Link 2', 'Link 3', 'Link4'];

const Navbar = () => {
const { data: session } = useSession();
const [anchorElNav, setAnchorElNav] = useState(null);
const [anchorElUser, setAnchorElUser] = useState(null);

const handleOpenNavMenu = (event: any) => {
setAnchorElNav(event.currentTarget);
};
const handleOpenUserMenu = (event: any) => {
setAnchorElUser(event.currentTarget);
};

const handleCloseNavMenu = () => {
setAnchorElNav(null);
};

const handleCloseUserMenu = () => {
setAnchorElUser(null);
};

function logoutHandler() {
signOut();
}

return (
<AppBar
position="static"
sx={{ zIndex: 100, position: 'fixed', top: 0, left: 0 }}
>
<Container maxWidth="xl">
<Toolbar disableGutters>
<AdbIcon sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} />
<Typography
variant="h6"
noWrap
sx={{
mr: 2,
display: { xs: 'none', md: 'flex' },
fontFamily: 'monospace',
fontWeight: 700,
letterSpacing: '.3rem',
color: 'inherit',
textDecoration: 'none',
}}
>
LOGO
</Typography>

<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }}>
<IconButton
size="large"
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleOpenNavMenu}
color="inherit"
>
<MenuIcon />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorElNav}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
open={Boolean(anchorElNav)}
onClose={handleCloseNavMenu}
sx={{
display: { xs: 'block', md: 'none' },
}}
>
{pages.map((page) => (
<MenuItem key={page} onClick={handleCloseNavMenu}>
<Typography textAlign="center">{page}</Typography>
</MenuItem>
))}
</Menu>
</Box>
<AdbIcon sx={{ display: { xs: 'flex', md: 'none' }, mr: 1 }} />
<Typography
variant="h5"
noWrap
component="a"
href=""
sx={{
mr: 2,
display: { xs: 'flex', md: 'none' },
flexGrow: 1,
fontFamily: 'monospace',
fontWeight: 700,
letterSpacing: '.3rem',
color: 'inherit',
textDecoration: 'none',
}}
>
LOGO
</Typography>
<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
{pages.map((page) => (
<Button
key={page}
onClick={handleCloseNavMenu}
sx={{ my: 2, color: 'white', display: 'block' }}
>
{page}
</Button>
))}
</Box>

<Box sx={{ flexGrow: 0 }}>
{session ? (
<>
<Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Image
src="https://www.business2community.com/wp-content/uploads/2017/08/blank-profile-picture-973460_640.png"
alt="profile picture"
width={40}
height={40}
style={{ borderRadius: '50%' }}
/>
</IconButton>
</Tooltip>
<Menu
sx={{ mt: '45px' }}
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
<MenuItem onClick={handleCloseUserMenu}>
<Link href={PROFILE_PAGE}>
<a
style={{
textDecoration: 'none',
color: 'inherit',
fontSize: 15,
marginLeft: 6,
}}
>
PROFILE
</a>
</Link>
</MenuItem>
<MenuItem onClick={handleCloseUserMenu}>
<Button color="inherit" onClick={logoutHandler}>
Logout
</Button>
</MenuItem>
</Menu>
</>
) : (
<Button color="inherit">
<Link href={LOGIN_PAGE}>
<a
style={{
textDecoration: 'none',
color: 'inherit',
fontSize: 17,
}}
>
Login
</a>
</Link>
</Button>
)}
</Box>
</Toolbar>
</Container>
</AppBar>
);
};
export default Navbar;

+ 5
- 0
components/loader/route-loader/CircularIndeterminate.mock.ts 查看文件

@@ -0,0 +1,5 @@
const base = {};

export const mockCircularIndeterminateProps = {
base,
};

+ 20
- 0
components/loader/route-loader/CircularIndeterminate.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import CircularIndeterminate from './CircularIndeterminate';
import { mockCircularIndeterminateProps } from './CircularIndeterminate.mock';

const obj = {
title: 'laoader/CircularIndeterminate',
component: CircularIndeterminate,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <CircularIndeterminate {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockCircularIndeterminateProps.base,
};

+ 57
- 0
components/loader/route-loader/CircularIndeterminate.tsx 查看文件

@@ -0,0 +1,57 @@
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';

const CircularIndeterminate = () => {
const router = useRouter();

const [loading, setLoading] = useState(false);

useEffect(() => {
const handleStart = (url: string) =>
url !== router.asPath && setLoading(true);
const handleComplete = (url: string) =>
url === router.asPath && setLoading(false);

router.events.on('routeChangeStart', handleStart);
router.events.on('routeChangeComplete', handleComplete);
router.events.on('routeChangeError', handleComplete);

return () => {
router.events.off('routeChangeStart', handleStart);
router.events.off('routeChangeComplete', handleComplete);
router.events.off('routeChangeError', handleComplete);
};
});
return (
loading && (
<Box
sx={{
display: 'flex',
zIndex: 99,
height: '100vh',
width: '100vw',
justifyContent: 'center',
alignItems: 'center',
position: 'fixed',
top: 0,
left: 0,
}}
>
<Box
sx={{
position: 'absolute',
top: '48%',
left: '48%',
marginX: 'auto',
}}
>
<CircularProgress color="inherit" size={60} thickness={4} />
</Box>
</Box>
)
);
};

export default CircularIndeterminate;

+ 14
- 0
components/mui/ErrorMessageComponent.tsx 查看文件

@@ -0,0 +1,14 @@
import { Typography } from '@mui/material';
import React from 'react';

interface IProps {
error: string;
}

const ErrorMessageComponent: React.FC<IProps> = ({ error }) => (
<Typography variant="body1" color="error" my={2}>
{error}
</Typography>
);

export default ErrorMessageComponent;

+ 50
- 0
components/pagination/filter-sort/FilterSortComponent.jsx 查看文件

@@ -0,0 +1,50 @@
import {
FormControl,
InputLabel,
MenuItem,
Select,
TextField,
} from '@mui/material';
import PropType from 'prop-types';

const FilterSortComponent = ({
sort,
handleSortChange,
filter,
handleFilterChange,
}) => {
return (
<>
<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="asc">Name - A-Z</MenuItem>
<MenuItem value="desc">Name - Z-A</MenuItem>
</Select>
</FormControl>
<TextField
sx={{ flexGrow: 1 }}
variant="outlined"
label="Filter"
placeholder="Filter"
value={filter}
onChange={handleFilterChange}
/>
</>
);
};

FilterSortComponent.propTypes = {
sort: PropType.string,
handleSortChange: PropType.func,
filter: PropType.string,
handleFilterChange: PropType.func,
};

export default FilterSortComponent;

+ 10
- 0
components/pagination/filter-sort/FilterSortComponent.mock.js 查看文件

@@ -0,0 +1,10 @@
const base = {
sort: '',
handleSortChange: () => {},
filter: '',
handleFilterChange: () => {},
};

export const mockFilterSortComponentProps = {
base,
};

+ 20
- 0
components/pagination/filter-sort/FilterSortComponent.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import FilterSortComponent from './FilterSortComponent';
import { mockFilterSortComponentProps } from './FilterSortComponent.mock';

const obj = {
title: 'pagination/FilterSortComponent',
component: FilterSortComponent,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <FilterSortComponent {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockFilterSortComponentProps.base,
};

+ 5
- 0
components/pagination/react-query/PaginationComponentRQ.mock.ts 查看文件

@@ -0,0 +1,5 @@
const base = {};

export const mockPaginationComponentQRProps = {
base,
};

+ 20
- 0
components/pagination/react-query/PaginationComponentRQ.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import PaginationComponentRQ from './PaginationComponentRQ';
import { mockPaginationComponentQRProps } from './PaginationComponentRQ.mock';

const obj = {
title: 'pagination/PaginationComponentRQ',
component: PaginationComponentRQ,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <PaginationComponentRQ {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockPaginationComponentQRProps.base,
};

+ 127
- 0
components/pagination/react-query/PaginationComponentRQ.tsx 查看文件

@@ -0,0 +1,127 @@
import { Box, Button, Grid, Paper, Typography } from '@mui/material';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { SINGLE_DATA_PAGE } from '../../../constants/pages';
import useDebounce from '../../../hooks/use-debounce';
import { usePagination } from '../../../hooks/use-pagination';
import { compare } from '../../../utils/helpers/sortHelpers';
import DataCard from '../../cards/data-card/DataCard';
import FilterSortComponent from '../filter-sort/FilterSortComponent';

const PaginationComponentRQ = () => {
const [pageIndex, setPageIndex] = useState(1);
const [filter, setFilter] = useState('');
const [sort, setSort] = useState('');
const { t } = useTranslation('pagination');
const { data: paginationData } = usePagination(pageIndex.toString());
const router = useRouter();
const debouncedFilter = useDebounce(filter, 500);

const handleFilterChange = (event: { target: HTMLInputElement }) => {
const filterText = event.target.value;
setFilter(filterText);
};

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

const loadSingleDataHandler = (id: string) => {
router.push(`${SINGLE_DATA_PAGE}${id}`);
};

const dataToDisplay = paginationData?.data
.filter((item) =>
item.name.toLowerCase().startsWith(debouncedFilter.toLowerCase())
)
.sort((a, b) => compare(a.name, b.name, sort))
.map((item, 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}
onClick={loadSingleDataHandler.bind(null, item.customID)}
>
<DataCard data={item} t={t} />
</Grid>
));

return (
<Paper
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'start',
py: 2,
minHeight: 400,
marginTop: 5,
}}
elevation={5}
>
<Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center">
{t('Title')}
</Typography>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'wrap',
mx: 2,
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
width: '100%',
}}
>
<FilterSortComponent
sort={sort}
handleSortChange={handleSortChange}
filter={filter}
handleFilterChange={handleFilterChange}
/>
</Box>
</Box>
<Grid container>{dataToDisplay}</Grid>
<Box
sx={{
width: '100%',
textAlign: 'center',
marginTop: 3,
}}
>
<Button
disabled={pageIndex === 1}
onClick={() => setPageIndex(pageIndex - 1)}
sx={{
marginRight: 5,
}}
>
{t('Btns.PrevBtn')}
</Button>
<Button
disabled={
paginationData ? pageIndex * 4 > paginationData?.dataCount : true
}
onClick={() => setPageIndex(pageIndex + 1)}
sx={{
marginRight: 5,
}}
>
{t('Btns.NextBtn')}
</Button>
</Box>
</Paper>
);
};

export default PaginationComponentRQ;

+ 5
- 0
components/pagination/swr/PaginationComponentSWR.mock.ts 查看文件

@@ -0,0 +1,5 @@
const base = {};

export const mockPaginationComponentSWRProps = {
base,
};

+ 20
- 0
components/pagination/swr/PaginationComponentSWR.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import PaginationComponentSWR from './PaginationComponentSWR';
import { mockPaginationComponentSWRProps } from './PaginationComponentSWR.mock';

const obj = {
title: 'pagination/PaginationComponentSWR',
component: PaginationComponentSWR,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <PaginationComponentSWR {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockPaginationComponentSWRProps.base,
};

+ 116
- 0
components/pagination/swr/PaginationComponentSWR.tsx 查看文件

@@ -0,0 +1,116 @@
import { Box, Button, Grid, Paper, Typography } from '@mui/material';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import useDebounce from '../../../hooks/use-debounce';
import useSWRWithFallbackData from '../../../hooks/use-swr-with-initial-data';
import { getData } from '../../../requests/dataRequest';
import { compare } from '../../../utils/helpers/sortHelpers';
import { IPerson } from '../../../utils/interface/personInterface';
import DataCard from '../../cards/data-card/DataCard';
import FilterSortComponent from '../filter-sort/FilterSortComponent';

const PaginationComponent = ({ initialData = {} }) => {
const [pageIndex, setPageIndex] = useState(1);
const [filter, setFilter] = useState('');
const [sort, setSort] = useState('');
const { t } = useTranslation('pagination');

const fetcher = (page: string) => getData(page);
const { data: paginationData }:any = useSWRWithFallbackData(pageIndex.toString(), fetcher, {
fallbackData: initialData,
});

const debouncedFilter = useDebounce(filter, 500);

const handleFilterChange = (event: {target: HTMLInputElement}) => {
const filterText = event.target.value;
setFilter(filterText);
};

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

const dataToDisplay = paginationData?.data
.filter((item: IPerson) =>
item.name.toLowerCase().startsWith(debouncedFilter.toLowerCase())
)
.sort((a: IPerson, b:IPerson) => compare(a.name, b.name, sort))
.map((item: IPerson, index: number) => (
// ! 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}>
<DataCard data={item} t={t} />
</Grid>
));

return (
<Paper
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'start',
py: 2,
minHeight: 400,
marginTop: 5,
}}
elevation={5}
>
<Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center">
{t('Title')}
</Typography>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'wrap',
mx: 2,
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
width: '100%',
}}
>
<FilterSortComponent
sort={sort}
handleSortChange={handleSortChange}
filter={filter}
handleFilterChange={handleFilterChange}
/>
</Box>
</Box>
<Grid container>{dataToDisplay}</Grid>
<Box
sx={{
width: '100%',
textAlign: 'center',
marginTop: 3,
}}
>
<Button
disabled={pageIndex === 1}
onClick={() => setPageIndex(pageIndex - 1)}
sx={{
marginRight: 5,
}}
>
{t('Btns.PrevBtn')}
</Button>
<Button
disabled={pageIndex * 4 > paginationData?.dataCount}
onClick={() => setPageIndex(pageIndex + 1)}
sx={{
marginRight: 5,
}}
>
{t('Btns.NextBtn')}
</Button>
</Box>
</Paper>
);
};

export default PaginationComponent;

+ 7
- 0
components/templates/base/BaseTemplate.mock.js 查看文件

@@ -0,0 +1,7 @@
const base = {
sampleTextProp: 'Hello world!',
};

export const mockBaseTemplateProps = {
base,
};

+ 2
- 0
components/templates/base/BaseTemplate.module.css 查看文件

@@ -0,0 +1,2 @@
.component {
}

+ 20
- 0
components/templates/base/BaseTemplate.stories.jsx 查看文件

@@ -0,0 +1,20 @@
import BaseTemplate from './BaseTemplate';
import { mockBaseTemplateProps } from './BaseTemplate.mocks';

const obj = {
title: 'templates/BaseTemplate',
component: BaseTemplate,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
}; //eslint-disable-line

export default obj;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <BaseTemplate {...args} />;

export const Base = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args

Base.args = {
...mockBaseTemplateProps.base,
};

+ 11
- 0
components/templates/base/BaseTemplate.tsx 查看文件

@@ -0,0 +1,11 @@
import styles from './BaseTemplate.module.css';

interface IProps {
sampleTextProp: string;
}

const BaseTemplate: React.FC<IProps> = ({ sampleTextProp }) => {
return <div className={styles.container}>{sampleTextProp}</div>;
};

export default BaseTemplate;

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

@@ -0,0 +1,6 @@
export const BASE_PAGE = '/';
export const LOGIN_PAGE = '/auth';
export const PROFILE_PAGE = '/profile';
export const REGISTER_PAGE = '/auth/register';
export const FORGOT_PASSWORD_PAGE = '/auth/forgot-password';
export const SINGLE_DATA_PAGE = '/single-data/';

+ 17
- 0
hooks/use-debounce.ts 查看文件

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

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

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

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

return debouncedValue;
};

export default useDebounce;

+ 10
- 0
hooks/use-pagination.ts 查看文件

@@ -0,0 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import { getData } from '../requests/dataRequest';

export const usePagination = (activePage: string) => {
return useQuery(['randomData', activePage], () => getData(activePage), {
keepPreviousData: true,
refetchOnMount: false,
refetchOnWindowFocus: false,
});
};

+ 23
- 0
hooks/use-swr-with-initial-data.ts 查看文件

@@ -0,0 +1,23 @@
import { useEffect, useRef } from 'react';
import useSWR from 'swr';

const useSWRWithFallbackData = (
key: string,
fetcher: any,
options = {
fallbackData: {},
}
) => {
const hasMounted = useRef(false);

useEffect(() => {
hasMounted.current = true;
}, []);

return useSWR(key, fetcher, {
...options,
fallbackData: hasMounted.current ? undefined : options?.fallbackData,
});
};

export default useSWRWithFallbackData;

+ 34
- 0
models/person.ts 查看文件

@@ -0,0 +1,34 @@
import { Schema, model, models } from 'mongoose';
import { IPerson } from '../utils/interface/personInterface';

const PersonSchema = new Schema<IPerson>({
name: {
type: String,
required: [true, 'Please provide a name.'],
maxlength: [60, 'Name cannot be more than 60 characters'],
trim: true,
},
age: {
type: Number,
required: [true, 'Please provide an age.'],
validate(value: number) {
if (value < 0) {
throw new Error('Age must be a postive number');
}
},
},
gender: {
type: String,
required: [true, 'Please provide a gender.'],
trim: true,
},
customID: {
type: String,
required: true,
unique: true,
},
});

const Person = models.Person || model<IPerson>('Person', PersonSchema);

module.exports = Person;

+ 87
- 0
models/user.ts 查看文件

@@ -0,0 +1,87 @@
import {
hashPassword,
verifyPassword,
} from '../utils/helpers/hashPasswordHelpers';
import { Schema, model, Model, models } from 'mongoose';
import { IUser } from '../utils/interface/userInterface';
const validator = require('validator');

interface UserModel extends Model<IUser, {}, {}> {
findByCredentials(username: string, password: string): object;
}

const UserSchema = new Schema<IUser, UserModel>({
fullName: {
type: String,
required: [true, 'Please provide a name.'],
maxlength: [60, 'Name cannot be more than 60 characters'],
trim: true,
},
username: {
type: String,
required: [true, 'Please provide a name.'],
maxlength: [60, 'Name cannot be more than 60 characters'],
trim: true,
unique: true,
},
email: {
type: String,
unique: true,
required: true,
trim: true,
lowercase: true,
validate(value: string) {
if (!validator.isEmail(value)) {
throw new Error('Email is invalid');
}
},
},
password: {
type: String,
required: true,
minlength: 7,
trim: true,
validate(value: string) {
if (value.toLowerCase().includes('password')) {
throw new Error('Password cannot contain "password"');
}
},
},
});

UserSchema.static(
'findByCredentials',
async function findByCredentials(username: string, password: string) {
const user = await User.findOne({ username });

if (!user) {
throw new Error('Unable to login');
}

const isMatch = await verifyPassword(password, user.password);

if (!isMatch) {
throw new Error('Unable to login');
}

const userData = {
fullName: user.fullName,
email: user.email,
username: user.username,
};
return userData;
}
);

UserSchema.pre('save', async function (next) {
const user = this;

if (user.isModified('password')) {
user.password = await hashPassword(user.password);
}

next();
});

const User = models.User || model<IUser, UserModel>('User', UserSchema);
module.exports = User;

+ 5
- 0
next-env.d.ts 查看文件

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

+ 6
- 0
next-i18next.config.js 查看文件

@@ -0,0 +1,6 @@
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
};

+ 0
- 0
next.config.js 查看文件


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

Loading…
取消
儲存