diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a2295ec..7954b09 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -22,6 +22,7 @@ module.exports = { rules: { "react/react-in-jsx-scope": "off", "react/jsx-props-no-spreading": "off", + "implicit-arrow-linebreak":"off", "react/require-default-props": "off", "import/no-extraneous-dependencies": "off", "@typescript-eslint/no-shadow": "off", @@ -41,10 +42,12 @@ module.exports = { "react/button-has-type": "off", "no-param-reassign": "off", "react/prop-types": "off", + "import/no-named-as-default": "off", "import/no-extraneous-dependencies": "off", "no-nested-ternary": "off", "import/prefer-default-export": "off", "no-return-assign": "off", + "react-refresh/only-export-components":"off", "jsx-a11y/label-has-associated-control": "off", "react/function-component-definition": [ "warn", @@ -72,7 +75,7 @@ module.exports = { "react/prop-types": "off", "jsx-a11y/control-has-associated-label": "off", "react-refresh/only-export-components": [ - "warn", + "off", { allowConstantExport: true }, ], "@typescript-eslint/quotes": ["off"], diff --git a/package-lock.json b/package-lock.json index b8797ab..35197a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,8 @@ "axios-mock-adapter": "^1.22.0", "eslint-config-airbnb-typescript": "^18.0.0", "expect-puppeteer": "^10.0.0", + "flowbite": "^2.4.1", + "flowbite-react": "^0.10.1", "gsap": "^3.12.5", "install": "^0.13.0", "jest-environment-jsdom": "^29.7.0", @@ -48,6 +50,7 @@ "redux": "^5.0.1", "redux-thunk": "^3.1.0", "swiper": "^11.1.4", + "tailwindcss": "^3.4.4", "vite-plugin-environment": "^1.1.3", "yup": "^1.4.0" }, @@ -105,7 +108,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "engines": { "node": ">=10" }, @@ -1549,6 +1551,20 @@ "@floating-ui/utils": "^0.2.3" } }, + "node_modules/@floating-ui/react": { + "version": "0.26.17", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.17.tgz", + "integrity": "sha512-ESD+jYWwqwVzaIgIhExrArdsCL1rOAzryG/Sjlu8yaD3Mtqi3uVyhbE2V7jD58Mo52qbzKz2eUY/Xgh5I86FCQ==", + "dependencies": { + "@floating-ui/react-dom": "^2.1.0", + "@floating-ui/utils": "^0.2.0", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/react-dom": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", @@ -1827,7 +1843,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -1844,7 +1859,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -1856,7 +1870,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -1868,7 +1881,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1885,7 +1897,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1900,7 +1911,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -2866,7 +2876,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" @@ -3234,6 +3243,51 @@ "node": ">=14.0.0" } }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -4193,6 +4247,11 @@ "redux-thunk": "*" } }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -4555,8 +4614,7 @@ "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, "node_modules/anymatch": { "version": "3.1.3", @@ -4573,8 +4631,7 @@ "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" }, "node_modules/argparse": { "version": "2.0.1", @@ -5069,7 +5126,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "engines": { "node": ">=8" }, @@ -5152,6 +5208,17 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -5190,7 +5257,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "engines": { "node": ">= 6" } @@ -5238,7 +5304,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -5262,7 +5327,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -5289,6 +5353,11 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/cli-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", @@ -5746,7 +5815,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "bin": { "cssesc": "bin/cssesc" }, @@ -5867,6 +5935,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.1.0.tgz", + "integrity": "sha512-OkL3+0pPWCqoBc/nhO9u6TIQNTK44fnBnzuVtJAbp13Naxw9R6u21x+8tVTka87AhDZ3htqZ2pSSsZl9fqL2Wg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", @@ -6005,8 +6084,7 @@ "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, "node_modules/diff": { "version": "4.0.2", @@ -6039,8 +6117,7 @@ "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, "node_modules/doctrine": { "version": "3.0.0", @@ -6094,8 +6171,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { "version": "1.4.812", @@ -6116,8 +6192,7 @@ "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/entities": { "version": "4.5.0", @@ -7097,6 +7172,11 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -7323,6 +7403,61 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, + "node_modules/flowbite": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.4.1.tgz", + "integrity": "sha512-I++vDsSOOlzHNuxY2OcFMNVC4CNzpPU2K14YHJ81cYrANXdzgizqniMB/1KQ219x8fqw+S0msY9Q45ZSXDqAPw==", + "dependencies": { + "@popperjs/core": "^2.9.3", + "flowbite-datepicker": "^1.3.0", + "mini-svg-data-uri": "^1.4.3" + } + }, + "node_modules/flowbite-datepicker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flowbite-datepicker/-/flowbite-datepicker-1.3.0.tgz", + "integrity": "sha512-CLVqzuoE2vkUvWYK/lJ6GzT0be5dlTbH3uuhVwyB67+PjqJWABm2wv68xhBf5BqjpBxvTSQ3mrmLHpPJ2tvrSQ==", + "dependencies": { + "@rollup/plugin-node-resolve": "^15.2.3", + "flowbite": "^2.0.0" + } + }, + "node_modules/flowbite-react": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/flowbite-react/-/flowbite-react-0.10.1.tgz", + "integrity": "sha512-T6rdfrEvIqrf7aIB+OLkuvDaa/h0Ufnl7/5vJR9JJ4IpKIvJm/JzhAiYmkD+jDj3HuILsN21+ZVV6gd4tlndYQ==", + "dependencies": { + "@floating-ui/core": "1.6.2", + "@floating-ui/react": "0.26.17", + "classnames": "2.5.1", + "debounce": "2.1.0", + "flowbite": "2.3.0", + "react-icons": "5.2.1", + "tailwind-merge": "2.3.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18", + "tailwindcss": "^3" + } + }, + "node_modules/flowbite-react/node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/flowbite-react/node_modules/flowbite": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.3.0.tgz", + "integrity": "sha512-pm3JRo8OIJHGfFYWgaGpPv8E+UdWy0Z3gEAGufw+G/1dusaU/P1zoBLiQpf2/+bYAi+GBQtPVG86KYlV0W+AFQ==", + "dependencies": { + "@popperjs/core": "^2.9.3", + "mini-svg-data-uri": "^1.4.3" + } + }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", @@ -7354,7 +7489,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -8079,7 +8213,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -8124,6 +8257,20 @@ "node": ">=4" } }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -8255,6 +8402,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -8558,7 +8710,6 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -9995,7 +10146,6 @@ "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", - "dev": true, "bin": { "jiti": "bin/jiti.js" } @@ -10201,7 +10351,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "dev": true, "engines": { "node": ">=14" }, @@ -10571,6 +10720,14 @@ "node": ">=4" } }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -10597,7 +10754,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -10724,7 +10880,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -13260,7 +13415,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "engines": { "node": ">= 6" } @@ -13505,8 +13659,7 @@ "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -13581,7 +13734,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -13597,7 +13749,6 @@ "version": "10.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, "engines": { "node": "14 || >=16.14" } @@ -13648,7 +13799,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -13767,7 +13917,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -13784,7 +13933,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "dependencies": { "camelcase-css": "^2.0.1" }, @@ -13803,7 +13951,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -13838,7 +13985,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.11" }, @@ -13857,7 +14003,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", - "dev": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -13869,8 +14014,7 @@ "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==", - "dev": true + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -14191,7 +14335,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "dependencies": { "pify": "^2.3.0" } @@ -14200,7 +14343,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -14626,7 +14768,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -14811,7 +14952,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -14824,14 +14964,12 @@ "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -14967,7 +15105,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -15027,7 +15164,6 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -15049,7 +15185,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "engines": { "node": ">= 6" } @@ -15058,7 +15193,6 @@ "version": "10.4.2", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", - "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -15138,11 +15272,27 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, + "node_modules/tailwind-merge": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz", + "integrity": "sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==", + "dependencies": { + "@babel/runtime": "^7.24.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", - "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -15179,7 +15329,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, "engines": { "node": ">=10" } @@ -15238,7 +15387,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "dependencies": { "any-promise": "^1.0.0" } @@ -15247,7 +15395,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -15347,8 +15494,7 @@ "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/ts-jest": { "version": "29.1.5", @@ -15688,8 +15834,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", @@ -15964,7 +16109,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -15981,7 +16125,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -15996,7 +16139,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -16007,20 +16149,17 @@ "node_modules/wrap-ansi-cjs/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -16029,7 +16168,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -16150,7 +16288,6 @@ "version": "2.4.5", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", - "dev": true, "bin": { "yaml": "bin.mjs" }, diff --git a/package.json b/package.json index 9125221..3d6b064 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "axios-mock-adapter": "^1.22.0", "eslint-config-airbnb-typescript": "^18.0.0", "expect-puppeteer": "^10.0.0", + "flowbite": "^2.4.1", + "flowbite-react": "^0.10.1", "gsap": "^3.12.5", "install": "^0.13.0", "jest-environment-jsdom": "^29.7.0", @@ -56,6 +58,7 @@ "redux": "^5.0.1", "redux-thunk": "^3.1.0", "swiper": "^11.1.4", + "tailwindcss": "^3.4.4", "vite-plugin-environment": "^1.1.3", "yup": "^1.4.0" }, diff --git a/src/__test__/cartManage.test.tsx b/src/__test__/cartManage.test.tsx new file mode 100644 index 0000000..69a8567 --- /dev/null +++ b/src/__test__/cartManage.test.tsx @@ -0,0 +1,40 @@ +import "@testing-library/jest-dom"; +import { render, screen, waitFor } from "@testing-library/react"; +import { Provider } from "react-redux"; +import { BrowserRouter as Router } from "react-router-dom"; + +import store from "../redux/store"; +import CartManagement from "../pages/CartManagement"; +import Warning from "../components/common/notify/Warning"; + +describe("Cart Management Page", () => { + beforeAll(() => { + jest + .spyOn(Object.getPrototypeOf(window.localStorage), "getItem") + .mockReturnValue("mockAccessToken"); + }); + + test("should render cart management page correctly", async () => { + render( + + + + + , + ); + + await waitFor(() => { + const spinner = screen.queryByRole("status"); + expect(spinner).toBeNull(); + }); + + expect().toBeDefined(); + // expect(screen.getByText("Cart Total")).toBeInTheDocument(); + // expect(screen.getByText("Update Cart")).toBeInTheDocument(); + }); + + afterAll(() => { + // Restore original getItem method after tests + jest.restoreAllMocks(); + }); +}); diff --git a/src/__test__/cartSlice.test.tsx b/src/__test__/cartSlice.test.tsx new file mode 100644 index 0000000..a275cc4 --- /dev/null +++ b/src/__test__/cartSlice.test.tsx @@ -0,0 +1,88 @@ +// @ts-nochec +import { configureStore } from "@reduxjs/toolkit"; + +import cartReducer, { + cartManage, + cartDelete, + addToCart, + removeFromCart, + updateCarts, + increaseQuantity, + decreaseQuantity, +} from "../redux/reducers/cartSlice"; +import axios from "../redux/api/api"; + +jest.mock("../redux/api/api"); + +describe("cartManageSlice", () => { + let store; + + beforeEach(() => { + store = configureStore({ + reducer: { + cart: cartReducer, + }, + }); + }); + + it("should handle initial state", () => { + expect(store.getState().cart).toEqual({ + isLoading: false, + data: [], + error: false, + delete: { isLoading: false, error: false }, + add: { isLoading: false, data: [], error: null }, + remove: { isLoading: false, error: false }, + update: { isLoading: false, data: [], error: false }, + }); + }); + + it("should handle cartManage.fulfilled", async () => { + const mockData = [{ id: "1", quantity: 2 }]; + (axios.get as jest.Mock).mockResolvedValue({ + data: { userCart: { items: mockData } }, + }); + + await store.dispatch(cartManage()); + + expect(store.getState().cart.data).toEqual(mockData); + expect(store.getState().cart.isLoading).toBeFalsy(); + }); + + it("should handle addToCart.fulfilled", async () => { + const mockItem = { id: "2", quantity: 1 }; + (axios.post as jest.Mock).mockResolvedValue({ data: mockItem }); + + await store.dispatch(addToCart({ productId: 2, quantity: 1 })); + + expect(store.getState().cart.data).toContainEqual(mockItem); + }); + + it("should handle removeFromCart.fulfilled", async () => { + (axios.put as jest.Mock).mockResolvedValue({ data: {} }); + // @ts-ignore + await store.dispatch(removeFromCart("1")); + + expect(store.getState().cart.remove.isLoading).toBeFalsy(); + }); + it("should handle clear FromCart.fulfilled", async () => { + (axios.delete as jest.Mock).mockResolvedValue({ data: {} }); + // @ts-ignore + await store.dispatch(cartDelete("1")); + + expect(store.getState().cart.delete.isLoading).toBeFalsy(); + }); + + it("should handle updateCarts.fulfilled", async () => { + const mockItem = { productId: "1", quantity: 3 }; + (axios.patch as jest.Mock).mockResolvedValue({ data: mockItem }); + + store.dispatch({ + type: "carts/cartManage/fulfilled", + payload: [{ productId: "1", quantity: 2 }], + }); + await store.dispatch(updateCarts({ productId: "1", quantity: 3 })); + + expect(store.getState().cart.data[0]).toEqual(undefined); + }); +}); diff --git a/src/__test__/getProfilesclice.test.tsx b/src/__test__/getProfilesclice.test.tsx index 0dc2483..68e6a54 100644 --- a/src/__test__/getProfilesclice.test.tsx +++ b/src/__test__/getProfilesclice.test.tsx @@ -1,8 +1,7 @@ -import { getProfile, profileSlice } from "../redux/reducers/profileSlice"; - -import "@testing-library/jest-dom"; import { render, screen } from "@testing-library/react"; +import { getProfile, profileSlice } from "../redux/reducers/profileSlice"; +import "@testing-library/jest-dom"; import DisplayProfileData from "../components/profile/getProfile"; const { reducer } = profileSlice; diff --git a/src/components/cards/ProductCard.tsx b/src/components/cards/ProductCard.tsx index 08a2e7c..a99bf92 100644 --- a/src/components/cards/ProductCard.tsx +++ b/src/components/cards/ProductCard.tsx @@ -1,10 +1,22 @@ import { IconButton, Rating, Typography } from "@mui/material"; import { FaEye } from "react-icons/fa"; import { CiHeart } from "react-icons/ci"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; +import { ToastContainer, toast } from "react-toastify"; +import { AxiosError } from "axios"; +import { useSelector } from "react-redux"; import { IProduct } from "../../types"; +import { useAppDispatch } from "../../redux/hooks"; +import { + addToCart, + cartManage, + removeFromCart, +} from "../../redux/reducers/cartSlice"; +import Warning from "../common/notify/Warning"; +import { RootState } from "../../redux/store"; +import { RegisterError } from "../../../type"; interface IProductCardProps { product: IProduct; @@ -12,59 +24,108 @@ interface IProductCardProps { const ProductCard: React.FC = ({ product }) => { const [isHovered, setIsHovered] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const dispatch = useAppDispatch(); const soleilFN = (price: number) => { if (price < 1000) { return price.toString(); - } if (price >= 1000 && price < 1000000) { + } + if (price >= 1000 && price < 1000000) { return `${(price / 1000).toFixed(1)}k`; } return `${(price / 1000000).toFixed(1)}M`; }; + const handleRemove = async (productId: number) => { + try { + // @ts-ignore + await dispatch(removeFromCart(productId)); + dispatch(cartManage()); + } catch (err) { + const error = err as AxiosError; + toast.error(`${error.message}`); + } + }; + const loading = useSelector( + (state: RootState) => state.cart.remove.isLoading, + ); + const userCart = useSelector((state: RootState) => state.cart.data); + + const alreadyInCart = userCart?.some( + (item) => + // @ts-ignore + item.product?.id === product.id, + ); + + const handleAddToCart = async () => { + if (!localStorage.getItem("accessToken")) { + toast.info("Please Log in to add to cart."); + return; + } + setIsLoading(true); + try { + await dispatch( + addToCart({ productId: product.id, quantity: 1 }), + ).unwrap(); + dispatch(cartManage()); + } catch (err) { + const error = err as AxiosError; + toast.error(`Failed to add product to cart: ${error.message}`); + } finally { + setIsLoading(false); + } + }; const name = product.name.length > 20 ? `${product.name.substring(0, 12)}...` : product.name; const date = new Date(product.createdAt).getDate(); - const currrentDate = new Date().getDate(); - - const diff = currrentDate - date; + const currentDate = new Date().getDate(); + const diff = currentDate - date; return ( -
+
{diff < 2 && ( -
-

New

+
+

New

)} - +
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > d-ss {isHovered && ( - +
+ {!alreadyInCart ? ( + + ) : ( + + )} +
)}
- -
-
+
+
= ({ product }) => { data-testid="dprod-detailbtn" > - {" "} diff --git a/src/components/common/auth/Loader.tsx b/src/components/common/auth/Loader.tsx new file mode 100644 index 0000000..360f8ba --- /dev/null +++ b/src/components/common/auth/Loader.tsx @@ -0,0 +1,10 @@ +const Spinner = () => ( +
+
+
+); + +export default Spinner; diff --git a/src/components/common/header/Header.tsx b/src/components/common/header/Header.tsx index 0de5c4f..d5360ed 100644 --- a/src/components/common/header/Header.tsx +++ b/src/components/common/header/Header.tsx @@ -10,6 +10,7 @@ import { useSelector } from "react-redux"; import { getProfile } from "../../../redux/reducers/profileSlice"; import Logo from "../auth/Logo"; import { RootState } from "../../../redux/store"; +import { cartManage } from "../../../redux/reducers/cartSlice"; import { useAppDispatch } from "../../../redux/hooks"; interface ISerachProps { @@ -40,6 +41,11 @@ const Header: React.FC = ({ searchQuery, setSearchQuery }) => { } } }, []); + const dispatches = useAppDispatch(); + useEffect(() => { + dispatch(cartManage()); + }, [dispatches]); + const userCart = useSelector((state: RootState) => state.cart.data); return ( = ({ searchQuery, setSearchQuery }) => { direction="row" className="flex justify-between items-center gap-2 bg-white" > - + - - Cart - $150.00 + + + Cart + + {/* $150.00 */} + + {!localStorage.getItem("accessToken") ? ( + "" + ) : ( +
+ {userCart.length} +
+ )}
diff --git a/src/components/common/notify/DeleteNotify.tsx b/src/components/common/notify/DeleteNotify.tsx new file mode 100644 index 0000000..b245a36 --- /dev/null +++ b/src/components/common/notify/DeleteNotify.tsx @@ -0,0 +1,51 @@ +import React, { useState } from "react"; +import { Button, Modal } from "flowbite-react"; +import { HiOutlineExclamationCircle } from "react-icons/hi"; + +const DeleteNotify = ({ onConfirm, onCancel }) => { + const [openModal, setOpenModal] = useState(false); + + const handleConfirm = () => { + onConfirm(); + setOpenModal(false); + }; + + return ( + <> + + setOpenModal(false)} + popup + > + + +
+ +

+ Are you sure you want to delete this product? +

+
+ + +
+
+
+
+ + ); +}; + +export default DeleteNotify; diff --git a/src/components/common/notify/Warning.tsx b/src/components/common/notify/Warning.tsx new file mode 100644 index 0000000..9134cc3 --- /dev/null +++ b/src/components/common/notify/Warning.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { Link } from "react-router-dom"; + +const Notification = () => ( +
+
+

+ You're not logged in to access cart, Please login to access cart +

+
+
+ Back home +
+
+ Log in +
+
+
+
+); + +export default Notification; diff --git a/src/pages/CartManagement.tsx b/src/pages/CartManagement.tsx new file mode 100644 index 0000000..29ba1f6 --- /dev/null +++ b/src/pages/CartManagement.tsx @@ -0,0 +1,249 @@ +// @ts-nocheck +import React, { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { IoChevronUpOutline, IoChevronDownSharp } from "react-icons/io5"; +import { MdOutlineClose } from "react-icons/md"; +import { ToastContainer, toast } from "react-toastify"; +import { AxiosError } from "axios"; + +import { RootState } from "../redux/store"; +import { + cartManage, + increaseQuantity, + decreaseQuantity, + cartDelete, + removeFromCart, + updateCarts, +} from "../redux/reducers/cartSlice"; +import Spinner from "../components/common/auth/Loader"; +import Warning from "../components/common/notify/Warning"; +import DeleteNotify from "../components/common/notify/DeleteNotify"; +import { useAppDispatch } from "../redux/hooks"; +import { RegisterError } from "../../type"; + +const CartManagement: React.FC = () => { + const dispatch = useAppDispatch(); + const loading = useSelector((state: RootState) => state.cart.isLoading); + const userCart = useSelector((state: RootState) => state.cart.data); + const error = useSelector((state: RootState) => state.cart.error); + const updateLoader = useSelector( + (state: RootState) => state.cart.update.isLoading, + ); + const [updatedQuantities, setUpdatedQuantities] = useState<{ + [key: number]: number; + }>({}); + + useEffect(() => { + dispatch(cartManage()); + }, [dispatch]); + + if (loading) { + return ; + } + if (!localStorage.getItem("accessToken")) { + return ; + } + if (error) { + return ( +
+

Error occurred during fetching 😎

+
+ ); + } + + const handleDelete = async () => { + await dispatch(cartDelete()); + dispatch(cartManage()); + }; + + const handleRemove = async (productId: string) => { + try { + await dispatch(removeFromCart(productId)); + dispatch(cartManage()); + toast.success("Product removed successfully"); + } catch (err) { + const error = err as AxiosError; + toast.error(`${error.message}`); + } + }; + + const handleUpdate = async () => { + try { + const updatePromises = Object.entries(updatedQuantities).map( + ([productId, quantity]) => dispatch(updateCarts({ productId: Number(productId), quantity })), + ); + + await Promise.all(updatePromises); + + setUpdatedQuantities({}); + dispatch(cartManage()); + } catch (err) { + const error = err as AxiosError; + toast.error(`${error.message}`); + } + }; + + const handleChange = (e: React.ChangeEvent, item: any) => { + const value = parseInt(e.target.value, 10); + setUpdatedQuantities({ + ...updatedQuantities, + [item.productId]: value, + }); + }; + + const total = userCart.reduce( + (acc, item) => acc + item.product?.price * item.quantity, + 0, + ); + + const hasUpdates = Object.keys(updatedQuantities).length > 0; + + return ( +
+ +
+

Home / Carts

+
+
+ + + {userCart.length === 0 ? ( + "" + ) : ( + + + + + + + )} + + + {userCart.length === 0 ? ( + + + + ) : ( + userCart.map((item: any) => ( + + + + + + + )) + )} + +
ProductPriceQuantitySubtotal
+ No items in the cart 😎 +
+
+
handleRemove(item.productId)} + > + +
+ {item.product?.name} + + {item.product?.name.length > 8 + ? `${item.product?.name.slice(0, 8)}...` + : item.product?.name} + +
+
+

