소스 검색

feat: init commit

pull/1/head
ntasicc 3 년 전
부모
커밋
b4a6dfe214
100개의 변경된 파일4868개의 추가작업 그리고 70개의 파일을 삭제
  1. 23
    0
      .eslintrc.json
  2. 24
    69
      .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. 33
    1
      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. 29
    0
      components/cards/data-card/DataCard.jsx
  45. 10
    0
      components/cards/data-card/DataCard.mock.js
  46. 20
    0
      components/cards/data-card/DataCard.stories.jsx
  47. 55
    0
      components/cards/data-details-card/DataDetailsCard.jsx
  48. 7
    0
      components/cards/data-details-card/DataDetailsCard.mock.js
  49. 20
    0
      components/cards/data-details-card/DataDetailsCard.stories.jsx
  50. 37
    0
      components/cards/hover-image-card/HoverImageCard.jsx
  51. 5
    0
      components/cards/hover-image-card/HoverImageCard.mock.js
  52. 20
    0
      components/cards/hover-image-card/HoverImageCard.stories.jsx
  53. 75
    0
      components/cards/hover-image-card/hover-image-card.module.css
  54. 36
    0
      components/cards/profile-card/ProfileCard.jsx
  55. 7
    0
      components/cards/profile-card/ProfileCard.mock.js
  56. 20
    0
      components/cards/profile-card/ProfileCard.stories.jsx
  57. 118
    0
      components/forms/contact/ContactForm.jsx
  58. 5
    0
      components/forms/contact/ContactForm.mcok.js
  59. 20
    0
      components/forms/contact/ContactForm.stories.jsx
  60. 79
    0
      components/forms/forgot-password/ForgotPasswordForm.jsx
  61. 5
    0
      components/forms/forgot-password/ForgotPasswordForm.mock.js
  62. 20
    0
      components/forms/forgot-password/ForgotPasswordForm.stories.jsx
  63. 143
    0
      components/forms/login/LoginForm.jsx
  64. 5
    0
      components/forms/login/LoginForm.mock.js
  65. 20
    0
      components/forms/login/LoginForm.stories.jsx
  66. 195
    0
      components/forms/register/RegisterForm.jsx
  67. 5
    0
      components/forms/register/RegisterForm.mock.js
  68. 20
    0
      components/forms/register/RegisterForm.stories.jsx
  69. 12
    0
      components/layout/base-layout/Layout.jsx
  70. 7
    0
      components/layout/base-layout/Layout.mock.js
  71. 20
    0
      components/layout/base-layout/Layout.stories.jsx
  72. 208
    0
      components/layout/navbar/Navbar.jsx
  73. 5
    0
      components/layout/navbar/Navbar.mock.js
  74. 20
    0
      components/layout/navbar/Navbar.stories.jsx
  75. 55
    0
      components/loader/route-loader/CircularIndeterminate.jsx
  76. 5
    0
      components/loader/route-loader/CircularIndeterminate.mock.js
  77. 20
    0
      components/loader/route-loader/CircularIndeterminate.stories.jsx
  78. 15
    0
      components/mui/ErrorMessageComponent.jsx
  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. 125
    0
      components/pagination/react-query/PaginationComponentRQ.jsx
  83. 5
    0
      components/pagination/react-query/PaginationComponentRQ.mock.js
  84. 20
    0
      components/pagination/react-query/PaginationComponentRQ.stories.jsx
  85. 115
    0
      components/pagination/swr/PaginationComponentSWR.jsx
  86. 5
    0
      components/pagination/swr/PaginationComponentSWR.mock.js
  87. 20
    0
      components/pagination/swr/PaginationComponentSWR.stories.jsx
  88. 93
    0
      components/sass-components/Button/Button.js
  89. 32
    0
      components/sass-components/IconButton/IconButton.js
  90. 187
    0
      components/sass-components/InputFields/BaseInputField.js
  91. 40
    0
      components/sass-components/InputFields/Checkbox.js
  92. 123
    0
      components/sass-components/InputFields/CurrencyField.js
  93. 33
    0
      components/sass-components/InputFields/EmailField.js
  94. 74
    0
      components/sass-components/InputFields/NumberField.js
  95. 74
    0
      components/sass-components/InputFields/PasswordField.js
  96. 130
    0
      components/sass-components/InputFields/PasswordStrength.js
  97. 45
    0
      components/sass-components/InputFields/PercentageField.js
  98. 49
    0
      components/sass-components/InputFields/PhoneNumberField.js
  99. 54
    0
      components/sass-components/InputFields/Radio.js
  100. 0
    0
      components/sass-components/InputFields/Search.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": "^_" }]
}
}

