| @@ -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": "^_" }] | |||
| } | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | |||
| # dependencies | |||
| /node_modules | |||
| /.pnp | |||
| .pnp.js | |||
| # testing | |||
| /coverage | |||
| # next.js | |||
| /.next/ | |||
| /out/ | |||
| # production | |||
| /build | |||
| # misc | |||
| .DS_Store | |||
| *.pem | |||
| # debug | |||
| npm-debug.log* | |||
| yarn-debug.log* | |||
| yarn-error.log* | |||
| .pnpm-debug.log* | |||
| # local env files | |||
| .env*.local | |||
| .env | |||
| # vercel | |||
| .vercel | |||
| @@ -0,0 +1,4 @@ | |||
| #!/usr/bin/env sh | |||
| . "$(dirname -- "$0")/_/husky.sh" | |||
| npx --no -- commitlint --edit $1 | |||
| @@ -0,0 +1,4 @@ | |||
| #!/usr/bin/env sh | |||
| . "$(dirname -- "$0")/_/husky.sh" | |||
| yarn lint | |||
| @@ -0,0 +1,4 @@ | |||
| #!/usr/bin/env sh | |||
| . "$(dirname -- "$0")/_/husky.sh" | |||
| yarn build | |||
| @@ -0,0 +1 @@ | |||
| engine-strict=true | |||
| @@ -0,0 +1 @@ | |||
| lts/fermium | |||
| @@ -0,0 +1,4 @@ | |||
| .yarn | |||
| .next | |||
| dist | |||
| node_modules | |||
| @@ -0,0 +1,6 @@ | |||
| { | |||
| "trailingComma": "es5", | |||
| "tabWidth": 2, | |||
| "semi": true, | |||
| "singleQuote": true | |||
| } | |||
| @@ -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', | |||
| }, | |||
| }; | |||
| @@ -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 }, | |||
| }; | |||
| @@ -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/**"] | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| { | |||
| "editor.defaultFormatter": "esbenp.prettier-vscode", | |||
| "editor.formatOnSave": true, | |||
| "editor.codeActionsOnSave": { | |||
| "source.fixAll": true, | |||
| "source.organizeImports": true | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). | |||
| ## Getting Started | |||
| First, run the development server: | |||
| ```bash | |||
| npm run dev | |||
| # or | |||
| yarn dev | |||
| ``` | |||
| Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. | |||
| You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. | |||
| [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. | |||
| The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. | |||
| ## Learn More | |||
| To learn more about Next.js, take a look at the following resources: | |||
| - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | |||
| - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. | |||
| You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! | |||
| ## Deploy on Vercel | |||
| The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. | |||
| Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. | |||
| @@ -0,0 +1,10 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <g id="Master" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | |||
| <g id="Icons" transform="translate(-40.000000, -151.000000)" stroke="#000000"> | |||
| <g id="Icons-/-Chevron-/-Down" transform="translate(40.000000, 151.000000)"> | |||
| <polyline id="Path" transform="translate(8.000000, 8.000000) rotate(-270.000000) translate(-8.000000, -8.000000) " points="5 2 11 8 5 14" stroke="#e2930a"></polyline> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @@ -0,0 +1,12 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <g id="Master" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g id="Buy-page" transform="translate(-136.000000, -852.000000)" stroke="currentColor"> | |||
| <g id="Filter-item-Copy-16" transform="translate(136.000000, 848.000000)"> | |||
| <g id="Icons-/-Checkbox-/-off" transform="translate(0.000000, 4.000000)"> | |||
| <polyline id="Path" transform="translate(8.000000, 8.000000) rotate(-270.000000) translate(-8.000000, -8.000000) " points="5 2 11 8 5 14"></polyline> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @@ -0,0 +1,6 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <g id="Caps-Lock" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <path d="M17,19 L17,21 L7,21 L7,19 L17,19 Z M12,3 L21,10 L17,10 L17,17 L7,17 L7,10 L3,10 L12,3 Z" id="Combined-Shape" fill="currentColor"></path> | |||
| </g> | |||
| </svg> | |||
| @@ -0,0 +1,10 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <g id="Master" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g id="Icons" transform="translate(-40.000000, -79.000000)" fill="currentColor"> | |||
| <g id="eye-off-outline" transform="translate(40.000000, 79.000000)"> | |||
| <path d="M2.47398116,3.99464566 L2.53252547,4.02549094 L13.8658588,11.3588243 C14.0358755,11.4688351 14.0845198,11.6958421 13.9745091,11.8658588 C13.8767217,12.0169847 13.6864928,12.0722116 13.5260188,12.0053543 L13.4674745,11.9745091 L2.1341412,4.64117572 C1.96412454,4.53116495 1.91548016,4.30415786 2.02549094,4.1341412 C2.1232783,3.98301528 2.31350719,3.92778845 2.47398116,3.99464566 Z M3.46302885,6.35365053 L4.1083929,6.7708723 C3.81029658,7.0585608 3.52305647,7.38077325 3.2494004,7.73654755 L3.04674798,8.0096561 L3.08366782,8.06547524 C4.27326632,9.84797951 6.06094208,10.962963 7.98004454,10.962963 C8.65722341,10.962963 9.32390587,10.8201119 9.95521088,10.5562937 L10.69564,11.0338727 C9.84843225,11.4648639 8.92701525,11.7037037 7.98004454,11.7037037 C5.75860617,11.7037037 3.72666166,10.4226157 2.40821861,8.41164019 C2.24142826,8.1600981 2.24649075,7.83544344 2.42215728,7.58739593 C2.74871545,7.13128497 3.09707817,6.71935421 3.46302885,6.35365053 Z M5.71548132,7.81273581 L6.50413753,8.32383034 C6.65572725,8.9862843 7.26257495,9.48148148 7.98812175,9.48148148 C8.07995333,9.48148148 8.1698834,9.47354859 8.25722954,9.45834774 L9.04753326,9.96841802 C8.73098269,10.1305066 8.37054437,10.2222222 7.98812175,10.2222222 C6.72856811,10.2222222 5.70749814,9.22729944 5.70749814,8 C5.70749814,7.93693045 5.7101946,7.87447455 5.71548132,7.81273581 Z M7.98004454,4.2962963 C9.10605419,4.2962963 10.2063142,4.63943012 11.2089405,5.27021664 C12.1313806,5.85055521 12.9459997,6.66069361 13.5686244,7.59770297 C13.7306028,7.84293225 13.7306028,8.15776219 13.569353,8.40188399 C13.2688682,8.86037696 12.9288642,9.28189795 12.5567195,9.65936669 L11.9125299,9.24126945 C12.2874179,8.87549361 12.6294637,8.45971211 12.9293712,8.00210112 C12.9300086,8.00103291 12.9300086,7.99966153 12.9300086,7.99944169 C11.736763,6.20380333 9.88220431,5.03703704 7.98004454,5.03703704 C7.33011363,5.03703704 6.68048528,5.17600946 6.04999663,5.44731057 L5.31648269,4.97253171 C6.16987895,4.52781301 7.07030912,4.2962963 7.98004454,4.2962963 Z M7.98812175,5.77777778 C9.24767539,5.77777778 10.2687454,6.77270056 10.2687454,8 C10.2687454,8.05834477 10.2664378,8.11616438 10.2619067,8.1733769 L9.4680009,7.65873003 C9.30983274,7.00504358 8.70728744,6.51851852 7.98812175,6.51851852 C7.90245898,6.51851852 7.81845084,6.52542142 7.73665123,6.53868749 L6.94186885,6.02489746 C7.25523458,5.8669741 7.61098796,5.77777778 7.98812175,5.77777778 Z" id="Combined-Shape"></path> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @@ -0,0 +1,4 @@ | |||
| <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path d="M7.98049 4.29626C5.93884 4.29626 3.94406 5.4623 2.4226 7.58736C2.24693 7.83541 2.24187 8.16007 2.40866 8.41161C3.7271 10.4226 5.75905 11.7037 7.98049 11.7037C10.1878 11.7037 12.2564 10.406 13.5698 8.40185C13.731 8.15773 13.731 7.8429 13.5691 7.59767C12.9464 6.66066 12.1318 5.85052 11.2094 5.27018C10.2068 4.6394 9.1065 4.29626 7.98049 4.29626ZM7.98049 5.03701C9.88265 5.03701 11.7372 6.20377 12.9305 7.99941C12.9305 7.99963 12.9305 8.001 12.9298 8.00207C11.746 9.80837 9.90566 10.9629 7.98049 10.9629C6.06138 10.9629 4.27371 9.84795 3.08411 8.06544L3.04719 8.00962C4.43378 6.07295 6.20601 5.03701 7.98049 5.03701Z" fill="currentColor"/> | |||
| <path d="M7.98765 5.77771C6.7281 5.77771 5.70703 6.77263 5.70703 7.99993C5.70703 9.22723 6.7281 10.2222 7.98765 10.2222C9.24721 10.2222 10.2683 9.22723 10.2683 7.99993C10.2683 6.77263 9.24721 5.77771 7.98765 5.77771ZM7.98765 6.51845C8.82736 6.51845 9.50807 7.18173 9.50807 7.99993C9.50807 8.81813 8.82736 9.48141 7.98765 9.48141C7.14795 9.48141 6.46724 8.81813 6.46724 7.99993C6.46724 7.18173 7.14795 6.51845 7.98765 6.51845Z" fill="currentColor"/> | |||
| </svg> | |||
| @@ -0,0 +1,10 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <g id="Master" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g id="Icons" transform="translate(-136.000000, -79.000000)" stroke="currentColor"> | |||
| <g id="Icons/search" transform="translate(136.000000, 79.000000)"> | |||
| <path d="M7,3 C4.790861,3 3,4.790861 3,7 C3,9.209139 4.790861,11 7,11 C9.209139,11 11,9.209139 11,7 C10.9998594,4.79091925 9.20908075,3.00014061 7,3 Z M10,10 L13,13" id="Combined-Shape"></path> | |||
| </g> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| @@ -0,0 +1,46 @@ | |||
| body { | |||
| margin: 0; | |||
| -webkit-font-smoothing: antialiased; | |||
| -moz-osx-font-smoothing: grayscale; | |||
| overflow-anchor: none; | |||
| } | |||
| * { | |||
| box-sizing: border-box; | |||
| } | |||
| html { | |||
| min-height: 100%; | |||
| font-size: 16px; | |||
| @include media-below($bp-xxl) { | |||
| font-size: 14px; | |||
| } | |||
| @include media-below($bp-xs) { | |||
| font-size: 13px; | |||
| } | |||
| @include media-below($bp-xxs) { | |||
| font-size: 10.5px; | |||
| } | |||
| } | |||
| html, | |||
| body, | |||
| #root { | |||
| @include flex-column; | |||
| flex: 1 0 auto; | |||
| } | |||
| input[type='search']::-webkit-search-decoration, | |||
| input[type='search']::-webkit-search-cancel-button, | |||
| input[type='search']::-webkit-search-results-button, | |||
| input[type='search']::-webkit-search-results-decoration { | |||
| -webkit-appearance: none; | |||
| } | |||
| ul { | |||
| list-style: none; | |||
| padding: 0; | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| @function pxToRem($target, $context: $base-font-size) { | |||
| @return ($target / $context) * 1rem; | |||
| } | |||
| @function pxToRemMd($target, $context: $base-font-size-md) { | |||
| @return ($target / $context) * 1rem; | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| .l-page { | |||
| @include flex-column; | |||
| flex: 1 1 auto; | |||
| padding-bottom: 7rem; | |||
| position:relative; | |||
| @include media-below($bp-xl) { | |||
| padding-bottom: 4rem; | |||
| } | |||
| } | |||
| .l-section { | |||
| padding: 0 3.25rem; | |||
| @include media-below($bp-xl) { | |||
| padding: 0; | |||
| } | |||
| } | |||
| @@ -0,0 +1,81 @@ | |||
| @mixin desktop { | |||
| @media (min-width: 1280px) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin desktop-lg { | |||
| @media (min-width: 1480px) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin tablet { | |||
| @media (max-width: 1024px) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin media-up($media) { | |||
| @media only screen and (min-width: $media) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin media-below($media) { | |||
| @media only screen and (max-width: #{$media - 0.02px}) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin media-between($mediaMin, $mediaMax) { | |||
| @media screen and (min-width: $mediaMin) and (max-width: #{$mediaMax - 0.02px}) { | |||
| @content; | |||
| } | |||
| } | |||
| @mixin flex-center { | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| } | |||
| @mixin flex-column { | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| @mixin button-clear { | |||
| border: none; | |||
| padding: 0; | |||
| background-color: transparent; | |||
| } | |||
| @mixin outline-none { | |||
| &, | |||
| &:active, | |||
| &:focus { | |||
| outline: none; | |||
| } | |||
| } | |||
| @mixin reset-position { | |||
| position: relative; | |||
| top: initial; | |||
| left: initial; | |||
| right: initial; | |||
| bottom: initial; | |||
| } | |||
| @mixin text-ellipsis { | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| @mixin line-clamp($lines) { | |||
| display: -webkit-box; | |||
| -webkit-line-clamp: $lines; | |||
| -webkit-box-orient: vertical; | |||
| overflow: hidden; | |||
| } | |||
| @@ -0,0 +1,244 @@ | |||
| // Overwrite | |||
| .ais-ClearRefinements-button { | |||
| color: $grey-11; | |||
| font-size: pxToRem(14px); | |||
| letter-spacing: 0; | |||
| line-height: 1.15; | |||
| background-color: transparent; | |||
| border: none; | |||
| text-decoration: underline; | |||
| position: relative; | |||
| transition: all 0.2s; | |||
| outline: none; | |||
| cursor: pointer; | |||
| &[disabled] { | |||
| pointer-events: none; | |||
| opacity: 0.5; | |||
| cursor: auto; | |||
| } | |||
| &:hover { | |||
| color: $color-primary-light; | |||
| } | |||
| &:active { | |||
| color: $color-primary-dark; | |||
| } | |||
| } | |||
| .ais-RefinementList { | |||
| margin-bottom: pxToRem(32px); | |||
| margin-left: pxToRem(16px); | |||
| &.c-filter__refinement--closed { | |||
| display: none; | |||
| } | |||
| } | |||
| .ais-RefinementList.expanded { | |||
| .ais-RefinementList-showMore::before { | |||
| transform: rotate(180deg); | |||
| } | |||
| } | |||
| .ais-RefinementList-showMore { | |||
| color: $color-primary; | |||
| font-size: pxToRem(14px); | |||
| font-weight: 600; | |||
| letter-spacing: 0; | |||
| line-height: 1.56; | |||
| text-align: center; | |||
| background-color: transparent; | |||
| border: none; | |||
| position: relative; | |||
| margin-left: pxToRem(20px); | |||
| outline: none; | |||
| transition: all ease-in-out 0.3s; | |||
| cursor: pointer; | |||
| &[disabled] { | |||
| display: none; | |||
| } | |||
| &:hover { | |||
| color: $color-primary-light; | |||
| } | |||
| &:active { | |||
| color: $color-primary-dark; | |||
| } | |||
| &::before { | |||
| content: ''; | |||
| background-image: url('../images/chevron-down.svg'); | |||
| fill: $color-primary; | |||
| -webkit-text-stroke-color: $color-primary; | |||
| background-position: center; | |||
| width: pxToRem(20px); | |||
| height: pxToRem(20px); | |||
| position: absolute; | |||
| left: pxToRem(-22px); | |||
| transition: all 0.2s; | |||
| } | |||
| } | |||
| .ais-SearchBox { | |||
| display: flex; | |||
| justify-content: flex-end; | |||
| margin-bottom: pxToRem(24px); | |||
| } | |||
| .ais-SearchBox-input { | |||
| border: none; | |||
| color: $blue; | |||
| font-size: pxToRem(16px); | |||
| letter-spacing: 0; | |||
| line-height: 1.5; | |||
| outline: none; | |||
| -moz-appearance: none; | |||
| -webkit-appearance: none; | |||
| flex-grow: 1; | |||
| &::placeholder { | |||
| color: $blue; | |||
| font-size: pxToRem(16px); | |||
| } | |||
| @include media-below($bp-xl) { | |||
| font-size: pxToRemMd(16px); | |||
| &::placeholder { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| } | |||
| .ais-SearchBox-form { | |||
| border: 1px solid $grey-6; | |||
| border-radius: $border-radius; | |||
| overflow: hidden; | |||
| padding: 0 pxToRem(12px); | |||
| height: pxToRem(33px); | |||
| align-items: center; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| min-width: pxToRem(340px); | |||
| } | |||
| .ais-SearchBox-submit, | |||
| .ais-SearchBox-reset { | |||
| border: none; | |||
| background: transparent; | |||
| outline: none; | |||
| height: pxToRem(16px); | |||
| > svg { | |||
| color: $blue-1; | |||
| fill: $blue-1; | |||
| } | |||
| } | |||
| .ais-SearchBox-submitIcon { | |||
| width: pxToRem(14px); | |||
| height: pxToRem(14px); | |||
| color: $blue-1; | |||
| fill: $blue-1; | |||
| } | |||
| .ais-SearchBox-resetIcon { | |||
| width: pxToRem(14px); | |||
| height: pxToRem(14px); | |||
| } | |||
| .ais-SearchBox-reset { | |||
| margin-left: pxToRem(10px); | |||
| cursor: pointer; | |||
| } | |||
| .c-plaid-link { | |||
| padding: 0 !important; | |||
| background: transparent !important; | |||
| border-width: 0 !important; | |||
| border-radius: 0 !important; | |||
| box-shadow: $box-shadow !important; | |||
| &.c-plaid-link--select-card { | |||
| margin-top: pxToRem(40px); | |||
| .c-select-card__button { | |||
| margin-top: 0; | |||
| } | |||
| } | |||
| } | |||
| .ais-InfiniteHitsWrap { | |||
| min-height: pxToRem(200px); | |||
| } | |||
| .ais-Highlight-highlighted { | |||
| background: #fff1d6; | |||
| font-style: normal; | |||
| } | |||
| .acsb-trigger { | |||
| display: none !important; | |||
| visibility: hidden !important; | |||
| width: 0 !important; | |||
| height: 0 !important; | |||
| } | |||
| .ais-CurrentRefinements-list { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| > :not(:last-child) { | |||
| margin-right: pxToRem(16px); | |||
| } | |||
| } | |||
| .ais-CurrentRefinements-item { | |||
| border-radius: $border-radius; | |||
| background-color: $dark-blue; | |||
| padding: pxToRem(4px) pxToRem(8px); | |||
| flex-shrink: 0; | |||
| margin-bottom: pxToRem(16px); | |||
| } | |||
| .ais-CurrentRefinements-item-link { | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| font-weight: 600; | |||
| color: $white; | |||
| display: flex; | |||
| align-items: center; | |||
| text-decoration: none; | |||
| } | |||
| .ais-CurrentRefinements-close { | |||
| color: $white; | |||
| width: pxToRem(24px); | |||
| margin-left: pxToRem(8px); | |||
| } | |||
| .recharts-surface { | |||
| overflow: visible; | |||
| } | |||
| .recharts-cartesian-axis-tick-value { | |||
| color: #9aa1a9; | |||
| font-size: 10px; | |||
| letter-spacing: 0; | |||
| line-height: 20px; | |||
| } | |||
| .recharts-tooltip-wrapper:empty{ | |||
| display: 'none', | |||
| } | |||
| .recharts-text{ | |||
| &.recharts-pie-label-text{ | |||
| font-size: 14px; | |||
| @include media-below($bp-xl) { | |||
| font-size: 12px; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,127 @@ | |||
| /** | |||
| * Reset | |||
| * | |||
| */ | |||
| *, | |||
| *:before, | |||
| *:after { | |||
| box-sizing: border-box; | |||
| } | |||
| *, | |||
| body, | |||
| button, | |||
| input, | |||
| textarea, | |||
| select { | |||
| text-rendering: optimizeLegibility; | |||
| -moz-osx-font-smoothing: grayscale; | |||
| } | |||
| body, | |||
| div, | |||
| dl, | |||
| dt, | |||
| dd, | |||
| ul, | |||
| ol, | |||
| li, | |||
| h1, | |||
| h2, | |||
| h3, | |||
| h4, | |||
| h5, | |||
| h6, | |||
| pre, | |||
| form, | |||
| fieldset, | |||
| button, | |||
| input, | |||
| textarea, | |||
| p, | |||
| blockquote, | |||
| th, | |||
| td { | |||
| margin: 0; | |||
| padding: 0; | |||
| } | |||
| table { | |||
| border-collapse: collapse; | |||
| border-spacing: 0; | |||
| } | |||
| fieldset, | |||
| img { | |||
| border: 0; | |||
| } | |||
| address, | |||
| caption, | |||
| cite, | |||
| code, | |||
| dfn, | |||
| em, | |||
| th, | |||
| var { | |||
| font-style: normal; | |||
| font-weight: normal; | |||
| } | |||
| strong { | |||
| font-weight: 800; | |||
| } | |||
| ol, | |||
| ul { | |||
| list-style: none; | |||
| } | |||
| caption, | |||
| th { | |||
| text-align: left; | |||
| } | |||
| q:before, | |||
| q:after { | |||
| content: ''; | |||
| } | |||
| abbr, | |||
| acronym { | |||
| border: 0; | |||
| } | |||
| svg { | |||
| flex-shrink: 0; | |||
| } | |||
| textarea, | |||
| input:matches([type='email'], [type='number'], [type='password'], [type='search'], [type='tel'], [type='text'], [type='url']) { | |||
| -webkit-appearance: none; | |||
| &::-webkit-autofill, | |||
| &::-webkit-contacts-auto-fill-button, | |||
| &::-webkit-credentials-auto-fill-button { | |||
| visibility: hidden; | |||
| user-select: none; | |||
| display: none !important; | |||
| position: absolute; | |||
| } | |||
| } | |||
| input[type='number']::-webkit-inner-spin-button, | |||
| input[type='number']::-webkit-outer-spin-button { | |||
| -webkit-appearance: none; | |||
| margin: 0; | |||
| &::-webkit-autofill, | |||
| &::-webkit-contacts-auto-fill-button, | |||
| &::-webkit-credentials-auto-fill-button { | |||
| visibility: hidden; | |||
| user-select: none; | |||
| display: none !important; | |||
| position: absolute; | |||
| } | |||
| } | |||
| @@ -0,0 +1,57 @@ | |||
| body, | |||
| div, | |||
| dl, | |||
| dt, | |||
| dd, | |||
| ul, | |||
| ol, | |||
| li, | |||
| h1, | |||
| h2, | |||
| h3, | |||
| h4, | |||
| h5, | |||
| h6, | |||
| pre, | |||
| form, | |||
| fieldset, | |||
| button, | |||
| input, | |||
| textarea, | |||
| p, | |||
| blockquote, | |||
| th, | |||
| td { | |||
| font-family: $font-family; | |||
| } | |||
| p { | |||
| vertical-align: middle; | |||
| display: inline-block; | |||
| word-break: break-word; | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| @include media-below($bp-md) { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| a { | |||
| font-size: inherit; | |||
| line-height: inherit; | |||
| color: inherit; | |||
| } | |||
| strong { | |||
| font-weight: bold; | |||
| } | |||
| h1, | |||
| h2, | |||
| h3, | |||
| h4, | |||
| h5, | |||
| h6 { | |||
| font-weight: 500; | |||
| } | |||
| @@ -0,0 +1,39 @@ | |||
| .u-mr-24 { | |||
| margin-right: 24px; | |||
| } | |||
| .u-ml-32 { | |||
| margin-left: pxToRem(32px); | |||
| } | |||
| .u-position-relative { | |||
| position: relative; | |||
| } | |||
| .u-column { | |||
| @include flex-column; | |||
| } | |||
| .u-display-none { | |||
| display: none; | |||
| } | |||
| .u-superscript { | |||
| font-size: pxToRem(14px); | |||
| font-weight: medium; | |||
| } | |||
| .u-text-align-right { | |||
| text-align: right; | |||
| } | |||
| .u-hide { | |||
| width: 0; | |||
| height: 0; | |||
| visibility: hidden; | |||
| display: none; | |||
| position: fixed; | |||
| top: -20px; | |||
| right: -20px; | |||
| z-index: -1; | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| $base-font-size: 16px; | |||
| $base-font-size-md: 14px; | |||
| $font-family: 'Avenir Next'; | |||
| // Colors | |||
| $color-primary: #024f86; | |||
| $color-primary-light: #024f86; | |||
| $color-primary-dark: #003246; | |||
| $yellow: #ffeac1; | |||
| $white: #ffffff; | |||
| $grey: #f4f4f4; | |||
| $grey-1: #ccced0; | |||
| $grey-2: #fafafa; | |||
| $grey-3: #c2c5c6; | |||
| $grey-4: #d8d8d8; | |||
| $grey-5: #808285; | |||
| $grey-6: #c9d6db; | |||
| $grey-7: rgba(128, 130, 133, 0.5); | |||
| $grey-8: rgba(201, 214, 219, 0.25); | |||
| $grey-9: #ebeff2; | |||
| $grey-10: #f0f5f6; | |||
| $grey-11: #8b8b8b; | |||
| $grey-12: #b0bfc540; | |||
| $grey-13: #9d9ea4; | |||
| $grey-14: #f7fafa; | |||
| $grey-15: #ebf2f2; | |||
| $dark-blue: #003246; | |||
| $blue: #4e7a8c; | |||
| $blue-1: #6e8fae; | |||
| $blue-2: #024f86; | |||
| $blue-3: #0f85ec; | |||
| $blue-4: #5c7e9f; | |||
| $blue-5: #dde5e7; | |||
| $black: #000; | |||
| $black-1: rgba(0, 0, 0, 0.3); | |||
| $black-2: rgba(32, 38, 43, 0.9); | |||
| $black-4: #172029; | |||
| $black-5: #272727; | |||
| $black-6: #1d2731; | |||
| $red: #ff5028; | |||
| $success: #09846b; | |||
| $success-1: #00876a; | |||
| $success-2: #008a68; | |||
| // Shadow | |||
| $button-shadow-hover: 0 5px 6px 0 rgba(112, 120, 135, 0.24); | |||
| $button-shadow-pressed: 0 2px 4px 0 rgba(112, 120, 135, 0.24); | |||
| $box-shadow: 0 3px 8px 0 rgba(112, 120, 135, 0.24); | |||
| $account-dropdown-shadow: 0 6px 38px 0 rgba(112, 120, 135, 0.56); | |||
| // Border Radius | |||
| $border-radius: 4px; | |||
| $border-radius-md: 8px; | |||
| // Breakpoints | |||
| $bp-xxs: 325px; | |||
| $bp-xs: 400px; | |||
| $bp-sm: 576px; | |||
| $bp-md: 768px; | |||
| $bp-lg: 992px; | |||
| $bp-xl: 1200px; | |||
| $bp-xxl: 1350px; | |||
| // z-index | |||
| $index-xxs: 1; | |||
| $index-xs: 2; | |||
| $index-sm: 3; | |||
| $index-md: 4; | |||
| $index-lg: 5; | |||
| $index-xl: 6; | |||
| $index-xxl: 7; | |||
| @@ -0,0 +1,60 @@ | |||
| .c-button { | |||
| display: inline-flex; | |||
| align-items: center; | |||
| border-radius: $border-radius; | |||
| background-color: $color-primary; | |||
| box-shadow: 0 2px 4px 0 rgba(112, 120, 135, 0.24); | |||
| border: transparent; | |||
| padding: 8px 0; | |||
| color: $white; | |||
| width: 100%; | |||
| text-align: center; | |||
| justify-content: center; | |||
| font-family: "Avenir Next"; | |||
| font-size: pxToRem(18px); | |||
| font-weight: 600; | |||
| letter-spacing: 0; | |||
| line-height: 26px; | |||
| outline: none; | |||
| text-transform: uppercase; | |||
| transition: all 0.3s ease-in-out; | |||
| cursor: pointer; | |||
| &.c-button--clean { | |||
| background: transparent; | |||
| border: 1px solid $color-primary; | |||
| color: $color-primary; | |||
| &:hover { | |||
| border-color: $color-primary-light; | |||
| color: $color-primary-light; | |||
| background-color: transparent; | |||
| } | |||
| &:active { | |||
| border-color: $color-primary-dark; | |||
| color: $color-primary-dark; | |||
| } | |||
| } | |||
| &.c-button--dropdown { | |||
| justify-content: flex-end; | |||
| padding: 8px 14px; | |||
| background-image: url("../../images/down.svg"); | |||
| background-repeat: no-repeat; | |||
| background-position: 8% 50%; | |||
| } | |||
| &[disabled] { | |||
| pointer-events: none; | |||
| opacity: 0.5; | |||
| } | |||
| &:hover { | |||
| background-color: $color-primary-light; | |||
| } | |||
| &:active { | |||
| background-color: $color-primary-dark; | |||
| } | |||
| } | |||
| @@ -0,0 +1,45 @@ | |||
| .c-auth-card { | |||
| max-width: pxToRem(624px); | |||
| width: 100%; | |||
| box-shadow: $box-shadow; | |||
| border-radius: $border-radius; | |||
| border: 1px solid $color-primary-light; | |||
| padding: pxToRem(24px) pxToRem(40px) pxToRem(32px); | |||
| @include media-below($bp-md) { | |||
| border: none; | |||
| box-shadow: none; | |||
| padding: 0; | |||
| max-width: 100%; | |||
| .c-auth-card__title { | |||
| text-align: left; | |||
| font-size: pxToRemMd(36px); | |||
| margin-bottom: pxToRemMd(6px); | |||
| } | |||
| .c-auth-card__subtitle { | |||
| font-size: pxToRemMd(16px); | |||
| text-align: left; | |||
| } | |||
| } | |||
| } | |||
| .c-auth-card__title { | |||
| text-align: left; | |||
| font-size: pxToRem(36px); | |||
| line-height: 1.22; | |||
| color: $dark-blue; | |||
| font-weight: 400; | |||
| margin-bottom: pxToRem(16px); | |||
| } | |||
| .c-auth-card__subtitle { | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| letter-spacing: 0; | |||
| color: $color-primary; | |||
| text-align: left; | |||
| width: 100%; | |||
| font-weight: 600; | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| .c-auth { | |||
| @include flex-center; | |||
| flex-direction: column; | |||
| padding-bottom: pxToRem(56px); | |||
| @include media-below($bp-md) { | |||
| padding: 0 pxToRemMd(24px) pxToRemMd(92px); | |||
| .c-auth__title { | |||
| margin: pxToRemMd(48px) auto; | |||
| font-size: pxToRemMd(24px); | |||
| line-height: 1.35; | |||
| } | |||
| } | |||
| } | |||
| .c-auth__title { | |||
| margin: pxToRem(56px) auto pxToRem(80px); | |||
| font-size: pxToRem(36px); | |||
| line-height: 1.22; | |||
| color: $dark-blue; | |||
| font-weight: bold; | |||
| } | |||
| @@ -0,0 +1,173 @@ | |||
| .c-btn { | |||
| @include outline-none; | |||
| @include button-clear; | |||
| @include flex-center; | |||
| font-size: pxToRem(18px); | |||
| line-height: 1.35; | |||
| padding: pxToRem(8px) pxToRem(8px); | |||
| border-radius: $border-radius; | |||
| box-shadow: $button-shadow-pressed; | |||
| color: inherit; | |||
| font-weight: 600; | |||
| letter-spacing: 0; | |||
| text-align: center; | |||
| text-transform: uppercase; | |||
| user-select: none; | |||
| white-space: nowrap; | |||
| min-width: pxToRem(120px); | |||
| flex-shrink: 0; | |||
| cursor: pointer; | |||
| transition: background-color 0.2s, color 0.2s; | |||
| &:disabled { | |||
| opacity: 0.5; | |||
| cursor: auto; | |||
| } | |||
| &.c-btn--primary { | |||
| background-color: $color-primary; | |||
| color: $white; | |||
| border: 1px solid $color-primary; | |||
| &:disabled { | |||
| &:hover { | |||
| background-color: $color-primary; | |||
| box-shadow: none; | |||
| } | |||
| } | |||
| &:hover { | |||
| background-color: $color-primary-light; | |||
| box-shadow: $button-shadow-hover; | |||
| } | |||
| &:focus, | |||
| &:active { | |||
| background-color: $color-primary-dark; | |||
| box-shadow: $button-shadow-pressed; | |||
| } | |||
| } | |||
| &.c-btn--primary-outlined { | |||
| background-color: transparent; | |||
| color: $color-primary; | |||
| border: 1px solid $color-primary; | |||
| &:disabled { | |||
| &:hover { | |||
| color: $color-primary; | |||
| border: 1px solid $color-primary; | |||
| } | |||
| } | |||
| &:hover { | |||
| color: $color-primary; | |||
| border: 1px solid $color-primary; | |||
| } | |||
| &:focus, | |||
| &:active { | |||
| color: $color-primary; | |||
| border: 1px solid $color-primary; | |||
| } | |||
| } | |||
| &.c-btn--blue { | |||
| background-color: $blue-3; | |||
| color: $white; | |||
| background-color: $blue-3; | |||
| } | |||
| &.c-btn--white { | |||
| background-color: $white; | |||
| color: $grey-3; | |||
| border: 1px solid $grey-4; | |||
| box-shadow: $box-shadow; | |||
| &:disabled { | |||
| &:hover { | |||
| background-color: $white; | |||
| color: $grey-3; | |||
| } | |||
| } | |||
| &:hover { | |||
| color: $grey-5; | |||
| } | |||
| &:focus, | |||
| &:active { | |||
| background-color: $grey; | |||
| } | |||
| } | |||
| &.c-btn--primary-clear { | |||
| background-color: transparent; | |||
| color: $color-primary; | |||
| border: none; | |||
| box-shadow: none; | |||
| padding: 0; | |||
| } | |||
| &.c-btn--auto { | |||
| min-width: auto; | |||
| } | |||
| &.c-btn--sm { | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| padding: pxToRem(4px) pxToRem(15px); | |||
| } | |||
| &.c-btn--capitalize { | |||
| text-transform: capitalize; | |||
| } | |||
| &.c-btn--bank-acount-card { | |||
| padding: 0 pxToRem(16px); | |||
| min-height: pxToRem(32px); | |||
| min-width: pxToRem(120px); | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| } | |||
| &.c-btn--hidden { | |||
| visibility: hidden; | |||
| height: 0; | |||
| } | |||
| @include media-below($bp-md) { | |||
| padding: pxToRemMd(4px) pxToRemMd(25px); | |||
| font-size: pxToRemMd(16px); | |||
| line-height: 1.5; | |||
| min-width: pxToRemMd(80px); | |||
| &.c-btn--auth { | |||
| padding: pxToRemMd(12px) pxToRemMd(25px); | |||
| line-height: 1.35; | |||
| font-size: pxToRemMd(18px); | |||
| } | |||
| &.c-btn--sm { | |||
| padding: pxToRemMd(4px) pxToRemMd(15px); | |||
| } | |||
| &.c-btn--bank-acount-card { | |||
| flex-grow: 1; | |||
| min-height: pxToRemMd(40px); | |||
| padding: pxToRemMd(8px) pxToRemMd(16px); | |||
| font-size: pxToRemMd(18px); | |||
| line-height: 1.33; | |||
| } | |||
| &.c-btn--lg { | |||
| padding: pxToRemMd(7.5px) pxToRemMd(15px); | |||
| font-size: pxToRemMd(18px); | |||
| line-height: 1.5; | |||
| } | |||
| } | |||
| @include media-below($bp-xs) { | |||
| white-space: unset; | |||
| } | |||
| } | |||
| @@ -0,0 +1,46 @@ | |||
| .c-error-page { | |||
| margin-top: pxToRem(120px); | |||
| @include media-below($bp-md) { | |||
| margin-top: pxToRemMd(120px); | |||
| .c-error-page__title { | |||
| font-size: pxToRemMd(160px); | |||
| margin-bottom: pxToRemMd(27px); | |||
| } | |||
| .c-error-page__text { | |||
| margin-bottom: pxToRem(24px); | |||
| } | |||
| } | |||
| } | |||
| .c-error-page__content-container { | |||
| @include flex-center; | |||
| } | |||
| .c-error-page__content { | |||
| @include flex-column; | |||
| align-items: center; | |||
| padding: 0 pxToRem(32px); | |||
| } | |||
| .c-error-page__title { | |||
| font-size: pxToRem(160px); | |||
| line-height: 1.35; | |||
| color: $dark-blue; | |||
| margin-bottom: pxToRem(32px); | |||
| color: $color-primary; | |||
| font-weight: bold; | |||
| } | |||
| .c-error-page__text { | |||
| font-weight: 600; | |||
| margin-bottom: pxToRem(24px); | |||
| text-align: center; | |||
| } | |||
| .c-error-page__button { | |||
| margin-bottom: pxToRem(16px); | |||
| min-width: pxToRem(250px); | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| .c-reset-security { | |||
| padding-top: pxToRem(56px); | |||
| @include media-below($bp-md) { | |||
| padding-top: pxToRemMd(40px); | |||
| .c-reset-security__button { | |||
| width: 100%; | |||
| margin-top: pxToRemMd(44px); | |||
| } | |||
| } | |||
| } | |||
| .c-reset-security__question { | |||
| color: $dark-blue; | |||
| font-weight: 600; | |||
| margin-bottom: pxToRem(20px); | |||
| } | |||
| .c-reset-security__button { | |||
| width: 100%; | |||
| margin-top: pxToRem(48px); | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| .c-icon-button { | |||
| @include flex-center; | |||
| @include outline-none; | |||
| @include button-clear; | |||
| user-select: none; | |||
| cursor: pointer; | |||
| } | |||
| @@ -0,0 +1,479 @@ | |||
| .c-input { | |||
| @include flex-column; | |||
| position: relative; | |||
| &.c-input--error { | |||
| .c-input__field, | |||
| .c-select__control, | |||
| .c-select__control:hover { | |||
| border-color: $red; | |||
| } | |||
| } | |||
| &.c-input--password { | |||
| .c-input__icon { | |||
| position: absolute; | |||
| right: 0; | |||
| top: 50%; | |||
| transform: translate(0, -50%); | |||
| margin-right: pxToRem(12px); | |||
| width: pxToRem(24px); | |||
| height: pxToRem(24px); | |||
| display: flex; | |||
| svg { | |||
| width: pxToRem(24px); | |||
| height: pxToRem(24px); | |||
| color: $black; | |||
| } | |||
| } | |||
| .c-input__caps-lock { | |||
| position: absolute; | |||
| right: 0; | |||
| top: 50%; | |||
| transform: translate(0, -50%); | |||
| margin-right: pxToRem(40px); | |||
| width: pxToRem(24px); | |||
| height: pxToRem(24px); | |||
| display: flex; | |||
| width: pxToRem(24px); | |||
| height: pxToRem(24px); | |||
| color: $black; | |||
| } | |||
| .c-input__field { | |||
| padding-right: pxToRem(72px); | |||
| } | |||
| } | |||
| &.c-input--demi-bold { | |||
| .c-input__field { | |||
| font-weight: 600; | |||
| } | |||
| } | |||
| &.c-input--search { | |||
| position: relative; | |||
| width: 100%; | |||
| .c-input__icon { | |||
| width: pxToRem(24px); | |||
| height: pxToRem(24px); | |||
| position: absolute; | |||
| right: 0; | |||
| top: 50%; | |||
| transform: translate(0, -50%); | |||
| color: $blue-1; | |||
| margin-right: pxToRem(12px); | |||
| } | |||
| &.c-input--search-management { | |||
| max-width: pxToRem(344px); | |||
| margin-right: pxToRem(24px); | |||
| .c-input__field { | |||
| height: pxToRem(34px); | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.5; | |||
| letter-spacing: 0; | |||
| } | |||
| } | |||
| } | |||
| &.c-input--center-text { | |||
| input { | |||
| text-align: center; | |||
| } | |||
| } | |||
| @include media-below($bp-xl) { | |||
| &.c-input--search { | |||
| &.c-input--search-management { | |||
| max-width: 100%; | |||
| margin-right: pxToRemMd(16px); | |||
| .c-input__field { | |||
| height: pxToRemMd(32px); | |||
| font-size: pxToRemMd(16px); | |||
| line-height: 1.5; | |||
| letter-spacing: 0; | |||
| } | |||
| } | |||
| } | |||
| .c-input__label { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| .c-input__field { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| .c-input__error { | |||
| font-size: pxToRemMd(14px); | |||
| } | |||
| .c-select__control { | |||
| &.c-select__control { | |||
| font-size: pxToRemMd(16px); | |||
| min-height: 0; | |||
| .c-select__input, | |||
| .c-select__placeholder { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| .c-select__indicator { | |||
| > svg { | |||
| width: pxToRemMd(16px); | |||
| height: pxToRemMd(16px); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .c-select__menu { | |||
| .c-select__option, | |||
| .c-select__menu-notice { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| .c-input__link { | |||
| a, | |||
| span { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| //Overide | |||
| .c-password-strength__container { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| .c-phone-number { | |||
| .PhoneInput { | |||
| font-size: pxToRemMd(16px); | |||
| &::placeholder { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| .PhoneInputInput { | |||
| font-size: pxToRemMd(16px); | |||
| } | |||
| } | |||
| } | |||
| &.c-input--dropdown-full-height { | |||
| .c-select__menu { | |||
| max-height: initial; | |||
| } | |||
| } | |||
| } | |||
| .c-input__label { | |||
| color: $blue; | |||
| font-size: pxToRem(16px); | |||
| font-weight: 600; | |||
| letter-spacing: 0; | |||
| line-height: 1.75; | |||
| margin-bottom: pxToRem(4px); | |||
| } | |||
| .c-input__field-wrap { | |||
| width: 100%; | |||
| position: relative; | |||
| } | |||
| .c-input__field { | |||
| @include outline-none; | |||
| border: 1px solid $grey-6; | |||
| border-radius: $border-radius; | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| height: pxToRem(50px); | |||
| padding: 0 pxToRem(12px); | |||
| color: $blue; | |||
| background-color: $white; | |||
| width: 100%; | |||
| &:disabled { | |||
| background-color: $grey-8; | |||
| border-color: $grey-6; | |||
| } | |||
| &:focus { | |||
| border-color: $color-primary; | |||
| } | |||
| } | |||
| .c-input__error { | |||
| position: absolute; | |||
| top: 100%; | |||
| left: 0; | |||
| right: 0; | |||
| color: $red; | |||
| font-size: pxToRem(14px); | |||
| line-height: 1.35; | |||
| font-weight: 500; | |||
| margin: pxToRem(4px) 0; | |||
| } | |||
| .c-select__control { | |||
| &.c-select__control { | |||
| @include outline-none; | |||
| border: 1px solid $grey-6; | |||
| border-radius: $border-radius; | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| height: pxToRem(50px); | |||
| padding: 0 pxToRem(12px); | |||
| color: $blue; | |||
| background-color: $white; | |||
| box-shadow: none; | |||
| &:hover { | |||
| border-color: $grey-6; | |||
| } | |||
| &.c-select__control--is-focused { | |||
| border-color: $color-primary; | |||
| box-shadow: none; | |||
| &:hover { | |||
| border-color: $color-primary; | |||
| } | |||
| &.c-select__control--menu-is-open{ | |||
| .c-select__indicator { | |||
| svg { | |||
| transform: rotate(-180deg); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .css-1uccc91-singleValue { | |||
| color: $blue; | |||
| margin: 0; | |||
| } | |||
| .css-b8ldur-Input { | |||
| margin: 0; | |||
| } | |||
| .c-select__value-container { | |||
| height: 100%; | |||
| padding: 0; | |||
| padding-right: pxToRem(32px); | |||
| } | |||
| .c-select__input, | |||
| .c-select__placeholder { | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| letter-spacing: 0; | |||
| color: $blue; | |||
| } | |||
| .c-select__indicator-separator { | |||
| display: none; | |||
| } | |||
| .c-select__indicator { | |||
| padding: 0; | |||
| > svg { | |||
| width: pxToRem(16px); | |||
| height: pxToRem(16px); | |||
| color: $blue; | |||
| transform: rotate(0); | |||
| transition: transform 0.2s; | |||
| } | |||
| } | |||
| &.c-select__control--is-disabled { | |||
| background-color: $grey-8; | |||
| } | |||
| } | |||
| } | |||
| .c-select__menu { | |||
| @include flex-column; | |||
| position: absolute; | |||
| top: 100%; | |||
| left: 0; | |||
| right: 0; | |||
| margin-top: pxToRem(4px); | |||
| margin-bottom: pxToRem(4px); | |||
| border: 1px solid $grey-6; | |||
| border-radius: $border-radius; | |||
| box-shadow: $box-shadow; | |||
| max-height: pxToRem(150px); | |||
| overflow: auto; | |||
| .c-select__menu-list { | |||
| @include flex-column; | |||
| padding: 0; | |||
| flex-grow: 1; | |||
| } | |||
| .c-select__option, | |||
| .c-select__menu-notice { | |||
| padding: pxToRem(12px) pxToRem(15px); | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| letter-spacing: 0; | |||
| color: $blue; | |||
| text-align: left; | |||
| &:hover { | |||
| background-color: $grey-2; | |||
| } | |||
| &.c-select__option--is-selected { | |||
| background-color: $grey-2; | |||
| } | |||
| &.c-select__option--is-focused { | |||
| background-color: $grey-2; | |||
| } | |||
| } | |||
| } | |||
| .c-input__link { | |||
| position: absolute; | |||
| top: 0; | |||
| right: 0; | |||
| a, | |||
| span { | |||
| color: $grey-11; | |||
| font-size: pxToRem(16px); | |||
| letter-spacing: 0; | |||
| line-height: 1.15; | |||
| text-decoration: underline; | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| //Overide | |||
| .c-password-strength__container { | |||
| margin-top: pxToRem(8px); | |||
| font-size: pxToRem(16px); | |||
| & .c-password-strength__line--wrapper { | |||
| border-radius: 8px; | |||
| overflow: hidden; | |||
| background-color: $grey; | |||
| height: pxToRem(5px); | |||
| .c-password-strength__line { | |||
| height: pxToRem(5px); | |||
| left: 0; | |||
| top: 0; | |||
| } | |||
| } | |||
| } | |||
| .c-password { | |||
| min-height: pxToRem(110px); | |||
| @include media-below($bp-xl) { | |||
| min-height: pxToRemMd(110px); | |||
| } | |||
| } | |||
| .c-phone-number { | |||
| .PhoneInput { | |||
| @include outline-none; | |||
| box-sizing: border-box; | |||
| border: 1px solid $grey-6; | |||
| border-radius: $border-radius; | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| min-height: pxToRem(50px); | |||
| color: $blue; | |||
| background-color: $white; | |||
| box-shadow: none; | |||
| width: 100%; | |||
| overflow: hidden; | |||
| &::placeholder { | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| } | |||
| &:disabled { | |||
| background-color: $grey-8; | |||
| border-color: $grey-6; | |||
| } | |||
| &.PhoneInput--focus { | |||
| border-color: $color-primary; | |||
| .PhoneInputCountry { | |||
| border-color: $color-primary; | |||
| } | |||
| } | |||
| } | |||
| .PhoneInputCountry { | |||
| @include flex-center; | |||
| width: pxToRem(96px); | |||
| border-right: 1px solid $grey-6; | |||
| } | |||
| .PhoneInputCountryIcon { | |||
| margin-right: pxToRem(16px); | |||
| width: auto; | |||
| height: auto; | |||
| border: none; | |||
| } | |||
| .PhoneInputCountryIconImg { | |||
| width: pxToRem(36px); | |||
| object-fit: contain; | |||
| } | |||
| .PhoneInputCountrySelectArrow { | |||
| border: none; | |||
| width: 0; | |||
| height: 0; | |||
| transform: translate(0); | |||
| border-left: pxToRem(8px) solid transparent; | |||
| border-right: pxToRem(8px) solid transparent; | |||
| border-top: pxToRem(8px) solid $blue; | |||
| } | |||
| .PhoneInputInput { | |||
| @include outline-none; | |||
| border-color: transparent; | |||
| height: 100%; | |||
| font-size: pxToRem(16px); | |||
| line-height: 1.75; | |||
| padding: 0; | |||
| color: $blue; | |||
| background-color: $white; | |||
| width: 100%; | |||
| margin: 0; | |||
| padding: 0 pxToRem(26px); | |||
| height: pxToRem(50px); | |||
| } | |||
| .PhoneInputCountry { | |||
| margin-right: 0; | |||
| } | |||
| &.c-input--error { | |||
| .PhoneInput { | |||
| border-color: $red; | |||
| .PhoneInputCountry { | |||
| border-color: $red; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| .c-loader__wrapper { | |||
| @include flex-column; | |||
| flex: 1 1 auto; | |||
| position: relative; | |||
| min-height: 0; | |||
| min-width: 0; | |||
| &.c-loader__wrapper--block { | |||
| box-shadow: $box-shadow; | |||
| .c-loader { | |||
| position: relative; | |||
| top: unset; | |||
| left: unset; | |||
| right: unset; | |||
| bottom: unset; | |||
| } | |||
| } | |||
| &.c-loader__wrapper--full-height { | |||
| height: 100%; | |||
| } | |||
| &.c-loader__wrapper--no-shadow { | |||
| box-shadow: none; | |||
| } | |||
| .c-loader { | |||
| @include flex-center; | |||
| width: 100%; | |||
| height: 100%; | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| bottom: 0; | |||
| padding: pxToRem(15px) 0; | |||
| background-color: rgba(255, 255, 255, 0.4); | |||
| z-index: $index-lg; | |||
| &.c-loader--page { | |||
| position: fixed; | |||
| .c-loader__icon { | |||
| border: 20px solid transparent; | |||
| width: pxToRem(200px); | |||
| height: pxToRem(200px); | |||
| border-bottom-color: $color-primary; | |||
| border-top-color: $color-primary; | |||
| } | |||
| } | |||
| } | |||
| .c-loader__icon { | |||
| border-radius: 50%; | |||
| border: 10px solid transparent; | |||
| border-bottom-color: $color-primary; | |||
| border-top-color: $color-primary; | |||
| animation: 1s loader-animation linear infinite; | |||
| width: pxToRem(100px); | |||
| height: pxToRem(100px); | |||
| } | |||
| @keyframes loader-animation { | |||
| 0% { | |||
| transform: rotate(0deg); | |||
| } | |||
| 100% { | |||
| transform: rotate(360deg); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,31 @@ | |||
| .c-login-card { | |||
| border: 1px solid $color-primary-light; | |||
| border-radius: $border-radius; | |||
| box-shadow: $box-shadow; | |||
| max-width: pxToRem(624px); | |||
| width: 100%; | |||
| margin: pxToRem(28px) auto 0; | |||
| padding: pxToRem(36px) pxToRem(40px) pxToRem(32px); | |||
| } | |||
| .c-login-card__note { | |||
| color: $color-primary; | |||
| font-weight: 600; | |||
| margin-bottom: pxToRem(37px); | |||
| } | |||
| .c-login-card__form { | |||
| display: grid; | |||
| grid-row-gap: pxToRem(24px); | |||
| } | |||
| .c-login-card__submit { | |||
| margin-top: pxToRem(24px); | |||
| width: 100%; | |||
| } | |||
| .c-login-card__question { | |||
| color: $blue; | |||
| font-weight: 600; | |||
| margin-bottom: pxToRem(16px); | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| .c-login { | |||
| &.c-login--user { | |||
| .c-login__form { | |||
| .c-input:first-child { | |||
| margin-bottom: pxToRem(20px); | |||
| } | |||
| } | |||
| } | |||
| @include media-below($bp-xl) { | |||
| .c-login__link { | |||
| margin-top: pxToRemMd(70px); | |||
| } | |||
| } | |||
| @include media-below($bp-md) { | |||
| .c-login__form { | |||
| margin: pxToRemMd(36px) 0 0; | |||
| } | |||
| .c-login__button { | |||
| margin-bottom: pxToRemMd(40px); | |||
| margin-top: pxToRemMd(36px); | |||
| } | |||
| .c-login__link { | |||
| margin-top: pxToRemMd(80px); | |||
| } | |||
| &.c-login--user { | |||
| .c-login__form { | |||
| .c-input:first-child { | |||
| margin-bottom: pxToRemMd(20px); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .c-login__link { | |||
| color: $color-primary; | |||
| font-weight: 600; | |||
| margin-top: pxToRem(40px); | |||
| width: max-content; | |||
| } | |||
| .c-login__form { | |||
| margin: pxToRem(36px) 0 0; | |||
| > form { | |||
| @include flex-column; | |||
| } | |||
| } | |||
| .c-login__button { | |||
| width: 100%; | |||
| margin-top: pxToRem(68px); | |||
| margin-bottom: pxToRem(24px); | |||
| } | |||
| .c-login__text { | |||
| text-align: center; | |||
| width: 100%; | |||
| color: $blue; | |||
| a { | |||
| color: $color-primary; | |||
| font-weight: bold; | |||
| letter-spacing: inherit; | |||
| font-size: inherit; | |||
| line-height: inherit; | |||
| } | |||
| } | |||
| @@ -0,0 +1,169 @@ | |||
| $header-height-desktop: pxToRem(80px); | |||
| $header-height-mobile: pxToRemMd(74px); | |||
| .c-modal-wrap { | |||
| position: fixed; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| bottom: 0; | |||
| z-index: $index-xl; | |||
| background-color: $black-1; | |||
| &.c-modal-wrap--no-bg { | |||
| background-color: transparent; | |||
| } | |||
| &.c-modal-wrap--over-modal { | |||
| background-color: transparent; | |||
| z-index: $index-xxl; | |||
| } | |||
| &.c-modal-wrap--sm { | |||
| .c-modal { | |||
| max-width: pxToRem(390px); | |||
| width: 100%; | |||
| } | |||
| .c-modal__header { | |||
| padding: pxToRem(12px); | |||
| } | |||
| } | |||
| .c-modal__header { | |||
| padding: pxToRem(12px); | |||
| } | |||
| &.c-modal-wrap--md { | |||
| .c-modal { | |||
| max-width: pxToRem(521px); | |||
| width: 100%; | |||
| } | |||
| .c-modal__header { | |||
| padding: pxToRem(12px) pxToRem(20px); | |||
| } | |||
| } | |||
| &.c-modal-wrap--lg { | |||
| .c-modal { | |||
| max-width: pxToRem(782px); | |||
| width: 100%; | |||
| } | |||
| .c-modal__header { | |||
| padding: pxToRem(12px) pxToRem(20px); | |||
| } | |||
| } | |||
| &.c-modal-wrap--close { | |||
| display: none; | |||
| } | |||
| @include media-below($bp-xl) { | |||
| &, | |||
| &.c-modal-wrap--sm, | |||
| &.c-modal-wrap--md { | |||
| .c-modal { | |||
| margin: $header-height-mobile auto $header-height-mobile; | |||
| max-height: calc(100vh - #{2 * $header-height-mobile}); | |||
| } | |||
| } | |||
| } | |||
| @include media-below($bp-md) { | |||
| &, | |||
| &.c-modal-wrap--sm, | |||
| &.c-modal-wrap--md { | |||
| .c-modal__header { | |||
| padding: pxToRemMd(16px); | |||
| } | |||
| .c-modal__title { | |||
| font-size: pxToRemMd(16px); | |||
| line-height: 1.4; | |||
| } | |||
| .c-modal__close { | |||
| width: pxToRemMd(24px); | |||
| height: pxToRemMd(24px); | |||
| } | |||
| .c-modal__back { | |||
| width: pxToRemMd(24px); | |||
| height: pxToRemMd(24px); | |||
| margin-right: pxToRemMd(8px); | |||
| } | |||
| .c-modal__back-button { | |||
| margin-left: -#{pxToRemMd(8px)}; | |||
| } | |||
| .c-modal, | |||
| &.c-modal-wrap--lg .c-modal { | |||
| max-width: 100%; | |||
| max-height: 100vh; | |||
| height: 100%; | |||
| margin: 0; | |||
| border-radius: 0; | |||
| } | |||
| &.c-modal-wrap--mobile-modal { | |||
| display: flex; | |||
| padding: 0 pxToRemMd(20px); | |||
| .c-modal { | |||
| height: auto; | |||
| margin: auto; | |||
| border-radius: 2px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .c-modal { | |||
| @include flex-column; | |||
| box-shadow: $box-shadow; | |||
| border-radius: $border-radius; | |||
| background-color: $white; | |||
| margin: $header-height-desktop auto $header-height-desktop; | |||
| max-height: calc(100vh - #{2 * $header-height-desktop}); | |||
| position: relative; | |||
| } | |||
| .c-modal__header { | |||
| display: flex; | |||
| align-items: center; | |||
| box-shadow: $box-shadow; | |||
| z-index: $index-xxs; | |||
| } | |||
| .c-modal__title { | |||
| @include text-ellipsis; | |||
| font-size: pxToRem(16px); | |||
| font-weight: 600; | |||
| line-height: 1.5; | |||
| color: $dark-blue; | |||
| padding-right: pxToRem(10px); | |||
| margin-right: auto; | |||
| } | |||
| .c-modal__close { | |||
| width: pxToRem(16px); | |||
| height: pxToRem(16px); | |||
| color: $dark-blue; | |||
| } | |||
| .c-modal__back { | |||
| width: pxToRem(16px); | |||
| height: pxToRem(16px); | |||
| color: $dark-blue; | |||
| margin-right: pxToRem(10px); | |||
| } | |||
| .c-modal__body { | |||
| @include flex-column; | |||
| flex: 1 1 auto; | |||
| overflow: auto; | |||
| } | |||
| @@ -0,0 +1,29 @@ | |||
| .c-radio { | |||
| display: flex; | |||
| cursor: pointer; | |||
| &.c-radio--selected { | |||
| border-color: $dark-blue; | |||
| } | |||
| } | |||
| .c-radio__field { | |||
| display: none; | |||
| } | |||
| .c-radio__indicator { | |||
| margin-top: pxToRem(4px); | |||
| margin-right: pxToRem(16px); | |||
| } | |||
| .c-radio__icon { | |||
| width: pxToRem(16px); | |||
| height: pxToRem(16px); | |||
| } | |||
| .c-radio__text { | |||
| font-size: pxToRem(14px); | |||
| line-height: 1.15; | |||
| color: $blue; | |||
| user-select: none; | |||
| } | |||
| @@ -0,0 +1,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', | |||
| ], | |||
| ], | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,10 @@ | |||
| const base = { | |||
| data: { name: 'John Doe', age: 30, gender: 'male' }, | |||
| t: (text: string) => { | |||
| return text; | |||
| }, | |||
| }; | |||
| export const mockDataCardProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,28 @@ | |||
| import { Divider, Paper, Typography } from '@mui/material'; | |||
| interface IProps { | |||
| data: { | |||
| name: string; | |||
| gender: string; | |||
| age: number; | |||
| }; | |||
| t: (x: string) => string; | |||
| } | |||
| const DataCard: React.FC<IProps> = ({ data, t }) => { | |||
| return ( | |||
| <Paper sx={{ p: 3, height: '100%' }} elevation={3}> | |||
| <Typography sx={{ fontWeight: 600 }}>{t('Name')}</Typography> | |||
| <Typography display="inline"> {data.name}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>{t('Age')}</Typography> | |||
| <Typography display="inline"> {data.age}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>{t('Gender')}</Typography> | |||
| <Typography display="inline"> {data.gender}</Typography> | |||
| <Divider /> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default DataCard; | |||
| @@ -0,0 +1,7 @@ | |||
| const base = { | |||
| profileData: { name: 'John Doe' }, | |||
| }; | |||
| export const mockDataDetailsCardProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,53 @@ | |||
| import Card from '@mui/material/Card'; | |||
| import CardContent from '@mui/material/CardContent'; | |||
| import Typography from '@mui/material/Typography'; | |||
| import Image from 'next/image'; | |||
| interface IProps { | |||
| data: { | |||
| name: string; | |||
| age: number; | |||
| }; | |||
| } | |||
| const DataDetailsCard: React.FC<IProps> = ({ data }) => { | |||
| return ( | |||
| <Card | |||
| sx={{ | |||
| maxWidth: 600, | |||
| height: 200, | |||
| marginX: 'auto', | |||
| marginY: 20, | |||
| boxShadow: 10, | |||
| display: 'flex', | |||
| }} | |||
| > | |||
| <Image | |||
| src="https://www.business2community.com/wp-content/uploads/2017/08/blank-profile-picture-973460_640.png" | |||
| alt="profile picture" | |||
| width={600} | |||
| height={500} | |||
| /> | |||
| <CardContent> | |||
| <Typography | |||
| gutterBottom | |||
| variant="h5" | |||
| component="div" | |||
| sx={{ | |||
| textAlign: 'center', | |||
| marginTop: 1, | |||
| marginBottom: 3, | |||
| }} | |||
| > | |||
| {data.name}, {data.age} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur | |||
| quis odio in libero fringilla pellentesque aliquet et mi. Quisque | |||
| maximus lectus a neque luctus, tempus auctor ipsum ultrices. | |||
| </Typography> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default DataDetailsCard; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockHoverImageCardProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -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; | |||
| @@ -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; | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| const base = { | |||
| profileData: { name: 'John Doe' }, | |||
| }; | |||
| export const mockProfilePageProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,35 @@ | |||
| import Card from '@mui/material/Card'; | |||
| import CardContent from '@mui/material/CardContent'; | |||
| import Typography from '@mui/material/Typography'; | |||
| import Image from 'next/image'; | |||
| interface IProps { | |||
| profileData: { | |||
| name: string; | |||
| }; | |||
| } | |||
| const ProfileCard: React.FC<IProps> = ({ profileData }) => { | |||
| return ( | |||
| <Card sx={{ maxWidth: 345, marginX: 'auto', marginY: 10, boxShadow: 10 }}> | |||
| <Image | |||
| src="https://www.business2community.com/wp-content/uploads/2017/08/blank-profile-picture-973460_640.png" | |||
| alt="profile picture" | |||
| width={600} | |||
| height={500} | |||
| /> | |||
| <CardContent> | |||
| <Typography gutterBottom variant="h5" component="div"> | |||
| {profileData.name} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur | |||
| quis odio in libero fringilla pellentesque aliquet et mi. Quisque | |||
| maximus lectus a neque luctus, tempus auctor ipsum ultrices. | |||
| </Typography> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default ProfileCard; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockContactFormProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,123 @@ | |||
| import { | |||
| Box, | |||
| Button, | |||
| Container, | |||
| Grid, | |||
| TextField, | |||
| Typography, | |||
| } from '@mui/material'; | |||
| import { useFormik } from 'formik'; | |||
| import { useTranslation } from 'next-i18next'; | |||
| import Link from 'next/link'; | |||
| import { BASE_PAGE } from '../../../constants/pages'; | |||
| import { contactSchema } from '../../../schemas/contactSchema'; | |||
| interface FormValues { | |||
| firstName: string; | |||
| lastName: string; | |||
| email: string; | |||
| message: string; | |||
| } | |||
| const ContactForm = () => { | |||
| const { t } = useTranslation(['forms', 'contact', 'common']); | |||
| const handleSubmit = (values: FormValues) => { | |||
| console.log('Values', values); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| firstName: '', | |||
| lastName: '', | |||
| email: '', | |||
| message: '', | |||
| }, | |||
| validationSchema: contactSchema, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <Container component="main" maxWidth="md"> | |||
| <Box | |||
| sx={{ | |||
| marginTop: 32, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| alignItems: 'center', | |||
| }} | |||
| > | |||
| <Typography component="h1" variant="h5"> | |||
| {t('contact:Title')} | |||
| </Typography> | |||
| <Box | |||
| component="form" | |||
| onSubmit={formik.handleSubmit} | |||
| sx={{ position: 'relative', mt: 1, p: 1 }} | |||
| > | |||
| <TextField | |||
| name="firstName" | |||
| label={t('forms:FirstName')} | |||
| margin="normal" | |||
| value={formik.values.firstName} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.firstName && Boolean(formik.errors.firstName)} | |||
| helperText={formik.touched.firstName && formik.errors.firstName} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="lastName" | |||
| label={t('forms:LastName')} | |||
| margin="normal" | |||
| value={formik.values.lastName} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.lastName && Boolean(formik.errors.lastName)} | |||
| helperText={formik.touched.lastName && formik.errors.lastName} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="email" | |||
| label={t('forms:Email')} | |||
| margin="normal" | |||
| value={formik.values.email} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.email && Boolean(formik.errors.email)} | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="message" | |||
| label={t('forms:Message')} | |||
| multiline | |||
| margin="normal" | |||
| value={formik.values.message} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.message && Boolean(formik.errors.message)} | |||
| helperText={formik.touched.message && formik.errors.message} | |||
| rows={4} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ mt: 3, mb: 2 }} | |||
| fullWidth | |||
| > | |||
| {t('contact:SendBtn')} | |||
| </Button> | |||
| <Grid container justifyContent="center"> | |||
| <Link href={BASE_PAGE}>{t('common:Back')}</Link> | |||
| </Grid> | |||
| </Box> | |||
| </Box> | |||
| </Container> | |||
| ); | |||
| }; | |||
| export default ContactForm; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockForgotPasswordFormProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,82 @@ | |||
| import { | |||
| Box, | |||
| Button, | |||
| Container, | |||
| Grid, | |||
| TextField, | |||
| Typography, | |||
| } from '@mui/material'; | |||
| import { useFormik } from 'formik'; | |||
| import { useTranslation } from 'next-i18next'; | |||
| import Link from 'next/link'; | |||
| import { LOGIN_PAGE } from '../../../constants/pages'; | |||
| import { forgotPasswordSchema } from '../../../schemas/forgotPasswordSchema'; | |||
| interface FormValues { | |||
| email: string; | |||
| } | |||
| const ForgotPasswordForm = () => { | |||
| const { t } = useTranslation(['forms', 'forgotPass', 'common']); | |||
| const handleSubmit = (values: FormValues) => { | |||
| console.log('Values', values); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| email: '', | |||
| }, | |||
| validationSchema: forgotPasswordSchema, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <Container component="main" maxWidth="md"> | |||
| <Box | |||
| sx={{ | |||
| marginTop: 32, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| alignItems: 'center', | |||
| }} | |||
| > | |||
| <Typography component="h1" variant="h5"> | |||
| {t('forgotPass:Title')} | |||
| </Typography> | |||
| <Box | |||
| component="form" | |||
| onSubmit={formik.handleSubmit} | |||
| sx={{ position: 'relative', mt: 1, p: 1 }} | |||
| > | |||
| <TextField | |||
| name="email" | |||
| label={t('forms:Email')} | |||
| margin="normal" | |||
| value={formik.values.email} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.email && Boolean(formik.errors.email)} | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ mt: 3, mb: 2 }} | |||
| fullWidth | |||
| > | |||
| {t('forgotPass:SendBtn')} | |||
| </Button> | |||
| <Grid container justifyContent="center"> | |||
| <Link href={LOGIN_PAGE}>{t('common:Back')}</Link> | |||
| </Grid> | |||
| </Box> | |||
| </Box> | |||
| </Container> | |||
| ); | |||
| }; | |||
| export default ForgotPasswordForm; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockLoginFormProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,148 @@ | |||
| import { | |||
| Box, | |||
| Button, | |||
| Container, | |||
| Grid, | |||
| IconButton, | |||
| InputAdornment, | |||
| TextField, | |||
| Typography, | |||
| } from '@mui/material'; | |||
| import { useFormik } from 'formik'; | |||
| import { signIn } from 'next-auth/react'; | |||
| import { useTranslation } from 'next-i18next'; | |||
| import Link from 'next/link'; | |||
| import { useRouter } from 'next/router'; | |||
| import { useState } from 'react'; | |||
| import { | |||
| BASE_PAGE, | |||
| FORGOT_PASSWORD_PAGE, | |||
| REGISTER_PAGE, | |||
| } from '../../../constants/pages'; | |||
| import { loginSchema } from '../../../schemas/loginSchema'; | |||
| import ErrorMessageComponent from '../../mui/ErrorMessageComponent'; | |||
| interface FormValues { | |||
| username: string; | |||
| password: string; | |||
| } | |||
| const LoginForm = () => { | |||
| const { t } = useTranslation(['forms', 'login']); | |||
| const [showPassword, setShowPassword] = useState(false); | |||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||
| const router = useRouter(); | |||
| const [error, setError] = useState({ hasError: false, errorMessage: '' }); | |||
| const submitHandler = async (values: FormValues) => { | |||
| const result = await signIn('credentials', { | |||
| redirect: false, | |||
| username: values.username, | |||
| password: values.password, | |||
| }); | |||
| if (!result?.error) { | |||
| router.replace(BASE_PAGE); | |||
| } else { | |||
| setError({ hasError: true, errorMessage: result.error }); | |||
| } | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| username: '', | |||
| password: '', | |||
| }, | |||
| validationSchema: loginSchema, | |||
| onSubmit: submitHandler, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <Container component="main" maxWidth="md"> | |||
| <Box | |||
| sx={{ | |||
| marginTop: 32, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| alignItems: 'center', | |||
| }} | |||
| > | |||
| <Typography component="h1" variant="h5"> | |||
| {t('login:Title')} | |||
| </Typography> | |||
| {error.hasError && <ErrorMessageComponent error={error.errorMessage} />} | |||
| <Box | |||
| component="form" | |||
| onSubmit={formik.handleSubmit} | |||
| sx={{ position: 'relative', mt: 1, p: 1 }} | |||
| > | |||
| <TextField | |||
| name="username" | |||
| label={t('forms:Username')} | |||
| margin="normal" | |||
| value={formik.values.username} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.username && Boolean(formik.errors.username)} | |||
| helperText={formik.touched.username && formik.errors.username} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="password" | |||
| label={t('forms:Password')} | |||
| margin="normal" | |||
| type={showPassword ? 'text' : 'password'} | |||
| value={formik.values.password} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.password && Boolean(formik.errors.password)} | |||
| helperText={formik.touched.password && formik.errors.password} | |||
| fullWidth | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <InputAdornment position="end"> | |||
| <IconButton | |||
| onClick={handleClickShowPassword} | |||
| onMouseDown={handleMouseDownPassword} | |||
| ></IconButton> | |||
| </InputAdornment> | |||
| ), | |||
| }} | |||
| /> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ mt: 3, mb: 2 }} | |||
| fullWidth | |||
| > | |||
| {t('login:LoginBtn')} | |||
| </Button> | |||
| <Grid container> | |||
| <Grid | |||
| item | |||
| xs={12} | |||
| md={6} | |||
| sx={{ textAlign: { xs: 'center', md: 'left' } }} | |||
| > | |||
| <Link href={FORGOT_PASSWORD_PAGE}> | |||
| {t('login:ForgotPassword')} | |||
| </Link> | |||
| </Grid> | |||
| <Grid | |||
| item | |||
| xs={12} | |||
| md={6} | |||
| sx={{ textAlign: { xs: 'center', md: 'right' } }} | |||
| > | |||
| <Link href={REGISTER_PAGE}>{t('login:NoAccount')}</Link> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </Box> | |||
| </Container> | |||
| ); | |||
| }; | |||
| export default LoginForm; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockRegisterFormProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,204 @@ | |||
| import { | |||
| Box, | |||
| Button, | |||
| Container, | |||
| Grid, | |||
| IconButton, | |||
| InputAdornment, | |||
| TextField, | |||
| Typography, | |||
| } from '@mui/material'; | |||
| import { useFormik } from 'formik'; | |||
| import { useTranslation } from 'next-i18next'; | |||
| import Link from 'next/link'; | |||
| import { useState } from 'react'; | |||
| import { FORGOT_PASSWORD_PAGE, LOGIN_PAGE } from '../../../constants/pages'; | |||
| import { createUser } from '../../../requests/accountRequests'; | |||
| import { registerSchema } from '../../../schemas/registerSchema'; | |||
| import ErrorMessageComponent from '../../mui/ErrorMessageComponent'; | |||
| interface FormValues { | |||
| fullName: string; | |||
| username: string; | |||
| email: string; | |||
| password: string; | |||
| confirmPassword: string; | |||
| } | |||
| const RegisterForm = () => { | |||
| const { t } = useTranslation(['forms', 'register']); | |||
| const [showPassword, setShowPassword] = useState(false); | |||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||
| const [showConfirmPassword, setShowConfirmPassword] = useState(false); | |||
| const handleClickShowConfirmPassword = () => | |||
| setShowConfirmPassword(!showConfirmPassword); | |||
| const handleMouseDownConfirmPassword = () => | |||
| setShowConfirmPassword(!showConfirmPassword); | |||
| const [error, setError] = useState({ hasError: false, errorMessage: '' }); | |||
| const submitHandler = async (values: FormValues) => { | |||
| try { | |||
| const result = await createUser( | |||
| values.fullName, | |||
| values.username, | |||
| values.email, | |||
| values.password | |||
| ); | |||
| console.log(result); | |||
| } catch (error) { | |||
| if (error instanceof Error) | |||
| setError({ hasError: true, errorMessage: error.message }); | |||
| } | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| fullName: '', | |||
| username: '', | |||
| email: '', | |||
| password: '', | |||
| confirmPassword: '', | |||
| }, | |||
| validationSchema: registerSchema, | |||
| onSubmit: submitHandler, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <Container component="main" maxWidth="md"> | |||
| <Box | |||
| sx={{ | |||
| marginTop: 10, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| alignItems: 'center', | |||
| }} | |||
| > | |||
| <Typography component="h1" variant="h5"> | |||
| {t('register:Title')} | |||
| </Typography> | |||
| {error.hasError && <ErrorMessageComponent error={error.errorMessage} />} | |||
| <Box | |||
| component="form" | |||
| onSubmit={formik.handleSubmit} | |||
| sx={{ position: 'relative', mt: 1, p: 1 }} | |||
| > | |||
| <TextField | |||
| name="fullName" | |||
| label={t('forms:FullName')} | |||
| margin="normal" | |||
| value={formik.values.fullName} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.fullName && Boolean(formik.errors.fullName)} | |||
| helperText={formik.touched.fullName && formik.errors.fullName} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="username" | |||
| label={t('forms:Username')} | |||
| margin="normal" | |||
| value={formik.values.username} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.username && Boolean(formik.errors.username)} | |||
| helperText={formik.touched.username && formik.errors.username} | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="email" | |||
| label={t('forms:Email')} | |||
| margin="normal" | |||
| value={formik.values.email} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.email && Boolean(formik.errors.email)} | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="password" | |||
| label={t('forms:Password')} | |||
| margin="normal" | |||
| type={showPassword ? 'text' : 'password'} | |||
| value={formik.values.password} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.password && Boolean(formik.errors.password)} | |||
| helperText={formik.touched.password && formik.errors.password} | |||
| fullWidth | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <InputAdornment position="end"> | |||
| <IconButton | |||
| onClick={handleClickShowPassword} | |||
| onMouseDown={handleMouseDownPassword} | |||
| ></IconButton> | |||
| </InputAdornment> | |||
| ), | |||
| }} | |||
| /> | |||
| <TextField | |||
| name="confirmPassword" | |||
| label={t('forms:ConfirmPassword')} | |||
| margin="normal" | |||
| type={showPassword ? 'text' : 'password'} | |||
| value={formik.values.confirmPassword} | |||
| onChange={formik.handleChange} | |||
| error={ | |||
| formik.touched.confirmPassword && | |||
| Boolean(formik.errors.confirmPassword) | |||
| } | |||
| helperText={ | |||
| formik.touched.confirmPassword && formik.errors.confirmPassword | |||
| } | |||
| fullWidth | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <InputAdornment position="end"> | |||
| <IconButton | |||
| onClick={handleClickShowConfirmPassword} | |||
| onMouseDown={handleMouseDownConfirmPassword} | |||
| ></IconButton> | |||
| </InputAdornment> | |||
| ), | |||
| }} | |||
| /> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ mt: 3, mb: 2 }} | |||
| fullWidth | |||
| > | |||
| {t('register:RegisterBtn')} | |||
| </Button> | |||
| <Grid container> | |||
| <Grid | |||
| item | |||
| xs={12} | |||
| md={6} | |||
| sx={{ textAlign: { xs: 'center', md: 'left' } }} | |||
| > | |||
| <Link href={FORGOT_PASSWORD_PAGE}> | |||
| {t('register:ForgotPassword')} | |||
| </Link> | |||
| </Grid> | |||
| <Grid | |||
| item | |||
| xs={12} | |||
| md={6} | |||
| sx={{ textAlign: { xs: 'center', md: 'right' } }} | |||
| > | |||
| <Link href={LOGIN_PAGE}>{t('register:HaveAccount')}</Link> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </Box> | |||
| </Container> | |||
| ); | |||
| }; | |||
| export default RegisterForm; | |||
| @@ -0,0 +1,7 @@ | |||
| const base = { | |||
| children: <h1>Test</h1>, | |||
| }; | |||
| export const mockLayoutProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,16 @@ | |||
| import Navbar from '../navbar/Navbar'; | |||
| interface IProps { | |||
| children: React.ReactNode; | |||
| } | |||
| const Layout: React.FC<IProps> = (props) => { | |||
| return ( | |||
| <> | |||
| <Navbar /> | |||
| <main>{props.children}</main> | |||
| </> | |||
| ); | |||
| }; | |||
| export default Layout; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockNavbarProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,208 @@ | |||
| import AdbIcon from '@mui/icons-material/Adb'; | |||
| import MenuIcon from '@mui/icons-material/Menu'; | |||
| import AppBar from '@mui/material/AppBar'; | |||
| import Box from '@mui/material/Box'; | |||
| import Button from '@mui/material/Button'; | |||
| import Container from '@mui/material/Container'; | |||
| import IconButton from '@mui/material/IconButton'; | |||
| import Menu from '@mui/material/Menu'; | |||
| import MenuItem from '@mui/material/MenuItem'; | |||
| import Toolbar from '@mui/material/Toolbar'; | |||
| import Tooltip from '@mui/material/Tooltip'; | |||
| import Typography from '@mui/material/Typography'; | |||
| import { signOut, useSession } from 'next-auth/react'; | |||
| import Image from 'next/image'; | |||
| import Link from 'next/link'; | |||
| import { useState } from 'react'; | |||
| import { LOGIN_PAGE, PROFILE_PAGE } from '../../../constants/pages'; | |||
| const pages = ['Link 1', 'Link 2', 'Link 3', 'Link4']; | |||
| const Navbar = () => { | |||
| const { data: session } = useSession(); | |||
| const [anchorElNav, setAnchorElNav] = useState(null); | |||
| const [anchorElUser, setAnchorElUser] = useState(null); | |||
| const handleOpenNavMenu = (event: any) => { | |||
| setAnchorElNav(event.currentTarget); | |||
| }; | |||
| const handleOpenUserMenu = (event: any) => { | |||
| setAnchorElUser(event.currentTarget); | |||
| }; | |||
| const handleCloseNavMenu = () => { | |||
| setAnchorElNav(null); | |||
| }; | |||
| const handleCloseUserMenu = () => { | |||
| setAnchorElUser(null); | |||
| }; | |||
| function logoutHandler() { | |||
| signOut(); | |||
| } | |||
| return ( | |||
| <AppBar | |||
| position="static" | |||
| sx={{ zIndex: 100, position: 'fixed', top: 0, left: 0 }} | |||
| > | |||
| <Container maxWidth="xl"> | |||
| <Toolbar disableGutters> | |||
| <AdbIcon sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} /> | |||
| <Typography | |||
| variant="h6" | |||
| noWrap | |||
| sx={{ | |||
| mr: 2, | |||
| display: { xs: 'none', md: 'flex' }, | |||
| fontFamily: 'monospace', | |||
| fontWeight: 700, | |||
| letterSpacing: '.3rem', | |||
| color: 'inherit', | |||
| textDecoration: 'none', | |||
| }} | |||
| > | |||
| LOGO | |||
| </Typography> | |||
| <Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }}> | |||
| <IconButton | |||
| size="large" | |||
| aria-label="account of current user" | |||
| aria-controls="menu-appbar" | |||
| aria-haspopup="true" | |||
| onClick={handleOpenNavMenu} | |||
| color="inherit" | |||
| > | |||
| <MenuIcon /> | |||
| </IconButton> | |||
| <Menu | |||
| id="menu-appbar" | |||
| anchorEl={anchorElNav} | |||
| anchorOrigin={{ | |||
| vertical: 'bottom', | |||
| horizontal: 'left', | |||
| }} | |||
| keepMounted | |||
| transformOrigin={{ | |||
| vertical: 'top', | |||
| horizontal: 'left', | |||
| }} | |||
| open={Boolean(anchorElNav)} | |||
| onClose={handleCloseNavMenu} | |||
| sx={{ | |||
| display: { xs: 'block', md: 'none' }, | |||
| }} | |||
| > | |||
| {pages.map((page) => ( | |||
| <MenuItem key={page} onClick={handleCloseNavMenu}> | |||
| <Typography textAlign="center">{page}</Typography> | |||
| </MenuItem> | |||
| ))} | |||
| </Menu> | |||
| </Box> | |||
| <AdbIcon sx={{ display: { xs: 'flex', md: 'none' }, mr: 1 }} /> | |||
| <Typography | |||
| variant="h5" | |||
| noWrap | |||
| component="a" | |||
| href="" | |||
| sx={{ | |||
| mr: 2, | |||
| display: { xs: 'flex', md: 'none' }, | |||
| flexGrow: 1, | |||
| fontFamily: 'monospace', | |||
| fontWeight: 700, | |||
| letterSpacing: '.3rem', | |||
| color: 'inherit', | |||
| textDecoration: 'none', | |||
| }} | |||
| > | |||
| LOGO | |||
| </Typography> | |||
| <Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}> | |||
| {pages.map((page) => ( | |||
| <Button | |||
| key={page} | |||
| onClick={handleCloseNavMenu} | |||
| sx={{ my: 2, color: 'white', display: 'block' }} | |||
| > | |||
| {page} | |||
| </Button> | |||
| ))} | |||
| </Box> | |||
| <Box sx={{ flexGrow: 0 }}> | |||
| {session ? ( | |||
| <> | |||
| <Tooltip title="Open settings"> | |||
| <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}> | |||
| <Image | |||
| src="https://www.business2community.com/wp-content/uploads/2017/08/blank-profile-picture-973460_640.png" | |||
| alt="profile picture" | |||
| width={40} | |||
| height={40} | |||
| style={{ borderRadius: '50%' }} | |||
| /> | |||
| </IconButton> | |||
| </Tooltip> | |||
| <Menu | |||
| sx={{ mt: '45px' }} | |||
| id="menu-appbar" | |||
| anchorEl={anchorElUser} | |||
| anchorOrigin={{ | |||
| vertical: 'top', | |||
| horizontal: 'right', | |||
| }} | |||
| keepMounted | |||
| transformOrigin={{ | |||
| vertical: 'top', | |||
| horizontal: 'right', | |||
| }} | |||
| open={Boolean(anchorElUser)} | |||
| onClose={handleCloseUserMenu} | |||
| > | |||
| <MenuItem onClick={handleCloseUserMenu}> | |||
| <Link href={PROFILE_PAGE}> | |||
| <a | |||
| style={{ | |||
| textDecoration: 'none', | |||
| color: 'inherit', | |||
| fontSize: 15, | |||
| marginLeft: 6, | |||
| }} | |||
| > | |||
| PROFILE | |||
| </a> | |||
| </Link> | |||
| </MenuItem> | |||
| <MenuItem onClick={handleCloseUserMenu}> | |||
| <Button color="inherit" onClick={logoutHandler}> | |||
| Logout | |||
| </Button> | |||
| </MenuItem> | |||
| </Menu> | |||
| </> | |||
| ) : ( | |||
| <Button color="inherit"> | |||
| <Link href={LOGIN_PAGE}> | |||
| <a | |||
| style={{ | |||
| textDecoration: 'none', | |||
| color: 'inherit', | |||
| fontSize: 17, | |||
| }} | |||
| > | |||
| Login | |||
| </a> | |||
| </Link> | |||
| </Button> | |||
| )} | |||
| </Box> | |||
| </Toolbar> | |||
| </Container> | |||
| </AppBar> | |||
| ); | |||
| }; | |||
| export default Navbar; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockCircularIndeterminateProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,57 @@ | |||
| import Box from '@mui/material/Box'; | |||
| import CircularProgress from '@mui/material/CircularProgress'; | |||
| import { useRouter } from 'next/router'; | |||
| import { useEffect, useState } from 'react'; | |||
| const CircularIndeterminate = () => { | |||
| const router = useRouter(); | |||
| const [loading, setLoading] = useState(false); | |||
| useEffect(() => { | |||
| const handleStart = (url: string) => | |||
| url !== router.asPath && setLoading(true); | |||
| const handleComplete = (url: string) => | |||
| url === router.asPath && setLoading(false); | |||
| router.events.on('routeChangeStart', handleStart); | |||
| router.events.on('routeChangeComplete', handleComplete); | |||
| router.events.on('routeChangeError', handleComplete); | |||
| return () => { | |||
| router.events.off('routeChangeStart', handleStart); | |||
| router.events.off('routeChangeComplete', handleComplete); | |||
| router.events.off('routeChangeError', handleComplete); | |||
| }; | |||
| }); | |||
| return ( | |||
| loading && ( | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| zIndex: 99, | |||
| height: '100vh', | |||
| width: '100vw', | |||
| justifyContent: 'center', | |||
| alignItems: 'center', | |||
| position: 'fixed', | |||
| top: 0, | |||
| left: 0, | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| position: 'absolute', | |||
| top: '48%', | |||
| left: '48%', | |||
| marginX: 'auto', | |||
| }} | |||
| > | |||
| <CircularProgress color="inherit" size={60} thickness={4} /> | |||
| </Box> | |||
| </Box> | |||
| ) | |||
| ); | |||
| }; | |||
| export default CircularIndeterminate; | |||
| @@ -0,0 +1,14 @@ | |||
| import { Typography } from '@mui/material'; | |||
| import React from 'react'; | |||
| interface IProps { | |||
| error: string; | |||
| } | |||
| const ErrorMessageComponent: React.FC<IProps> = ({ error }) => ( | |||
| <Typography variant="body1" color="error" my={2}> | |||
| {error} | |||
| </Typography> | |||
| ); | |||
| export default ErrorMessageComponent; | |||
| @@ -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; | |||
| @@ -0,0 +1,10 @@ | |||
| const base = { | |||
| sort: '', | |||
| handleSortChange: () => {}, | |||
| filter: '', | |||
| handleFilterChange: () => {}, | |||
| }; | |||
| export const mockFilterSortComponentProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockPaginationComponentQRProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,127 @@ | |||
| import { Box, Button, Grid, Paper, Typography } from '@mui/material'; | |||
| import { useTranslation } from 'next-i18next'; | |||
| import { useRouter } from 'next/router'; | |||
| import { useState } from 'react'; | |||
| import { SINGLE_DATA_PAGE } from '../../../constants/pages'; | |||
| import useDebounce from '../../../hooks/use-debounce'; | |||
| import { usePagination } from '../../../hooks/use-pagination'; | |||
| import { compare } from '../../../utils/helpers/sortHelpers'; | |||
| import DataCard from '../../cards/data-card/DataCard'; | |||
| import FilterSortComponent from '../filter-sort/FilterSortComponent'; | |||
| const PaginationComponentRQ = () => { | |||
| const [pageIndex, setPageIndex] = useState(1); | |||
| const [filter, setFilter] = useState(''); | |||
| const [sort, setSort] = useState(''); | |||
| const { t } = useTranslation('pagination'); | |||
| const { data: paginationData } = usePagination(pageIndex.toString()); | |||
| const router = useRouter(); | |||
| const debouncedFilter = useDebounce(filter, 500); | |||
| const handleFilterChange = (event: { target: HTMLInputElement }) => { | |||
| const filterText = event.target.value; | |||
| setFilter(filterText); | |||
| }; | |||
| const handleSortChange = (event: { target: HTMLInputElement }) => { | |||
| const sort = event.target.value; | |||
| setSort(sort); | |||
| }; | |||
| const loadSingleDataHandler = (id: string) => { | |||
| router.push(`${SINGLE_DATA_PAGE}${id}`); | |||
| }; | |||
| const dataToDisplay = paginationData?.data | |||
| .filter((item) => | |||
| item.name.toLowerCase().startsWith(debouncedFilter.toLowerCase()) | |||
| ) | |||
| .sort((a, b) => compare(a.name, b.name, sort)) | |||
| .map((item, index) => ( | |||
| // ! DON'T USE index for key, this is for example only | |||
| <Grid | |||
| item | |||
| sx={{ p: 2 }} | |||
| xs={12} | |||
| sm={6} | |||
| md={4} | |||
| lg={3} | |||
| key={index} | |||
| onClick={loadSingleDataHandler.bind(null, item.customID)} | |||
| > | |||
| <DataCard data={item} t={t} /> | |||
| </Grid> | |||
| )); | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| justifyContent: 'start', | |||
| py: 2, | |||
| minHeight: 400, | |||
| marginTop: 5, | |||
| }} | |||
| elevation={5} | |||
| > | |||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | |||
| {t('Title')} | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| flexWrap: 'wrap', | |||
| mx: 2, | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| <FilterSortComponent | |||
| sort={sort} | |||
| handleSortChange={handleSortChange} | |||
| filter={filter} | |||
| handleFilterChange={handleFilterChange} | |||
| /> | |||
| </Box> | |||
| </Box> | |||
| <Grid container>{dataToDisplay}</Grid> | |||
| <Box | |||
| sx={{ | |||
| width: '100%', | |||
| textAlign: 'center', | |||
| marginTop: 3, | |||
| }} | |||
| > | |||
| <Button | |||
| disabled={pageIndex === 1} | |||
| onClick={() => setPageIndex(pageIndex - 1)} | |||
| sx={{ | |||
| marginRight: 5, | |||
| }} | |||
| > | |||
| {t('Btns.PrevBtn')} | |||
| </Button> | |||
| <Button | |||
| disabled={ | |||
| paginationData ? pageIndex * 4 > paginationData?.dataCount : true | |||
| } | |||
| onClick={() => setPageIndex(pageIndex + 1)} | |||
| sx={{ | |||
| marginRight: 5, | |||
| }} | |||
| > | |||
| {t('Btns.NextBtn')} | |||
| </Button> | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default PaginationComponentRQ; | |||
| @@ -0,0 +1,5 @@ | |||
| const base = {}; | |||
| export const mockPaginationComponentSWRProps = { | |||
| base, | |||
| }; | |||
| @@ -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, | |||
| }; | |||
| @@ -0,0 +1,116 @@ | |||
| import { Box, Button, Grid, Paper, Typography } from '@mui/material'; | |||
| import { useTranslation } from 'next-i18next'; | |||
| import { useState } from 'react'; | |||
| import useDebounce from '../../../hooks/use-debounce'; | |||
| import useSWRWithFallbackData from '../../../hooks/use-swr-with-initial-data'; | |||
| import { getData } from '../../../requests/dataRequest'; | |||
| import { compare } from '../../../utils/helpers/sortHelpers'; | |||
| import { IPerson } from '../../../utils/interface/personInterface'; | |||
| import DataCard from '../../cards/data-card/DataCard'; | |||
| import FilterSortComponent from '../filter-sort/FilterSortComponent'; | |||
| const PaginationComponent = ({ initialData = {} }) => { | |||
| const [pageIndex, setPageIndex] = useState(1); | |||
| const [filter, setFilter] = useState(''); | |||
| const [sort, setSort] = useState(''); | |||
| const { t } = useTranslation('pagination'); | |||
| const fetcher = (page: string) => getData(page); | |||
| const { data: paginationData }:any = useSWRWithFallbackData(pageIndex.toString(), fetcher, { | |||
| fallbackData: initialData, | |||
| }); | |||
| const debouncedFilter = useDebounce(filter, 500); | |||
| const handleFilterChange = (event: {target: HTMLInputElement}) => { | |||
| const filterText = event.target.value; | |||
| setFilter(filterText); | |||
| }; | |||
| const handleSortChange = (event: { target: HTMLInputElement }) => { | |||
| const sort = event.target.value; | |||
| setSort(sort); | |||
| }; | |||
| const dataToDisplay = paginationData?.data | |||
| .filter((item: IPerson) => | |||
| item.name.toLowerCase().startsWith(debouncedFilter.toLowerCase()) | |||
| ) | |||
| .sort((a: IPerson, b:IPerson) => compare(a.name, b.name, sort)) | |||
| .map((item: IPerson, index: number) => ( | |||
| // ! DON'T USE index for key, this is for example only | |||
| <Grid item sx={{ p: 2 }} xs={12} sm={6} md={4} lg={3} key={index}> | |||
| <DataCard data={item} t={t} /> | |||
| </Grid> | |||
| )); | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| justifyContent: 'start', | |||
| py: 2, | |||
| minHeight: 400, | |||
| marginTop: 5, | |||
| }} | |||
| elevation={5} | |||
| > | |||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | |||
| {t('Title')} | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| flexWrap: 'wrap', | |||
| mx: 2, | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| <FilterSortComponent | |||
| sort={sort} | |||
| handleSortChange={handleSortChange} | |||
| filter={filter} | |||
| handleFilterChange={handleFilterChange} | |||
| /> | |||
| </Box> | |||
| </Box> | |||
| <Grid container>{dataToDisplay}</Grid> | |||
| <Box | |||
| sx={{ | |||
| width: '100%', | |||
| textAlign: 'center', | |||
| marginTop: 3, | |||
| }} | |||
| > | |||
| <Button | |||
| disabled={pageIndex === 1} | |||
| onClick={() => setPageIndex(pageIndex - 1)} | |||
| sx={{ | |||
| marginRight: 5, | |||
| }} | |||
| > | |||
| {t('Btns.PrevBtn')} | |||
| </Button> | |||
| <Button | |||
| disabled={pageIndex * 4 > paginationData?.dataCount} | |||
| onClick={() => setPageIndex(pageIndex + 1)} | |||
| sx={{ | |||
| marginRight: 5, | |||
| }} | |||
| > | |||
| {t('Btns.NextBtn')} | |||
| </Button> | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default PaginationComponent; | |||
| @@ -0,0 +1,7 @@ | |||
| const base = { | |||
| sampleTextProp: 'Hello world!', | |||
| }; | |||
| export const mockBaseTemplateProps = { | |||
| base, | |||
| }; | |||
| @@ -0,0 +1,2 @@ | |||
| .component { | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| import BaseTemplate from './BaseTemplate'; | |||
| import { mockBaseTemplateProps } from './BaseTemplate.mocks'; | |||
| const obj = { | |||
| title: 'templates/BaseTemplate', | |||
| component: BaseTemplate, | |||
| // More on argTypes: https://storybook.js.org/docs/react/api/argtypes | |||
| argTypes: {}, | |||
| }; //eslint-disable-line | |||
| export default obj; | |||
| // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args | |||
| const Template = (args) => <BaseTemplate {...args} />; | |||
| export const Base = Template.bind({}); | |||
| // More on args: https://storybook.js.org/docs/react/writing-stories/args | |||
| Base.args = { | |||
| ...mockBaseTemplateProps.base, | |||
| }; | |||
| @@ -0,0 +1,11 @@ | |||
| import styles from './BaseTemplate.module.css'; | |||
| interface IProps { | |||
| sampleTextProp: string; | |||
| } | |||
| const BaseTemplate: React.FC<IProps> = ({ sampleTextProp }) => { | |||
| return <div className={styles.container}>{sampleTextProp}</div>; | |||
| }; | |||
| export default BaseTemplate; | |||
| @@ -0,0 +1,6 @@ | |||
| export const BASE_PAGE = '/'; | |||
| export const LOGIN_PAGE = '/auth'; | |||
| export const PROFILE_PAGE = '/profile'; | |||
| export const REGISTER_PAGE = '/auth/register'; | |||
| export const FORGOT_PASSWORD_PAGE = '/auth/forgot-password'; | |||
| export const SINGLE_DATA_PAGE = '/single-data/'; | |||
| @@ -0,0 +1,17 @@ | |||
| import { useEffect, useState } from 'react'; | |||
| const useDebounce = (value: string, delay: number) => { | |||
| const [debouncedValue, setDebouncedValue] = useState(value); | |||
| useEffect(() => { | |||
| const timer = setTimeout(() => setDebouncedValue(value), delay || 500); | |||
| return () => { | |||
| clearTimeout(timer); | |||
| }; | |||
| }, [value, delay]); | |||
| return debouncedValue; | |||
| }; | |||
| export default useDebounce; | |||
| @@ -0,0 +1,10 @@ | |||
| import { useQuery } from '@tanstack/react-query'; | |||
| import { getData } from '../requests/dataRequest'; | |||
| export const usePagination = (activePage: string) => { | |||
| return useQuery(['randomData', activePage], () => getData(activePage), { | |||
| keepPreviousData: true, | |||
| refetchOnMount: false, | |||
| refetchOnWindowFocus: false, | |||
| }); | |||
| }; | |||
| @@ -0,0 +1,23 @@ | |||
| import { useEffect, useRef } from 'react'; | |||
| import useSWR from 'swr'; | |||
| const useSWRWithFallbackData = ( | |||
| key: string, | |||
| fetcher: any, | |||
| options = { | |||
| fallbackData: {}, | |||
| } | |||
| ) => { | |||
| const hasMounted = useRef(false); | |||
| useEffect(() => { | |||
| hasMounted.current = true; | |||
| }, []); | |||
| return useSWR(key, fetcher, { | |||
| ...options, | |||
| fallbackData: hasMounted.current ? undefined : options?.fallbackData, | |||
| }); | |||
| }; | |||
| export default useSWRWithFallbackData; | |||
| @@ -0,0 +1,34 @@ | |||
| import { Schema, model, models } from 'mongoose'; | |||
| import { IPerson } from '../utils/interface/personInterface'; | |||
| const PersonSchema = new Schema<IPerson>({ | |||
| name: { | |||
| type: String, | |||
| required: [true, 'Please provide a name.'], | |||
| maxlength: [60, 'Name cannot be more than 60 characters'], | |||
| trim: true, | |||
| }, | |||
| age: { | |||
| type: Number, | |||
| required: [true, 'Please provide an age.'], | |||
| validate(value: number) { | |||
| if (value < 0) { | |||
| throw new Error('Age must be a postive number'); | |||
| } | |||
| }, | |||
| }, | |||
| gender: { | |||
| type: String, | |||
| required: [true, 'Please provide a gender.'], | |||
| trim: true, | |||
| }, | |||
| customID: { | |||
| type: String, | |||
| required: true, | |||
| unique: true, | |||
| }, | |||
| }); | |||
| const Person = models.Person || model<IPerson>('Person', PersonSchema); | |||
| module.exports = Person; | |||
| @@ -0,0 +1,87 @@ | |||
| import { | |||
| hashPassword, | |||
| verifyPassword, | |||
| } from '../utils/helpers/hashPasswordHelpers'; | |||
| import { Schema, model, Model, models } from 'mongoose'; | |||
| import { IUser } from '../utils/interface/userInterface'; | |||
| const validator = require('validator'); | |||
| interface UserModel extends Model<IUser, {}, {}> { | |||
| findByCredentials(username: string, password: string): object; | |||
| } | |||
| const UserSchema = new Schema<IUser, UserModel>({ | |||
| fullName: { | |||
| type: String, | |||
| required: [true, 'Please provide a name.'], | |||
| maxlength: [60, 'Name cannot be more than 60 characters'], | |||
| trim: true, | |||
| }, | |||
| username: { | |||
| type: String, | |||
| required: [true, 'Please provide a name.'], | |||
| maxlength: [60, 'Name cannot be more than 60 characters'], | |||
| trim: true, | |||
| unique: true, | |||
| }, | |||
| email: { | |||
| type: String, | |||
| unique: true, | |||
| required: true, | |||
| trim: true, | |||
| lowercase: true, | |||
| validate(value: string) { | |||
| if (!validator.isEmail(value)) { | |||
| throw new Error('Email is invalid'); | |||
| } | |||
| }, | |||
| }, | |||
| password: { | |||
| type: String, | |||
| required: true, | |||
| minlength: 7, | |||
| trim: true, | |||
| validate(value: string) { | |||
| if (value.toLowerCase().includes('password')) { | |||
| throw new Error('Password cannot contain "password"'); | |||
| } | |||
| }, | |||
| }, | |||
| }); | |||
| UserSchema.static( | |||
| 'findByCredentials', | |||
| async function findByCredentials(username: string, password: string) { | |||
| const user = await User.findOne({ username }); | |||
| if (!user) { | |||
| throw new Error('Unable to login'); | |||
| } | |||
| const isMatch = await verifyPassword(password, user.password); | |||
| if (!isMatch) { | |||
| throw new Error('Unable to login'); | |||
| } | |||
| const userData = { | |||
| fullName: user.fullName, | |||
| email: user.email, | |||
| username: user.username, | |||
| }; | |||
| return userData; | |||
| } | |||
| ); | |||
| UserSchema.pre('save', async function (next) { | |||
| const user = this; | |||
| if (user.isModified('password')) { | |||
| user.password = await hashPassword(user.password); | |||
| } | |||
| next(); | |||
| }); | |||
| const User = models.User || model<IUser, UserModel>('User', UserSchema); | |||
| module.exports = User; | |||
| @@ -0,0 +1,5 @@ | |||
| /// <reference types="next" /> | |||
| /// <reference types="next/image-types/global" /> | |||
| // NOTE: This file should not be edited | |||
| // see https://nextjs.org/docs/basic-features/typescript for more information. | |||
| @@ -0,0 +1,6 @@ | |||
| module.exports = { | |||
| i18n: { | |||
| defaultLocale: 'en', | |||
| locales: ['en'], | |||
| }, | |||
| }; | |||