diff --git a/package-lock.json b/package-lock.json index b93be7e..2d4082d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,13 @@ "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^13.5.0", "@types/react-redux": "^7.1.24", + "@types/react-toastify": "^4.1.0", "bootstrap": "^5.1.3", "dotenv": "^16.0.1", "firebase": "^9.8.3", + "formik": "^2.2.9", "get-google-fonts": "^1.2.2", + "moment": "^2.29.4", "react": "^18.2.0", "react-bootstrap": "^2.4.0", "react-dom": "^18.2.0", @@ -24,8 +27,11 @@ "react-redux": "^8.0.2", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", + "react-stripe-checkout": "^2.6.3", + "react-toastify": "^9.0.8", "react-toggle-dark-mode": "^1.1.0", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "yup": "^0.32.11" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.36.2", @@ -6035,6 +6041,11 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/lodash": { + "version": "4.14.186", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.186.tgz", + "integrity": "sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==" + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -6118,6 +6129,15 @@ "redux": "^4.0.0" } }, + "node_modules/@types/react-toastify": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@types/react-toastify/-/react-toastify-4.1.0.tgz", + "integrity": "sha512-u7Ie/7LHBsPVz/iJxi/WlRDS7Gh9csCJACTDXx+pSLuZCm94xpkwzhM3jV1L5ZxP/in0Gp2tFbJ91VrSGr1gyQ==", + "deprecated": "This is a stub types definition. react-toastify provides its own type definitions, so you do not need this installed.", + "dependencies": { + "react-toastify": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -8258,6 +8278,14 @@ "node": ">=6" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -13185,6 +13213,42 @@ "node": ">= 6" } }, + "node_modules/formik": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.2.9.tgz", + "integrity": "sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "dependencies": { + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^1.10.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/formik/node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/formik/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -17089,8 +17153,7 @@ "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "peer": true + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -18748,6 +18811,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -18771,6 +18842,11 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "node_modules/nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + }, "node_modules/nanoid": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", @@ -21006,6 +21082,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, "node_modules/protobufjs": { "version": "6.11.3", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", @@ -21321,6 +21402,11 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "node_modules/react-icons-kit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-icons-kit/-/react-icons-kit-2.0.0.tgz", @@ -21896,6 +21982,23 @@ "async-limiter": "~1.0.0" } }, + "node_modules/react-stripe-checkout": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-stripe-checkout/-/react-stripe-checkout-2.6.3.tgz", + "integrity": "sha512-lnsCaAdlmwPGGMbQoI8FXtQUgEm+ktzPZ/ipAw4j0HYf80kef7CivGx6QitmgEn99/aa5hI/dmVXwfVZW/Mzfg==" + }, + "node_modules/react-toastify": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.8.tgz", + "integrity": "sha512-EwM+teWt49HSHx+67qI08yLAW1zAsBxCXLCsUfxHYv1W7/R3ZLhrqKalh7j+kjgPna1h5LQMSMwns4tB4ww2yQ==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-toggle-dark-mode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/react-toggle-dark-mode/-/react-toggle-dark-mode-1.1.0.tgz", @@ -22303,6 +22406,15 @@ "node": ">=0.8" } }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -24311,6 +24423,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -24393,6 +24510,11 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tough-cookie": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", @@ -24869,15 +24991,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -25834,6 +25947,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yup": { + "version": "0.32.11", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", + "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/lodash": "^4.14.175", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/zdog": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/zdog/-/zdog-1.1.3.tgz", @@ -30295,6 +30425,11 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "@types/lodash": { + "version": "4.14.186", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.186.tgz", + "integrity": "sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==" + }, "@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -30378,6 +30513,14 @@ "redux": "^4.0.0" } }, + "@types/react-toastify": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@types/react-toastify/-/react-toastify-4.1.0.tgz", + "integrity": "sha512-u7Ie/7LHBsPVz/iJxi/WlRDS7Gh9csCJACTDXx+pSLuZCm94xpkwzhM3jV1L5ZxP/in0Gp2tFbJ91VrSGr1gyQ==", + "requires": { + "react-toastify": "*" + } + }, "@types/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -31983,6 +32126,11 @@ "shallow-clone": "^3.0.0" } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -35694,6 +35842,32 @@ "mime-types": "^2.1.12" } }, + "formik": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.2.9.tgz", + "integrity": "sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==", + "requires": { + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^1.10.0" + }, + "dependencies": { + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -38709,8 +38883,7 @@ "lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "peer": true + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "lodash.camelcase": { "version": "4.3.0", @@ -40120,6 +40293,11 @@ "minimist": "^1.2.6" } }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -40140,6 +40318,11 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + }, "nanoid": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", @@ -41579,6 +41762,11 @@ } } }, + "property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, "protobufjs": { "version": "6.11.3", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", @@ -41819,6 +42007,11 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "react-icons-kit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-icons-kit/-/react-icons-kit-2.0.0.tgz", @@ -42264,6 +42457,19 @@ } } }, + "react-stripe-checkout": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-stripe-checkout/-/react-stripe-checkout-2.6.3.tgz", + "integrity": "sha512-lnsCaAdlmwPGGMbQoI8FXtQUgEm+ktzPZ/ipAw4j0HYf80kef7CivGx6QitmgEn99/aa5hI/dmVXwfVZW/Mzfg==" + }, + "react-toastify": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.8.tgz", + "integrity": "sha512-EwM+teWt49HSHx+67qI08yLAW1zAsBxCXLCsUfxHYv1W7/R3ZLhrqKalh7j+kjgPna1h5LQMSMwns4tB4ww2yQ==", + "requires": { + "clsx": "^1.1.1" + } + }, "react-toggle-dark-mode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/react-toggle-dark-mode/-/react-toggle-dark-mode-1.1.0.tgz", @@ -42593,6 +42799,11 @@ "psl": "^1.1.28", "punycode": "^2.1.1" } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -44140,6 +44351,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -44203,6 +44419,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "tough-cookie": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", @@ -44563,11 +44784,6 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -45331,6 +45547,20 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" }, + "yup": { + "version": "0.32.11", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", + "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/lodash": "^4.14.175", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + } + }, "zdog": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/zdog/-/zdog-1.1.3.tgz", diff --git a/package.json b/package.json index 4014373..5913d7a 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,13 @@ "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^13.5.0", "@types/react-redux": "^7.1.24", + "@types/react-toastify": "^4.1.0", "bootstrap": "^5.1.3", "dotenv": "^16.0.1", "firebase": "^9.8.3", + "formik": "^2.2.9", "get-google-fonts": "^1.2.2", + "moment": "^2.29.4", "react": "^18.2.0", "react-bootstrap": "^2.4.0", "react-dom": "^18.2.0", @@ -19,8 +22,11 @@ "react-redux": "^8.0.2", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", + "react-stripe-checkout": "^2.6.3", + "react-toastify": "^9.0.8", "react-toggle-dark-mode": "^1.1.0", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "yup": "^0.32.11" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.tsx b/src/App.tsx index 960415d..08d5b23 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import React, { useEffect } from "react"; import { Routes, Route } from "react-router-dom"; -import { Home } from "./pages/Home"; -import { Signup } from "./pages/Signup"; -import { Login } from "./pages/Login"; -import ForgotPassword from "./pages/ForgotPassword"; +import { Home } from "./containers/Home"; +import Signup from "./containers/Signup"; +import Login from "./containers/Login"; +import ForgotPassword from "./containers/ForgotPassword"; // import Profile from "./components/Profile"; -import UpdateProfile from "./pages/UpdateProfile"; +import UpdateProfile from "./containers/UpdateProfile"; import ProtectedRoute from "./components/ProtectedRoute"; import { AuthContextProvider } from "./context/AuthContext"; // import AddProducts from "./components/AddProducts"; -import Cart from "./pages/Cart"; -import Admin from "./pages/Admin"; +import Cart from "./containers/Cart"; +import Checkout from "./containers/Checkout"; +import Contact from "./containers/Contact"; import { Navbar } from "./components/Navbar"; -import { Contact } from "./pages/Contact"; +import Admin from "./pages/AdminPage"; import PageNotFound from "./pages/PageNotFound"; import { onAuthStateChanged } from "firebase/auth"; import { auth } from "./config/config"; @@ -20,6 +22,7 @@ import { useAppDispatch } from "./redux/store"; import { logOut } from "./redux/features/auth/authService"; import { setUser } from "./redux/features/auth/authSlice"; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ApplicationProps { } @@ -47,7 +50,7 @@ const App: React.FunctionComponent = () => { } /> } /> } /> - } /> + } /> {/* } /> */} } /> {/* } /> */} @@ -55,6 +58,7 @@ const App: React.FunctionComponent = () => { } /> {/* } /> */} } /> + } /> } /> diff --git a/src/components/AddProducts.tsx b/src/components/AddProducts.tsx deleted file mode 100644 index db7a642..0000000 --- a/src/components/AddProducts.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import React, { useState } from 'react'; -import { storage, db } from '../config/config'; -import { ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage'; -import { collection, addDoc } from 'firebase/firestore'; -import ProgressBar from 'react-bootstrap/ProgressBar'; - -const AddProducts = () => { - const [title, setTitle] = useState(''); - const [description, setDescription] = useState(''); - const [price, setPrice] = useState(0); - const [image, setImage] = useState(null); - const [imageError, setImageError] = useState(''); - const [successMsg, setSuccessMsg] = useState(''); - const [uploadError, setUploadError] = useState(''); - const [uploading, setUploading] = useState(false); - const [progress, setProgress] = useState(0); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [error, setError] = useState(''); - - const types = ['image/jpg', 'image/jpeg', 'image/png', 'image/PNG']; - - const handleProductImg = (e: any) => { - const selectedFile = e.target.files[0]; - if (selectedFile) { - if (selectedFile && types.includes(selectedFile.type)) { - setImage(selectedFile); - setImageError(''); - } else { - setImage(null); - setImageError('please select a valid image file type (png or jpg)'); - } - } else { - setImageError('please select your file'); - } - }; - - const handleAddProducts = (e: any) => { - e.preventDefault(); - // console.log(title, description, price); - // console.log(image); - - const storageRef = ref(storage, '/product-images/' + image?.name); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const uploadTask = uploadBytesResumable(storageRef, image!); - - uploadTask.on( - 'state_changed', - (snapshot) => { - const prog = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; - setProgress(prog); - setUploading(true); - // console.log(prog); - }, - (error) => setUploadError(error.message), - () => { - getDownloadURL(uploadTask.snapshot.ref).then((url) => { - addDoc(collection(db, 'Products'), { - title, - description, - price: Number(price), - url - }) - .then(() => { - setUploading(false); - setSuccessMsg('Product added successfully'); - setTitle('Product '); - setDescription('Lorem ipsum dolor sit amet'); - setPrice(10); - // empty html input file element - const fileInput = document.getElementById('file') as HTMLInputElement; - fileInput.value = ''; - - setImageError(''); - setUploadError(''); - setTimeout(() => { - setSuccessMsg(''); - }, 3000); - }) - .catch((error: any) => setUploadError(error.message)); - }); - } - ); - }; - - return ( -
-