+ 24
- 69
.gitignore 파일 보기

@@ -1,78 +1,33 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# Dependency directories
node_modules/
jspm_packages/
# dependencies
/node_modules
/.pnp
.pnp.js

# TypeScript v1 declaration files
typings/
# testing
/coverage

# Optional npm cache directory
.npm
# next.js
/.next/
/out/

# Optional eslint cache
.eslintcache
# production
/build

# Optional REPL history
.node_repl_history
# misc
.DS_Store
*.pem

# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# dotenv environment variables file
# local env files
.env*.local
.env

# parcel-bundler cache (https://parceljs.org/)
.cache

# next.js build output
.next

# nuxt.js build output
.nuxt

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless

# FuseBox cache
.fusebox/

# 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
}
}

+ 33
- 1
README.md 파일 보기

@@ -1,2 +1,34 @@
# coffee
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',
],
],
},
};

+ 29
- 0
components/cards/data-card/DataCard.jsx 파일 보기

@@ -0,0 +1,29 @@
import { Divider, Paper, Typography } from '@mui/material';
import PropType from 'prop-types';

const DataCard = ({ 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>
);
};

DataCard.propTypes = {
data: PropType.shape({
name: PropType.string,
age: PropType.number,
gender: PropType.string,
}),
t: PropType.func,
};

export default DataCard;

+ 10
- 0
components/cards/data-card/DataCard.mock.js 파일 보기

@@ -0,0 +1,10 @@
const base = {
data: { name: 'John Doe', age: 30, gender: 'male' },
t: (text) => {
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,
};

+ 55
- 0
components/cards/data-details-card/DataDetailsCard.jsx 파일 보기

@@ -0,0 +1,55 @@
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
import Image from 'next/image';
import PropType from 'prop-types';

const DataDetailsCard = ({ 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>
);
};

DataDetailsCard.propTypes = {
data: PropType.shape({
name: PropType.string,
gender: PropType.string,
}),
};

export default DataDetailsCard;

+ 7
- 0
components/cards/data-details-card/DataDetailsCard.mock.js 파일 보기

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

+ 37
- 0
components/cards/hover-image-card/HoverImageCard.jsx 파일 보기

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

+ 5
- 0
components/cards/hover-image-card/HoverImageCard.mock.js 파일 보기

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

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

+ 36
- 0
components/cards/profile-card/ProfileCard.jsx 파일 보기

@@ -0,0 +1,36 @@
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
import Image from 'next/image';
import PropType from 'prop-types';

const ProfileCard = ({ 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>
);
};

ProfileCard.propTypes = {
profileData: PropType.shape({
name: PropType.string,
}),
};

export default ProfileCard;

+ 7
- 0
components/cards/profile-card/ProfileCard.mock.js 파일 보기

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

+ 118
- 0
components/forms/contact/ContactForm.jsx 파일 보기

@@ -0,0 +1,118 @@
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 React from 'react';
import { BASE_PAGE } from '../../../constants/pages';
import { contactSchema } from '../../../schemas/contactSchema';
const ContactForm = () => {
const { t } = useTranslation('forms', 'contact', 'common');
const handleSubmit = (values) => {
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/contact/ContactForm.mcok.js 파일 보기

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

+ 79
- 0
components/forms/forgot-password/ForgotPasswordForm.jsx 파일 보기

@@ -0,0 +1,79 @@
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 React from 'react';
import { LOGIN_PAGE } from '../../../constants/pages';
import { forgotPasswordSchema } from '../../../schemas/forgotPasswordSchema';

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

const handleSubmit = (values) => {
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/forgot-password/ForgotPasswordForm.mock.js 파일 보기

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

+ 143
- 0
components/forms/login/LoginForm.jsx 파일 보기

@@ -0,0 +1,143 @@
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';

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) => {
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/login/LoginForm.mock.js 파일 보기

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

+ 195
- 0
components/forms/register/RegisterForm.jsx 파일 보기

@@ -0,0 +1,195 @@
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';

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) => {
try {
const result = await createUser(
values.fullName,
values.username,
values.email,
values.password
);
console.log(result);
} catch (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;

+ 5
- 0
components/forms/register/RegisterForm.mock.js 파일 보기

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

+ 12
- 0
components/layout/base-layout/Layout.jsx 파일 보기

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

function Layout(props) {
return (
<>
<Navbar />
<main>{props.children}</main>
</>
);
}

export default Layout;

+ 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,
};

+ 208
- 0
components/layout/navbar/Navbar.jsx 파일 보기

@@ -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) => {
setAnchorElNav(event.currentTarget);
};
const handleOpenUserMenu = (event) => {
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/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,
};

+ 55
- 0
components/loader/route-loader/CircularIndeterminate.jsx 파일 보기

@@ -0,0 +1,55 @@
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) => url !== router.asPath && setLoading(true);
const handleComplete = (url) => 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;

+ 5
- 0
components/loader/route-loader/CircularIndeterminate.mock.js 파일 보기

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

+ 15
- 0
components/mui/ErrorMessageComponent.jsx 파일 보기

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

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

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

export default ErrorMessageComponent;

+ 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,
};

+ 125
- 0
components/pagination/react-query/PaginationComponentRQ.jsx 파일 보기

@@ -0,0 +1,125 @@
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);
const router = useRouter();
const debouncedFilter = useDebounce(filter, 500);

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

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