+ RWF + {item.product?.price} +

+
+
+ handleChange(e, item)} + /> +
+
+

+ RWF + {item.product?.price + * ((updatedQuantities[item.productId] ?? item.quantity) + || 0)} +

+
+
+ {userCart.length === 0 ? ( + "" + ) : ( +
+
+ +
+
+ {}} /> +
+
+ )} +
+ {userCart.length === 0 ? ( + "" + ) : ( +
+

Cart Total

+
+

+ Subtotal +

+ + RWF + {total} + +
+
+
+

+ Shipping +

+ Free +
+
+
+

+ Total +

+ + RWF + {total} + +
+
+
+ Proceed to Checkout +
+
+ )} +
+
+ ); +}; + +export default CartManagement; diff --git a/src/redux/reducers/cartSlice.ts b/src/redux/reducers/cartSlice.ts new file mode 100644 index 0000000..a06e266 --- /dev/null +++ b/src/redux/reducers/cartSlice.ts @@ -0,0 +1,215 @@ +// @ts-nocheck +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; +import { AxiosError } from "axios"; + +import axios from "../api/api"; +import { UserCart } from "../../../type"; + +export const cartManage = createAsyncThunk( + "fetchCartItem", + async (_, { rejectWithValue }) => { + try { + const response = await axios.get("/carts", { + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + }, + }); + return response.data.userCart.items as UserCart; + } catch (err) { + const error = err as AxiosError; + return rejectWithValue(error.response?.data); + } + }, +); + +export const cartDelete = createAsyncThunk( + "deleteCart", + async (_, { rejectWithValue }) => { + try { + const response = await axios.delete("/carts", { + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + }, + }); + return response; + } catch (err) { + const error = err as AxiosError; + return rejectWithValue(error.response?.data || error.message); + } + }, +); + +export const addToCart = createAsyncThunk( + "addToCart", + async ( + { productId, quantity }: { productId: number; quantity: number }, + { rejectWithValue }, + ) => { + try { + const response = await axios.post( + "/carts", + { productId, quantity }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + }, + }, + ); + return response.data; + } catch (err) { + const error = err as AxiosError; + return rejectWithValue(error.response?.data || error.message); + } + }, +); + +export const removeFromCart = createAsyncThunk( + "cart/removeItem", + async (productId, { rejectWithValue }) => { + try { + const response = await axios.put( + "/carts", + { productId }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + }, + }, + ); + return response.data; + } catch (err) { + const error = err as AxiosError; + return rejectWithValue(error.response?.data || error.message); + } + }, +); + +export const updateCarts = createAsyncThunk( + "update", + async ( + { productId, quantity }: { productId: string; quantity: number }, + { rejectWithValue }, + ) => { + try { + const response = await axios.patch( + "/carts", + { productId, quantity }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + }, + }, + ); + console.log(response); + + return response.data; + } catch (err) { + const error = err as AxiosError; + return rejectWithValue(error.response?.data || error.message); + } + }, +); + +const cartManageSlice = createSlice({ + name: "carts", + initialState: { + isLoading: false, + data: [] as UserCart[], + error: false, + delete: { + isLoading: false, + error: false, + }, + add: { + isLoading: false, + data: [], + error: null, + }, + remove: { + isLoading: false, + error: false, + }, + update: { + isLoading: false, + data: [], + error: false, + }, + }, + reducers: { + increaseQuantity: (state, action: PayloadAction) => { + const item = state.data.find((item) => item.id === action.payload); + if (item) { + item.quantity += 1; + } + }, + decreaseQuantity: (state, action: PayloadAction) => { + const item = state.data.find((item) => item.id === action.payload); + if (item && item.quantity > 1) { + item.quantity -= 1; + } + }, + }, + extraReducers: (builder) => { + builder + .addCase(cartManage.pending, (state) => { + state.isLoading = true; + }) + .addCase(cartManage.fulfilled, (state, action) => { + state.isLoading = false; + state.data = action.payload; + }) + .addCase(cartManage.rejected, (state) => { + state.isLoading = false; + state.error = true; + }) + .addCase(cartDelete.pending, (state) => { + state.delete.isLoading = true; + }) + .addCase(cartDelete.fulfilled, (state) => { + state.delete.isLoading = false; + }) + .addCase(cartDelete.rejected, (state) => { + state.delete.error = true; + }) + .addCase(removeFromCart.pending, (state) => { + state.remove.isLoading = true; + }) + .addCase(removeFromCart.fulfilled, (state) => { + state.remove.isLoading = false; + }) + .addCase(removeFromCart.rejected, (state) => { + state.remove.error = true; + }) + .addCase(addToCart.pending, (state) => { + state.add.isLoading = true; + }) + .addCase(addToCart.fulfilled, (state, action) => { + state.add.isLoading = false; + state.data.push(action.payload); + }) + .addCase(addToCart.rejected, (state, action) => { + state.add.isLoading = false; + state.add.error = action.payload as string; + }) + .addCase(updateCarts.pending, (state) => { + state.update.isLoading = true; + }) + .addCase(updateCarts.fulfilled, (state, action) => { + state.update.isLoading = false; + const updatedItem = action.payload; + const index = state.data.findIndex( + (item) => item.id === updatedItem.id, + ); + if (index !== -1) { + state.data[index] = updatedItem; + } + }) + .addCase(updateCarts.rejected, (state, action) => { + state.update.isLoading = false; + state.update.error = action.payload as string; + }); + }, +}); + +export const { increaseQuantity, decreaseQuantity } = cartManageSlice.actions; +export default cartManageSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index c0fb2cd..5c04a01 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -10,6 +10,7 @@ import updatePasswordApiSlice from "./api/updatePasswordApiSlice"; import profileSlice from "./reducers/profileSlice"; import updateProfileSlice from "./reducers/updateProfileSlice"; import productsReducer from "./reducers/productsSlice"; +import cartsReducer from "./reducers/cartSlice"; const store = configureStore({ reducer: { @@ -23,6 +24,7 @@ const store = configureStore({ usersProfile: profileSlice, updateUsersProfile: updateProfileSlice, products: productsReducer, + cart: cartsReducer, }, }); export type RootState = ReturnType; diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 6eae79e..b49c886 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -19,6 +19,7 @@ import UserManagement from "../dashboard/admin/UserManagement"; import Settings from "../dashboard/admin/Settings"; import Analytics from "../dashboard/admin/Analytics"; import Dashboard from "../dashboard/admin/Dashboard"; +import CartManagement from "../pages/CartManagement"; const AppRoutes = () => ( @@ -26,6 +27,7 @@ const AppRoutes = () => ( } /> } /> } /> + } /> } /> } /> diff --git a/tailwind.config.js b/tailwind.config.js index 7f13125..19c5e14 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,13 @@ +// eslint-disable-next-line import/no-unresolved +import flowbite from "flowbite-react/tailwind"; /** @type {import('tailwindcss').Config} */ export default { - content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + "./node_modules/flowbite/**/*.js", + flowbite.content(), + ], theme: { extend: { colors: { @@ -22,5 +29,10 @@ export default { }, }, }, - plugins: [], + plugins: [ + /* eslint-disable global-require */ + require("flowbite/plugin"), + flowbite.plugin(), + /* eslint-disable global-require */ + ], }; diff --git a/type.d.ts b/type.d.ts index 554b500..420e7e3 100644 --- a/type.d.ts +++ b/type.d.ts @@ -58,4 +58,22 @@ export type Profile = { postalCode: string; country: string; }; +export interface UserCart { + id: number; + userId: number; + items: CartItem[]; + total: string; + quantity: number; + product: number; + price: number; + createdAt: string; + updatedAt: string; +} + +export interface CartState { + isLoading: boolean; + data: UserCart | null; + error: boolean | string; +} + export type DispatchType = (args: ArticleAction) => ArticleAction;