-

-

Add Products

-
- {successMsg &&
{successMsg}
} -
- {error ?

{error}

: null} -
-
- - setTitle(e.target.value)} - value={title} - > -

- - setDescription(e.target.value)} - value={description} - > -

- - setPrice(Number(e.target.value))} - value={price} - > -

- - - - {imageError && ( - <> -

-
{imageError}
- - )} -
- {uploading && ( -
- -
- )} -
-
- -
-
- {uploadError && ( - <> -
-
{uploadError}
- - )} -
- ); -}; - -export default AddProducts; diff --git a/src/components/AddProductsComponent.tsx b/src/components/AddProductsComponent.tsx new file mode 100644 index 0000000..41513df --- /dev/null +++ b/src/components/AddProductsComponent.tsx @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react'; +import { Formik } from 'formik'; +import { Alert, Button, Form, InputGroup } from 'react-bootstrap'; +import * as Yup from 'yup'; + + +const AddProductsComponent = ({ error, handleAddProduct, handleProductImg, progress }: { error: string | null | undefined, handleAddProduct: any, handleProductImg: any, progress: number }) => { + + + return ( +
+
+
+

Add Product

+
+ { + setTimeout(() => { + handleAddProduct(values); + setSubmitting(false); + }, 400); + }} + > + {({ + values, + errors, + touched, + handleChange, + handleBlur, + handleSubmit, + isSubmitting, + }) => ( + +
+ + Title + + + {errors.title && touched.title && errors.title} + + + + + Description + + {errors.description && touched.description && errors.description} + + + + Price + + {errors.price && touched.price && errors.price} + + + + Image + + + + {errors?.image && touched?.image && errors?.image} + + + +
+ )} +
+
+
+
+ ); +}; + + +export default AddProductsComponent; diff --git a/src/components/CartProduct.tsx b/src/components/CartProduct.tsx index e6849f2..63f2a25 100644 --- a/src/components/CartProduct.tsx +++ b/src/components/CartProduct.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import React from 'react'; import { Icon } from 'react-icons-kit'; import { plus } from 'react-icons-kit/feather/plus'; diff --git a/src/components/EditProducts.tsx b/src/components/EditProducts.tsx deleted file mode 100644 index 6fe791a..0000000 --- a/src/components/EditProducts.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { Button, Form } from 'react-bootstrap'; -import { storage, db } from '../config/config'; -import { - collection, - getDocs, - doc, - deleteDoc, - updateDoc -} from 'firebase/firestore'; -import { ref, deleteObject } from 'firebase/storage'; - -const EditProducts = () => { - - const [products, setProducts] = useState([]); - const [loading, setLoading] = useState(false); - const [successMsg, setSuccessMsg] = useState(''); - const [edit, setEdit] = useState(false); - const [editButtonActive, setEditButton] = useState(false); - const [error, setError] = useState(''); - const titleRef = useRef(null); - const priceRef = useRef(null); - const descRef = useRef(null); - const imageRef = useRef(null); - const [pid, setPid] = useState(''); - - const getProducts = async () => { - setLoading(true); - setError(''); - try { - const products = await getDocs(collection(db, 'Products')); - const productsArray = products.docs.map((doc) => ({ - id: doc.id, - ...doc.data() - })); - setProducts(productsArray); - setLoading(false); - } catch (error: any) { - setError(error.message); - setLoading(false); - } - }; - - useEffect(() => { - getProducts(); - }, []); - - const deleteProduct = async (id: any, url: any) => { - // setLoading(true); - setError(''); - - try { - await deleteDoc(doc(db, 'Products', id)); - await deleteObject(ref(storage, url)); - // timeout for success message - setSuccessMsg('Product deleted successfully'); - setTimeout(() => { - setSuccessMsg(''); - }, 2000); - setLoading(false); - getProducts(); - } catch (error: any) { - setError(error.message); - // refresh this page - setTimeout(() => { - window.location.reload(); - }, 3000); - // setLoading(false); - } - }; - - // edit product data information in firebase firestore and firebase storage database when edit button is clicked and product data is changed - const editProduct = async (id: any, title: any, description: any, price: any, url: any) => { - // setLoading(true); - setError(''); - - try { - await updateDoc(doc(db, 'Products', id), { - title, - price, - description, - url - }); - // await updateObject(ref(storage, url), { - // name, - // price, - // description, - // category, - // quantity - // }); // updateObject is used to update the image in firebase storage database - setSuccessMsg('Product Edited successfully'); - setTimeout(() => { - setSuccessMsg(''); - }, 2000); - // setLoading(false); - getProducts(); - } catch (error: any) { - setError(error.message); - setLoading(false); - } - }; - const handleEdit = (pid: any) => { - setPid(pid); // set pid to the product id so that we can update only that row in the table - setEdit(true); - setEditButton(true); - }; - - return ( -

Edit

-
- {successMsg &&
{successMsg}
} - {loading ? ( - <> -
Loading...
-
-
- - ) : null} - {error ?

{error}

: null} -
- - - - - - - - - - - - - - - {// map through products array and display products data - // if edit button is clicked, replace td with input fields - // if edit button is clicked and product data is changed in the table row, update product data in firebase firestore and firebase storage database - products.map((product: any) => ( - - - - - - - - - - ))} - -
IDTitleDescriptionPriceImageDeleteEdit
{product.id} - {edit && product.id === pid - ? ( - - ) - : ( - product.title - )} - - {edit && product.id === pid - ? ( - - ) - : ( - product.description - )} - - {edit && product.id === pid - ? ( - - ) - : ( - product.price - )} - - {edit && product.id === pid - ? ( - - ) - : ( - {product.title} - )} - - - - {editButtonActive && product.id === pid ? ( - - ) : ( - - )} -
-
- - ); -}; - -export default EditProducts; diff --git a/src/components/EditProductsComponent.tsx b/src/components/EditProductsComponent.tsx new file mode 100644 index 0000000..3d31cc7 --- /dev/null +++ b/src/components/EditProductsComponent.tsx @@ -0,0 +1,188 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react'; +import { Button, Form, Modal } from 'react-bootstrap'; +import { Formik } from 'formik'; + +const EditProductsComponent = ({ products, loading, successMsg, error, deleteProduct, editProduct, edit, pid, handleEdit, editButtonActive, setEdit, setEditButton, setPid }: { + products: any; + loading: boolean; + successMsg: string; + error: string | null | undefined; + deleteProduct: (id: string, url: string) => void; + editProduct: (ID: string, name: string, price: string, description: string, image: string) => void; + edit: boolean; + pid: string; + handleEdit: (id: string) => void; + editButtonActive: boolean; + setEdit: (edit: boolean) => void; + setEditButton: (editButtonActive: boolean) => void; + setPid: (pid: string) => void; +}) => { + + return ( +
+

Edit Products

+ {/*

Edit

*/} +
+ {successMsg &&
{successMsg}
} + {loading ? ( + <> +
Loading...
+
+
+ + ) : null} + {error ?

{error}

: null} +
+ {/* */} + +
+ + + + + + + + + + + + + { + // if edit button is clicked, show a form and update the product data in firebase firestore + // use formik to handle form data + products.map((product: any) => ( + + + + + + + + + + ))} + +
IDTitleDescriptionPriceImageDeleteEdit
{product.ID}{product.title}{product.description}{product.price} + + {editButtonActive && pid === product.ID ? ( + <> +
+ + {/*
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
*/} + { + editProduct(product.ID, values.title, values.description, values.price, values.url); + resetForm({}); + setEditButton(false); + }} + > + + {({ values, handleChange, handleSubmit }) => ( + setEditButton(false)}> +
+ + Edit Product + + + + + Title + + + + + Price + + + + + Description + + + + + Image + + + + {/* */} + + + + + + +
+
+ )} + +
+ + ) + : null} +
+
+ ); +}; + +export default EditProductsComponent; + + diff --git a/src/components/FetchCustomerOrders.tsx b/src/components/FetchCustomerOrders.tsx new file mode 100644 index 0000000..b35bd4a --- /dev/null +++ b/src/components/FetchCustomerOrders.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const FetchCustomerOrders = () => { + return ( +
FetchCustomerOrders
+ ) +} + +export default FetchCustomerOrders \ No newline at end of file diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 4d5392c..fe660f2 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import logo from '../images/logo.svg'; diff --git a/src/components/Payment.tsx b/src/components/Payment.tsx new file mode 100644 index 0000000..56a8194 --- /dev/null +++ b/src/components/Payment.tsx @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react' +import { useAppDispatch, useAppSelector } from '../redux/store' + +const Payment = ({ totalPrice, totalQty }: { totalPrice: number, totalQty: number }) => { + + return ( +
Payment
+ ) +} + +export default Payment \ No newline at end of file diff --git a/src/components/Product.tsx b/src/components/Product.tsx index 6139a36..2b08f7a 100644 --- a/src/components/Product.tsx +++ b/src/components/Product.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import React from 'react'; import { Icon } from 'react-icons-kit'; // import { ecommerce_basket_plus } from 'react-icons-kit/linea/ecommerce_basket_plus' diff --git a/src/components/Products.tsx b/src/components/Products.tsx index db72edc..4a608bc 100644 --- a/src/components/Products.tsx +++ b/src/components/Products.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import React, { memo } from 'react'; import { Product } from './Product'; diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index e4ed8c3..1bb7e65 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import React, { useState } from 'react'; import { Card, Button, Alert } from 'react-bootstrap'; import { useNavigate } from 'react-router-dom'; diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 41a7c57..3f2ac9c 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import React, { useEffect, useState } from 'react'; import { getAuth, onAuthStateChanged } from 'firebase/auth'; import { useNavigate } from 'react-router-dom'; diff --git a/src/components/ShoppingCart.tsx b/src/components/ShoppingCart.tsx index 52cc877..0906122 100644 --- a/src/components/ShoppingCart.tsx +++ b/src/components/ShoppingCart.tsx @@ -1,5 +1,6 @@ // Work in progress //Is not used yet +/* eslint-disable @typescript-eslint/no-unused-vars */ import Offcanvas from 'react-bootstrap/Offcanvas'; import React from 'react'; diff --git a/src/containers/AddProducts.tsx b/src/containers/AddProducts.tsx new file mode 100644 index 0000000..4fecddc --- /dev/null +++ b/src/containers/AddProducts.tsx @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React, { useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../redux/store'; +import { addProduct } from '../redux/features/admin/adminService'; +import AddProductsComponent from '../components/AddProductsComponent'; + + +const AddProducts = () => { + // const [title, setTitle] = useState(''); + // const [description, setDescription] = useState(''); + // const [price, setPrice] = useState(0); + const [image, setImage] = useState(null); + // const [imageError, setImageError] = useState(''); + // const [successMsg, setSuccessMsg] = useState(''); + // const [uploadError, setUploadError] = useState(''); + // const [uploading, setUploading] = useState(false); + // const [progress, setProgress] = useState(0); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + // const [error, setError] = useState(''); + + const types = ['image/jpg', 'image/jpeg', 'image/png', 'image/PNG']; + + const handleProductImg = (e: any) => { + const selectedFile = e.target.files[0]; + if (selectedFile) { + if (selectedFile && types.includes(selectedFile.type)) { + setImage(selectedFile); + // setImageError(''); + } else { + setImage(null); + // setImageError('please select a valid image file type (png or jpg)'); + } + } else { + // setImageError('please select your file'); + } + }; + + // const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const error = useAppSelector((state) => state.adminReducer.error); + // eslint-disable-next-line no-var + var progres = useAppSelector((state) => state.adminReducer.progress); + + const handleAddProduct = async (product: { title: string; description: string; price: number; }) => { + // make a copy of the product object with types that match the database + const productToUpload = { + title: product.title, + description: product.description, + price: product.price, + image: image + }; + + // console.debug(productToUpload); + await dispatch(addProduct(productToUpload)) + .then(() => { + // setSuccessMsg('Product added successfully'); + }) + .catch((error) => { + console.error(error); + // setUploadError(error.message); + }); + + }; + + + return ; + +}; + + + + + +export default AddProducts; diff --git a/src/containers/Cart.tsx b/src/containers/Cart.tsx new file mode 100644 index 0000000..3b75836 --- /dev/null +++ b/src/containers/Cart.tsx @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React, { useEffect } from 'react' +import { useNavigate } from 'react-router-dom'; +import CartPage from '../pages/CartPage'; +// import { useNavigate } from 'react-router-dom'; +import { fetchCart } from '../redux/features/cart/cartService'; +import { useAppDispatch, useAppSelector } from '../redux/store'; + +const Cart = () => { + // const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + + const userEmail = useAppSelector((state) => state.authReducer.user?.email); + // console.log('userEmail: ' + userEmail); + + useEffect(() => { + if (userEmail !== null && userEmail !== undefined && userEmail !== '') { + dispatch(fetchCart(userEmail)); + } + }, [dispatch, userEmail]); + + + const cartItems = useAppSelector((state) => state.cartReducer.cartItems); + // console.log(cartItems.length); + + return ( + + ) +} + +export default Cart \ No newline at end of file diff --git a/src/containers/Checkout.tsx b/src/containers/Checkout.tsx new file mode 100644 index 0000000..4ee7876 --- /dev/null +++ b/src/containers/Checkout.tsx @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react' +import CheckoutPage from '../pages/CheckoutPage' +import { useAppDispatch, useAppSelector } from '../redux/store'; +import { createSelector } from '@reduxjs/toolkit'; +// import StripeCheckout from 'react-stripe-checkout'; +import { createOrder } from '../redux/features/order/orderService'; +import moment from 'moment'; +import { deleteAllFromCart } from '../redux/features/cart/cartService'; + +const Checkout = () => { + const dispatch = useAppDispatch(); + + const userEmail = useAppSelector((state) => state.authReducer.user?.email); + // console.debug('userEmail: ' + userEmail); + + // use createselector to get total price + const total = createSelector( + (state: any) => state.cartReducer.cartItems, + (cartItems) => cartItems.reduce((acc: any, item: any) => acc + item.totalPrice, 0) + ) + + const totalPrice = useAppSelector(total); + + // use createselector to get cart items + const cartItems = createSelector( + (state: any) => state.cartReducer.cartItems, + (cartItems) => cartItems + ) + + const cartItemsList = useAppSelector(cartItems); + + // console.debug('cartItemsList: ' + cartItemsList.length); + + + const pay = () => { + + // dispatch(deleteAllFromCart(userEmail)); + } + + const addOrder = (values: any) => { + // make an orderID using the current date and time in nanoseconds + const orderID = moment().format('YYYYMMDDHHmmssSSS'); + // console.debug('orderID: ' + orderID); + dispatch(createOrder({ order: { id: orderID, cartItemsList, totalPrice, ...values }, userEmail })); + + dispatch(deleteAllFromCart(userEmail)); + + } + return ( + + + ) +} + +export default Checkout \ No newline at end of file diff --git a/src/containers/Contact.tsx b/src/containers/Contact.tsx new file mode 100644 index 0000000..b4f049e --- /dev/null +++ b/src/containers/Contact.tsx @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react'; +import { ContactPage } from '../pages/ContactPage'; +import { useAppDispatch, useAppSelector } from '../redux/store'; +import { clearError } from '../redux/features/auth/authSlice'; +import { sendMessage } from '../redux/features/auth/authService'; + + + +const Contact = () => { + const dispatch = useAppDispatch(); + const error = useAppSelector((state) => state.authReducer.error); + + const sendMsg = async ({ name, email, message }: { name: any, email: any, message: any }) => { + + await dispatch(sendMessage({ submittedName: name, submittedEmail: email, submittedMessage: message })); + + }; + + + return ( + dispatch(clearError())} sendMsg={sendMsg} /> + ) +} + +export default Contact \ No newline at end of file diff --git a/src/containers/EditProducts.tsx b/src/containers/EditProducts.tsx new file mode 100644 index 0000000..9b784d7 --- /dev/null +++ b/src/containers/EditProducts.tsx @@ -0,0 +1,152 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React, { useEffect, useRef, useState } from 'react'; +import { Button, Form } from 'react-bootstrap'; +import { storage, db } from '../config/config'; +import { + collection, + getDocs, + doc, + deleteDoc, + updateDoc +} from 'firebase/firestore'; +import { ref, deleteObject } from 'firebase/storage'; +import { useNavigate } from 'react-router-dom'; +import { useAppDispatch, useAppSelector } from '../redux/store'; +import { clearError, setError } from '../redux/features/admin/adminSlice'; +import { fetchProducts } from '../redux/features/product/productService'; +import { editProduct, deleteProduct } from '../redux/features/admin/adminService'; +import EditProductsComponent from '../components/EditProductsComponent'; + +const EditProducts = () => { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const error = useAppSelector((state) => state.authReducer.error); + + // const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(false); + const [successMsg, setSuccessMsg] = useState(''); + const [edit, setEdit] = useState(false); + const [editButtonActive, setEditButton] = useState(false); + const [pid, setPid] = useState(''); + + const getProducts = async () => { + const produkts = useAppSelector((state) => state.productReducer.products); + return produkts; + }; + // setLoading(true); + // // dispatch clearError from adminSlice + // dispatch(clearError()); + // // fetch products using productSlice redux and service + // try { + // const productsArray = await dispatch(fetchProducts()); + // setProducts(productsArray); + // } catch (error: any) { + // console.debug(error); + // } + + + // try { + // const products = await getDocs(collection(db, 'Products')); + // const productsArray = products.docs.map((doc) => ({ + // id: doc.id, + // ...doc.data() + // })); + // setProducts(productsArray); + // setLoading(false); + // } catch (error: any) { + // // setError(error.message); + // setLoading(false); + // } + // }; + useEffect(() => { + dispatch(fetchProducts()); + }, []); + + // use selector to get products from redux store + const products = useAppSelector((state) => state.productReducer.products); + + // console.debug('products', products); + + // useEffect(() => { + // getProducts(); + // }, []); + + const deletePrd = async (id: any, url: any) => { + // setLoading(true); + dispatch(clearError()); + + try { + // dispatch deleteProduct from productSlice redux and service + await dispatch(deleteProduct({ id, url })); + // await deleteDoc(doc(db, 'Products', id)); + // await deleteObject(ref(storage, url)); + // // timeout for success message + // setSuccessMsg('Product deleted successfully'); + // setTimeout(() => { + // setSuccessMsg(''); + // }, 2000); + // setLoading(false); + // getProducts(); + dispatch(fetchProducts()); + + } catch (error: any) { + // setError(error.message); + dispatch(clearError()); + // refresh this page + // setTimeout(() => { + // window.location.reload(); + // }, 3000); + // setLoading(false); + } + }; + + // edit product data information in firebase firestore and firebase storage database when edit button is clicked and product data is changed + const editPrd = async (id: any, title: any, description: any, price: any) => { + // setLoading(true); + dispatch(clearError()); + // setError(''); + + // console.debug('editPrd', id, title, description, price); + + try { + // dispatch editProduct from productSlice redux and service + await dispatch(editProduct({ id, title, description, price })); + + // await updateDoc(doc(db, 'Products', id), { + // title, + // price, + // description, + // url + // }); + // await updateObject(ref(storage, url), { + // name, + // price, + // description, + // category, + // quantity + // }); // updateObject is used to update the image in firebase storage database + // setSuccessMsg('Product Edited successfully'); + // setTimeout(() => { + // setSuccessMsg(''); + // }, 2000); + // setLoading(false); + dispatch(fetchProducts()); + + } catch (error: any) { + // setError(error.message); + // console.debug(error); + dispatch(setError(error.message)); + setLoading(false); + } + }; + const handleEdit = (pid: any) => { + // console.debug('handleEdit', pid); + setPid(pid); // set pid to the product id so that we can update only that row in the table + setEdit(true); + setEditButton(true); + }; + + return ; +}; + +export default EditProducts; diff --git a/src/containers/ForgotPassword.tsx b/src/containers/ForgotPassword.tsx new file mode 100644 index 0000000..879192c --- /dev/null +++ b/src/containers/ForgotPassword.tsx @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react' +import { useNavigate } from 'react-router-dom'; +import ForgotPasswordPage from '../pages/ForgotPasswordPage'; +import { sendResetEmail } from '../redux/features/auth/authService' +import { clearError } from '../redux/features/auth/authSlice'; +import { useAppDispatch, useAppSelector } from '../redux/store' + +const ForgotPassword = () => { + // const [email, setEmail] = useState(''); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const error = useAppSelector((state) => state.authReducer.error); + + const resetPassword = async (email: string) => { + // e.preventDefault(); + // setError(''); + try { + dispatch(sendResetEmail(email)); + // await forgotPassword(email); + // setMessage('An email has been sent! Please check your email (and spams) to reset the password!'); + setTimeout( + function () { + navigate('../Login'); + } + , 10000); + } catch (error: any) { + console.debug(error); + } + }; + return ( + dispatch(clearError())} resetPassword={resetPassword} /> + ) +} + +export default ForgotPassword \ No newline at end of file diff --git a/src/containers/Home.tsx b/src/containers/Home.tsx new file mode 100644 index 0000000..0c3df91 --- /dev/null +++ b/src/containers/Home.tsx @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React, { useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../redux/store'; +import { fetchProducts } from '../redux/features/product/productService'; +import { HomePage } from '../pages/HomePage'; + +export const Home = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(fetchProducts()); + }, []); + + // use selector to get products from redux store + const products = useAppSelector((state) => state.productReducer.products); + + + return ( + + ); +}; diff --git a/src/containers/Login.tsx b/src/containers/Login.tsx new file mode 100644 index 0000000..996d501 --- /dev/null +++ b/src/containers/Login.tsx @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react' +import { LoginPage } from '../pages/LoginPage'; +import { useNavigate } from 'react-router-dom'; +import { signIn } from '../redux/features/auth/authService'; +import { useAppDispatch, useAppSelector } from '../redux/store'; +import { clearError, setShowPassword } from '../redux/features/auth/authSlice'; + + +const Login = () => { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const error = useAppSelector((state) => state.authReducer.error); + const showPassword = useAppSelector((state) => state.authReducer.showPassword); + + + const loginToApp = async ({ email, password }: { email: any, password: any }) => { + + // dispatch signIn + const response = await dispatch(signIn({ submittedEmail: email, submittedPassword: password })); + // if there is no error, navigate to home page + if (response.payload) { + navigate('/'); + } + }; + + + return ( + dispatch(setShowPassword(!showPassword))} loginToApp={loginToApp} error={error} clearErr={() => dispatch(clearError())} /> + + ) +} + +export default Login \ No newline at end of file diff --git a/src/containers/ShowOrders.tsx b/src/containers/ShowOrders.tsx new file mode 100644 index 0000000..c66b94f --- /dev/null +++ b/src/containers/ShowOrders.tsx @@ -0,0 +1,64 @@ +import React, { useEffect } from 'react' +import { useAppDispatch, useAppSelector } from '../redux/store'; +import { fetchAllOrders } from '../redux/features/admin/adminService'; +import moment from 'moment'; + +const ShowOrders = () => { + // const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const error = useAppSelector((state) => state.authReducer.error); + + + useEffect(() => { + dispatch(fetchAllOrders()); + }, []); + + const orders = useAppSelector((state) => state.adminReducer.orders); + + // console.log(orders); + + // get all orderItems from orders + const orderItems = orders.map((order) => order.orderItems); + // console.log(orderItems); + + return ( +
+