const loadSingleDataHandler = (id) => {
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={pageIndex * 4 > paginationData?.dataCount}
onClick={() => setPageIndex(pageIndex + 1)}
sx={{
marginRight: 5,
}}
>
{t('Btns.NextBtn')}
</Button>
</Box>
</Paper>
);
};

export default PaginationComponentRQ;

+ 5
- 0
components/pagination/react-query/PaginationComponentRQ.mock.js 파일 보기

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

+ 115
- 0
components/pagination/swr/PaginationComponentSWR.jsx 파일 보기

@@ -0,0 +1,115 @@
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 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) => getData(page);
const { data: paginationData } = useSWRWithFallbackData(pageIndex, fetcher, {
fallbackData: initialData,
});

const debouncedFilter = useDebounce(filter, 500);

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

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

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

+ 5
- 0
components/pagination/swr/PaginationComponentSWR.mock.js 파일 보기

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

+ 93
- 0
components/sass-components/Button/Button.js 파일 보기

@@ -0,0 +1,93 @@
import React, { useRef } from 'react';
import PropType from 'prop-types';

const Button = ({
variant,
size,
children,
authButton,
type,
onClick,
textTransform,
className,
disabled,
hidden,
minWidth,
...restProps
}) => {
const buttonRef = useRef(null);

function styles() {
let style = 'c-btn';

if (variant) {
style += ` c-btn--${variant}`;
}

if (size) {
style += ` c-btn--${size}`;
}

if (textTransform) {
style += ` c-btn--${textTransform}`;
}

if (authButton) {
style += ` c-btn--auth`;
}

if (minWidth) {
style += ` c-btn--${minWidth}`;
}

if (hidden) {
style += ` c-btn--hidden`;
}

if (className) {
style += ` ${className}`;
}

return style;
}

function handleClick() {
buttonRef.current.blur();
if (typeof onClick === 'function') {
onClick();
}
}

return (
<button
ref={buttonRef}
className={styles()}
onClick={handleClick}
type={type}
disabled={disabled}
{...restProps}
>
{children}
</button>
);
};

