-
-
- X
-
-
Edit Transaction
-
-
- Cancel
-
+ <>
+
-
+ >
);
};
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 (
+
+
+
+
+ Date
+ Type
+ Category
+ Comment
+ Sum
+
+
+
+
+
+ {sortedData.map(item => (
+
+ ))}
+
+
+
+ );
+};
+
+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)}
+
+
+
+
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+ );
+};
+
+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}`}
-
Delete
-
+
+
+ Date
+
+ {formatData(transactionDate)}
+
+
+
+ Type
+
+ {type === 'INCOME' ? '+' : '-'}
+
+
+
+ Category
+
+ {getTransactionCategory(categoryId)}
+
+
+
+ Comment
+ {comment}
+
+
+ Sum
+
+ {type === 'INCOME'
+ ? formatNumberWithSpaces(amount)
+ : formatNumberWithSpaces(amount * -1)}
+
+
+
+
+ Delete
+
+
+
+
+
+ Edit
+
+
+
);
};
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;