From d03c197b4ba40ff7d817b33c4ba459f8a0f3900f Mon Sep 17 00:00:00 2001 From: naomimariuta <157979968+naomimariuta@users.noreply.github.com> Date: Mon, 4 Nov 2024 03:20:23 +0200 Subject: [PATCH] transactions and currency --- package-lock.json | 171 ++++++++++ package.json | 3 + src/Pages/CurrencyTab/CurrencyTab.jsx | 261 ++++++++++++++- src/Pages/CurrencyTab/CurrencyTab.module.css | 169 ++++++++++ src/Pages/DashboardPage/DashboardPage.jsx | 1 - .../DashboardPage/DashboardPage.module.css | 10 +- src/Pages/Home/Home.jsx | 76 ++++- src/Pages/Home/Home.module.css | 76 ++++- .../transactionAction.jsx | 6 - .../transactionsReducer.jsx | 22 -- .../AddTransactionForm/AddTransactionForm.jsx | 4 +- .../AddTransactionForm/customStyles.js | 7 +- src/components/App/App.jsx | 32 +- src/components/Balance/Balance.jsx | 30 +- src/components/Balance/Balance.module.css | 52 ++- .../ButtonAddTransactions.modules.css | 4 +- src/components/ButtonForm/ButtonForm.jsx | 32 ++ .../ButtonForm/ButtonForm.module.css | 80 +++++ src/components/Currency/Currency.jsx | 311 ++++++++++++++++-- src/components/Currency/Currency.module.css | 181 ++++++++-- .../EditTransactionForm.jsx | 249 ++++++++++---- .../EditTransactionForm.module.css | 256 ++++++++++++-- src/components/Logo/Logo.jsx | 20 ++ src/components/Logo/Logo.module.css | 58 ++++ .../ModalDeleteTransaction.jsx | 88 +++++ .../ModalDeleteTransaction.module.css | 106 ++++++ .../ModalEditTransaction.jsx | 62 ++-- .../ModalEditTransaction.module.css | 115 +++++-- .../TransactionsDesktop.jsx | 63 ++++ .../TransactionsDesktop.module.css | 58 ++++ .../TransactionsDesktopRow.jsx | 105 ++++++ .../TransactionsDesktopRow.module.css | 97 ++++++ .../TransactionsItem/TransactionsItem.jsx | 119 +++++-- .../TransactionsItem.module.css | 148 +++++++-- .../TransactionsList/TransactionsList.jsx | 74 ++--- .../TransactionsList.module.css | 37 +-- src/components/common/allCategories.js | 150 +++++++++ .../common/formatNumberWithSpaces.js | 19 ++ src/components/common/useCurrency.js | 50 +++ src/components/images/icons/sprite.svg | 77 +++++ src/redux/Statistics/operations.js | 1 + src/redux/Statistics/slice.js | 8 +- src/redux/operations/authOperations.js | 14 + .../operations/transactionsOperations.js | 34 +- src/redux/selectors/authSelectors.js | 1 + src/redux/selectors/transactionsSelector.js | 20 +- src/redux/slices/AuthSlice.jsx | 45 ++- src/redux/slices/GlobalSlice.jsx | 18 + src/redux/slices/transactionsSlice.js | 84 ++--- 49 files changed, 3194 insertions(+), 510 deletions(-) create mode 100644 src/Pages/CurrencyTab/CurrencyTab.module.css delete mode 100644 src/actions pentru test doar/transactionAction.jsx delete mode 100644 src/actions pentru test doar/transactionsReducer.jsx create mode 100644 src/components/ButtonForm/ButtonForm.jsx create mode 100644 src/components/ButtonForm/ButtonForm.module.css create mode 100644 src/components/Logo/Logo.jsx create mode 100644 src/components/Logo/Logo.module.css create mode 100644 src/components/ModalDeleteTransaction/ModalDeleteTransaction.jsx create mode 100644 src/components/ModalDeleteTransaction/ModalDeleteTransaction.module.css create mode 100644 src/components/TransactionsDesktop/TransactionsDesktop.jsx create mode 100644 src/components/TransactionsDesktop/TransactionsDesktop.module.css create mode 100644 src/components/TransactionsDesktopRow/TransactionsDesktopRow.jsx create mode 100644 src/components/TransactionsDesktopRow/TransactionsDesktopRow.module.css create mode 100644 src/components/common/allCategories.js create mode 100644 src/components/common/formatNumberWithSpaces.js create mode 100644 src/components/common/useCurrency.js create mode 100644 src/components/images/icons/sprite.svg diff --git a/package-lock.json b/package-lock.json index f66569c..d0c16e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@chakra-ui/react": "^3.0.1", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", + "@hookform/resolvers": "^3.9.1", "@reduxjs/toolkit": "^2.3.0", "@testing-library/jest-dom": "^5.16.3", "@testing-library/react": "^12.1.4", @@ -23,6 +24,7 @@ "react-chartjs-2": "^5.2.0", "react-datepicker": "^7.5.0", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.1", "react-icons": "^5.3.0", "react-loader-spinner": "^6.1.6", "react-modal": "^3.16.1", @@ -31,6 +33,7 @@ "react-responsive": "^10.0.0", "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", + "react-select": "^5.8.2", "react-toastify": "^10.0.6", "redux": "^5.0.1", "redux-persist": "^6.0.0", @@ -3305,6 +3308,15 @@ "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", "license": "MIT" }, + "node_modules/@hookform/resolvers": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", + "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -4524,6 +4536,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", + "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -9283,6 +9304,16 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", @@ -13095,6 +13126,12 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -15394,6 +15431,22 @@ "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", "license": "MIT" }, + "node_modules/react-hook-form": { + "version": "7.53.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.1.tgz", + "integrity": "sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-icons": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", @@ -15622,6 +15675,27 @@ } } }, + "node_modules/react-select": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.2.tgz", + "integrity": "sha512-a/LkOckoI62710gGPQSQqUp7A10fGbH/ya3/IR49qaq3XoBvwymgD5mJgtiHxBDsutyEQfdKNycWVh8Cg8UCjw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-toastify": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", @@ -15635,6 +15709,22 @@ "react-dom": ">=18" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -17570,6 +17660,20 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", @@ -21015,6 +21119,12 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, + "@hookform/resolvers": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", + "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -21923,6 +22033,14 @@ "@types/react": "*" } }, + "@types/react-transition-group": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", + "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "requires": { + "@types/react": "*" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -25801,6 +25919,15 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "dom-serializer": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", @@ -28542,6 +28669,11 @@ "fs-monkey": "^1.0.4" } }, + "memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -30044,6 +30176,12 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" }, + "react-hook-form": { + "version": "7.53.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.1.tgz", + "integrity": "sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==", + "requires": {} + }, "react-icons": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", @@ -30192,6 +30330,22 @@ "workbox-webpack-plugin": "^6.4.1" } }, + "react-select": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.2.tgz", + "integrity": "sha512-a/LkOckoI62710gGPQSQqUp7A10fGbH/ya3/IR49qaq3XoBvwymgD5mJgtiHxBDsutyEQfdKNycWVh8Cg8UCjw==", + "requires": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + } + }, "react-toastify": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", @@ -30200,6 +30354,17 @@ "clsx": "^2.1.0" } }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -31584,6 +31749,12 @@ "requires-port": "^1.0.0" } }, + "use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "requires": {} + }, "use-sync-external-store": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", diff --git a/package.json b/package.json index 459c47f..af58f9a 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@chakra-ui/react": "^3.0.1", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", + "@hookform/resolvers": "^3.9.1", "@reduxjs/toolkit": "^2.3.0", "@testing-library/jest-dom": "^5.16.3", "@testing-library/react": "^12.1.4", @@ -19,6 +20,7 @@ "react-chartjs-2": "^5.2.0", "react-datepicker": "^7.5.0", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.1", "react-icons": "^5.3.0", "react-loader-spinner": "^6.1.6", "react-modal": "^3.16.1", @@ -27,6 +29,7 @@ "react-responsive": "^10.0.0", "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", + "react-select": "^5.8.2", "react-toastify": "^10.0.6", "redux": "^5.0.1", "redux-persist": "^6.0.0", diff --git a/src/Pages/CurrencyTab/CurrencyTab.jsx b/src/Pages/CurrencyTab/CurrencyTab.jsx index 95a1081..a2ab82d 100644 --- a/src/Pages/CurrencyTab/CurrencyTab.jsx +++ b/src/Pages/CurrencyTab/CurrencyTab.jsx @@ -1,12 +1,267 @@ -import Currency from '../../components/Currency/Currency'; import React from 'react'; import { useMediaQuery } from 'react-responsive'; +import styles from './CurrencyTab.module.css'; +import useCurrency from 'components/common/useCurrency'; +import Loader from 'components/Loader/Loader'; const CurrencyTab = () => { - const isDesktopOrLaptop = useMediaQuery({ + const { data, loading, error } = useCurrency(); + + const rateGBP = data?.rates['GBP']; + const rateEUR = data?.rates['EUR']; + const rateBuyGBP = rateGBP ? rateGBP.toFixed(2) : 'N/A'; + const rateSellGBP = rateGBP ? (1 / rateGBP).toFixed(2) : 'N/A'; + const rateBuyEUR = rateEUR ? rateEUR.toFixed(2) : 'N/A'; + const rateSellEUR = rateEUR ? (1 / rateEUR).toFixed(2) : 'N/A'; + + const isMobile = useMediaQuery({ + query: '(max-width: 767px)', + }); + const isMinTablet = useMediaQuery({ query: '(min-width: 768px)', }); - return <>{!isDesktopOrLaptop && }; + + const isMaxTablet = useMediaQuery({ + query: '(max-width: 1279px)', + }); + const isDesktop = useMediaQuery({ + query: '(min-width: 1280px)', + }); + + return ( +
+ {loading ? ( + + ) : error ? ( +
{error}
+ ) : ( + <> + + + + + + + + + + + + + + + + + + + + +
CurrencyPurchaseSale
GBP{rateBuyGBP}{rateSellGBP}
EUR{rateBuyEUR}{rateSellEUR}
+ + {isMobile ? ( +
+ + + + + + + + + + + + + + + + +
+ ) : ( + '' + )} + {isMinTablet & isMaxTablet ? ( +
+ + + + + + + + + + + + + + + + +
+ ) : ( + '' + )} + {isDesktop ? ( +
+
{rateBuyGBP}
+
{rateBuyEUR}
+ + + + + + + + + + + + + + + + +
+ ) : ( + '' + )} + + )} +
+ ); }; export default CurrencyTab; diff --git a/src/Pages/CurrencyTab/CurrencyTab.module.css b/src/Pages/CurrencyTab/CurrencyTab.module.css new file mode 100644 index 0000000..7f568c0 --- /dev/null +++ b/src/Pages/CurrencyTab/CurrencyTab.module.css @@ -0,0 +1,169 @@ +body .container { + padding-left: 0px; + padding-right: 0px; +} + +.tablewrapper { + position: relative; + background-color: rgba(74, 86, 226, 0.1); + padding-top: 25px; + width: 280px; + height: 220px; + margin: 0 auto; + + @media only screen and (min-width: 768px) { + min-width: 336px; + margin: 0; + } + + @media only screen and (min-width: 1280px) { + width: 100%; + max-height: 395px; + height: 350px; + } +} + +.table { + color: white; + + width: 100%; + font-size: 16px; + line-height: normal; + border-collapse: collapse; + padding-left: 20px; +} + +.tablehead { + background: rgba(255, 255, 255, 0.2); + font-weight: 600; +} + +.tableheader { + padding-top: 13px; + padding-bottom: 13px; + width: 33%; + padding-left: 20px; + + @media only screen and (min-width: 1280px) { + &:first-child { + padding-left: 62px; + } + &:last-child { + padding-right: 111px; + } + } +} + +.tabledata { + padding-top: 8px; + padding-bottom: 8px; + width: 30%; + padding-left: 25px; + + &:last-child { + padding-left: 38px; + } + + @media only screen and (min-width: 1280px) { + text-align: center; + + &:first-child { + padding-left: 62px; + } + &:last-child { + padding-right: 131px; + } + } +} + +.svg { + position: absolute; + width: 100%; +} + +.tablegraph { + position: relative; + width: 100%; + top: -6px; + + &::before, + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 6px; + height: 6px; + border-radius: 50%; + background-color: #563eaf; + border: solid 1px #ff868d; + z-index: 1000; + } + + &::before { + z-index: 1; + left: 33px; + top: 23px; + } + + &::after { + z-index: auto; + left: 211px; + top: 2px; + } + + @media only screen and (min-width: 768px) { + &::before { + left: 42px; + top: 21px; + } + &::after { + left: 254px; + top: -2px; + } + } + + @media only screen and (min-width: 1280px) { + &::before { + left: 58px; + top: 29px; + width: 9px; + height: 9px; + } + &::after { + left: 364px; + top: -4px; + width: 9px; + height: 9px; + } + top: 26px; + } +} + +.svggradient { + width: 100%; + position: absolute; + top: 6px; + + @media only screen and (min-width: 1280px) { + top: 20px; + } +} + +.gbp, +.eur { + position: absolute; + color: var(--dashboard-text, #ff868d); + font-size: 12px; + font-weight: 400; + line-height: normal; +} + +.gbp { + left: 48px; + top: 8px; +} + +.eur { + left: 352px; + top: -27px; +} diff --git a/src/Pages/DashboardPage/DashboardPage.jsx b/src/Pages/DashboardPage/DashboardPage.jsx index 6ae83ae..8e71c5f 100644 --- a/src/Pages/DashboardPage/DashboardPage.jsx +++ b/src/Pages/DashboardPage/DashboardPage.jsx @@ -9,7 +9,6 @@ import Loader from 'components/Loader/Loader'; import { Outlet } from 'react-router-dom'; const DashboardPage = () => { - // const dispatch = useDispatch(); const isTablet = useMediaQuery({ query: '(min-width: 768px)' }); return ( diff --git a/src/Pages/DashboardPage/DashboardPage.module.css b/src/Pages/DashboardPage/DashboardPage.module.css index d62f792..81f6a98 100644 --- a/src/Pages/DashboardPage/DashboardPage.module.css +++ b/src/Pages/DashboardPage/DashboardPage.module.css @@ -1,12 +1,12 @@ .section { - min-height: calc(100dvh - 61px); - padding-top: 75px; - padding-bottom: 32px; + min-height: calc(100dvh - 104px); + padding-top: 0px; + padding-bottom: 40px; overflow: hidden; background: linear-gradient(219.62deg, #6d54eb 28.31%, #652392 66.76%); - padding-left: 20px; - padding-right: 20px; + padding-left: 32px; + padding-right: 32px; } .sharedSectionElements .navAndBalanceContainer { diff --git a/src/Pages/Home/Home.jsx b/src/Pages/Home/Home.jsx index df33ac7..11b07fb 100644 --- a/src/Pages/Home/Home.jsx +++ b/src/Pages/Home/Home.jsx @@ -1,29 +1,71 @@ -import React from 'react'; -import { useDispatch } from 'react-redux'; +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import ButtonAddTransactions from '../../components/ButtonAddTransactions/ButtonAddTransactions'; import TransactionsList from '../../components/TransactionsList/TransactionsList'; -import { addTransaction } from '../../actions pentru test doar/transactionAction'; import styles from './Home.module.css'; +import { fetchTransactions } from '../../redux/operations/transactionsOperations'; +import ModalAddTransaction from '../../components/ModalAddTransaction/ModalAddTransaction'; +import ModalEditTransaction from '../../components/ModalEditTransaction/ModalEditTransaction'; +import { useMediaQuery } from 'react-responsive'; +import { selectAllTransactions } from '../../redux/selectors/transactionsSelector'; +import ModalDeleteTransaction from '../../components/ModalDeleteTransaction/ModalDeleteTransaction'; +import TransactionsDesktop from '../../components/TransactionsDesktop/TransactionsDesktop'; +import Balance from '../../components/Balance/Balance'; const Home = () => { const dispatch = useDispatch(); + const screenCondition = useMediaQuery({ query: '(min-width: 768px)' }); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isEditModalOpen, setisEditModalOpen] = useState(false); + const data = useSelector(selectAllTransactions); - const handleAddTestTransaction = () => { - const testTransaction = { - id: new Date().getTime(), - description: 'Test Transaction', - amount: 100, - date: new Date().toISOString(), - }; - dispatch(addTransaction(testTransaction)); - }; + useEffect(() => { + dispatch(fetchTransactions()); + }, [dispatch]); return ( -
- - - -
+ <> +
+ {!screenCondition && } + + {screenCondition && ( +
+ setIsDeleteModalOpen(true)} + openEditModal={() => setisEditModalOpen(true)} + /> +
+ )} + + {!screenCondition && ( + setIsDeleteModalOpen(true)} + openEditModal={() => setisEditModalOpen(true)} + /> + )} + + +
+ + <> + {isAddModalOpen && ( + setIsAddModalOpen(false)} /> + )} + + {isDeleteModalOpen && ( + setIsDeleteModalOpen(false)} + /> + )} + + {isEditModalOpen && ( + setisEditModalOpen(false)} /> + )} + + ); }; diff --git a/src/Pages/Home/Home.module.css b/src/Pages/Home/Home.module.css index c8681eb..9ab2bed 100644 --- a/src/Pages/Home/Home.module.css +++ b/src/Pages/Home/Home.module.css @@ -1,18 +1,72 @@ -.homeTabContainer { - width: 100%; - margin: 0 auto; - padding: 20px; -} - -.homeSection { +.HomePage { display: flex; flex-direction: column; - align-items: center; justify-content: center; + align-items: center; + margin-top: 20px; +} + +@media screen and (min-width: 768px) { + .tableContainer { + height: calc(-440px + 100dvh); + overflow-y: auto; /* Bara de derulare verticală */ + } } -@media only screen and (min-width: 768px) { - .homeTabContainer { - padding: 0; +@media screen and (min-width: 1280px) { + .HomePage { + display: inline-flex; + justify-content: center; + align-self: center; + margin-left: 64px; } + .tableContainer { + height: calc(-220px + 100dvh); + } +} + +/* Stilizare pentru scrollbar în Google Chrome și Safari */ + +::-webkit-scrollbar { + width: 10px; /* Lățimea scrollbarului */ +} + +::-webkit-scrollbar-track { + background: transparent; /* Fundalul track-ului scrollbarului */ +} + +::-webkit-scrollbar-thumb { + background: linear-gradient( + 270.02deg, + #2e1746 3.2%, + #2e225f 99.98% + ); /* Culoarea thumb-ului (butonul) scrollbarului */ + border-radius: 5px; /* Rotunjirea colțurilor thumb-ului */ +} + +::-webkit-scrollbar-thumb:hover { + background: #555; /* Culoarea thumb-ului la hover */ +} + +/* Stilizare pentru scrollbar în Firefox */ + +.scrollbar { + scrollbar-width: thin; /* Lățimea scrollbarului */ +} + +.scrollbar-track { + background: #f1f1f1; /* Fundalul track-ului scrollbarului */ +} + +.scrollbar-thumb { + background: linear-gradient( + 270.02deg, + #2e1746 3.2%, + #2e225f 99.98% + ); /* Culoarea thumb-ului (butonul) scrollbarului */ + border-radius: 5px; /* Rotunjirea colțurilor thumb-ului */ +} + +.scrollbar-thumb:hover { + background: #555; /* Culoarea thumb-ului la hover */ } diff --git a/src/actions pentru test doar/transactionAction.jsx b/src/actions pentru test doar/transactionAction.jsx deleted file mode 100644 index 4e86de0..0000000 --- a/src/actions pentru test doar/transactionAction.jsx +++ /dev/null @@ -1,6 +0,0 @@ -export const ADD_TRANSACTION = 'ADD_TRANSACTION'; - -export const addTransaction = (transaction) => ({ - type: ADD_TRANSACTION, - payload: transaction, -}); diff --git a/src/actions pentru test doar/transactionsReducer.jsx b/src/actions pentru test doar/transactionsReducer.jsx deleted file mode 100644 index 0b9f993..0000000 --- a/src/actions pentru test doar/transactionsReducer.jsx +++ /dev/null @@ -1,22 +0,0 @@ -const initialState = { - items: [], - loading: false, - error: null, -}; - -const transactionsReducer = (state = initialState, action) => { - switch (action.type) { - case 'FETCH_TRANSACTIONS_REQUEST': - return { ...state, loading: true, error: null }; - case 'FETCH_TRANSACTIONS_SUCCESS': - return { ...state, loading: false, items: action.payload }; - case 'FETCH_TRANSACTIONS_FAILURE': - return { ...state, loading: false, error: action.payload }; - case 'ADD_TRANSACTION': - return { ...state, items: [...state.items, action.payload] }; - default: - return state; - } -}; - -export default transactionsReducer; diff --git a/src/components/AddTransactionForm/AddTransactionForm.jsx b/src/components/AddTransactionForm/AddTransactionForm.jsx index 5d0369e..9fd11fc 100644 --- a/src/components/AddTransactionForm/AddTransactionForm.jsx +++ b/src/components/AddTransactionForm/AddTransactionForm.jsx @@ -12,7 +12,7 @@ import { useSelector } from 'react-redux'; import Select from 'react-select'; import { customStyles } from './customStyles'; import { useDispatch } from 'react-redux'; -import { addTransactions } from '../../redux/Transactions/operations'; +import { addTransaction } from '../../redux/operations/transactionsOperations'; import { closeAddModal } from '../../redux/Modals/slice'; import CustomDropIndicator from '../CustomDropIndicator/CustomDropIndicator'; @@ -85,7 +85,7 @@ function AddTransactionForm() { delete data.switch; - dispatch(addTransactions(data)); + dispatch(addTransaction(data)); dispatch(closeAddModal()); }; diff --git a/src/components/AddTransactionForm/customStyles.js b/src/components/AddTransactionForm/customStyles.js index 760d88b..3f5c91c 100644 --- a/src/components/AddTransactionForm/customStyles.js +++ b/src/components/AddTransactionForm/customStyles.js @@ -45,7 +45,7 @@ // }), // }; -import { CgOverflow } from 'react-icons/cg'; +// import { CgOverflow } from 'react-icons/cg'; export const customStyles = { option: provided => { @@ -81,11 +81,10 @@ export const customStyles = { borderRadius: '8px', boxShadow: 'none', backgroundColor: 'rgba(74, 86, 226, 0.10)', - border: 0, + // border: 0, background: 'transparent', - color: 'transparent', - boxShadow: 'none', + // color: 'transparent', display: 'flex', flexWrap: 'nowrap', borderColor: 'transparent', diff --git a/src/components/App/App.jsx b/src/components/App/App.jsx index eb311ff..82dff6e 100644 --- a/src/components/App/App.jsx +++ b/src/components/App/App.jsx @@ -12,16 +12,17 @@ import CurrencyTab from 'Pages/CurrencyTab/CurrencyTab'; import PrivateRoutes from '../../routes/PrivateRoutes'; import PublicRoutes from '../../routes/PublicRoutes'; import Loader from 'components/Loader/Loader'; +import { user } from '../../redux/selectors/authSelectors'; +import { useMediaQuery } from 'react-responsive'; const App = () => { - const { isAuthenticated, isLoading } = useSelector(state => ({ - isAuthenticated: Boolean(state.auth.user), - isLoading: state.auth.loading - })); + const isAuthenticated = Boolean(useSelector(user)); + const isOnMobile = useMediaQuery({ query: '(max-width: 768px)' }); + // const isLoading = useSelector(auth.loading); - if (isLoading) { - return ; - } + // if (isLoading) { + // return ; + // } return ( <> @@ -40,16 +41,21 @@ const App = () => { } /> } /> } /> - } /> + {isOnMobile && ( + } /> + )} {/* Catch-all redirect */} - - } + + } /> @@ -70,4 +76,4 @@ const App = () => { ); }; -export default App; \ No newline at end of file +export default App; diff --git a/src/components/Balance/Balance.jsx b/src/components/Balance/Balance.jsx index eedc607..559b315 100644 --- a/src/components/Balance/Balance.jsx +++ b/src/components/Balance/Balance.jsx @@ -1,14 +1,28 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { selectTotalBalance } from '../../redux/selectors/transactionsSelector'; +import { useEffect } from 'react'; + +import { selectBalance } from '../../redux/selectors/authSelectors'; +import styles from './Balance.module.css'; +import { useDispatch, useSelector } from 'react-redux'; +import { getUserInfo } from '../../redux/operations/authOperations'; +import { formatNumberWithSpaces } from '../common/formatNumberWithSpaces'; + +function Balance() { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(getUserInfo()); + }, [dispatch]); + + const balance = useSelector(selectBalance); -const Balance = () => { - const totalBalance = useSelector(selectTotalBalance); return ( -
-

Total balance: {totalBalance}

+
+

Your balance

+

+ ₴ {balance ? formatNumberWithSpaces(balance) : '0.00'} +

); -}; +} export default Balance; diff --git a/src/components/Balance/Balance.module.css b/src/components/Balance/Balance.module.css index 1591dde..2005fd5 100644 --- a/src/components/Balance/Balance.module.css +++ b/src/components/Balance/Balance.module.css @@ -1,29 +1,47 @@ -/* Balance.module.css */ - .balance { - font-size: 1.5rem; - font-weight: bold; - margin-bottom: 20px; - color: #28a745; + background: rgba(82, 59, 126, 0.6); + box-shadow: 0px 4px 60px 0px rgba(0, 0, 0, 0.25); + padding: 8px 32px 11.2px 32px; + height: 80px; + border-radius: 8px; + display: flex; + flex-direction: column; + gap: 11px; + align-self: center; + justify-content: center; + width: 100%; } -/* Desktop */ -@media (min-width: 1024px) { - .balance { - font-size: 2rem; - } +.balance h3 { + font-size: 12px; + font-weight: 400; + line-height: 13px; + color: rgba(255, 255, 255, 0.4); + text-transform: uppercase; +} + +.balance p { + color: #fbfbfb; + font-size: 30px; + font-weight: 700; + line-height: 36px; +} + +.balance p span { + font-weight: 700; + color: #fbfbfb; } -/* Tablet */ -@media (min-width: 768px) and (max-width: 1023px) { +@media screen and (min-width: 768px) { .balance { - font-size: 1.75rem; + width: 336px; + padding: 8px 40px 11.2px 40px; + align-self: start; } } -/* Mobile */ -@media (max-width: 767px) { +@media screen and (min-width: 1280px) { .balance { - font-size: 1.25rem; + width: 480px; } } diff --git a/src/components/ButtonAddTransactions/ButtonAddTransactions.modules.css b/src/components/ButtonAddTransactions/ButtonAddTransactions.modules.css index 8bc624f..9f3dd98 100644 --- a/src/components/ButtonAddTransactions/ButtonAddTransactions.modules.css +++ b/src/components/ButtonAddTransactions/ButtonAddTransactions.modules.css @@ -28,7 +28,7 @@ border-radius: 100%; } -@media screen and (min-width: 768px) { +/* @media screen and (min-width: 768px) { .btn { position: absolute; } @@ -42,4 +42,4 @@ max-width: 1280px; transform: translateX(-50%); } -} +} */ diff --git a/src/components/ButtonForm/ButtonForm.jsx b/src/components/ButtonForm/ButtonForm.jsx new file mode 100644 index 0000000..38fe034 --- /dev/null +++ b/src/components/ButtonForm/ButtonForm.jsx @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import styles from './ButtonForm.module.css'; + +const ButtonForm = ({ + type, + text, + handlerFunction, + variant, + isDisabled = false, +}) => { + return ( + + ); +}; + +// Definim tipurile așteptate pentru fiecare prop: +ButtonForm.propTypes = { + type: PropTypes.oneOf(['button', 'submit', 'reset']), // Tipul trebuie să fie unul din valorile specificate. + text: PropTypes.string.isRequired, // Textul trebuie să fie un șir de caractere și este necesar. + handlerFunction: PropTypes.func, // Funcția de gestionare a evenimentului trebuie să fie o funcție, dar nu este necesară. + variant: PropTypes.string, // Variantul poate fi un șir de caractere, dar nu este necesar. + isDisabled: PropTypes.bool, // Este o valoare booleană, dar nu este necesară. +}; + +export default ButtonForm; diff --git a/src/components/ButtonForm/ButtonForm.module.css b/src/components/ButtonForm/ButtonForm.module.css new file mode 100644 index 0000000..390a181 --- /dev/null +++ b/src/components/ButtonForm/ButtonForm.module.css @@ -0,0 +1,80 @@ +.buttonForm { + width: 280px; + height: 50px; + border-radius: 20px; + box-shadow: 1px 9px 15px 0px rgba(0, 0, 0, 0.2); + + border: none; + cursor: pointer; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + text-transform: uppercase; + font-size: 18px; + font-weight: 400; + line-height: 27px; + letter-spacing: 0.1em; + transition: all 0.5s ease 0s; +} + +/* Variant styles: 1) Multi color button, 2) White button */ + +/* 1) Multi color button */ +.multiColorButtton { + color: rgba(251, 251, 251, 1); + background: linear-gradient( + 96.76deg, + #ffc727 -16.42%, + #9e40ba 97.04%, + #7000ff 150.71% + ); +} + +.multiColorButtton:hover { + transform: scale(1.05); + background-image: linear-gradient( + to right, + #7000ff 0%, + #9e40ba 31%, + #ffc727 100% + ); + background-size: 200%; + background-position: right center; + transition: all 1s ease 0s; +} + +.multiColorButtton:active { + transform: scale(1.08); + font-size: 19px; +} + +/* 2) White button */ +.whiteButtton { + color: #623f8b; + background: #fcfcfc; +} + +.whiteButtton:hover { + transform: scale(1.05); + background-image: linear-gradient( + to right, + rgb(255, 255, 255) 0%, + rgb(98, 63, 139) 31%, + rgb(255, 255, 255) 100% + ); + background-size: 200%; + background-position: right center; + transition: all 1s ease 0s; +} + +.whiteButtton:active { + transform: scale(1.08); + font-size: 19px; +} + +@media screen and (min-width: 768px) { + .formButton { + width: 300px; + } +} diff --git a/src/components/Currency/Currency.jsx b/src/components/Currency/Currency.jsx index 2497908..154b460 100644 --- a/src/components/Currency/Currency.jsx +++ b/src/components/Currency/Currency.jsx @@ -1,45 +1,288 @@ -// src/components/Currency/Currency.jsx -import React, { useEffect, useState } from 'react'; -import { useMediaQuery } from 'react-responsive'; import styles from './Currency.module.css'; +import axios from 'axios'; +import { useEffect, useState } from 'react'; +import { useMediaQuery } from 'react-responsive'; const Currency = () => { - const [exchangeRates, setExchangeRates] = useState([]); - const isTabletOrDesktop = useMediaQuery({ query: '(min-width: 768px)' }); + const [data, setData] = useState(null); - // Fetch data for currency exchange rates from an API endpoint (placeholder) useEffect(() => { - // Exemplu de API - de înlocuit cu API-ul real - fetch('https://api.exchangerate-api.com/v4/latest/USD') - .then(response => response.json()) - .then(data => { - // Extragem doar câteva cursuri valutare importante - setExchangeRates([ - { currency: 'EUR', rate: data.rates.EUR }, - { currency: 'GBP', rate: data.rates.GBP }, - { currency: 'RON', rate: data.rates.RON }, - ]); - }) - .catch(error => console.error('Eroare la fetch:', error)); + const fetchDataAndStore = async () => { + try { + const response = await axios.get( + 'https://openexchangerates.org/api/latest.json?app_id=db45438d2f2f4a75b5a6d5ac8236394e' + ); + const newData = response.data; + const fetchTime = new Date().getTime(); + localStorage.setItem( + 'MONO', + JSON.stringify({ data: newData, fetchTime }) + ); + setData(newData); + } catch (error) { + console.error('Error fetching data:', error); + } + }; + + const storedData = localStorage.getItem('MONO'); + if (storedData) { + const { data, fetchTime } = JSON.parse(storedData); + const currentTime = new Date().getTime(); + if (currentTime - fetchTime < 3600000) { + setData(data); + } else { + fetchDataAndStore(); + } + } else { + fetchDataAndStore(); + } }, []); + const rateGBP = data?.rates['GBP']; + const rateEUR = data?.rates['EUR']; + const rateBuyGBP = rateGBP ? rateGBP.toFixed(2) : 'N/A'; + const rateSellGBP = rateGBP ? (1 / rateGBP).toFixed(2) : 'N/A'; + const rateBuyEUR = rateEUR ? rateEUR.toFixed(2) : 'N/A'; + const rateSellEUR = rateEUR ? (1 / rateEUR).toFixed(2) : 'N/A'; + + const isMobile = useMediaQuery({ + query: '(max-width: 767px)', + }); + const isMinTablet = useMediaQuery({ + query: '(min-width: 768px)', + }); + + const isMaxTablet = useMediaQuery({ + query: '(max-width: 1279px)', + }); + const isDesktop = useMediaQuery({ + query: '(min-width: 1280px)', + }); + return ( -
-

Currency Rates

-
- {exchangeRates.map(rate => ( -
- {rate.currency} - - {rate.rate.toFixed(2)} - -
- ))} -
+
+ + + + + + + + + + + + + + + + + + + + +
CurrencyPurchaseSale
GBP{rateBuyGBP}{rateSellGBP}
EUR{rateBuyEUR}{rateSellEUR}
+ + {isMobile ? ( +
+ + + + + + + + + + + + + + + + +
+ ) : ( + '' + )} + {isMinTablet & isMaxTablet ? ( +
+ + + + + + + + + + + + + + + + +
+ ) : ( + '' + )} + {isDesktop ? ( +
+
{rateBuyGBP}
+
{rateBuyEUR}
+ + + + + + + + + + + + + + + + +
+ ) : ( + '' + )}
); }; diff --git a/src/components/Currency/Currency.module.css b/src/components/Currency/Currency.module.css index 51aeccf..f5894a0 100644 --- a/src/components/Currency/Currency.module.css +++ b/src/components/Currency/Currency.module.css @@ -1,44 +1,163 @@ -/* src/components/Currency/Currency.css */ +.tablewrapper { + position: relative; + background-color: rgba(74, 86, 226, 0.1); + width: 320px; + height: 220px; + margin: 0 auto; -.currency-container { - display: flex; - flex-direction: column; - padding: 16px; - border: 1px solid #e0e0e0; - border-radius: 8px; - background-color: #ffffff; - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); + @media only screen and (min-width: 768px) { + min-width: 336px; + margin: 0; + } + + @media only screen and (min-width: 1280px) { + width: 100%; + max-height: 395px; + height: 350px; + } } -.currency-container.desktop { - max-width: 300px; +.table { + color: white; + + width: 100%; + font-size: 16px; + line-height: normal; + border-collapse: collapse; + padding-left: 20px; } -.currency-title { - font-size: 18px; - font-weight: bold; - color: #333333; - margin-bottom: 12px; +.tablehead { + background: rgba(255, 255, 255, 0.2); + font-weight: 600; } -.currency-rates { - display: flex; - flex-direction: column; - gap: 8px; +.tableheader { + padding-top: 13px; + padding-bottom: 13px; + width: 33%; + padding-left: 20px; + + @media only screen and (min-width: 1280px) { + &:first-child { + padding-left: 62px; + } + &:last-child { + padding-right: 111px; + } + } } -.currency-item { - display: flex; - justify-content: space-between; - font-size: 16px; +.tabledata { + padding-top: 8px; + padding-bottom: 8px; + width: 30%; + padding-left: 25px; + + &:last-child { + padding-left: 46px; + } + + @media only screen and (min-width: 1280px) { + text-align: center; + + &:first-child { + padding-left: 62px; + } + &:last-child { + padding-right: 131px; + } + } } -.currency-code { - color: #555555; - font-weight: 500; +.svg { + position: absolute; + width: 100%; } -.currency-rate { - color: #008080; /* Exemplu de culoare pentru rate */ - font-weight: bold; -} \ No newline at end of file +.tablegraph { + position: relative; + width: 100%; + top: -6px; + + &::before, + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 6px; + height: 6px; + border-radius: 50%; + background-color: #563eaf; + border: solid 1px #ff868d; + z-index: 1000; + } + + &::before { + z-index: 1; + left: 39px; + top: 21px; + } + + &::after { + z-index: auto; + left: 242px; + top: -2px; + } + + @media only screen and (min-width: 768px) { + &::before { + left: 42px; + top: 21px; + } + &::after { + left: 254px; + top: -2px; + } + } + + @media only screen and (min-width: 1280px) { + &::before { + left: 58px; + top: 29px; + width: 9px; + height: 9px; + } + &::after { + left: 364px; + top: -4px; + width: 9px; + height: 9px; + } + top: 26px; + } +} + +.svggradient { + width: 100%; + position: absolute; + top: 6px; + + @media only screen and (min-width: 1280px) { + top: 20px; + } +} + +.gbp, +.eur { + position: absolute; + color: var(--dashboard-text, #ff868d); + font-size: 12px; + font-weight: 400; + line-height: normal; +} + +.gbp { + left: 48px; + top: 8px; +} + +.eur { + left: 352px; + top: -27px; +} diff --git a/src/components/EditTransactionForm/EditTransactionForm.jsx b/src/components/EditTransactionForm/EditTransactionForm.jsx index 9513211..9f33f77 100644 --- a/src/components/EditTransactionForm/EditTransactionForm.jsx +++ b/src/components/EditTransactionForm/EditTransactionForm.jsx @@ -1,70 +1,207 @@ -import React from 'react'; +import { useState } from 'react'; import PropTypes from 'prop-types'; -import { Formik, Form, Field, ErrorMessage } from 'formik'; +import styles from './EditTransactionForm.module.css'; + +import icons from '../images/icons/sprite.svg'; +import { useMediaQuery } from 'react-responsive'; + +import { ErrorMessage, Field, Form, Formik } from 'formik'; import * as Yup from 'yup'; -import DatePicker from 'react-datepicker'; +import { useDispatch } from 'react-redux'; +import ReactDatePicker, { registerLocale } from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; +import enUS from 'date-fns/locale/en-US'; // Importăm localizarea pentru engleză +import { + transactionCategories, + getTransactionId, +} from '../common/allCategories'; + +import { useSelector } from 'react-redux'; +import { selectTransactionForUpdate } from '../../redux/selectors/transactionsSelector'; +import { editTransaction } from '../../redux/operations/transactionsOperations'; +import { getUserInfo } from '../../redux/operations/authOperations'; +import { FiCalendar } from 'react-icons/fi'; +import { Controller, useForm } from 'react-hook-form'; +import ButtonForm from '../ButtonForm/ButtonForm'; + +// Înregistram localizarea pentru utilizarea în componenta ReactDatePicker +registerLocale('en-US', enUS); + +const EditTransactionForm = ({ closeModal }) => { + const transactionForUpdate = useSelector(selectTransactionForUpdate); + const { control, setValue } = useForm(); // Declararea controlului și funcției setValue + + const isOnIncomeTab = transactionForUpdate.type === 'INCOME' ? true : false; + + const screenCondition = useMediaQuery({ query: '(min-width: 768px)' }); + + const dispatch = useDispatch(); + + const [startDate, setStartDate] = useState( + new Date(transactionForUpdate.transactionDate) + ); + const initialValues = { + categoryId: transactionForUpdate.categoryId, + amount: transactionForUpdate.amount, + transactionDate: transactionForUpdate.transactionDate, + comment: transactionForUpdate.comment, + }; -const EditTransactionForm = ({ initialValues, onSubmit }) => { - const validationSchema = Yup.object({ - type: Yup.string.oneOf(['income', 'expense']).required('Type is required!'), - sum: Yup.number.required('Sum is required!'), - date: Yup.date.required('Date is required!'), - comment: Yup.string() - .max(200, 'Comment must be 200 characters or less') - .nullable(), - }); + const validationSchema = isOnIncomeTab + ? Yup.object({ + amount: Yup.string().required('Required*'), + comment: Yup.string().required('Required*'), + }) + : Yup.object({ + amount: Yup.string().required('Required*'), + comment: Yup.string().required('Required*'), + category: Yup.string().required('Required*'), + }); + + const handleSubmit = (values, { setSubmitting, setStatus }) => { + setSubmitting(true); + console.log(transactionForUpdate.id); + + dispatch( + editTransaction({ + transactionId: transactionForUpdate.id, + transactionData: { + transactionDate: startDate, + type: isOnIncomeTab ? 'INCOME' : 'EXPENSE', + categoryId: getTransactionId( + values.transactionCategories || 'Income' + ), + comment: values.comment, + amount: isOnIncomeTab ? values.amount : 0 - values.amount, + }, + }) + ) + .unwrap() + .then(() => { + closeModal(); + dispatch(getUserInfo()); + }) + .catch(error => { + setStatus({ success: false, error: error }); + setSubmitting(false); + }); + }; + + const handleDateChange = dateChange => { + setValue('transactionDate', dateChange, { + shouldDirty: true, + }); + setStartDate(dateChange); + }; return ( - - {({ setFieldValue, isSubmitting }) => ( -
- - - - - - - - - - - - - - setFieldValue('date', date)} - /> - - - - - - - - +
+ {screenCondition && ( + )} - + + {({ isSubmitting }) => ( +
+

Edit transaction

+ +
+ + Income + + / + + Expense + +
+ +
+ {!isOnIncomeTab && ( +
+ + + {transactionCategories.map(item => ( + + ))} + + +
+ )} + +
+ + +
+ +
+ ( + + )} + /> + +
+ +
+ + +
+
+ +
+ + closeModal()} + /> +
+
+ )} +
+
); }; EditTransactionForm.propTypes = { - initialValues: PropTypes.shape({ - type: PropTypes.oneOf(['income', 'expense']).isRequired, - sum: PropTypes.number.isRequired, - date: PropTypes.instanceOf(Date).isRequired, - comment: PropTypes.string, - }).isRequired, - onSubmit: PropTypes.func.isRequired, + closeModal: PropTypes.func.isRequired, }; export default EditTransactionForm; diff --git a/src/components/EditTransactionForm/EditTransactionForm.module.css b/src/components/EditTransactionForm/EditTransactionForm.module.css index 2621c3a..1f33665 100644 --- a/src/components/EditTransactionForm/EditTransactionForm.module.css +++ b/src/components/EditTransactionForm/EditTransactionForm.module.css @@ -1,42 +1,250 @@ -/* EditTransactionForm.module.css */ +.modalContent { + position: relative; +} -form { +.modalContent form { display: flex; flex-direction: column; - gap: 10px; + align-items: center; + gap: 40px; } -label { - font-weight: bold; - color: #333; +/* Ascunde săgețile de incrementare/decrementare din câmpul număr */ +.inputField input[type='number']::-webkit-inner-spin-button, +.inputField input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; } -input, -select, -.react-datepicker-wrapper { +form .formTitle { + color: rgba(251, 251, 251, 1); + font-size: 24px; + font-weight: 400; + line-height: 36px; +} + +form .inputWrapper { width: 100%; - padding: 8px; - border: 1px solid #ddd; - border-radius: 5px; + display: flex; + flex-direction: column; + gap: 40px; } -button[type='submit'] { - background-color: #28a745; - color: #fff; - padding: 10px; +.inputWrapper .inputField input, +.inputWrapper .inputField select { + width: 100%; + outline: none; border: none; - border-radius: 5px; + border-bottom: 1px solid rgba(255, 255, 255, 0.4); + background-color: transparent; + padding: 0 20px 8px 20px; + font-size: 18px; + font-weight: 600; + line-height: 27px; + color: rgba(255, 255, 255, 0.6); + transition: all 0.35s ease-in-out; + position: relative; + color: #ffffff99; +} + +.inputWrapper .inputField select { + cursor: pointer; + color: #ffffff99; +} + +.inputWrapper .inputField select option { + cursor: pointer; + box-shadow: 0px 4px 60px 0px rgba(0, 0, 0, 0.25); + background-color: rgba(83, 61, 186, 0.7); + /* background-color: rgba(80, 48, 154, 0.7); */ + /* background-color: rgba(106, 70, 165, 0.52); */ + /* background-color: rgba(133, 93, 175, 0.13); */ + color: rgba(251, 251, 251, 1); +} + +.inputField input:focus, +.inputField select:focus { + border-bottom: 1px solid #ffffff; + color: #ffffff; + transition: all 0.35s ease-in-out; +} + +.inputField input::placeholder { + color: #ffffff99; + transition: all 0.35s ease-in-out; +} + +.inputField input:focus::placeholder { + color: #ffffff; + transition: all 0.35s ease-in-out; +} + +.inputField p { + color: rgb(255, 134, 141); + font-size: 14px; + padding-left: 12px; + /* padding-left: 20px; */ +} + +.inputField.date { + position: relative; +} + +.inputField.date input { cursor: pointer; + width: 100%; + color: #ffffff99; + transition: all 0.5s ease 0s; +} + +.inputField.date input:focus { + color: #ffffff; + transition: all 0.5s ease 0s; +} + +.icon { + position: absolute; + top: 0; + right: 17px; + color: #734aef; + width: 24px; + height: 24px; + pointer-events: none; +} + +form .buttonsWrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} + +.switcheWrapper { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; +} + +.switcheWrapper span { + color: rgba(255, 255, 255, 0.6); + font-size: 16px; + font-weight: 600; + line-height: 24px; } -button[type='submit']:disabled { - background-color: #ccc; +/* .switcheWrapper span:first-child { + margin-right: -20px; +} */ + +.switcheWrapper span.income { + color: rgba(255, 182, 39, 1); + font-weight: bold; } -/* Responsive Styling */ -@media (max-width: 767px) { - input, - select { - padding: 6px; +.switcheWrapper span.expense { + color: rgba(255, 134, 141, 1); + font-weight: bold; +} + +@media screen and (min-width: 768px) { + .modalContent { + width: 540px; + height: 589px; + display: flex; + flex-direction: column; + justify-content: center; + + padding: 40px 73px; + } + + .modalContent .closeButton { + background-color: transparent; + display: flex; + align-items: center; + justify-content: center; + border: none; + padding: 0; + position: absolute; + top: 20px; + right: 20px; + cursor: pointer; + transition: all 0.5s ease 0s; + width: 30px; + height: 30px; + border-radius: 100%; + display: flex; + justify-content: center; + align-content: center; + } + + .modalContent .closeButton:hover { + transform: scale(1.1); + background-color: rgba(251, 251, 251, 1); + transition: all 0.5s ease 0s; + } + + .modalContent .closeButton svg { + width: 16px; + height: 16px; + stroke: rgba(251, 251, 251, 1); + } + + .closeButton:hover > svg { + stroke: red; + } + + form .formTitle { + font-size: 30px; + line-height: 45px; + } + + form .inputWrapper { + flex-direction: row; + flex-wrap: wrap; + gap: 32px; + row-gap: 40px; + } + + .inputField.category { + width: 100%; + } + + .inputField.amount { + flex: 1; + } + + .inputField.date { + flex: 1; } + + .icon { + right: 8px; + } + + .inputField.comment { + width: 100%; + } +} + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill::first-line { + transition: background-color 5000s ease-in-out 0s; + background-color: transparent !important; + color: rgba(255, 255, 255, 0.6) !important; +} +input:-moz-autofill, +textarea:-moz-autofill { + transition: background-color 5000s ease-in-out 0s; + background-color: transparent !important; + color: rgba(255, 255, 255, 0.6) !important; +} + +input:-ms-autofill, +textarea:-ms-autofill { + transition: background-color 5000s ease-in-out 0s; + background-color: transparent !important; + color: rgba(255, 255, 255, 0.6) !important; } diff --git a/src/components/Logo/Logo.jsx b/src/components/Logo/Logo.jsx new file mode 100644 index 0000000..6c59a47 --- /dev/null +++ b/src/components/Logo/Logo.jsx @@ -0,0 +1,20 @@ +import styles from './Logo.module.css'; +import icons from '../images/icons/sprite.svg'; +import PropTypes from 'prop-types'; + +const Logo = ({ variant }) => { + return ( + + + + + Money Guard + + ); +}; + +Logo.propTypes = { + variant: PropTypes.string.isRequired, +}; + +export default Logo; diff --git a/src/components/Logo/Logo.module.css b/src/components/Logo/Logo.module.css new file mode 100644 index 0000000..bc41710 --- /dev/null +++ b/src/components/Logo/Logo.module.css @@ -0,0 +1,58 @@ +.logo { + display: flex; + flex-direction: column; + align-items: center; + text-decoration: none; +} + +.logo span { + text-transform: capitalize; + color: rgba(251, 251, 251, 1); + font-weight: 400; +} + +/*Variant: navbarLogo => Logo placed in navbar */ +.navbarLogo svg { + width: 17.11px; + height: 17.11px; +} + +.navbarLogo span { + font-size: 12.84px; + line-height: 19.26px; +} + +@media screen and (min-width: 768px) { + .navbarLogo svg { + width: 23.49px; + height: 22.47px; + } + + .navbarLogo span { + font-size: 17.1px; + line-height: 25.65px; + } +} + +/*Variant: formLogo => Logo placed in form (auth or log out) */ +.formLogo svg { + width: 25.46px; + height: 25.46px; +} + +.formLogo span { + font-size: 19.11px; + line-height: 28.67px; +} + +@media screen and (min-width: 768px) { + .formLogo svg { + width: 35.95px; + height: 35.97px; + } + + .formLogo span { + font-size: 26.96px; + line-height: 40.44px; + } +} diff --git a/src/components/ModalDeleteTransaction/ModalDeleteTransaction.jsx b/src/components/ModalDeleteTransaction/ModalDeleteTransaction.jsx new file mode 100644 index 0000000..d9aa494 --- /dev/null +++ b/src/components/ModalDeleteTransaction/ModalDeleteTransaction.jsx @@ -0,0 +1,88 @@ +import styles from './ModalDeleteTransaction.module.css'; +import { useMediaQuery } from 'react-responsive'; +import PropTypes from 'prop-types'; + +import Logo from 'components/Logo/Logo'; +import { useEffect, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { deleteTransaction } from '../../redux/operations/transactionsOperations'; +import { selectTrasactionIdForDelete } from '../../redux/selectors/transactionsSelector'; +import { getUserInfo } from '../../redux/operations/authOperations'; +import ButtonForm from 'components/ButtonForm/ButtonForm'; + +const ModalDeleteTransaction = ({ closeModal }) => { + const dispatch = useDispatch(); + + const trasactionIdForDelete = useSelector(selectTrasactionIdForDelete); + + const modalRef = useRef(); + + useEffect(() => { + document.body.style.overflow = 'hidden'; + + const addCloseEvent = event => { + event.key === 'Escape' && closeModal(); + }; + document.addEventListener('keydown', addCloseEvent); + + return () => { + document.body.style.overflow = 'auto'; + document.removeEventListener('keydown', addCloseEvent); + }; + }); + + const closeOnClickOutside = event => { + event.currentTarget === event.target && closeModal(); + }; + + const screenCondition = useMediaQuery({ query: '(min-width: 768px)' }); + + const handleDeleteClick = () => [ + dispatch(deleteTransaction(trasactionIdForDelete)) + .unwrap() + .then(() => { + closeModal(); + dispatch(getUserInfo()); + }) + .catch(error => { + console.log(error); + }), + ]; + + return ( +
+
+
+ {screenCondition && } + +

Are you sure you want to detete this transaction?

+ +
+ + closeModal()} + /> +
+
+
+
+ ); +}; + +ModalDeleteTransaction.propTypes = { + closeModal: PropTypes.func.isRequired, +}; + +export default ModalDeleteTransaction; diff --git a/src/components/ModalDeleteTransaction/ModalDeleteTransaction.module.css b/src/components/ModalDeleteTransaction/ModalDeleteTransaction.module.css new file mode 100644 index 0000000..a19c458 --- /dev/null +++ b/src/components/ModalDeleteTransaction/ModalDeleteTransaction.module.css @@ -0,0 +1,106 @@ +.deleteModal { + position: fixed; + top: 0; + left: 0; + + height: 100dvh; + width: 100%; + + z-index: 150; + + /* transform: scale(0); + opacity: 0; + transition: all 0.35s ease-in-out; */ +} + +.deleteModal.isOpen { + opacity: 1; + transform: scale(1); + transition: all 0.35s ease-in-out; +} + +.modalBg { + /* background-color: #ffffff1a; */ + + background: linear-gradient( + 0deg, + rgba(83, 61, 186, 1) 0%, + rgba(80, 48, 154, 1) 43.14%, + rgba(106, 70, 165, 1) 73.27%, + rgba(133, 93, 175, 1) 120.03% + ); + + box-shadow: 0px 4px 60px 0px #00000040; + backdrop-filter: blur(50px); + width: 100%; + height: 100dvh; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + overflow: hidden; +} + +.modalContent { + display: flex; + flex-direction: column; + align-items: center; + gap: 40px; +} + +.modalContent p { + color: #ffffff; + font-size: 18px; + font-weight: 400; + line-height: 27px; + text-align: center; +} + +.modalContent .buttonsWrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} + +@media screen and (min-width: 768px) { + .deleteModal { + /* background-color: hsla(256, 75%, 20%, 0.4); */ + background-color: #220d5b3b; + + backdrop-filter: blur(3px); + + min-width: 100%; + min-height: 100dvh; + display: flex; + justify-content: center; + align-items: center; + } + + .modalBg { + width: 533px; + height: 447px; + border-radius: 8px; + /* background: linear-gradient(270.02deg, #2e1746 3.2%, #2e225f 99.98%); */ + + background: linear-gradient( + 0deg, + rgba(83, 61, 186, 0.7) 0%, + rgba(80, 48, 154, 0.7) 43.14%, + rgba(106, 70, 165, 0.525) 73.27%, + rgba(133, 93, 175, 0.133) 120.03% + ); + + background: linear-gradient( + 0deg, + rgba(83, 61, 186, 1) 0%, + rgba(80, 48, 154, 1) 43.14%, + rgba(106, 70, 165, 1) 73.27%, + rgba(133, 93, 175, 1) 120.03% + ); + + background: rgba(82, 59, 126, 1); + } +} diff --git a/src/components/ModalEditTransaction/ModalEditTransaction.jsx b/src/components/ModalEditTransaction/ModalEditTransaction.jsx index 9130cd1..c340324 100644 --- a/src/components/ModalEditTransaction/ModalEditTransaction.jsx +++ b/src/components/ModalEditTransaction/ModalEditTransaction.jsx @@ -1,44 +1,46 @@ -import React from 'react'; +import { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; -import EditTransactionForm from '../EditTransactionForm/EditTransactionForm'; -import style from './ModalEditTransaction.module.css'; +import styles from './ModalEditTransaction.module.css'; +import EditTransactionForm from 'components/EditTransactionForm/EditTransactionForm'; -const ModalEditTransaction = ({ isOpen, initialValues, onSubmit, onClose }) => { - if (!isOpen) return null; +const ModalEditTransaction = ({ closeModal }) => { + const modalRef = useRef(); - const handleBackgroundClick = e => { - if (e.target.className === 'modal-overlay') onClose(); + useEffect(() => { + document.body.style.overflow = 'hidden'; + + const addCloseEvent = event => { + event.key === 'Escape' && closeModal(); + }; + document.addEventListener('keydown', addCloseEvent); + + return () => { + document.body.style.overflow = 'auto'; + document.removeEventListener('keydown', addCloseEvent); + }; + }); + + const closeOnClickOutside = event => { + event.target === event.currentTarget && closeModal(); }; return ( -
-
- -

Edit Transaction

- - + <> +
+
+ +
-
+ ); }; ModalEditTransaction.propTypes = { - isOpen: PropTypes.bool.isRequired, - initialValues: PropTypes.shape({ - type: PropTypes.oneOf(['income', 'expense']).isRequired, - sum: PropTypes.number.isRequired, - date: PropTypes.instanceOf(Date).isRequired, - comment: PropTypes.string, - }).isRequired, - onSubmit: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, + closeModal: PropTypes.func.isRequired, }; export default ModalEditTransaction; diff --git a/src/components/ModalEditTransaction/ModalEditTransaction.module.css b/src/components/ModalEditTransaction/ModalEditTransaction.module.css index 2a30f3c..50e9413 100644 --- a/src/components/ModalEditTransaction/ModalEditTransaction.module.css +++ b/src/components/ModalEditTransaction/ModalEditTransaction.module.css @@ -1,45 +1,96 @@ -/* ModalEditTransaction.module.css */ - -.modal-overlay { +.editModal { position: fixed; - top: 0; + top: 0px; left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.6); + + height: 100dvh; + width: 100%; + + z-index: 100; + /* opacity: 0; + transform: scale(0.5); + transition: all 0.35s ease-in-out; */ + + overflow: auto; +} + +.editModal.isOpen { + opacity: 1; + transform: scale(1); + transition: all 0.35s ease-in-out; +} + +.modalBg { + /* background-color: #ffffff1a; */ + + background: linear-gradient( + 0deg, + rgba(83, 61, 186, 1) 0%, + rgba(80, 48, 154, 1) 43.14%, + rgba(106, 70, 165, 1) 73.27%, + rgba(133, 93, 175, 1) 120.03% + ); + + box-shadow: 0px 4px 60px 0px #00000040; + backdrop-filter: blur(50px); + width: 100%; + height: 100%; + display: flex; + flex-direction: column; align-items: center; justify-content: center; - z-index: 1000; -} -.modal-content { - background-color: #fff; - border-radius: 10px; - padding: 30px; - width: 90%; - max-width: 500px; + overflow: hidden; } -.modal-close { - position: absolute; - top: 15px; - right: 15px; - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; -} +@media screen and (min-width: 768px) { + .editModal { + /* background-color: hsla(256, 75%, 20%, 0.4); */ + background-color: rgba(34, 13, 91, 0.231); + + backdrop-filter: blur(3px); + + min-width: 100%; + min-height: 100dvh; + display: flex; + justify-content: center; + align-items: center; + + padding: 60px; + padding-top: 60px; -/* Responsive Styling */ -@media (max-width: 767px) { - .modal-content { - padding: 20px; + /* overflow: auto; + padding-top: 60px; + padding-bottom: 60px; */ } - .modal-close { - top: 10px; - right: 10px; - font-size: 1.25rem; + .modalBg { + /* width: 540px; + height: 514px; */ + + width: fit-content; + height: fit-content; + + border-radius: 8px; + /* background: linear-gradient(270.02deg, #2e1746 3.2%, #2e225f 99.98%); */ + + background: linear-gradient( + 0deg, + rgba(83, 61, 186, 0.7) 0%, + rgba(80, 48, 154, 0.7) 43.14%, + rgba(106, 70, 165, 0.525) 73.27%, + rgba(133, 93, 175, 0.133) 120.03% + ); + + background: linear-gradient( + 0deg, + rgba(83, 61, 186, 1) 0%, + rgba(80, 48, 154, 1) 43.14%, + rgba(106, 70, 165, 1) 73.27%, + rgba(133, 93, 175, 1) 120.03% + ); + + background: rgba(82, 59, 126, 1); } } diff --git a/src/components/TransactionsDesktop/TransactionsDesktop.jsx b/src/components/TransactionsDesktop/TransactionsDesktop.jsx new file mode 100644 index 0000000..ede941e --- /dev/null +++ b/src/components/TransactionsDesktop/TransactionsDesktop.jsx @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import TransactionsDesktopRow from '../../components/TransactionsDesktopRow/TransactionsDesktopRow'; +import styles from './TransactionsDesktop.module.css'; + +const TransactionsDesktop = ({ data, openDeleteModal, openEditModal }) => { + // Definim funcția de comparație pentru sortare: + const compareTransactions = (a, b) => { + const dateComparison = + new Date(a.transactionDate) - new Date(b.transactionDate); + // Verificăm dacă tranzacțiile au aceeași dată: + if (dateComparison === 0) { + // Dacă tranzacția A este de tip "Income", o afișăm înaintea celei de tip "Expense": + if (a.type === 'INCOME' && b.type !== 'INCOME') { + return -1; + } + // Dacă tranzacția B este de tip "Income", o afișăm înaintea celei de tip "Expense": + if (a.type !== 'INCOME' && b.type === 'INCOME') { + return 1; + } + } + // Returnăm rezultatul comparării datelor: + return dateComparison; + }; + + // Sortăm datele utilizând funcția de comparație definită mai sus: + const sortedData = [...data].sort(compareTransactions); + + return ( +
+ + + + + + + + + + + + + + {sortedData.map(item => ( + + ))} + +
DateTypeCategoryCommentSum
+
+ ); +}; + +TransactionsDesktop.propTypes = { + data: PropTypes.arrayOf(PropTypes.object).isRequired, + openDeleteModal: PropTypes.func.isRequired, + openEditModal: PropTypes.func.isRequired, +}; + +export default TransactionsDesktop; diff --git a/src/components/TransactionsDesktop/TransactionsDesktop.module.css b/src/components/TransactionsDesktop/TransactionsDesktop.module.css new file mode 100644 index 0000000..8e5dba7 --- /dev/null +++ b/src/components/TransactionsDesktop/TransactionsDesktop.module.css @@ -0,0 +1,58 @@ +.TransactionTable { + /* background-color: aquamarine; */ + display: flex; + justify-content: center; + align-items: flex-end; +} + +.table { + border-collapse: collapse; + width: 100%; +} + +.tableHead { + color: #fbfbfb; + background: #523b7e99; + box-shadow: 0px 4px 60px 0px #00000040; + backdrop-filter: blur(50px); +} + +.tableHeadRow { + text-align: center; + font-size: 16px; + font-weight: 600; + line-height: 24px; +} + +.dateColumn { + padding-top: 16px; + padding-bottom: 16px; + padding-left: 20px; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + width: 100px; + text-align: left; +} + +.deleteColumn { + padding-right: 20px; + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; +} + +.typeColumn { + width: 100px; +} + +.categoryColumn { + width: 170px; +} + +.commentColumn { + min-width: 150px; + text-align: left; +} + +.sumColumn { + min-width: 80px; +} diff --git a/src/components/TransactionsDesktopRow/TransactionsDesktopRow.jsx b/src/components/TransactionsDesktopRow/TransactionsDesktopRow.jsx new file mode 100644 index 0000000..beca1e4 --- /dev/null +++ b/src/components/TransactionsDesktopRow/TransactionsDesktopRow.jsx @@ -0,0 +1,105 @@ +import PropTypes from 'prop-types'; +import { formatData, getTransactionCategory } from '../common/allCategories'; +import icons from '../images/icons/sprite.svg'; +import styles from './TransactionsDesktopRow.module.css'; +import { + setTrasactionForUpdate, + setTrasactionIdForDelete, +} from '../../redux/slices/transactionsSlice'; +import { useDispatch } from 'react-redux'; +import { formatNumberWithSpaces } from '../common/formatNumberWithSpaces'; + +const TransactionsDesktopRow = ({ + transaction, + openDeleteModal, + openEditModal, +}) => { + const { type, categoryId, comment, amount, transactionDate } = transaction; + + const dispatch = useDispatch(); + + const handleDeleteClick = () => { + openDeleteModal(); + dispatch(setTrasactionIdForDelete(transaction.id)); + }; + + const handleEditClick = () => { + openEditModal(); + dispatch( + setTrasactionForUpdate({ + id: transaction.id, + type: transaction.type, + categoryId: transaction.categoryId, + amount: transaction.amount, + transactionDate: transaction.transactionDate, + comment: transaction.comment, + }) + ); + }; + + let textClass = ''; + + // Determine class based on data + if (type === 'INCOME') { + textClass = styles.incomeText; // Access class from CSS module + } else if (type === 'EXPENSE') { + textClass = styles.expenseText; + } + + return ( + + + {formatData(transactionDate)} + + + {type === 'INCOME' ? '+' : '-'} + + + {getTransactionCategory(categoryId)} + + {comment} + + {type === 'INCOME' + ? formatNumberWithSpaces(amount) + : formatNumberWithSpaces(amount * -1)} + + + + + + + + + + + ); +}; + +TransactionsDesktopRow.propTypes = { + transaction: PropTypes.shape({ + id: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + categoryId: PropTypes.string.isRequired, + comment: PropTypes.string.isRequired, + amount: PropTypes.number.isRequired, + transactionDate: PropTypes.string.isRequired, + }).isRequired, + openDeleteModal: PropTypes.func.isRequired, + openEditModal: PropTypes.func.isRequired, +}; + +export default TransactionsDesktopRow; diff --git a/src/components/TransactionsDesktopRow/TransactionsDesktopRow.module.css b/src/components/TransactionsDesktopRow/TransactionsDesktopRow.module.css new file mode 100644 index 0000000..be0255a --- /dev/null +++ b/src/components/TransactionsDesktopRow/TransactionsDesktopRow.module.css @@ -0,0 +1,97 @@ +.editIcon { + width: 14px; + height: 14px; + transition: all 0.5s ease 0s; +} + +.dataRow { + text-align: center; + color: #fbfbfb; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + font-size: 14px; + font-weight: 400; + line-height: 21px; +} + +.incomeText { + color: #ffc727; + font-weight: bold; + letter-spacing: 1px; +} + +.expenseText { + color: #ff868d; + font-weight: bold; + letter-spacing: 1px; +} + +.editButton { + color: transparent; /* Change the color as desired */ + stroke: rgba(255, 255, 255, 0.6); + cursor: pointer; + background: transparent; + border: none; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0; + cursor: pointer; + transition: all 0.5s ease 0s; +} + +.editButton:hover { + transform: scale(1.2); + transition: all 0.5s ease 0s; +} + +.editButton:hover .editIcon { + stroke: gray; + color: white; + transition: all 0.5s ease 0s; +} + +.deleteButton { + padding: 4px 12px; + font-size: 14px; + font-weight: 400; + line-height: 21px; + border-radius: 18px; + border: none; + color: #ffffff; + background: linear-gradient( + 96.76deg, + #ffc727 -16.42%, + #9e40ba 97.04%, + #7000ff 150.71% + ); + cursor: pointer; + transition: all 0.5s ease 0s; +} + +.deleteButton:hover { + background: linear-gradient(95deg, #7000ff -15%, #9e40ba 95%, #ffc727 150%); + transform: scale(1.1); + transition: all 0.5s ease 0s; +} + +.TransactionDateColumn { + padding-top: 16px; + padding-bottom: 16px; + padding-left: 20px; +} + +.TransactionCommentColumn { + text-align: left; +} + +.TransactionDeleteColumn { + padding-right: 10px; +} + +.TransactionEditColumn { + display: flex; + align-items: center; + justify-content: flex-end; + height: 56px; + margin-right: 10px; +} diff --git a/src/components/TransactionsItem/TransactionsItem.jsx b/src/components/TransactionsItem/TransactionsItem.jsx index 4d856d0..ba571f4 100644 --- a/src/components/TransactionsItem/TransactionsItem.jsx +++ b/src/components/TransactionsItem/TransactionsItem.jsx @@ -1,32 +1,109 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import styles from './TransactionsItem.module.css'; +import icons from '../images/icons/sprite.svg'; +import { formatData, getTransactionCategory } from '../common/allCategories.js'; +import { useDispatch } from 'react-redux'; +import { + setTrasactionForUpdate, + setTrasactionIdForDelete, +} from '../../redux/slices/transactionsSlice'; +import { formatNumberWithSpaces } from '../../components/common/formatNumberWithSpaces'; -const TransactionsItem = ({ transaction, onDelete }) => { - const { date, type, category, comment, amount } = transaction; +const TransactionsItem = ({ transaction, openDeleteModal, openEditModal }) => { + const { type, categoryId, comment, amount, transactionDate } = transaction; + + const dispatch = useDispatch(); + + const handleDeleteClick = () => { + openDeleteModal(); + dispatch(setTrasactionIdForDelete(transaction.id)); + }; + + const handleEditClick = () => { + openEditModal(); + dispatch( + setTrasactionForUpdate({ + id: transaction.id, + type: transaction.type, + categoryId: transaction.categoryId, + amount: transaction.amount, + transactionDate: transaction.transactionDate, + comment: transaction.comment, + }) + ); + }; + + let textClass = ''; + let borderClass = ''; + + // Determine class based on data + if (type === 'INCOME') { + textClass = styles.incomeText; // Access class from CSS module + borderClass = styles.incomeBorder; + } else if (type === 'EXPENSE') { + textClass = styles.expenseText; + borderClass = styles.expenseBorder; + } return ( -
-

Date: {new Date(date).toLocaleDateString()}

-

Type: {type}

-

Category: {category}

-

Comment: {comment}

-

Amount: {type === 'income' ? `+${amount}` : `-${amount}`}

- -
+
  • +
    + Date + + {formatData(transactionDate)} + +
    +
    + Type + + {type === 'INCOME' ? '+' : '-'} + +
    +
    + Category + + {getTransactionCategory(categoryId)} + +
    +
    + Comment + {comment} +
    +
    + Sum + + {type === 'INCOME' + ? formatNumberWithSpaces(amount) + : formatNumberWithSpaces(amount * -1)} + +
    +
    + + +
    +
  • ); }; TransactionsItem.propTypes = { - transaction: PropTypes.shape({ - date: PropTypes.string.isRequired, - type: PropTypes.oneOf(['income', 'expense']).isRequired, - category: PropTypes.string, - comment: PropTypes.string, - amount: PropTypes.number.isRequired, - }).isRequired, - onDelete: PropTypes.func.isRequired, + transaction: PropTypes.object.isRequired, + openDeleteModal: PropTypes.func.isRequired, + openEditModal: PropTypes.func.isRequired, }; export default TransactionsItem; diff --git a/src/components/TransactionsItem/TransactionsItem.module.css b/src/components/TransactionsItem/TransactionsItem.module.css index abf0146..6590e26 100644 --- a/src/components/TransactionsItem/TransactionsItem.module.css +++ b/src/components/TransactionsItem/TransactionsItem.module.css @@ -1,34 +1,138 @@ -/* TransactionsItem.module.css */ +.TransactionItem { + display: flex; + flex-direction: column; + border-radius: 10px; + background-color: #ffffff1a; + box-shadow: 0px 4px 60px 0px #00000040; + /* backdrop-filter: blur(50px); */ + color: white; + font-size: 16px; -.transaction-item { - border: 1px solid #ddd; - border-radius: 8px; - padding: 15px; - background-color: #f9f9f9; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 100%; } -.transaction-item.income { - border-left: 4px solid #28a745; +.row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 30px; + padding: 12px 20px 12px 15px; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); } -.transaction-item.expense { - border-left: 4px solid #dc3545; +.deleteButton { + padding: 4px 12px; + font-size: 14px; + font-weight: 400; + line-height: 21px; + border-radius: 18px; + border: none; + color: #ffffff; + background: linear-gradient( + 96.76deg, + #ffc727 -16.42%, + #9e40ba 97.04%, + #7000ff 150.71% + ); + + cursor: pointer; + transition: all 0.5s ease 0s; +} + +.deleteButton:hover { + background: linear-gradient(95deg, #7000ff -15%, #9e40ba 95%, #ffc727 150%); + transform: scale(1.2); + transition: all 0.5s ease 0s; + font-weight: 600; +} + +.editButton { + background: transparent; + border: none; + display: flex; + align-items: center; + justify-content: space-between; + gap: 5.27px; + padding: 0; + cursor: pointer; + transition: all 0.5s ease 0s; +} + +.editText { + display: flex; + justify-content: center; + align-items: flex-end; + color: rgba(255, 255, 255, 0.6); + transition: all 0.5s ease 0s; +} + +.editIcon { + width: 14px; + height: 14px; + fill: rgba(255, 255, 255, 0.6); + stroke: rgba(255, 255, 255, 0.6); + color: transparent; + transition: all 0.5s ease 0s; +} + +.editButton:hover { + transform: scale(1.2); + transition: all 0.5s ease 0s; +} + +.editButton:hover .editIcon { + stroke: #7000ff; + transition: all 0.5s ease 0s; +} + +.editButton:hover .editText { + color: #7000ff; + font-weight: 600; + transition: all 0.5s ease 0s; +} + +.fixData { + font-weight: 600; + line-height: 24px; +} + +.dynamicData { + font-weight: 400; + line-height: 24px; +} + +.sixthRow { + border: 0; +} + +.incomeText { + color: #ffc727; + font-weight: bold; + letter-spacing: 1px; +} + +.expenseText { + color: #ff868d; + font-weight: bold; + letter-spacing: 1px; +} + +.incomeBorder { + border-left: 5px solid #ffb627; + transition: all 0.5s ease 0s; } -.transaction-item p { - margin: 5px 0; +.incomeBorder:hover { + box-shadow: -5px 0 8px 0 #ffc727; + transition: all 0.5s ease 0s; } -/* Responsive Styling */ -@media (min-width: 1024px) { - .transaction-item { - padding: 20px; - } +.expenseBorder { + border-left: 5px solid #ff868d; + transition: all 0.5s ease 0s; } -@media (max-width: 767px) { - .transaction-item { - padding: 10px; - } +.expenseBorder:hover { + transition: all 0.5s ease 0s; + box-shadow: -5px 0px 8px 0px #ff868d; } diff --git a/src/components/TransactionsList/TransactionsList.jsx b/src/components/TransactionsList/TransactionsList.jsx index ba134d6..a65d5cc 100644 --- a/src/components/TransactionsList/TransactionsList.jsx +++ b/src/components/TransactionsList/TransactionsList.jsx @@ -1,56 +1,38 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { - fetchTransactions, - deleteTransaction, -} from '../../redux/operations/transactionsOperations'; -import { - selectTransactions, - selectLoading, - selectError, -} from '../../redux/selectors/transactionsSelector'; -import style from './TransactionsList.module.css'; -import TransactionsItem from '../TransactionsItem/TransactionsItem'; +import PropTypes from 'prop-types'; +import TransactionsItem from '../../components/TransactionsItem/TransactionsItem'; +import styles from './TransactionsList.module.css'; -const TransactionsList = () => { - const dispatch = useDispatch(); - const transactions = useSelector(selectTransactions) || []; - const loading = useSelector(selectLoading); - const error = useSelector(selectError); - - useEffect(() => { - dispatch(fetchTransactions()); - }, [dispatch]); - - const handleDelete = (id, type, amount) => { - dispatch(deleteTransaction({ id, type, amount })); - }; - - if (loading) return

    Loading...

    ; - if (error) { - const errorMessage = - typeof error === 'object' && error.message - ? error.message - : JSON.stringify(error); - return

    Error: {errorMessage}

    ; - } - if (Array.isArray(transactions) && transactions.length === 0) { - return

    No transactions found.

    ; - } +const TransactionsList = ({ data, openDeleteModal, openEditModal }) => { + // Sortează array-ul de tranzacții + const sortedData = [...data].sort((a, b) => { + // Sortare după dată + if (a.transactionDate < b.transactionDate) return -1; + if (a.transactionDate > b.transactionDate) return 1; + // Sortare după tipul de tranzacție (Income va fi primul) + if (a.type === 'INCOME' && b.type === 'EXPENSE') return -1; + if (a.type === 'EXPENSE' && b.type === 'INCOME') return 1; + // Dacă sunt la aceeași dată și același tip de tranzacție, nu se schimbă ordinea + return 0; + }); return ( -
    - {transactions.map(transaction => ( +
      + {sortedData.map(item => ( - handleDelete(transaction.id, transaction.type, transaction.amount) - } + key={item.id} + transaction={item} + openDeleteModal={openDeleteModal} + openEditModal={openEditModal} /> ))} -
    + ); }; +TransactionsList.propTypes = { + data: PropTypes.arrayOf(PropTypes.object).isRequired, + openDeleteModal: PropTypes.func.isRequired, + openEditModal: PropTypes.func.isRequired, +}; + export default TransactionsList; diff --git a/src/components/TransactionsList/TransactionsList.module.css b/src/components/TransactionsList/TransactionsList.module.css index 33ed72b..36da416 100644 --- a/src/components/TransactionsList/TransactionsList.module.css +++ b/src/components/TransactionsList/TransactionsList.module.css @@ -1,34 +1,7 @@ -/* TransactionsList.module.css */ - -.transactions-list { +.TransactionList { + display: flex; + flex-direction: column; + gap: 30px; + margin-top: 32px; width: 100%; - max-width: 800px; - margin: 0 auto; -} - -/* Desktop */ -@media (min-width: 1024px) { - .transactions-list { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 20px; - } -} - -/* Tablet */ -@media (min-width: 768px) and (max-width: 1023px) { - .transactions-list { - display: grid; - grid-template-columns: 1fr; - gap: 15px; - } -} - -/* Mobile */ -@media (max-width: 767px) { - .transactions-list { - display: flex; - flex-direction: column; - gap: 10px; - } } diff --git a/src/components/common/allCategories.js b/src/components/common/allCategories.js new file mode 100644 index 0000000..76ab384 --- /dev/null +++ b/src/components/common/allCategories.js @@ -0,0 +1,150 @@ +const transactionCategories = [ + { + id: 'c9d9e447-1b83-4238-8712-edc77b18b739', + name: 'Main expenses', + type: 'EXPENSE', + }, + { + id: '27eb4b75-9a42-4991-a802-4aefe21ac3ce', + name: 'Products', + type: 'EXPENSE', + }, + { + id: '128673b5-2f9a-46ae-a428-ec48cf1effa1', + name: 'Household products', + type: 'EXPENSE', + }, + { + id: '3caa7ba0-79c0-40b9-ae1f-de1af1f6e386', + name: 'Car', + type: 'EXPENSE', + }, + { + id: 'bbdd58b8-e804-4ab9-bf4f-695da5ef64f4', + name: 'Self care', + type: 'EXPENSE', + }, + { + id: '76cc875a-3b43-4eae-8fdb-f76633821a34', + name: 'Child care', + type: 'EXPENSE', + }, + { + id: '1272fcc4-d59f-462d-ad33-a85a075e5581', + name: 'Education', + type: 'EXPENSE', + }, + { + id: 'c143130f-7d1e-4011-90a4-54766d4e308e', + name: 'Leisure', + type: 'EXPENSE', + }, + { + id: '3acd0ecd-5295-4d54-8e7c-d3908f4d0402', + name: 'Entertainment', + type: 'EXPENSE', + }, + { + id: '719626f1-9d23-4e99-84f5-289024e437a8', + name: 'Other expenses', + type: 'EXPENSE', + }, + { + id: '063f1132-ba5d-42b4-951d-44011ca46262', + name: 'Income', + type: 'INCOME', + }, +]; + +const getTransactionId = transactionCategory => { + const transactionTargeted = transactionCategories.find( + item => item.name === transactionCategory + ); + + return transactionTargeted.id; +}; + +const getTransactionCategory = transactionId => { + const transactionTargeted = transactionCategories.find( + item => item.id === transactionId + ); + + return transactionTargeted.name; +}; + +const formatData = unixData => { + const year = new Date(unixData).getFullYear(); + const month = new Date(unixData).getMonth() + 1; + const day = new Date(unixData).getDate(); + + const doubleDigitsFormatmonth = String(month).padStart(2, 0); + const doubleDigitsFormatDay = String(day).padStart(2, 0); + + return `${doubleDigitsFormatDay}.${doubleDigitsFormatmonth}.${year}`; +}; + +const Months_OPTIONS = [ + { value: 1, label: 'January' }, + { value: 2, label: 'February' }, + { value: 3, label: 'March' }, + { value: 4, label: 'April' }, + { value: 5, label: 'May' }, + { value: 6, label: 'June' }, + { value: 7, label: 'July' }, + { value: 8, label: 'August' }, + { value: 9, label: 'September' }, + { value: 10, label: 'October' }, + { value: 11, label: 'November' }, + { value: 12, label: 'December' }, +]; + +const CURRENT_YEAR = new Date().getFullYear(); + +const YEARS_OPTIONS = [CURRENT_YEAR, CURRENT_YEAR - 1, CURRENT_YEAR - 2]; + +const getTrasactionCategoryColor = category => { + switch (category) { + case 'Main expenses': + return 'rgba(254, 208, 87, 1)'; + + case 'Products': + return 'rgba(255, 0, 255, 1)'; + + case 'Car': + return 'rgba(253, 148, 152, 1)'; + + case 'Self care': + return 'rgba(197, 186, 255, 1)'; + + case 'Child care': + return 'rgba(127, 255, 0, 1)'; + + case 'Household products': + return 'rgba(74, 86, 226, 1)'; + + case 'Education': + return 'rgba(0, 255, 255, 1)'; + + case 'Leisure': + return 'rgba(255, 119, 0, 1)'; + + case 'Other expenses': + return 'rgba(0, 173, 132, 1)'; + + case 'Entertainment': + return 'rgba(177, 15, 72, 1)'; + + default: + return 'rgb(128, 128, 128)'; + } +}; + +export { + transactionCategories, + getTransactionId, + getTransactionCategory, + formatData, + Months_OPTIONS, + YEARS_OPTIONS, + getTrasactionCategoryColor, +}; diff --git a/src/components/common/formatNumberWithSpaces.js b/src/components/common/formatNumberWithSpaces.js new file mode 100644 index 0000000..c4f01cc --- /dev/null +++ b/src/components/common/formatNumberWithSpaces.js @@ -0,0 +1,19 @@ +/** +Funcție auxiliară pentru formatrarea numarului afisat in balanta si in Sum (amount). +-Se grupeaza cate de 3 cifre despartite de un mic spatiu. +-Limitare la 2 zecimale. +*/ + +export function formatNumberWithSpaces(number) { + // Limităm numărul la două zecimale: + const roundedNumber = Number(number).toFixed(2); + + // Separăm partea întreagă de cea zecimală: + const parts = roundedNumber.split('.'); + + // Formatăm partea întreagă cu spații: + const integerPartWithSpaces = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ' '); + + // Returnăm numărul formatat cu spații și două zecimale: + return `${integerPartWithSpaces}.${parts[1]}`; +} diff --git a/src/components/common/useCurrency.js b/src/components/common/useCurrency.js new file mode 100644 index 0000000..56d6935 --- /dev/null +++ b/src/components/common/useCurrency.js @@ -0,0 +1,50 @@ +import { useEffect, useState } from 'react'; +import axios from 'axios'; + +const useCurrency = () => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchDataAndStore = async () => { + setLoading(true); + try { + const response = await axios.get( + 'https://openexchangerates.org/api/latest.json?app_id=' + + process.env.REACT_APP_OXR_API_KEY + ); + const newData = response.data; + const fetchTime = new Date().getTime(); + localStorage.setItem( + 'MONO', + JSON.stringify({ data: newData, fetchTime }) + ); + setData(newData); + setError(null); + } catch (err) { + setError('Failed to fetch data'); + } finally { + setLoading(false); + } + }; + + const storedData = localStorage.getItem('MONO'); + if (storedData) { + const { data, fetchTime } = JSON.parse(storedData); + const currentTime = new Date().getTime(); + if (currentTime - fetchTime < 3600000) { + setData(data); + setLoading(false); + } else { + fetchDataAndStore(); + } + } else { + fetchDataAndStore(); + } + }, []); + + return { data, loading, error }; +}; + +export default useCurrency; diff --git a/src/components/images/icons/sprite.svg b/src/components/images/icons/sprite.svg new file mode 100644 index 0000000..ebb9a3d --- /dev/null +++ b/src/components/images/icons/sprite.svg @@ -0,0 +1,77 @@ + \ No newline at end of file diff --git a/src/redux/Statistics/operations.js b/src/redux/Statistics/operations.js index a3cc9bc..4af84ec 100644 --- a/src/redux/Statistics/operations.js +++ b/src/redux/Statistics/operations.js @@ -30,6 +30,7 @@ export const getTransactionsCategories = createAsyncThunk( async (_, thunkApi) => { try { const { data } = await userTransactionsApi.get(''); + console.log(data); return data; } catch (error) { return thunkApi.rejectWithValue(error.message); diff --git a/src/redux/Statistics/slice.js b/src/redux/Statistics/slice.js index 6bc3ace..72904e6 100644 --- a/src/redux/Statistics/slice.js +++ b/src/redux/Statistics/slice.js @@ -7,7 +7,13 @@ import { const initialState = { summary: [], - categories: [], + categories: [ + { + id: '063f1132-ba5d-42b4-951d-44011ca46262', + name: 'Income', + type: 'INCOME', + }, + ], isStatisticsLoading: false, isStatisticsError: null, }; diff --git a/src/redux/operations/authOperations.js b/src/redux/operations/authOperations.js index b328832..0c8985d 100644 --- a/src/redux/operations/authOperations.js +++ b/src/redux/operations/authOperations.js @@ -49,3 +49,17 @@ export const logout = createAsyncThunk( } } ); + +export const getUserInfo = createAsyncThunk( + 'auth/getUserInfo', + async (_, thunkAPI) => { + try { + const response = await axios.get(`${BASE_URL}/users/current`); + + return response.data; + } catch (error) { + console.log(error); + return thunkAPI.rejectWithValue(error.message); + } + } +); diff --git a/src/redux/operations/transactionsOperations.js b/src/redux/operations/transactionsOperations.js index d4b792e..280cdf1 100644 --- a/src/redux/operations/transactionsOperations.js +++ b/src/redux/operations/transactionsOperations.js @@ -1,5 +1,6 @@ import axios from '../axiosConfig'; import { createAsyncThunk } from '@reduxjs/toolkit'; +import { toast } from 'react-toastify'; const BASE_URL = 'https://wallet.b.goit.study/api'; @@ -16,6 +17,7 @@ export const fetchTransactions = createAsyncThunk( }); return response.data; // Returnează datele obținute de la API } catch (error) { + toast.error('Something went wrong wen fetching transactions!'); return rejectWithValue( error.response ? error.response.data : error.message ); @@ -27,15 +29,15 @@ export const fetchTransactions = createAsyncThunk( export const addTransaction = createAsyncThunk( 'transactions/addTransaction', async (transactionData, { rejectWithValue }) => { - const token = localStorage.getItem('token'); try { - const response = await axios.post('/transactions', transactionData, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const response = await axios.post( + `${BASE_URL}/transactions`, + transactionData + ); + toast.success('Transaction added successfully! ^_^'); return response.data; // Returnează datele tranzacției adăugate } catch (error) { + toast.error('Transaction not saved, something went wrong!'); return rejectWithValue(error.response?.data || error.message); // Trimite eroarea în Redux } } @@ -47,13 +49,19 @@ export const editTransaction = createAsyncThunk( async ({ id, transactionData }, { rejectWithValue }) => { const token = localStorage.getItem('token'); try { - const response = await axios.put(`/transactions/${id}`, transactionData, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const response = await axios.patch( + `${BASE_URL}/transactions/${id}`, + transactionData, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + toast.success('Transaction modified successfully! ^_^'); return response.data; // Returnează datele tranzacției editate } catch (error) { + toast.error('Transaction not modified, somethig went wrong!'); return rejectWithValue(error.response?.data || error.message); // Trimite eroarea în Redux } } @@ -65,13 +73,15 @@ export const deleteTransaction = createAsyncThunk( async (id, { rejectWithValue }) => { const token = localStorage.getItem('token'); try { - await axios.delete(`/transactions/${id}`, { + await axios.delete(`${BASE_URL}/transactions/${id}`, { headers: { Authorization: `Bearer ${token}`, }, }); + toast.success('Transaction deleted successfully !'); return id; // Returnează ID-ul tranzacției pentru a-l elimina din state-ul Redux } catch (error) { + toast.error('Transaction not deleted, something went wrong!'); return rejectWithValue(error.response?.data || error.message); // Trimite eroarea în Redux } } diff --git a/src/redux/selectors/authSelectors.js b/src/redux/selectors/authSelectors.js index d10a383..89cc02a 100644 --- a/src/redux/selectors/authSelectors.js +++ b/src/redux/selectors/authSelectors.js @@ -1,2 +1,3 @@ export const auth = state => state.auth; export const user = state => state.auth.user; +export const selectBalance = state => state.auth.user.balance; diff --git a/src/redux/selectors/transactionsSelector.js b/src/redux/selectors/transactionsSelector.js index ca5f7c1..0558b60 100644 --- a/src/redux/selectors/transactionsSelector.js +++ b/src/redux/selectors/transactionsSelector.js @@ -1,4 +1,16 @@ -export const selectTransactions = state => state.transactions.items; -export const selectTotalBalance = state => state.transactions.totalBalance; -export const selectLoading = state => state.transactions.loading; -export const selectError = state => state.transactions.error; +const selectAllTransactions = state => state.transactions.items; +const selectTotalBalance = state => state.transactions.totalBalance; +const selectError = state => state.transactions.error; +const selectTrasactionIdForDelete = state => + state.transactions.trasactionIdForDelete; + +const selectTransactionForUpdate = state => + state.transactions.transactionForUpdate; + +export { + selectAllTransactions, + selectTotalBalance, + selectError, + selectTransactionForUpdate, + selectTrasactionIdForDelete, +}; diff --git a/src/redux/slices/AuthSlice.jsx b/src/redux/slices/AuthSlice.jsx index 3e442ae..320f78b 100644 --- a/src/redux/slices/AuthSlice.jsx +++ b/src/redux/slices/AuthSlice.jsx @@ -1,10 +1,15 @@ import { createSlice } from '@reduxjs/toolkit'; -import { login, register, logout } from '../operations/authOperations'; +import { + login, + register, + logout, + getUserInfo, +} from '../operations/authOperations'; const initialState = { - user: null, - loading: false, + user: { username: null, email: null, balance: null }, error: null, + isLoading: false, }; const authSlice = createSlice({ @@ -19,34 +24,46 @@ const authSlice = createSlice({ builder // Login .addCase(login.pending, state => { - state.loading = true; - state.error = null; + state.isLoading = true; }) .addCase(login.fulfilled, (state, action) => { - state.loading = false; - state.user = action.payload; + state.user = { ...action.payload }; + state.error = null; + state.isLoading = false; }) .addCase(login.rejected, (state, action) => { - state.loading = false; state.error = action.payload; + state.isLoading = false; }) // Register .addCase(register.pending, state => { - state.loading = true; - state.error = null; + state.isLoading = true; }) .addCase(register.fulfilled, (state, action) => { - state.loading = false; - state.user = action.payload; + state.user = { ...action.payload }; + state.isLoading = false; + state.error = null; }) .addCase(register.rejected, (state, action) => { - state.loading = false; state.error = action.payload; + state.isLoading = false; }) // Logout .addCase(logout.fulfilled, state => { - state.user = null; + state.user = { username: null, email: null, balance: null }; state.error = null; + state.isLoading = false; + }) + .addCase(logout.pending, state => { + state.isLoading = true; + }) + .addCase(logout.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }) + //Info pentru balance + .addCase(getUserInfo.fulfilled, (state, action) => { + state.user = action.payload; }); }, }); diff --git a/src/redux/slices/GlobalSlice.jsx b/src/redux/slices/GlobalSlice.jsx index 1e6d712..e16f055 100644 --- a/src/redux/slices/GlobalSlice.jsx +++ b/src/redux/slices/GlobalSlice.jsx @@ -10,6 +10,24 @@ const globalSlice = createSlice({ state.isLoading = action.payload; }, }, + extraReducers: builder => { + // Acțiuni pentru setarea automată a `isLoading` pentru operațiuni din `transactions` + builder + .addMatcher( + action => action.type.endsWith('/pending'), + state => { + state.isLoading = true; + } + ) + .addMatcher( + action => + action.type.endsWith('/fulfilled') || + action.type.endsWith('/rejected'), + state => { + state.isLoading = false; + } + ); + }, }); export const { setLoading } = globalSlice.actions; diff --git a/src/redux/slices/transactionsSlice.js b/src/redux/slices/transactionsSlice.js index 9ccafaa..2b28d58 100644 --- a/src/redux/slices/transactionsSlice.js +++ b/src/redux/slices/transactionsSlice.js @@ -10,61 +10,66 @@ import { const initialState = { items: [], categories: [], - totalBalance: 0, - loading: false, error: null, + isLoading: false, + trasactionIdForDelete: '', + transactionForUpdate: { + id: '', + type: '', + }, }; const transactionsSlice = createSlice({ name: 'transactions', initialState, - reducers: {}, + reducers: { + setTrasactionIdForDelete: (state, action) => { + state.trasactionIdForDelete = action.payload; + }, + setTrasactionForUpdate: (state, action) => { + state.transactionForUpdate = action.payload; + console.log(state.transactionForUpdate.id); + }, + }, extraReducers: builder => { builder .addCase(fetchTransactions.pending, state => { - state.loading = true; - state.error = null; + state.isLoading = true; }) .addCase(fetchTransactions.fulfilled, (state, action) => { - state.items = action.payload.transactions; - state.totalBalance = action.payload.balance; - state.loading = false; + console.log('Transactios: ', action.payload); + state.items = action.payload; + state.error = null; + state.isLoading = false; }) .addCase(fetchTransactions.rejected, (state, action) => { - state.loading = false; + state.error = action.payload; + state.isLoading = false; + }) + .addCase(addTransaction.pending, state => { + state.isLoading = true; + }) + .addCase(addTransaction.rejected, (state, action) => { + state.isLoading = false; state.error = action.payload; }) .addCase(addTransaction.fulfilled, (state, action) => { - const transaction = action.payload; - const { type, amount } = transaction; - - state.items.push(transaction); - state.totalBalance += type === 'income' ? amount : -amount; + state.isLoading = false; + state.error = null; + state.items.push(action.payload); + }) + .addCase(editTransaction.pending, state => { + state.isLoading = true; + }) + .addCase(editTransaction.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload; }) .addCase(editTransaction.fulfilled, (state, action) => { - const index = state.items.findIndex( - item => item.id === action.payload.id - ); - if (index !== -1) { - const oldTransaction = state.items[index]; - const newTransaction = action.payload; - const { amount: oldAmount, type: oldType } = oldTransaction; - const { amount: newAmount, type: newType } = newTransaction; - - if (oldType === 'income') { - state.totalBalance -= oldAmount; - } else { - state.totalBalance += oldAmount; - } - - if (newType === 'income') { - state.totalBalance += newAmount; - } else { - state.totalBalance -= newAmount; - } - - state.items[index] = newTransaction; - } + state.isLoading = false; + state.error = null; + const index = state.items.findIndex(el => el.id === action.payload.id); + state.items.splice(index, 1, action.payload); }) .addCase(deleteTransaction.fulfilled, (state, action) => { const { id, type, amount } = action.payload; @@ -77,18 +82,17 @@ const transactionsSlice = createSlice({ } }) .addCase(fetchCategories.pending, state => { - state.loading = true; state.error = null; }) .addCase(fetchCategories.fulfilled, (state, action) => { state.categories = action.payload.categories; - state.loading = false; }) .addCase(fetchCategories.rejected, (state, action) => { - state.loading = false; state.error = action.payload; }); }, }); +export const { setTrasactionIdForDelete, setTrasactionForUpdate } = + transactionsSlice.actions; export default transactionsSlice.reducer;