Button.propTypes = {
children: PropType.node,
textTransform: PropType.oneOf(['uppercase', 'capitalize']),
size: PropType.oneOf(['sm', 'md', 'lg', 'xl']),
authButton: PropType.bool,
variant: PropType.string,
type: PropType.oneOf(['button', 'submit', 'reset']),
onClick: PropType.func,
className: PropType.string,
disabled: PropType.bool,
minWidth: PropType.oneOf(['auto']),
hidden: PropType.bool,
};

Button.defaultProps = {
type: 'button',
};

export default Button;

+ 32
- 0
components/sass-components/IconButton/IconButton.js 파일 보기

@@ -0,0 +1,32 @@
import React, { useRef } from 'react';
import PropType from 'prop-types';

const IconButton = ({ children, onClick, className }) => {
const buttonRef = useRef(null);

function handleClick() {
buttonRef.current.blur();
if (typeof onClick === 'function') {
onClick();
}
}

return (
<button
type="button"
ref={buttonRef}
onClick={handleClick}
className={`c-icon-button ${className && className}`}
>
{children}
</button>
);
};

IconButton.propTypes = {
children: PropType.node,
onClick: PropType.func,
className: PropType.string,
};

export default IconButton;

+ 187
- 0
components/sass-components/InputFields/BaseInputField.js 파일 보기

@@ -0,0 +1,187 @@
import { ErrorMessage } from 'formik';
import PropTypes from 'prop-types';
import React, { useEffect, useRef, useState } from 'react';
import { ReactComponent as CapsLock } from '../../../assets/images/svg/caps-lock.svg';
import { ReactComponent as EyeOff } from '../../../assets/images/svg/eye-off.svg';
import { ReactComponent as EyeOn } from '../../../assets/images/svg/eye-on.svg';
import { ReactComponent as Search } from '../../../assets/images/svg/search.svg';
import IconButton from '../IconButton/IconButton';

const BaseInputField = ({
type,
label,
field,
form,
placeholder,
clearPlaceholderOnFocus = true,
isSearch,
className,
disabled,
centerText,
link,
errorMessage,
autoFocus,
isCapsLockOn,
...props
}) => {
const [inputPlaceholder, setPlaceholder] = useState(placeholder);

const inputField = useRef(null);

useEffect(() => {
if (autoFocus) {
inputField.current.focus();
}
}, [autoFocus, inputField]);

useEffect(() => {
if (errorMessage) {
form.setFieldError(field.name, errorMessage);
}
}, [errorMessage]); // eslint-disable-line

useEffect(() => {
setPlaceholder(placeholder);
}, [placeholder]);

const [inputType, setInputType] = useState('password');
const passwordInput = type === 'password' ? ' c-input--password' : '';

const showPassword = () => {
if (inputType === 'password') {
setInputType('text');
} else {
setInputType('password');
}
};

// Nester Formik Field Names get bugged because of Undefined values, so i had to fix it like this
// If you ask why 0 and 1? I dont see a need for forms to be nested more then 2 levels?
const fieldName = field.name.split('.');

const formError =
fieldName[0] && fieldName[1]
? form.errors[fieldName[0]] && form.errors[fieldName[0]][fieldName[1]]
: form.errors[fieldName[0]];

const formTouched =
fieldName[0] && fieldName[1]
? form.touched[fieldName[0]] && form.touched[fieldName[0]][fieldName[1]]
: form.touched[fieldName[0]];

function styles() {
let style = 'c-input';

if (formError && formTouched) {
style += ` c-input--error`;
}

if (type === 'password') {
style += ` c-input--password`;
}

if (isSearch) {
style += ` c-input--search`;
}

if (centerText) {
style += ` c-input--center-text`;
}

if (type === 'number') {
style += ` c-input--demi-bold`;
}

if (className) {
style += ` ${className}`;
}

return style;
}

const additionalActions = () => {
if (!clearPlaceholderOnFocus) {
return null;
}

return {
onFocus: () => {
setPlaceholder('');
},
onBlur: (e) => {
setPlaceholder(placeholder);
field.onBlur(e);
},
};
};
return (
<div className={styles()}>
{!!label && (
<label className="c-input__label" htmlFor={field.name}>
{label}
</label>
)}
{link && <div className="c-input__link">{link}</div>}
<div className="c-input__field-wrap">
<input
ref={inputField}
type={type === 'password' ? inputType : type}
placeholder={inputPlaceholder}
disabled={disabled}
{...field}
{...props}
{...additionalActions()}
className="c-input__field"
/>
{!!isSearch && <Search className="c-input__icon" />}
{!!passwordInput && (
<>
{isCapsLockOn && <CapsLock className="c-input__caps-lock" />}
<IconButton
onClick={() => {
showPassword();
}}
className="c-input__icon"
>
{inputType === 'password' ? <EyeOff /> : <EyeOn />}
</IconButton>
</>
)}
</div>

<ErrorMessage name={field.name}>
{(errorMessage) => (
<span className="c-input__error">{errorMessage}</span>
)}
</ErrorMessage>
</div>
);
};
BaseInputField.propTypes = {
type: PropTypes.string,
field: PropTypes.shape({
name: PropTypes.string,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
}),
form: PropTypes.shape({
errors: PropTypes.shape({}),
setFieldError: PropTypes.func,
touched: PropTypes.shape({}),
}),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]),
placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
disabled: PropTypes.bool,
isSearch: PropTypes.bool,
className: PropTypes.string,
link: PropTypes.node,
errorMessage: PropTypes.string,
centerText: PropTypes.bool,
clearPlaceholderOnFocus: PropTypes.bool,
demiBold: PropTypes.bool,
touched: PropTypes.bool,
autoFocus: PropTypes.bool,
isCapsLockOn: PropTypes.bool,
};