Orders

+
+
+ + + + + + + + + + + + {/* // display every orderItem in orderItemsList array */ + orderItems.map((orderItemsList) => { + return orderItemsList.map((orderItem: any) => { + return ( + + + + + + + + ) + }) + }) + } + +
Order IDCustomer NameOrder TotalOrder StatusConfirm/Send
{orderItem.ID}{orderItem.name}{orderItem.totalPrice}Pending + +
+
+
+ ) +} + +export default ShowOrders \ No newline at end of file diff --git a/src/containers/Signup.tsx b/src/containers/Signup.tsx new file mode 100644 index 0000000..3c7b384 --- /dev/null +++ b/src/containers/Signup.tsx @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react' +import { useNavigate } from 'react-router-dom'; +import { signUp } from '../redux/features/auth/authService'; +import { useAppDispatch, useAppSelector } from '../redux/store'; +import { SignupPage } from '../pages/SignupPage'; +import { clearError } from '../redux/features/auth/authSlice'; + + +const Signup = () => { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const error = useAppSelector((state) => state.authReducer.error); + + const Register = async ({ name, email, password }: { name: any, email: any, password: any }) => { + + // dispatch signUp action + const response = await dispatch(signUp({ submittedName: name, submittedEmail: email, submittedPassword: password })); + + // TODO: we currently get error 400 from backend in the console exposing the API key if there is an error such as user already exists, there is no way to suppress it according to: + // https://stackoverflow.com/questions/49096911/firebase-createuserwithemailandpassword-returning-http-post-error-failure-alon + // if there is no error, navigate to home page + if (response.payload) { + navigate('/'); + } + + }; + return ( + dispatch(clearError())} /> + ) +} + +export default Signup \ No newline at end of file diff --git a/src/containers/UpdateProfile.tsx b/src/containers/UpdateProfile.tsx new file mode 100644 index 0000000..add504a --- /dev/null +++ b/src/containers/UpdateProfile.tsx @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react'; +// import { useNavigate } from 'react-router-dom'; +import { useAppSelector } from '../redux/store'; +import { useAppDispatch } from '../redux/store'; +import { updateProfilee, updateEmaill, updatePass } from '../redux/features/auth/authService'; +import UpdateProfilePage from '../pages/UpdateProfilePage'; +import { setShowPassword } from '../redux/features/auth/authSlice'; + +const UpdateProfile = () => { + // const navigate = useNavigate(); + const user = useAppSelector((state: any) => state.authReducer.user); + const dispatch = useAppDispatch(); + const { error } = useAppSelector((state: any) => state.authReducer); + const showPassword = useAppSelector((state) => state.authReducer.showPassword); + + const updateProfile = async ({ name, email, password, passwordConfirm }: { name: string, email: string, password: string, passwordConfirm: string }) => { + + const promises = []; + + if (name !== user.name) { + promises.push(dispatch(updateProfilee(name))); + } + + if (email !== user.email) { + promises.push(dispatch(updateEmaill(email))); + } + + if (password !== '' && password === passwordConfirm) { + promises.push(dispatch(updatePass(password))); + } + + await Promise.all(promises); + + // if (error === null || error === undefined || error === '') { + // setTimeout(() => { + // navigate('/'); + // }, 3000); + // } + } + + return ( + dispatch(setShowPassword(!showPassword))} /> + ); +}; + +export default UpdateProfile; diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx deleted file mode 100644 index a5149fc..0000000 --- a/src/pages/Admin.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable no-unused-vars */ -import React, { useState } from 'react'; -import { Button } from 'react-bootstrap'; -import AddProducts from '../components/AddProducts'; -import EditProducts from '../components/EditProducts'; - - -const Admin = () => { - - const [addProductsButtonClicked, setAddProducts] = useState(false); - const [editProductsButtonClicked, setEditProducts] = useState(false); - - return ( -
-
-

