diff --git a/.gitignore b/.gitignore index 51a7d98a37..ebf9dc8fbb 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ dist-ssr *.sln *.sw? -*storybook.log \ No newline at end of file +*storybook.log +storybook-static \ No newline at end of file diff --git a/.storybook/main.ts b/.storybook/main.ts index 69ac0074d7..5ac752a5ed 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -8,6 +8,7 @@ const config: StorybookConfig = { "@storybook/addon-essentials", "@chromatic-com/storybook", "@storybook/addon-interactions", + "@storybook/addon-themes" ], framework: { name: "@storybook/react-vite", diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 37914b18f2..adad302cef 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,4 +1,6 @@ import type { Preview } from "@storybook/react"; +import GlobalStyles from "../src/GlobalStyles"; +import { withThemeFromJSXProvider } from "@storybook/addon-themes"; const preview: Preview = { parameters: { @@ -8,7 +10,38 @@ const preview: Preview = { date: /Date$/i, }, }, + viewport: { + viewports: { + mobile: { + name: "Mobile", + styles: { + width: "360px", + height: "640px", + }, + }, + tablet: { + name: "Tablet", + styles: { + width: "768px", + height: "1024px", + }, + }, + desktop: { + name: "Desktop", + styles: { + width: "1024px", + height: "768px", + }, + }, + }, + }, }, }; +export const decorators = [ + withThemeFromJSXProvider({ + GlobalStyles, + }), +]; + export default preview; diff --git a/README.md b/README.md index 8d917806e0..a4dce16c69 100644 --- a/README.md +++ b/README.md @@ -1 +1,41 @@ # react-payments + +## ✅ 프로그램 설명 + +사용자에게 올바른 카드 정보를 입력 받고 카드 프리뷰를 실시간으로 보여준다 + +## 🛠️ 기능 요구 사항 + +### 번호를 입력 받는다 + +- [x] 카드 번호를 입력 받는다 +- [x] 카드 유효기간을 입력 받는다 +- [x] cvc 번호를 입력 받는다 + +### 카드 프리뷰 + +- 카드 이미지 + + - [x] 카드 번호를 입력하면 카드 프리뷰에 입력한 값을 보여준다 + - [x] 카드 번호 세 번째~네 번째 칸에 값을 입력할 경우 암호화 한다 + - [x] 카드 유효기간을 입력할 경우 카드 프리뷰에 입력한 값을 보여준다 + +- 카드 아이콘 + - [] 카드 번호가 4로 시작할 경우 카드 프리뷰에 visa 아이콘을 보여준다 + - [] 카드 번호가 51~52로 시작하거나 2221~2720으로 시작할 경우 카드 프리뷰에 master card 아이콘을 보여준다 + +### 입력 받은 값에 대해 유효성 검사를 실시한다 + +- 카드번호 + - [x] 각 카드 번호는 숫자여야 한다 + - [x] 각 카드 번호는 4자리여야 한다 +- 카드 유효 기간 + - [x] 유효 기간 YY은 현재 연도보다 크거나 같아야 한다 + - [x] 유효 기간 MM은 1~31 사이 숫자를 입력해야 한다 +- cvc번호 + - [x] cvc 번호는 3자리로 입력해야 한다 + +### 에러 처리 + +- [x] 입력 값이 유효하지 않은 경우 input에 border 색상이 변경된다 +- [x] 입력 값이 유효하지 않은 경우 에러 메세지를 출력한다 diff --git a/package-lock.json b/package-lock.json index 26f5ab58da..408e30b200 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "styled-components": "^6.1.17" }, "devDependencies": { "@chromatic-com/storybook": "^3.2.6", @@ -18,6 +19,7 @@ "@storybook/addon-interactions": "^8.6.12", "@storybook/addon-links": "^8.6.12", "@storybook/addon-onboarding": "^8.6.12", + "@storybook/addon-themes": "^8.6.12", "@storybook/blocks": "^8.6.12", "@storybook/react": "^8.6.12", "@storybook/react-vite": "^8.6.12", @@ -25,10 +27,12 @@ "@types/react": "^19.1.1", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react-swc": "^3.8.1", + "chromatic": "^11.28.0", "eslint": "^9.24.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-storybook": "^0.12.0", + "gh-pages": "^6.3.0", "storybook": "^8.6.12", "typescript": "~5.8.3", "typescript-eslint": "^8.29.1", @@ -499,6 +503,27 @@ "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", @@ -1858,6 +1883,23 @@ "storybook": "^8.6.12" } }, + "node_modules/@storybook/addon-themes": { + "version": "8.6.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-themes/-/addon-themes-8.6.12.tgz", + "integrity": "sha512-eqE40MUKTz9lLEOusXjRuDC7DwCSIwlgEnlbvhhEEme8IeKf2di6yvlhenY4nn5QfkUwY1POnZxfJ2OpXj0gqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.12" + } + }, "node_modules/@storybook/addon-toolbars": { "version": "8.6.12", "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.6.12.tgz", @@ -2610,6 +2652,12 @@ "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", "dev": true }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -3044,6 +3092,16 @@ "dequal": "^2.0.3" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3074,6 +3132,13 @@ "dev": true, "license": "0BSD" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3228,6 +3293,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001610", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001610.tgz", @@ -3333,6 +3407,23 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3360,6 +3451,26 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -3370,8 +3481,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/debug": { "version": "4.3.4", @@ -3444,6 +3554,19 @@ "node": ">=6" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3491,6 +3614,13 @@ "integrity": "sha512-Rer6wc3ynLelKNM4lOCg7/zPQj8tPOCB2hzD32PX9wd3hgRRi9MxEbmkFCokzcEhRVMiOVLjnL9ig9cefJ+6+Q==", "dev": true }, + "node_modules/email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3936,6 +4066,34 @@ "node": ">=16.0.0" } }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/filesize": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.1.tgz", @@ -3957,6 +4115,24 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4027,6 +4203,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4098,6 +4289,29 @@ "node": ">= 0.4" } }, + "node_modules/gh-pages": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz", + "integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.4", + "commander": "^13.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^11.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4144,6 +4358,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4161,8 +4396,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "optional": true + "dev": true }, "node_modules/graphemer": { "version": "1.4.0", @@ -4635,6 +4869,32 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/map-or-similar": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", @@ -4737,7 +4997,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -4829,6 +5088,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -4898,6 +5167,16 @@ "dev": true, "license": "ISC" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathval": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", @@ -4912,7 +5191,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -4927,6 +5205,75 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/polished": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", @@ -4978,6 +5325,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5348,6 +5701,12 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5384,6 +5743,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5398,7 +5767,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -5565,6 +5933,91 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/styled-components": { + "version": "6.1.17", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.17.tgz", + "integrity": "sha512-97D7DwWanI7nN24v0D4SvbfjLE9656umNSJZkBkDIWL37aZqG/wRQ+Y9pWtXyBIM/NSfcBzHLErEsqHmJNSVUg==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5637,6 +6090,29 @@ "node": ">=8.0" } }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", @@ -5660,6 +6136,12 @@ "node": ">=6" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/tween-functions": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", @@ -6581,6 +7063,24 @@ "strip-ansi": "^7.1.0" } }, + "@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "requires": { + "@emotion/memoize": "^0.8.1" + } + }, + "@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, "@esbuild/aix-ppc64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", @@ -7320,6 +7820,15 @@ "ts-dedent": "^2.0.0" } }, + "@storybook/addon-themes": { + "version": "8.6.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-themes/-/addon-themes-8.6.12.tgz", + "integrity": "sha512-eqE40MUKTz9lLEOusXjRuDC7DwCSIwlgEnlbvhhEEme8IeKf2di6yvlhenY4nn5QfkUwY1POnZxfJ2OpXj0gqQ==", + "dev": true, + "requires": { + "ts-dedent": "^2.0.0" + } + }, "@storybook/addon-toolbars": { "version": "8.6.12", "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.6.12.tgz", @@ -7757,6 +8266,11 @@ "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", "dev": true }, + "@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" + }, "@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -8028,6 +8542,12 @@ "dequal": "^2.0.3" } }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, "assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -8051,6 +8571,12 @@ } } }, + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, "available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -8149,6 +8675,11 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==" + }, "caniuse-lite": { "version": "1.0.30001610", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001610.tgz", @@ -8206,6 +8737,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8229,6 +8772,21 @@ "which": "^2.0.1" } }, + "css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==" + }, + "css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -8238,8 +8796,7 @@ "csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "debug": { "version": "4.3.4", @@ -8285,6 +8842,15 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -8323,6 +8889,12 @@ "integrity": "sha512-Rer6wc3ynLelKNM4lOCg7/zPQj8tPOCB2hzD32PX9wd3hgRRi9MxEbmkFCokzcEhRVMiOVLjnL9ig9cefJ+6+Q==", "dev": true }, + "email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "dev": true + }, "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -8636,6 +9208,23 @@ "flat-cache": "^4.0.0" } }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true + }, + "filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + } + }, "filesize": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.1.tgz", @@ -8651,6 +9240,17 @@ "to-regex-range": "^5.0.1" } }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -8696,6 +9296,17 @@ "signal-exit": "^4.0.1" } }, + "fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -8743,6 +9354,21 @@ "es-object-atoms": "^1.0.0" } }, + "gh-pages": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz", + "integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==", + "dev": true, + "requires": { + "async": "^3.2.4", + "commander": "^13.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^11.1.0" + } + }, "glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -8772,6 +9398,20 @@ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, "gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -8782,8 +9422,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "optional": true + "dev": true }, "graphemer": { "version": "1.4.0", @@ -9102,6 +9741,23 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, "map-or-similar": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", @@ -9175,8 +9831,7 @@ "nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" }, "natural-compare": { "version": "1.4.0", @@ -9233,6 +9888,12 @@ "p-limit": "^3.0.2" } }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, "package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -9284,6 +9945,12 @@ } } }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "pathval": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", @@ -9293,8 +9960,7 @@ "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "picomatch": { "version": "2.3.1", @@ -9302,6 +9968,54 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, "polished": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", @@ -9328,6 +10042,11 @@ "source-map-js": "^1.2.1" } }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9578,6 +10297,11 @@ "has-property-descriptors": "^1.0.2" } }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9599,6 +10323,12 @@ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -9608,8 +10338,7 @@ "source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, "storybook": { "version": "8.6.12", @@ -9706,6 +10435,56 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + } + } + }, + "styled-components": { + "version": "6.1.17", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.17.tgz", + "integrity": "sha512-97D7DwWanI7nN24v0D4SvbfjLE9656umNSJZkBkDIWL37aZqG/wRQ+Y9pWtXyBIM/NSfcBzHLErEsqHmJNSVUg==", + "requires": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "dependencies": { + "postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + } + } + }, + "stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9754,6 +10533,23 @@ "is-number": "^7.0.0" } }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + } + } + }, "ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", @@ -9771,6 +10567,11 @@ "strip-bom": "^3.0.0" } }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "tween-functions": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", diff --git a/package.json b/package.json index 67142d9399..3a81a3d54b 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,24 @@ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "chromatic": "npx chromatic --project-token=chpt_179054950321022", + "predeploy": "npm run build", + "deploy": "gh-pages -d dist" }, "dependencies": { "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "styled-components": "^6.1.17" }, "devDependencies": { - "@eslint/js": "^9.24.0", "@chromatic-com/storybook": "^3.2.6", + "@eslint/js": "^9.24.0", "@storybook/addon-essentials": "^8.6.12", "@storybook/addon-interactions": "^8.6.12", "@storybook/addon-links": "^8.6.12", "@storybook/addon-onboarding": "^8.6.12", + "@storybook/addon-themes": "^8.6.12", "@storybook/blocks": "^8.6.12", "@storybook/react": "^8.6.12", "@storybook/react-vite": "^8.6.12", @@ -29,10 +34,12 @@ "@types/react": "^19.1.1", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react-swc": "^3.8.1", + "chromatic": "^11.28.0", "eslint": "^9.24.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-storybook": "^0.12.0", + "gh-pages": "^6.3.0", "storybook": "^8.6.12", "typescript": "~5.8.3", "typescript-eslint": "^8.29.1", diff --git a/public/default.png b/public/default.png new file mode 100644 index 0000000000..c5333cd257 Binary files /dev/null and b/public/default.png differ diff --git a/public/mastercard.png b/public/mastercard.png new file mode 100644 index 0000000000..239c0c4350 Binary files /dev/null and b/public/mastercard.png differ diff --git a/public/visa.png b/public/visa.png new file mode 100644 index 0000000000..b728b553b8 Binary files /dev/null and b/public/visa.png differ diff --git a/src/App.tsx b/src/App.tsx index ef7e3632d2..c3b67a93b8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,11 @@ -import "./App.css"; +import AddCard from "./pages/AddCard"; +import GlobalStyles from "./GlobalStyles"; function App() { return ( <> -