export default BaseInputField;

+ 40
- 0
components/sass-components/InputFields/Checkbox.js 파일 보기

@@ -0,0 +1,40 @@
import PropTypes from 'prop-types';
import React from 'react';
import { ReactComponent as Checked } from '../../../assets/images/svg/checked.svg';
import { ReactComponent as Unchecked } from '../../../assets/images/svg/unchecked.svg';

const Checkbox = ({ className, children, name, onChange, checked, field }) => (
<label htmlFor={name} className={`c-checkbox ${className || ''}`}>
<input
name={name}
id={name}
className="c-checkbox__field"
type="checkbox"
checked={checked}
{...field}
onChange={onChange || field.onChange}
/>

<div className="c-checkbox__indicator">
{checked ? (
<Checked className="c-checkbox__icon" />
) : (
<Unchecked className="c-checkbox__icon" />
)}
</div>
<div className="c-checkbox__text">{children}</div>
</label>
);

Checkbox.propTypes = {
children: PropTypes.node,
onChange: PropTypes.func,
checked: PropTypes.bool,
name: PropTypes.string,
field: PropTypes.shape({
onChange: PropTypes.func,
}),
className: PropTypes.string,
};

export default Checkbox;

+ 123
- 0
components/sass-components/InputFields/CurrencyField.js 파일 보기

@@ -0,0 +1,123 @@
import { ErrorMessage, useField } from 'formik';
import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import CurrencyInput from 'react-currency-input-field';
import { formatMoneyNumeral } from '../../../util/helpers/numeralHelpers';
import {
K_KEYCODE,
MINUS_SYMBOL,
NUMPAD_MINUS_SYMBOL,
NUMPAD_PLUS_SYMBOL,
PLUS_SYMBOL,
} from '../../constants/keyCodeConstants';