-

-

Admin Page

-
- -

- -
- - -
- - {addProductsButtonClicked - ? ( - - ) - : null} - -

- - {editProductsButtonClicked && ( - - )} -
- -
- -
// end of container - ); -}; - -export default Admin; diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx new file mode 100644 index 0000000..3c71d31 --- /dev/null +++ b/src/pages/AdminPage.tsx @@ -0,0 +1,56 @@ +/* eslint-disable no-unused-vars */ +import React from 'react'; +import { Button } from 'react-bootstrap'; +import AddProducts from '../containers/AddProducts'; +import EditProducts from '../containers/EditProducts'; +import ShowOrders from '../containers/ShowOrders'; + + + +const AdminPage = () => { + + return ( +
+
+
+
+
+

Admin Dashboard

+
+
+ +
+
+
+ + + +
+ + {/* // add border to the right of the edit products component */} +
+ + +
+ + + +
+
+ +
+ +
+
+ + + +
+ +
+
+
// end of container + ); +}; + +export default AdminPage; diff --git a/src/pages/Cart.tsx b/src/pages/Cart.tsx deleted file mode 100644 index 786fd14..0000000 --- a/src/pages/Cart.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import React, { useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { CartProduct } from '../components/CartProduct'; -import { useAppDispatch, useAppSelector } from '../redux/store'; -import { fetchCart } from '../redux/features/cart/cartService'; - -export const Cart = () => { - - const navigate = useNavigate(); - const dispatch = useAppDispatch(); - - const userEmail = useAppSelector((state) => state.authReducer.user?.email); - // console.log('userEmail: ' + userEmail); - - useEffect(() => { - if (userEmail !== null && userEmail !== undefined && userEmail !== '') { - dispatch(fetchCart(userEmail)); - } - }, [dispatch, userEmail]); - - - const cartItems = useAppSelector((state) => state.cartReducer.cartItems); - // console.log(cartItems.length); - - return ( - <> -