React Payments

+ + ); } diff --git a/src/GlobalStyles.tsx b/src/GlobalStyles.tsx new file mode 100644 index 0000000000..8d80fce91c --- /dev/null +++ b/src/GlobalStyles.tsx @@ -0,0 +1,86 @@ +import { createGlobalStyle } from "styled-components"; + +const GlobalStyles = createGlobalStyle` + /* design token */ + :root { + --color-yellow: #ddcd78; + --color-black: #333333; + --color-gray: #8b95a1; + --color-white: #ffffff; + --color-red: #ff3d3d; + + --font-size-header: 18px; + --font-size-body: 11px; + --font-size-caption: 9.5px; + --font-size-subheader: 14px; + + --font-weight-header: bold; + --font-weight-body: normal; + --font-weight-caption: normal; + } + + /* reset css */ + html, body, div, span, object, iframe, + h1, h2, h3, h4, h5, h6, p, blockquote, pre, + a, abbr, address, cite, code, del, dfn, em, + img, ins, kbd, q, s, samp, small, strong, sub, + sup, var, b, i, dl, dt, dd, ol, ul, li, + fieldset, form, label, legend, table, caption, + tbody, tfoot, thead, tr, th, td, + article, aside, canvas, details, embed, + figure, figcaption, footer, header, hgroup, + menu, nav, output, ruby, section, summary, + time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + } + + article, aside, details, figcaption, figure, + footer, header, hgroup, menu, nav, section { + display: block; + } + + body { + line-height: 1; + background-color: #f5f5f5; + display: flex; + justify-content: center; + align-items: center; + } + + ol, ul { + list-style: none; + } + + blockquote, q { + quotes: none; + } + + blockquote::before, blockquote::after, + q::before, q::after { + content: ''; + content: none; + } + + table { + border-collapse: collapse; + border-spacing: 0; + } + + /* settings */ + input[type='number']::-webkit-outer-spin-button, + input[type='number']::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + input[type='number'] { + -moz-appearance: textfield; + } +`; + +export default GlobalStyles; diff --git a/src/component/CardInput.tsx b/src/component/CardInput.tsx new file mode 100644 index 0000000000..a80116a0c7 --- /dev/null +++ b/src/component/CardInput.tsx @@ -0,0 +1,67 @@ +import type { Dispatch, SetStateAction } from "react"; +import styled from "styled-components"; +import type { ComponentProps } from "react"; +import type { CardInputProps } from "../types/CardInputTypes"; +import { useState } from "react"; + +interface InputProps extends ComponentProps<"input"> { + inputKey: keyof CardInputProps; + setCardInput: Dispatch>; + validate: (value: string) => string | undefined; + handleErrorMessage: (input: string) => void; +} + +const CardInput = ({ + inputKey, + setCardInput, + validate, + handleErrorMessage, + ...restProps +}: InputProps) => { + const [isError, setIsError] = useState(false); + + const handleCardNumber = (e: React.ChangeEvent) => { + const value = e.target.value; + const errorMessage = validate(value); + if (errorMessage && errorMessage.length > 0) { + handleErrorMessage(errorMessage); + setIsError(true); + return; + } + + handleErrorMessage(""); + setIsError(false); + + setCardInput((prev: CardInputProps) => ({ + ...prev, + [inputKey]: value, + })); + }; + + return ( + + ); +}; + +const InputField = styled.input<{ $isError: boolean }>` + width: 100%; + padding: 8px; + background-color: var(--color-white); + border: 1px solid var(--color-gray); + border-color: ${({ $isError }) => + $isError ? "var(--color-red)" : "var(--color-gray)"}; + border-radius: 4px; + &:focus { + outline: none; + border-color: ${({ $isError }) => + $isError ? "var(--color-red)" : "var(--color-black)"}; + } +`; + +export default CardInput; diff --git a/src/component/CardNumber.tsx b/src/component/CardNumber.tsx new file mode 100644 index 0000000000..e5d336809f --- /dev/null +++ b/src/component/CardNumber.tsx @@ -0,0 +1,51 @@ +import { maskingNumber } from "../util/maskingNumber"; +import styled from "styled-components"; +import type { CardInputProps } from "../types/CardInputTypes"; + +interface CardNumberProps { + cardNumber: CardInputProps; +} + +const CardNumber = ({ cardNumber }: CardNumberProps) => { + return ( + <> + + {cardNumber.first} + {cardNumber.second} + + {cardNumber.third && maskingNumber(String(cardNumber.third).length)} + + + {cardNumber.fourth && maskingNumber(String(cardNumber.fourth).length)} + + + + + {cardNumber.MM && `${cardNumber.MM}`} + {cardNumber.YY && `/${cardNumber.YY}`} + + + ); +}; + +const CardInformation = styled.div` + color: var(--color-white); + font-size: var(--font-size-subheader); + font-weight: var(--font-weight-caption); + letter-spacing: 2.56px; + min-width: 40px; +`; + +const CardMaskingInformation = styled(CardInformation)` + letter-spacing: -2px; +`; + +const CardNumberContainer = styled.div` + display: flex; + gap: 10px; + align-items: center; + margin-top: 16px; + margin-bottom: 8px; +`; + +export default CardNumber; diff --git a/src/component/Description.tsx b/src/component/Description.tsx new file mode 100644 index 0000000000..6186b29c7c --- /dev/null +++ b/src/component/Description.tsx @@ -0,0 +1,35 @@ +import styled from "styled-components"; + +interface DescriptionProps { + headText: string; + detailText?: string; +} + +const Description = ({ headText, detailText }: DescriptionProps) => { + return ( + + {headText} + {detailText} + + ); +}; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +`; + +const HeaderText = styled.span` + font-size: var(--font-size-header); + font-weight: var(--font-weight-header); + color: var(--color-black); +`; + +const DetailText = styled.span` + font-size: var(--font-size-caption); + font-weight: var(--font-weight-caption); + color: var(--color-gray); +`; + +export default Description; diff --git a/src/component/InputGroup.tsx b/src/component/InputGroup.tsx new file mode 100644 index 0000000000..e4e2f5a663 --- /dev/null +++ b/src/component/InputGroup.tsx @@ -0,0 +1,56 @@ +import styled from "styled-components"; + +interface InputGroupProps { + label: string; + children: React.ReactNode; + errorMessages: string; + id: string; +} + +const InputGroup = ({ + label, + children, + errorMessages, + id, +}: InputGroupProps) => { + return ( + + + {children} + + {errorMessages} + + + ); +}; + +export const InputContainer = styled.div` + display: flex; + gap: 10px; +`; + +const Label = styled.label` + color: var(--color-black); + font-size: var(--font-size-body); + font-weight: var(--font-weight-body); +`; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +const ErrorMessage = styled.p` + color: var(--color-red); + font-size: var(--font-size-caption); + font-weight: var(--font-weight-caption); +`; + +const ErrorMessageContainer = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +`; + +export default InputGroup; diff --git a/src/component/card/Card.stories.tsx b/src/component/card/Card.stories.tsx new file mode 100644 index 0000000000..1d25b1feea --- /dev/null +++ b/src/component/card/Card.stories.tsx @@ -0,0 +1,84 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import Card from "./Card"; + +const meta: Meta = { + title: "Components/Card", + component: Card, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], +}; + +export default meta; +type Story = StoryObj; + +const renderBrandCard = (cardType: "default" | "visa" | "mastercard") => { + const cardNumber = { + first: null, + second: null, + third: null, + fourth: null, + MM: null, + YY: null, + CVC: null, + }; + return ; +}; + +export const Default: Story = { + render: () => renderBrandCard("default"), +}; + +export const Visa: Story = { + render: () => renderBrandCard("visa"), +}; + +export const Mastercard: Story = { + render: () => renderBrandCard("mastercard"), +}; + +export const InputTwoCardNumber: Story = { + args: { + cardNumber: { + first: 1111, + second: 2222, + third: null, + fourth: null, + MM: null, + YY: null, + CVC: null, + }, + }, + render: (args) => , +}; + +export const InputFourCardNumber: Story = { + args: { + cardNumber: { + first: 1111, + second: 2222, + third: 3333, + fourth: 4444, + MM: null, + YY: null, + CVC: null, + }, + }, + render: (args) => , +}; + +export const InputExpiration: Story = { + args: { + cardNumber: { + first: 1111, + second: 2222, + third: 3333, + fourth: 4444, + MM: 12, + YY: 25, + CVC: null, + }, + }, + render: (args) => , +}; diff --git a/src/component/card/Card.tsx b/src/component/card/Card.tsx new file mode 100644 index 0000000000..c8c3a22998 --- /dev/null +++ b/src/component/card/Card.tsx @@ -0,0 +1,52 @@ +import styled from "styled-components"; +import cardBrandLogo from "../../constants/cardBrandLogo"; +import type { CardInputProps } from "../../types/CardInputTypes"; +import CardNumber from "../CardNumber"; + +interface CardProps { + cardNumber: CardInputProps | null; + cardType: "visa" | "mastercard" | "default"; +} + +const Card = ({ cardNumber, cardType }: CardProps) => { + return ( + + + + + + {cardNumber ? : null} + + ); +}; + +const CardContainer = styled.div` + box-sizing: border-box; + width: 212px; + height: 132px; + background-color: var(--color-black); + border-radius: 4px; + box-shadow: 3px 3px 5px 0 rgba(0, 0, 0, 0.1); + padding: 8px 12px; +`; + +const ChipContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const CardGoldChip = styled.div` + width: 36px; + height: 22px; + background-color: var(--color-yellow); + border-radius: 4px; + box-shadow: 0px 4px 4px 0 rgba(0, 0, 0, 0.1); +`; + +const CardBrandLogo = styled.img` + width: 36px; + height: 22px; +`; + +export default Card; diff --git a/src/component/inputSections/cardNumberInputs/CardNumberInputs.stories.tsx b/src/component/inputSections/cardNumberInputs/CardNumberInputs.stories.tsx new file mode 100644 index 0000000000..8b9f62e607 --- /dev/null +++ b/src/component/inputSections/cardNumberInputs/CardNumberInputs.stories.tsx @@ -0,0 +1,102 @@ +import type { Dispatch, SetStateAction } from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import type { CardInputProps } from "../../../types/CardInputTypes"; +import { useErrorMessages } from "../../../hook/useErrorMessages"; +import { useState } from "react"; +import CardNumberInputs from "./CardNumberInputs"; +import { getFirstErrorMessage } from "../../../util/getFirstErrorMessage"; +import ERROR_MESSAGE from "../../../constants/errorMessage"; +import { within, userEvent } from "@storybook/test"; +const meta: Meta = { + title: "Components/CardNumberInputs", + component: CardNumberInputs, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], +}; +export default meta; + +type Story = StoryObj; + +const renderCardNumberInputs = ( + setCardInput: Dispatch>, + errorMessages: string, + handleErrorMessages: (key: keyof CardInputProps, message: string) => void +) => { + return ( + + ); +}; + +export const Default: Story = { + render: () => { + const Component = () => { + const [, setCardInput] = useState({ + first: null, + second: null, + third: null, + fourth: null, + MM: null, + YY: null, + CVC: null, + }); + const { errorMessages, handleErrorMessages } = useErrorMessages(); + const cardErrorMessage = getFirstErrorMessage([ + errorMessages.first, + errorMessages.second, + errorMessages.third, + errorMessages.fourth, + ]); + + return renderCardNumberInputs( + setCardInput, + cardErrorMessage, + handleErrorMessages + ); + }; + + return ; + }, +}; + +export const InputError: Story = { + render: () => { + const Component = () => { + const [, setCardInput] = useState({ + first: null, + second: null, + third: null, + fourth: null, + MM: null, + YY: null, + CVC: null, + }); + const { errorMessages, handleErrorMessages } = useErrorMessages(); + + const cardErrorMessage = getFirstErrorMessage([ + ERROR_MESSAGE.NOT_A_NUMBER, + errorMessages.second, + errorMessages.third, + errorMessages.fourth, + ]); + + return renderCardNumberInputs( + setCardInput, + cardErrorMessage, + handleErrorMessages + ); + }; + + return ; + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const inputs = await canvas.getAllByPlaceholderText("1234"); + await userEvent.type(inputs[0], "abcd"); + }, +}; diff --git a/src/component/inputSections/cardNumberInputs/CardNumberInputs.tsx b/src/component/inputSections/cardNumberInputs/CardNumberInputs.tsx new file mode 100644 index 0000000000..3efdb48f3a --- /dev/null +++ b/src/component/inputSections/cardNumberInputs/CardNumberInputs.tsx @@ -0,0 +1,48 @@ +import { validateCardNumber } from "../../../validation/validation"; +import InputGroup from "../../InputGroup"; +import CardInput from "../../CardInput"; +import type { ErrorMessagesProps } from "../../../types/ErrorMessagesType"; +import type { CardInputProps } from "../../../types/CardInputTypes"; +import type { Dispatch, SetStateAction } from "react"; + +interface CardNumberInputProps { + errorMessages: string; + handleErrorMessages: (key: keyof ErrorMessagesProps, message: string) => void; + setCardInput: Dispatch>; +} + +const CardNumberInputs = ({ + errorMessages, + setCardInput, + handleErrorMessages, +}: CardNumberInputProps) => { + const cardNumberKeys: (keyof Pick< + CardInputProps, + "first" | "second" | "third" | "fourth" + >)[] = ["first", "second", "third", "fourth"]; + + return ( + + {cardNumberKeys.map((key) => ( + + handleErrorMessages(key, message) + } + /> + ))} + + ); +}; + +export default CardNumberInputs; diff --git a/src/component/inputSections/cvcInputs/CVCInputs.stories.tsx b/src/component/inputSections/cvcInputs/CVCInputs.stories.tsx new file mode 100644 index 0000000000..a4a961b9fb --- /dev/null +++ b/src/component/inputSections/cvcInputs/CVCInputs.stories.tsx @@ -0,0 +1,102 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import CVCInputs from "./CVCInputs"; +import { type Dispatch, type SetStateAction, useState } from "react"; +import type { CardInputProps } from "../../../types/CardInputTypes"; +import { useErrorMessages } from "../../../hook/useErrorMessages"; +import { getFirstErrorMessage } from "../../../util/getFirstErrorMessage"; +import ERROR_MESSAGE from "../../../constants/errorMessage"; +import { within, userEvent } from "@storybook/test"; +const meta: Meta = { + title: "Components/CVCInputs", + component: CVCInputs, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +const renderCVCInputs = ( + setCardInput: Dispatch>, + errorMessages: string, + handleErrorMessages: (key: keyof CardInputProps, message: string) => void +) => { + return ( + + ); +}; + +export const Default: Story = { + render: () => { + const Component = () => { + const [, setCardInput] = useState({ + first: null, + second: null, + third: null, + fourth: null, + MM: null, + YY: null, + CVC: null, + }); + const { errorMessages, handleErrorMessages } = useErrorMessages(); + const cardErrorMessage = getFirstErrorMessage([ + errorMessages.first, + errorMessages.second, + errorMessages.third, + errorMessages.fourth, + ]); + + return renderCVCInputs( + setCardInput, + cardErrorMessage, + handleErrorMessages + ); + }; + + return ; + }, +}; + +export const InputError: Story = { + render: () => { + const Component = () => { + const [, setCardInput] = useState({ + first: null, + second: null, + third: null, + fourth: null, + MM: null, + YY: null, + CVC: null, + }); + const { errorMessages, handleErrorMessages } = useErrorMessages(); + + const cardErrorMessage = getFirstErrorMessage([ + ERROR_MESSAGE.NOT_A_NUMBER, + errorMessages.second, + errorMessages.third, + errorMessages.fourth, + ]); + + return renderCVCInputs( + setCardInput, + cardErrorMessage, + handleErrorMessages + ); + }; + + return ; + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const inputs = await canvas.getByPlaceholderText("123"); + await userEvent.type(inputs, "abcd"); + }, +}; diff --git a/src/component/inputSections/cvcInputs/CVCInputs.tsx b/src/component/inputSections/cvcInputs/CVCInputs.tsx new file mode 100644 index 0000000000..eeb1e13345 --- /dev/null +++ b/src/component/inputSections/cvcInputs/CVCInputs.tsx @@ -0,0 +1,34 @@ +import { validateCardCVC } from "../../../validation/validation"; +import CardInput from "../../CardInput"; +import InputGroup from "../../InputGroup"; +import type { ErrorMessagesProps } from "../../../types/ErrorMessagesType"; +import type { Dispatch, SetStateAction } from "react"; +import type { CardInputProps } from "../../../types/CardInputTypes"; + +interface CVCInputsProps { + errorMessages: string; + setCardInput: Dispatch>; + handleErrorMessages: (key: keyof ErrorMessagesProps, message: string) => void; +} + +const CVCInputs = ({ + errorMessages, + setCardInput, + handleErrorMessages, +}: CVCInputsProps) => { + return ( + + handleErrorMessages("CVC", message)} + /> + + ); +}; + +export default CVCInputs; diff --git a/src/component/inputSections/expirationDateInputs/ExpirationDateInputs.stories.tsx b/src/component/inputSections/expirationDateInputs/ExpirationDateInputs.stories.tsx new file mode 100644 index 0000000000..2d7ff3ecf2 --- /dev/null +++ b/src/component/inputSections/expirationDateInputs/ExpirationDateInputs.stories.tsx @@ -0,0 +1,139 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { type Dispatch, type SetStateAction, useState } from "react"; +import type { CardInputProps } from "../../../types/CardInputTypes"; +import { useErrorMessages } from "../../../hook/useErrorMessages"; +import { getFirstErrorMessage } from "../../../util/getFirstErrorMessage"; +import ERROR_MESSAGE from "../../../constants/errorMessage"; +import { within, userEvent } from "@storybook/test"; +import ExpirationDateInputs from "./ExpirationDateInputs"; +const meta: Meta = { + title: "Components/ExpirationDateInputs", + component: ExpirationDateInputs, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +const renderExpirationDateInputs = ( + setCardInput: Dispatch>, + errorMessages: string, + handleErrorMessages: (key: keyof CardInputProps, message: string) => void +) => { + return ( + + ); +}; + +export const Default: Story = { + render: () => { + const Component = () => { + const [, setCardInput] = useState({ + first: null, + second: null, + third: null, + fourth: null, + MM: null, + YY: null, + CVC: null, + }); + const { errorMessages, handleErrorMessages } = useErrorMessages(); + const cardErrorMessage = getFirstErrorMessage([ + errorMessages.first, + errorMessages.second, + errorMessages.third, + errorMessages.fourth, + ]); + + return renderExpirationDateInputs( + setCardInput, + cardErrorMessage, + handleErrorMessages + ); + }; + + return ; + }, +}; + +export const NumberInputError: Story = { + render: () => { + const Component = () => { + const [, setCardInput] = useState({ + first: null, + second: null, + third: null, + fourth: null, + MM: null, + YY: null, + CVC: null, + }); + const { errorMessages, handleErrorMessages } = useErrorMessages(); + + const cardErrorMessage = getFirstErrorMessage([ + ERROR_MESSAGE.NOT_A_NUMBER, + errorMessages.second, + errorMessages.third, + errorMessages.fourth, + ]); + + return renderExpirationDateInputs( + setCardInput, + cardErrorMessage, + handleErrorMessages + ); + }; + + return ; + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const input = await canvas.getByPlaceholderText("MM"); + await userEvent.type(input, "abcd"); + }, +}; + +export const RangeInputError: Story = { + render: () => { + const Component = () => { + const [, setCardInput] = useState({ + first: null, + second: null, + third: null, + fourth: null, + MM: null, + YY: null, + CVC: null, + }); + const { errorMessages, handleErrorMessages } = useErrorMessages(); + + const cardErrorMessage = getFirstErrorMessage([ + ERROR_MESSAGE.INVALID_EXPIRATION_YEAR, + errorMessages.second, + errorMessages.third, + errorMessages.fourth, + ]); + + return renderExpirationDateInputs( + setCardInput, + cardErrorMessage, + handleErrorMessages + ); + }; + + return ; + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const input = await canvas.getByPlaceholderText("MM"); + await userEvent.type(input, "23"); + }, +}; diff --git a/src/component/inputSections/expirationDateInputs/ExpirationDateInputs.tsx b/src/component/inputSections/expirationDateInputs/ExpirationDateInputs.tsx new file mode 100644 index 0000000000..3eac86e463 --- /dev/null +++ b/src/component/inputSections/expirationDateInputs/ExpirationDateInputs.tsx @@ -0,0 +1,52 @@ +import InputGroup from "../../InputGroup"; +import CardInput from "../../CardInput"; +import type { CardInputProps } from "../../../types/CardInputTypes"; +import type { ErrorMessagesProps } from "../../../types/ErrorMessagesType"; +import type { Dispatch, SetStateAction } from "react"; +import { + validateCardExpirationDateMM, + validateCardExpirationDateYY, +} from "../../../validation/validation"; + +interface ExpirationDateInputsProps { + errorMessages: string; + setCardInput: Dispatch>; + handleErrorMessages: (key: keyof ErrorMessagesProps, message: string) => void; +} + +const ExpirationDateInputs = ({ + errorMessages, + setCardInput, + handleErrorMessages, +}: ExpirationDateInputsProps) => { + const expirationDateKeys: (keyof Pick)[] = [ + "MM", + "YY", + ]; + return ( + + {expirationDateKeys.map((key) => ( + handleErrorMessages(key, message)} + /> + ))} + + ); +}; + +export default ExpirationDateInputs; diff --git a/src/constants/cardBrandLogo.ts b/src/constants/cardBrandLogo.ts new file mode 100644 index 0000000000..3ba8015a60 --- /dev/null +++ b/src/constants/cardBrandLogo.ts @@ -0,0 +1,7 @@ +const cardBrandLogo = { + visa: "./visa.png", + mastercard: "./mastercard.png", + default: "./default.png", +}; + +export default cardBrandLogo; diff --git a/src/constants/errorMessage.ts b/src/constants/errorMessage.ts new file mode 100644 index 0000000000..77c2f7c952 --- /dev/null +++ b/src/constants/errorMessage.ts @@ -0,0 +1,7 @@ +const ERROR_MESSAGE = { + NOT_A_NUMBER: "숫자만 입력 가능합니다", + INVALID_EXPIRATION_MONTH: "유효기간 월의 범위가 올바르지 않습니다 (1~12)", + INVALID_EXPIRATION_YEAR: "유효기간이 지난 카드입니다", +}; + +export default ERROR_MESSAGE; diff --git a/src/hook/useErrorMessages.ts b/src/hook/useErrorMessages.ts new file mode 100644 index 0000000000..df82c21cdd --- /dev/null +++ b/src/hook/useErrorMessages.ts @@ -0,0 +1,26 @@ +import { useState } from "react"; +import type { ErrorMessagesProps } from "../types/ErrorMessagesType"; + +export const useErrorMessages = () => { + const [errorMessages, setErrorMessages] = useState({ + first: "", + second: "", + third: "", + fourth: "", + MM: "", + YY: "", + CVC: "", + }); + + const handleErrorMessages = ( + key: keyof ErrorMessagesProps, + message: string + ) => { + setErrorMessages((prev: ErrorMessagesProps) => ({ + ...prev, + [key]: message, + })); + }; + + return { errorMessages, handleErrorMessages }; +}; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/main.tsx b/src/main.tsx index 3d7150da80..95e2bdc2ca 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,9 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import './index.css' +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.tsx"; -ReactDOM.createRoot(document.getElementById('root')!).render( +ReactDOM.createRoot(document.getElementById("root")!).render( - , -) + +); diff --git a/src/pages/AddCard.tsx b/src/pages/AddCard.tsx new file mode 100644 index 0000000000..6797b57f79 --- /dev/null +++ b/src/pages/AddCard.tsx @@ -0,0 +1,94 @@ +import Card from "../component/card/Card"; +import Description from "../component/Description"; +import styled from "styled-components"; +import { useState } from "react"; +import { justifyBrandLogo } from "../util/justifyBrandLogo"; +import CardNumberInputs from "../component/inputSections/cardNumberInputs/CardNumberInputs"; +import type { CardInputProps } from "../types/CardInputTypes"; +import { useErrorMessages } from "../hook/useErrorMessages"; +import ExpirationDateInputs from "../component/inputSections/expirationDateInputs/ExpirationDateInputs"; +import CVCInputs from "../component/inputSections/cvcInputs/CVCInputs"; +import { getFirstErrorMessage } from "../util/getFirstErrorMessage"; + +const AddCard = () => { + const [cardInput, setCardInput] = useState({ + first: null, + second: null, + third: null, + fourth: null, + MM: null, + YY: null, + CVC: null, + }); + + const { errorMessages, handleErrorMessages } = useErrorMessages(); + + return ( + + +
+ + + + + + + + + +
+ ); +}; + +const Wrap = styled.div` + display: flex; + flex-direction: column; + align-items: center; + width: 376px; + padding: 30px; + background-color: var(--color-white); + padding-top: 77px; + height: 700px; + gap: 45px; +`; + +const Form = styled.form` + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + gap: 16px; +`; + +export default AddCard; diff --git a/src/types/CardInputTypes.ts b/src/types/CardInputTypes.ts new file mode 100644 index 0000000000..3bac677338 --- /dev/null +++ b/src/types/CardInputTypes.ts @@ -0,0 +1,9 @@ +export interface CardInputProps { + first: number | null; + second: number | null; + third: number | null; + fourth: number | null; + MM: number | null; + YY: number | null; + CVC: number | null; +} diff --git a/src/types/ErrorMessagesType.ts b/src/types/ErrorMessagesType.ts new file mode 100644 index 0000000000..aec1d1a699 --- /dev/null +++ b/src/types/ErrorMessagesType.ts @@ -0,0 +1,9 @@ +export interface ErrorMessagesProps { + first: string; + second: string; + third: string; + fourth: string; + MM: string; + YY: string; + CVC: string; +} diff --git a/src/util/getFirstErrorMessage.ts b/src/util/getFirstErrorMessage.ts new file mode 100644 index 0000000000..70673cc493 --- /dev/null +++ b/src/util/getFirstErrorMessage.ts @@ -0,0 +1,3 @@ +export const getFirstErrorMessage = (errorMessagesArray: string[]) => { + return errorMessagesArray.filter((message) => message.length !== 0)[0]; +}; diff --git a/src/util/justifyBrandLogo.ts b/src/util/justifyBrandLogo.ts new file mode 100644 index 0000000000..bc44a1ae11 --- /dev/null +++ b/src/util/justifyBrandLogo.ts @@ -0,0 +1,13 @@ +export const justifyBrandLogo = (cardNumber: number) => { + if (cardNumber.toString().startsWith("0")) return "default"; + if (cardNumber.toString().startsWith("4")) return "visa"; + + if ( + cardNumber.toString().startsWith("51") || + cardNumber.toString().startsWith("52") + ) + return "mastercard"; + if (cardNumber >= 2221 && cardNumber <= 2720) return "mastercard"; + + return "default"; +}; diff --git a/src/util/maskingNumber.ts b/src/util/maskingNumber.ts new file mode 100644 index 0000000000..90c66b858e --- /dev/null +++ b/src/util/maskingNumber.ts @@ -0,0 +1,3 @@ +export const maskingNumber = (count: number) => { + return "•".repeat(count); +}; diff --git a/src/validation/validation.ts b/src/validation/validation.ts new file mode 100644 index 0000000000..2e9c24daf6 --- /dev/null +++ b/src/validation/validation.ts @@ -0,0 +1,44 @@ +import ERROR_MESSAGE from "../constants/errorMessage"; + +export const validateCardNumber = (cardNumber: string) => { + if (cardNumber.length === 0) return; + + if (isNaN(Number(cardNumber))) { + return ERROR_MESSAGE.NOT_A_NUMBER; + } + + return ""; +}; + +export const validateCardExpirationDateMM = (expirationDate: string) => { + if (expirationDate.length === 0) return; + + if (isNaN(Number(expirationDate))) { + return ERROR_MESSAGE.NOT_A_NUMBER; + } + if (Number(expirationDate) < 1 || Number(expirationDate) > 12) { + return ERROR_MESSAGE.INVALID_EXPIRATION_MONTH; + } + return ""; +}; + +export const validateCardExpirationDateYY = (expirationDate: string) => { + if (expirationDate.length === 0) return; + + if (isNaN(Number(expirationDate))) { + return ERROR_MESSAGE.NOT_A_NUMBER; + } + if (Number(expirationDate) < 25 || Number(expirationDate) > 99) { + return ERROR_MESSAGE.INVALID_EXPIRATION_YEAR; + } + return ""; +}; + +export const validateCardCVC = (cvc: string) => { + if (cvc.length === 0) return; + + if (isNaN(Number(cvc))) { + return ERROR_MESSAGE.NOT_A_NUMBER; + } + return ""; +}; diff --git a/vite.config.ts b/vite.config.ts index 861b04b356..97384c228d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) + base: "/react-payments/", +});