const CurrencyField = ({
autoFocus,
notCentered,
notBold,
label,
onChange,
value,
...props
}) => {
const [field, meta] = useField(props);
const inputField = useRef(null);
function styles() {
let style = 'c-currency-field';

if (meta.error && meta.touched) {
style += ` c-currency-field--error`;
}

if (notCentered) {
style += ` c-currency-field--not-centered`;
}

if (notBold) {
style += ` c-currency-field--not-bold`;
}

return style;
}

useEffect(() => {
if (autoFocus) {
inputField.current.focus();
}
}, [autoFocus, inputField]);

const onKeydownHandler = (event) => {
if (
event.keyCode === MINUS_SYMBOL ||
event.keyCode === PLUS_SYMBOL ||
event.keyCode === NUMPAD_MINUS_SYMBOL ||
event.keyCode === NUMPAD_PLUS_SYMBOL ||
event.keyCode === K_KEYCODE
) {
event.preventDefault();
}
};

const prefix = formatMoneyNumeral(0);
const prefixSymbol = () => {
if (prefix.includes('CAD')) {
return 'CAD ';
}

return '$';
};

return (
<div className={styles()}>
{!!label && (
<label className="c-currency-field__label" htmlFor={field.name}>
{label}
</label>
)}
{value ? (
<CurrencyInput
{...props}
prefix={prefixSymbol()}
onValueChange={(value) => {
onChange(value ? Number(value) : '');
}}
onKeyDown={(event) => onKeydownHandler(event)}
ref={inputField}
defaultValue={0}
value={value}
/>
) : (
<CurrencyInput
{...props}
prefix={prefixSymbol()}
onValueChange={(value) => {
onChange(value ? Number(value) : '');
}}
onKeyDown={(event) => onKeydownHandler(event)}
ref={inputField}
/>
)}
<ErrorMessage name={field.name}>
{(errorMessage) => (
<span className="c-currency-field__error">{errorMessage}</span>
)}
</ErrorMessage>
</div>
);
};

CurrencyField.propTypes = {
field: PropTypes.shape({
name: PropTypes.string,
}),
form: PropTypes.shape({}),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]),
disabled: PropTypes.bool,
onChange: PropTypes.func,
autoFocus: PropTypes.bool,
notCentered: PropTypes.bool,
notBold: PropTypes.bool,
value: PropTypes.number,
};

export default CurrencyField;

+ 33
- 0
components/sass-components/InputFields/EmailField.js 파일 보기

@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';

import BaseInputField from './BaseInputField';

const EmailField = ({
field,
form,
label,
placeholder,
disabled,
...props
}) => (
<BaseInputField
type="email"
label={label}
placeholder={placeholder}
disabled={disabled}
form={form}
field={field}
{...props}
/>
);

EmailField.propTypes = {
field: PropTypes.shape({}),
form: PropTypes.shape({}),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]),
placeholder: PropTypes.string,
disabled: PropTypes.bool,
};

export default EmailField;

+ 74
- 0
components/sass-components/InputFields/NumberField.js 파일 보기

@@ -0,0 +1,74 @@
import React from 'react';
import PropTypes from 'prop-types';

import BaseInputField from './BaseInputField';
import {
PERIOD_SYMBOL,
COMMA_SYMBOL,
PLUS_SYMBOL,
MINUS_SYMBOL,
NUMPAD_PERIOD_SYMBOL,
NUMPAD_MINUS_SYMBOL,
NUMPAD_PLUS_SYMBOL,
DOWN_ARROW_KEYCODE,
UP_ARROW_KEYCODE,
} from '../../constants/keyCodeConstants';

const NumberField = ({
field,
form,
label,
placeholder,
disabled,
preventAllExceptNumbers,
...props
}) => {
const onKeydownHandler = (event) => {
if (preventAllExceptNumbers) {
if (
event.keyCode === PERIOD_SYMBOL ||
event.keyCode === COMMA_SYMBOL ||
event.keyCode === NUMPAD_PERIOD_SYMBOL ||
event.keyCode === DOWN_ARROW_KEYCODE ||
event.keyCode === UP_ARROW_KEYCODE
) {
event.preventDefault();
}
}

if (
event.keyCode === PLUS_SYMBOL ||
event.keyCode === MINUS_SYMBOL ||
event.keyCode === NUMPAD_MINUS_SYMBOL ||
event.keyCode === NUMPAD_PLUS_SYMBOL ||
event.keyCode === DOWN_ARROW_KEYCODE ||
event.keyCode === UP_ARROW_KEYCODE
) {
event.preventDefault();
}
};

return (
<BaseInputField
type="number"
label={label}
placeholder={placeholder}
disabled={disabled}
form={form}
field={field}
{...props}
onKeyDown={(event) => onKeydownHandler(event)}
/>
);
};

NumberField.propTypes = {
field: PropTypes.shape({}),
form: PropTypes.shape({}),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]),
placeholder: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
disabled: PropTypes.bool,
preventAllExceptNumbers: PropTypes.bool,
};