- {/* if cart is empty then show this message to user "No products in the cart"*/} - {cartItems?.length === 0 && ( -
-

Cart

-
-

No products in the cart

-
-
- Continue Shopping -
-
- )} - {cartItems?.length > 0 && ( - <> -
-
-
-
-
-

Cart

-
-
-
- - - - - - - - - - - - - {cartItems.map((product: any) => ( - - ))} - -
Product ImageProductPriceQuantitySubtotalDelete
-
-
-
-
-
- -
-

Total: {cartItems.reduce((acc: any, item: any) => acc + item.totalPrice, 0)}

-
- -
- -
- -
-
-
-
-
-
-
- - )} - - ); -}; - -export default Cart; diff --git a/src/pages/CartPage.tsx b/src/pages/CartPage.tsx new file mode 100644 index 0000000..f6afa28 --- /dev/null +++ b/src/pages/CartPage.tsx @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react'; +import { CartProduct } from '../components/CartProduct'; + +export const CartPage = ({ cartItems, navigate }: { cartItems: any, navigate: any }) => { + + return ( + <> +
+
+
+
+
+

Cart

+
+
+
+ + + + + + + + + + + + + {/* // if cartItems length is 0, show empty fragment */} + {cartItems.length === 0 ? ( + <> + + ) : ( + // else show cart items + cartItems.map((item: any) => ( + + )) + )} + +
Product ImageProductPriceQuantitySubtotalDelete
+
+
+
+
+
+ +
+

Total: {cartItems.reduce((acc: any, item: any) => acc + item.totalPrice, 0)}

+
+ +
+ +
+ +
+
+
+
+
+
+
+ + ); +}; + +export default CartPage; + diff --git a/src/pages/CheckoutPage.tsx b/src/pages/CheckoutPage.tsx new file mode 100644 index 0000000..9e4b905 --- /dev/null +++ b/src/pages/CheckoutPage.tsx @@ -0,0 +1,181 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react' +import { Form, Button, Row, Col, Container } from 'react-bootstrap'; +import { Formik } from 'formik'; +import * as Yup from 'yup'; +// import { createOrder } from '../redux/features/order/orderService'; +import { toast, ToastContainer, ToastOptions } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +interface CheckoutPageProps { + totalPrice: number; + cartItems: any; + pay: any; + userEmail: string | undefined | null; + addOrder: any; +} + +const CheckoutPage = ({ totalPrice, cartItems, pay, userEmail, addOrder }: CheckoutPageProps) => { + + const validationSchema = Yup.object().shape({ + name: Yup.string().required('name is required'), + address: Yup.string().required('Address is required'), + city: Yup.string().required('City is required'), + zip: Yup.string().required('Zip is required'), + country: Yup.string().required('Country is required'), + }); + + const notify = (message: string, type: string) => { + const options: ToastOptions = { + position: 'top-right', + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + }; + switch (type) { + case 'success': + toast.success(message, options); + break; + case 'error': + toast.error(message, options); + break; + default: + toast(message, options); + break; + } + }; + + + return ( + + + + +

Checkout

+ { + addOrder(values); + pay(); + setSubmitting(false); + notify('Order placed successfully', 'success'); + }} + + > + {({ + values, + errors, + touched, + handleChange, + handleBlur, + handleSubmit, + isSubmitting, + }) => ( +
+ + Name + + + {errors.name} + + + + Address + + + {errors.address} + + + + City + + + {errors.city} + + + + Postal Code + + + {errors.zip} + + + + + Country + + + {errors.country} + + + +
+ +
+ )} +
+ +
+
+ ) +} + +export default CheckoutPage + diff --git a/src/pages/Contact.tsx b/src/pages/Contact.tsx deleted file mode 100644 index 5eb6f2b..0000000 --- a/src/pages/Contact.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// WIP -import React from 'react' - -export const Contact = () => { - return ( - //add a div with a class of container -
-

Contact us

-
-
-
- - - - - -