export default NumberField;

+ 74
- 0
components/sass-components/InputFields/PasswordField.js 파일 보기

@@ -0,0 +1,74 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';

import BaseInputField from './BaseInputField';
import PasswordStrength from './PasswordStrength';

const PasswordField = ({
field,
form,
label,
placeholder,
disabled,
shouldTestPasswordStrength,
autoFocus,
...props
}) => {
const [passwordValue, setPasswordValue] = useState('');
const [isCapsLockOn, setIsCapsLockOn] = useState(false);

const onChange = (e) => {
if (shouldTestPasswordStrength) {
const { value } = e.target;
setPasswordValue(value);
}

field.onChange(e);
};

const onKeyDown = (keyEvent) => {
if (keyEvent.getModifierState('CapsLock')) {
setIsCapsLockOn(true);
} else {
setIsCapsLockOn(false);
}
};

return (
<div className="c-password">
<BaseInputField
type="password"
label={label}
placeholder={placeholder}
disabled={disabled}
form={form}
field={field}
{...props}
onChange={onChange}
autoFocus={autoFocus}
onKeyDown={onKeyDown}
isCapsLockOn={isCapsLockOn}
/>
{shouldTestPasswordStrength && (
<PasswordStrength
passwordValue={passwordValue}
shouldTestPasswordStrength
/>
)}
</div>
);
};

PasswordField.propTypes = {
field: PropTypes.shape({
onChange: PropTypes.func,
}),
form: PropTypes.shape({}),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]),
placeholder: PropTypes.string,
disabled: PropTypes.bool,
shouldTestPasswordStrength: PropTypes.bool,
autoFocus: PropTypes.bool,
};

export default PasswordField;

+ 130
- 0
components/sass-components/InputFields/PasswordStrength.js 파일 보기

@@ -0,0 +1,130 @@
import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import owasp from 'owasp-password-strength-test';
import i18next from 'i18next';

owasp.config({
minOptionalTestsToPass: 3,
});

const passwordStrengthOptions = [
{
strength: 'weak',
color: '#FF5028',
},
{
strength: 'average',
color: '#FDB942',
},
{
strength: 'good',
color: '#06BEE7',
},
{
strength: 'strong',
color: '#00876A',
},
];

/**
* User must pass a required test and at least 3 optional.
* @param result - owasp result
* @returns {number} - index of password strength 0-3
*/
function getPasswordStrengthIndex(result) {
// requirement for strong password is required test passed and at least 3 optional tests
if (result.strong) {
return 3;
}

if (!result.strong && result.optionalTestsPassed >= 3) {
return 2;
}

if (result.optionalTestsPassed <= 0) {
return 0;
}

return result.optionalTestsPassed - 1;
}

const PasswordStrength = ({
shouldTestPasswordStrength,
passwordValue,
passwordStrengthTestsRequired,
}) => {
const strengthContainer = useRef(null);
const [passwordStrength, setPasswordStrength] = useState({
width: 0,
color: 'red',
});
const [error, setError] = useState('');

useEffect(() => {
if (shouldTestPasswordStrength && passwordValue) {
const bBox = strengthContainer.current.getBoundingClientRect();
const result = owasp.test(passwordValue);

const passwordStrengthIndex = getPasswordStrengthIndex(result);
const passwordOption = passwordStrengthOptions[passwordStrengthIndex];

const width = !passwordValue
? 0
: (bBox.width * (passwordStrengthIndex + 1)) /
passwordStrengthTestsRequired;

setPasswordStrength({ width, color: passwordOption.color });
const strength = i18next.t(`password.${passwordOption.strength}`);
setError(i18next.t('login.passwordStrength', { strength }));
}
}, [
passwordValue,
shouldTestPasswordStrength,
passwordStrengthTestsRequired,
]);

if (!shouldTestPasswordStrength || !passwordValue) {
return null;
}

const renderError = () => {
if (!error) {
return null;
}
return (
<div
className="c-input--error"
style={{
color: passwordStrength.color,
}}
>
{error}
</div>
);
};
return (
<div ref={strengthContainer} className="c-password-strength__container">
<div className="c-password-strength__line--wrapper">
<div
className="c-password-strength__line"
style={{
backgroundColor: passwordStrength.color,
width: passwordStrength.width,
}}
/>
</div>
{renderError()}
</div>
);
};
PasswordStrength.propTypes = {
shouldTestPasswordStrength: PropTypes.bool,
passwordValue: PropTypes.string,
passwordStrengthTestsRequired: PropTypes.number,
};

PasswordStrength.defaultProps = {
passwordStrengthTestsRequired: 4,
};

export default PasswordStrength;

+ 45
- 0
components/sass-components/InputFields/PercentageField.js 파일 보기

@@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import NumberFormat from 'react-number-format';

import TextField from './TextField';

const PercentageField = ({ field, ...props }) => {
const handleOnChange = (percentageField) => {
const { floatValue } = percentageField;

if (!props.onChange) {
throw Error('Provide an onChange handler');
}
if (floatValue > 100) {
return props.onChange('100');
}

if (floatValue <= 0 || !floatValue) {
return props.onChange('0');
}

return props.onChange(floatValue.toString());
};

return (
<NumberFormat
format="###%"
value={field.value}
customInput={TextField}
field={field}
{...props}
onValueChange={handleOnChange}
onChange={() => {}}
/>
);
};

PercentageField.propTypes = {
onChange: PropTypes.func,
field: PropTypes.shape({
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}),
};

export default PercentageField;

+ 49
- 0
components/sass-components/InputFields/PhoneNumberField.js 파일 보기

@@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ErrorMessage, useField } from 'formik';
import PhoneInput from 'react-phone-number-input';
import 'react-phone-number-input/style.css';

const PhoneNumberField = ({ label, ...props }) => {
const [field, meta] = useField(props);
const inputErrorClassName =
meta.error && meta.touched ? 'c-input--error' : '';

return (
<div className={`c-input c-phone-number ${inputErrorClassName}`}>
{!!label && (
<label className="c-input__label" htmlFor={field.name}>
{label}
</label>
)}
<PhoneInput
international
defaultCountry="US"
{...field}
{...props}
onChange={(value) => {
props.onPhoneChange(value);
}}
countryOptionsOrder={['US']}
/>
<ErrorMessage name={field.name}>
{(errorMessage) => (
<span className="c-input__error">{errorMessage}</span>
)}
</ErrorMessage>
</div>
);
};

PhoneNumberField.propTypes = {
field: PropTypes.shape({
name: PropTypes.string,
}),
form: PropTypes.shape({}),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]),
disabled: PropTypes.bool,
onChange: PropTypes.func,
onPhoneChange: PropTypes.func,
};

export default PhoneNumberField;

+ 54
- 0
components/sass-components/InputFields/Radio.js 파일 보기

@@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React from 'react';
import { ReactComponent as RadioOff } from '../../../assets/images/svg/radio-off.svg';
import { ReactComponent as RadioOn } from '../../../assets/images/svg/radio-on.svg';

const Checkbox = ({
className,
children,
name,
checked,
field,
value,
selected,
id,
}) => (
<label
htmlFor={name}
className={`c-radio ${selected ? 'c-radio--selected' : ''} ${
className || ''
}`}
>
<input
name={name}
id={id}
className="c-radio__field"
type="radio"
checked={checked}
value={value}
{...field}
/>
<div className="c-radio__indicator">
{selected ? (
<RadioOn className="c-radio__icon" />
) : (
<RadioOff className="c-radio__icon" />
)}
</div>
<div className="c-radio__text">{children}</div>
</label>
);

Checkbox.propTypes = {
children: PropTypes.node,
checked: PropTypes.bool,
name: PropTypes.string,
field: PropTypes.shape({}),
form: PropTypes.shape({}),
className: PropTypes.string,
value: PropTypes.string,
selected: PropTypes.bool,
id: PropTypes.string,
};

export default Checkbox;

+ 0
- 0
components/sass-components/InputFields/Search.js 파일 보기


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.

Loading…
취소
저장