├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── assets │ ├── search.svg │ ├── user.svg │ └── transactions.svg ├── components │ ├── Loader │ │ ├── index.js │ │ └── styles.css │ ├── Header │ │ ├── styles.css │ │ └── index.js │ ├── NoTransactions.js │ ├── Cards.js │ ├── Modals │ │ ├── AddExpense.js │ │ └── AddIncome.js │ ├── TransactionSearch.js │ ├── Signup.js │ └── Dashboard.js ├── index.js ├── App.js ├── styles.css ├── index.css ├── firebase.js └── App.css ├── .gitignore ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVIVASHISHTA29/personal-finance-tracker/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVIVASHISHTA29/personal-finance-tracker/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVIVASHISHTA29/personal-finance-tracker/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Loader/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./styles.css"; 3 | function Loader() { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 | ); 12 | } 13 | 14 | export default Loader; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import { ToastContainer, toast } from "react-toastify"; 6 | import "react-toastify/dist/ReactToastify.css"; 7 | const root = ReactDOM.createRoot(document.getElementById("root")); 8 | root.render( 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; 3 | import Dashboard from "./components/Dashboard"; 4 | import SignUpSignIn from "./components/Signup"; 5 | 6 | function App() { 7 | return ( 8 | 9 | 10 | } /> 11 | } /> 12 | 13 | 14 | ); 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | position: sticky; 3 | width: 100%; 4 | padding: 0.75rem 1.5rem; 5 | background-color: var(--theme); 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | } 10 | 11 | .navbar-heading { 12 | color: #fff; 13 | font-weight: 500; 14 | margin: 0px; 15 | font-size: 1.2rem; 16 | } 17 | .navbar-link { 18 | color: #e3e3e3; 19 | font-weight: 500; 20 | margin: 0px; 21 | font-size: 1rem; 22 | } 23 | 24 | .navbar-link:hover { 25 | color: #fff; 26 | transition: all 0.3s; 27 | } 28 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "~antd/dist/antd.css"; 2 | @import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"); 3 | /* Add your custom styles below */ 4 | 5 | :root { 6 | --theme: #2970ff; 7 | } 8 | body { 9 | margin: 0; 10 | font-family: "Montserrat", sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | input { 16 | font-family: "Montserrat", sans-serif; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Header/styles.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | position: sticky; 3 | width: 100%; 4 | padding: 0.75rem 1.5rem; 5 | background-color: var(--theme); 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | } 10 | 11 | .navbar-heading { 12 | color: #fff; 13 | font-weight: 500; 14 | margin: 0px; 15 | font-size: 1.2rem; 16 | } 17 | .navbar-link { 18 | color: #e3e3e3; 19 | font-weight: 500; 20 | margin: 0px; 21 | font-size: 1rem; 22 | cursor: pointer; 23 | } 24 | 25 | .navbar-link:hover { 26 | color: #fff; 27 | transition: all 0.3s; 28 | } 29 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/NoTransactions.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import transactions from "../assets/transactions.svg"; 3 | function NoTransactions() { 4 | return ( 5 |
15 | 16 |

17 | You Have No Transactions Currently 18 |

19 |
20 | ); 21 | } 22 | 23 | export default NoTransactions; 24 | -------------------------------------------------------------------------------- /src/firebase.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | 3 | import { getAuth, GoogleAuthProvider } from "firebase/auth"; 4 | import { getFirestore, doc, setDoc } from "firebase/firestore"; 5 | 6 | const firebaseConfig = { 7 | apiKey: "AIzaSyAHExHfMDRu2lVecgYxgW86TXDF-dvsWrY", 8 | authDomain: "personal-finance-tracker-ec56e.firebaseapp.com", 9 | projectId: "personal-finance-tracker-ec56e", 10 | storageBucket: "personal-finance-tracker-ec56e.appspot.com", 11 | messagingSenderId: "669303047696", 12 | appId: "1:669303047696:web:5b4b41cc896752c03e6c95", 13 | measurementId: "G-7X36E69HYW", 14 | }; 15 | 16 | const app = initializeApp(firebaseConfig); 17 | const db = getFirestore(app); 18 | const auth = getAuth(app); 19 | const provider = new GoogleAuthProvider(); 20 | export { db, auth, provider, doc, setDoc }; 21 | -------------------------------------------------------------------------------- /src/components/Loader/styles.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | background-color: #fff; 3 | position: fixed; 4 | width: 100vw; 5 | height: 90vh; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | .lds-ripple { 12 | display: inline-block; 13 | position: relative; 14 | width: 80px; 15 | height: 80px; 16 | } 17 | .lds-ripple div { 18 | position: absolute; 19 | border: 4px solid var(--theme); 20 | opacity: 1; 21 | border-radius: 50%; 22 | animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite; 23 | } 24 | .lds-ripple div:nth-child(2) { 25 | animation-delay: -0.5s; 26 | } 27 | @keyframes lds-ripple { 28 | 0% { 29 | top: 36px; 30 | left: 36px; 31 | width: 0; 32 | height: 0; 33 | opacity: 0; 34 | } 35 | 4.9% { 36 | top: 36px; 37 | left: 36px; 38 | width: 0; 39 | height: 0; 40 | opacity: 0; 41 | } 42 | 5% { 43 | top: 36px; 44 | left: 36px; 45 | width: 0; 46 | height: 0; 47 | opacity: 1; 48 | } 49 | 100% { 50 | top: 0px; 51 | left: 0px; 52 | width: 72px; 53 | height: 72px; 54 | opacity: 0; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "personal-finance-tracker", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/charts": "^1.4.2", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "antd": "^4.24.10", 11 | "firebase": "^9.21.0", 12 | "papaparse": "^5.4.1", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-firebase-hooks": "^5.1.1", 16 | "react-router-dom": "^6.11.1", 17 | "react-scripts": "5.0.1", 18 | "react-toastify": "^9.1.3", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import "./styles.css"; 3 | import { Link, useNavigate } from "react-router-dom"; 4 | import { useAuthState } from "react-firebase-hooks/auth"; 5 | import { auth } from "../../firebase"; 6 | import userSvg from "../../assets/user.svg"; 7 | function Header() { 8 | const [user] = useAuthState(auth); 9 | const navigate = useNavigate(); 10 | function logout() { 11 | auth.signOut(); 12 | navigate("/"); 13 | } 14 | 15 | useEffect(() => { 16 | if (!user) { 17 | navigate("/"); 18 | } else { 19 | navigate("/dashboard"); 20 | } 21 | }, [user, navigate]); 22 | 23 | return ( 24 |
25 |

Financly.

26 | {user ? ( 27 |

28 | 29 | 34 | 35 | Logout 36 |

37 | ) : ( 38 | <> 39 | )} 40 |
41 | ); 42 | } 43 | 44 | export default Header; 45 | -------------------------------------------------------------------------------- /src/components/Cards.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Card, Row } from "antd"; 3 | 4 | function Cards({ 5 | currentBalance, 6 | income, 7 | expenses, 8 | showExpenseModal, 9 | showIncomeModal, 10 | cardStyle, 11 | reset, 12 | }) { 13 | return ( 14 | 22 | 23 |

Current Balance

24 |

₹{currentBalance}

25 |
26 | Reset Balance 27 |
28 |
29 | 30 | 31 |

Total Income

32 |

₹{income}

33 |
38 | Add Income 39 |
40 |
41 | 42 | 43 |

Total Expenses

44 |

₹{expenses}

45 |
46 | Add Expense 47 |
48 |
49 |
50 | ); 51 | } 52 | 53 | export default Cards; 54 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/Modals/AddExpense.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Card, 4 | Col, 5 | Row, 6 | Button, 7 | Modal, 8 | Form, 9 | Input, 10 | DatePicker, 11 | Select, 12 | } from "antd"; 13 | function AddExpenseModal({ 14 | isExpenseModalVisible, 15 | handleExpenseCancel, 16 | onFinish, 17 | }) { 18 | const [form] = Form.useForm(); 19 | return ( 20 | 27 |
{ 31 | onFinish(values, "expense"); 32 | form.resetFields(); 33 | }} 34 | > 35 | 46 | 47 | 48 | 56 | 57 | 58 | 66 | 67 | 68 | 74 | 80 | 81 | 82 | 85 | 86 |
87 |
88 | ); 89 | } 90 | 91 | export default AddExpenseModal; 92 | -------------------------------------------------------------------------------- /src/components/Modals/AddIncome.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Card, 4 | Col, 5 | Row, 6 | Button, 7 | Modal, 8 | Form, 9 | Input, 10 | DatePicker, 11 | Select, 12 | } from "antd"; 13 | 14 | function AddIncomeModal({ 15 | isIncomeModalVisible, 16 | handleIncomeCancel, 17 | onFinish, 18 | }) { 19 | const [form] = Form.useForm(); 20 | return ( 21 | 28 |
{ 32 | onFinish(values, "income"); 33 | form.resetFields(); 34 | }} 35 | > 36 | 47 | 48 | 49 | 57 | 58 | 59 | 67 | 68 | 69 | 75 | 81 | 82 | 83 | 86 | 87 |
88 |
89 | ); 90 | } 91 | 92 | export default AddIncomeModal; 93 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .blue-text { 2 | color: var(--theme); 3 | } 4 | .wrapper { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | width: 100vw; 9 | height: 90vh; 10 | } 11 | .signup-signin-container { 12 | width: 30vw; 13 | min-width: 400px; 14 | box-shadow: 0px 0px 30px 8px rgba(227, 227, 227, 0.75); 15 | border-radius: 1rem; 16 | padding: 2rem; 17 | } 18 | 19 | .input-wrapper { 20 | margin: 1rem 0rem; 21 | width: 100%; 22 | } 23 | .input-wrapper p { 24 | margin-bottom: 0px; 25 | color: #000; 26 | } 27 | 28 | .input-wrapper input, 29 | .custom-input { 30 | border: 0px; 31 | border-bottom: 1px solid #000; 32 | padding: 0.5rem 0rem; 33 | width: 100%; 34 | border-radius: 0; 35 | } 36 | 37 | .custom-input { 38 | margin-top: -1rem !important; 39 | } 40 | 41 | .input-wrapper input::placeholder, 42 | .custom-input::placeholder { 43 | color: rgba(0, 0, 0, 0.5); 44 | } 45 | 46 | .input-wrapper input:focus, 47 | .custom-input:focus { 48 | outline: none; 49 | opacity: 1; 50 | transition: all 0.3s; 51 | } 52 | 53 | .btn { 54 | color: var(--theme); 55 | text-align: center; 56 | width: 100%; 57 | margin: 0.5rem 0rem; 58 | padding: 0.5rem !important; 59 | background-color: #fff; 60 | border: 1px solid var(--theme); 61 | border-radius: 0.25rem; 62 | cursor: pointer; 63 | display: flex !important; 64 | align-items: center; 65 | justify-content: center; 66 | height: auto !important; 67 | } 68 | 69 | .btn:hover { 70 | background-color: var(--theme); 71 | color: #fff; 72 | transition: all 0.3s; 73 | } 74 | 75 | .btn-blue { 76 | background-color: var(--theme); 77 | color: #fff; 78 | } 79 | 80 | .btn-blue:hover { 81 | background-color: #fff !important; 82 | color: var(--theme) !important; 83 | transition: all 0.3s; 84 | } 85 | 86 | .input-flex { 87 | display: flex; 88 | justify-content: flex-start; 89 | align-items: center; 90 | gap: 0.5rem; 91 | width: 100%; 92 | box-shadow: 0px 0px 30px 8px rgba(227, 227, 227, 0.75); 93 | border-radius: 0.5rem; 94 | padding: 0rem 0.5rem; 95 | } 96 | 97 | .input-flex input { 98 | width: 100%; 99 | padding: 0.5rem; 100 | border: 0; 101 | } 102 | 103 | .input-flex input:focus { 104 | outline: none; 105 | } 106 | 107 | .select-input { 108 | width: 30%; 109 | margin-right: 10; 110 | display: flex; 111 | align-items: center; 112 | box-shadow: 0px 0px 30px 8px rgba(227, 227, 227, 0.75); 113 | border-radius: 0.5rem !important; 114 | padding: 0.2rem 0.5rem; 115 | } 116 | 117 | .select-input-2 > div { 118 | border-width: 0px !important; 119 | border-bottom-width: 1px !important; 120 | border-radius: 0 !important; 121 | border-color: #000 !important; 122 | } 123 | 124 | .select-input > div { 125 | border: 0px solid #fff !important; 126 | } 127 | 128 | .my-table { 129 | box-shadow: 0px 0px 30px 8px rgba(227, 227, 227, 0.75); 130 | border-radius: 0.5rem !important; 131 | padding: 2rem; 132 | margin-bottom: 4rem; 133 | } 134 | 135 | .dashboard-container { 136 | padding-bottom: 1rem; 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /src/assets/transactions.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/TransactionSearch.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import { Input, Table, Select, Radio } from "antd"; 3 | import { SearchOutlined } from "@ant-design/icons"; 4 | import search from "../assets/search.svg"; 5 | import { parse } from "papaparse"; 6 | import { toast } from "react-toastify"; 7 | import { useNavigate } from "react-router-dom"; 8 | const { Search } = Input; 9 | const { Option } = Select; 10 | 11 | const TransactionSearch = ({ 12 | transactions, 13 | exportToCsv, 14 | addTransaction, 15 | fetchTransactions, 16 | }) => { 17 | const [searchTerm, setSearchTerm] = useState(""); 18 | const [selectedTag, setSelectedTag] = useState(""); 19 | const [typeFilter, setTypeFilter] = useState(""); 20 | const [sortKey, setSortKey] = useState(""); 21 | const fileInput = useRef(); 22 | 23 | function importFromCsv(event) { 24 | event.preventDefault(); 25 | try { 26 | parse(event.target.files[0], { 27 | header: true, 28 | complete: async function (results) { 29 | // Now results.data is an array of objects representing your CSV rows 30 | for (const transaction of results.data) { 31 | // Write each transaction to Firebase, you can use the addTransaction function here 32 | console.log("Transactions", transaction); 33 | const newTransaction = { 34 | ...transaction, 35 | amount: parseInt(transaction.amount), 36 | }; 37 | await addTransaction(newTransaction, true); 38 | } 39 | }, 40 | }); 41 | toast.success("All Transactions Added"); 42 | fetchTransactions(); 43 | event.target.files = null; 44 | } catch (e) { 45 | toast.error(e.message); 46 | } 47 | } 48 | 49 | const columns = [ 50 | { 51 | title: "Name", 52 | dataIndex: "name", 53 | key: "name", 54 | }, 55 | { 56 | title: "Type", 57 | dataIndex: "type", 58 | key: "type", 59 | }, 60 | { 61 | title: "Date", 62 | dataIndex: "date", 63 | key: "date", 64 | }, 65 | { 66 | title: "Amount", 67 | dataIndex: "amount", 68 | key: "amount", 69 | }, 70 | { 71 | title: "Tag", 72 | dataIndex: "tag", 73 | key: "tag", 74 | }, 75 | ]; 76 | 77 | const filteredTransactions = transactions.filter((transaction) => { 78 | const searchMatch = searchTerm 79 | ? transaction.name.toLowerCase().includes(searchTerm.toLowerCase()) 80 | : true; 81 | const tagMatch = selectedTag ? transaction.tag === selectedTag : true; 82 | const typeMatch = typeFilter ? transaction.type === typeFilter : true; 83 | 84 | return searchMatch && tagMatch && typeMatch; 85 | }); 86 | 87 | const sortedTransactions = [...filteredTransactions].sort((a, b) => { 88 | if (sortKey === "date") { 89 | return new Date(a.date) - new Date(b.date); 90 | } else if (sortKey === "amount") { 91 | return a.amount - b.amount; 92 | } else { 93 | return 0; 94 | } 95 | }); 96 | 97 | const dataSource = sortedTransactions.map((transaction, index) => ({ 98 | key: index, 99 | ...transaction, 100 | })); 101 | 102 | return ( 103 |
109 |
118 |
119 | 120 | setSearchTerm(e.target.value)} 123 | /> 124 |
125 | 136 |
137 | 138 | {/* */} 148 |
149 |
158 |

My Transactions

159 | 160 | setSortKey(e.target.value)} 163 | value={sortKey} 164 | > 165 | No Sort 166 | Sort by Date 167 | Sort by Amount 168 | 169 |
177 | 180 | 183 | 191 |
192 |
193 | 194 | 195 | 196 | 197 | ); 198 | }; 199 | 200 | export default TransactionSearch; 201 | -------------------------------------------------------------------------------- /src/components/Signup.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { auth, provider, db } from "../firebase"; 4 | import { 5 | createUserWithEmailAndPassword, 6 | signInWithEmailAndPassword, 7 | signInWithPopup, 8 | } from "firebase/auth"; 9 | import { doc, getDoc, setDoc } from "firebase/firestore"; 10 | import Header from "./Header"; 11 | import { toast } from "react-toastify"; 12 | 13 | const SignUpSignIn = () => { 14 | const [name, setName] = useState(""); 15 | const [email, setEmail] = useState(""); 16 | const [password, setPassword] = useState(""); 17 | const [confirmPassword, setConfirmPassword] = useState(""); 18 | const [loading, setLoading] = useState(false); 19 | const [flag, setFlag] = useState(false); 20 | const navigate = useNavigate(); 21 | 22 | const createUserDocument = async (user) => { 23 | setLoading(true); 24 | if (!user) return; 25 | 26 | const userRef = doc(db, "users", user.uid); 27 | const userData = await getDoc(userRef); 28 | 29 | if (!userData.exists()) { 30 | const { displayName, email, photoURL } = user; 31 | const createdAt = new Date(); 32 | 33 | try { 34 | await setDoc(userRef, { 35 | name: displayName ? displayName : name, 36 | email, 37 | photoURL: photoURL ? photoURL : "", 38 | createdAt, 39 | }); 40 | toast.success("Account Created!"); 41 | setLoading(false); 42 | } catch (error) { 43 | toast.error(error.message); 44 | console.error("Error creating user document: ", error); 45 | setLoading(false); 46 | } 47 | } 48 | }; 49 | 50 | const signUpWithEmail = async (e) => { 51 | setLoading(true); 52 | e.preventDefault(); 53 | try { 54 | const result = await createUserWithEmailAndPassword( 55 | auth, 56 | email, 57 | password 58 | ); 59 | const user = result.user; 60 | await createUserDocument(user); 61 | toast.success("Successfully Signed Up!"); 62 | setLoading(false); 63 | navigate("/dashboard"); 64 | } catch (error) { 65 | toast.error(error.message); 66 | console.error( 67 | "Error signing up with email and password: ", 68 | error.message 69 | ); 70 | setLoading(false); 71 | } 72 | }; 73 | 74 | const signInWithEmail = async (e) => { 75 | setLoading(true); 76 | e.preventDefault(); 77 | try { 78 | const result = await signInWithEmailAndPassword(auth, email, password); 79 | const user = result.user; 80 | navigate("/dashboard"); 81 | toast.success("Logged In Successfully!"); 82 | setLoading(false); 83 | } catch (error) { 84 | toast.error(error.message); 85 | console.error( 86 | "Error signing in with email and password: ", 87 | error.message 88 | ); 89 | setLoading(false); 90 | } 91 | }; 92 | 93 | const signInWithGoogle = async () => { 94 | setLoading(true); 95 | try { 96 | const result = await signInWithPopup(auth, provider); 97 | const user = result.user; 98 | await createUserDocument(user); 99 | toast.success("User Authenticated Successfully!"); 100 | setLoading(false); 101 | navigate("/dashboard"); 102 | } catch (error) { 103 | setLoading(false); 104 | toast.error(error.message); 105 | console.error("Error signing in with Google: ", error.message); 106 | } 107 | }; 108 | 109 | return ( 110 | <> 111 |
112 |
113 | {flag ? ( 114 |
115 |

116 | Log In on Financely. 117 |

118 |
119 |
120 |

Email

121 | setEmail(e.target.value)} 126 | /> 127 |
128 | 129 |
130 |

Password

131 | setPassword(e.target.value)} 136 | /> 137 |
138 | 139 | 146 | 147 |

or

148 | 155 |

setFlag(!flag)} 157 | style={{ 158 | textAlign: "center", 159 | marginBottom: 0, 160 | marginTop: "0.5rem", 161 | cursor: "pointer", 162 | }} 163 | > 164 | Or Don't Have An Account? Click Here. 165 |

166 |
167 | ) : ( 168 |
169 |

170 | Sign Up on Financely. 171 |

172 |
173 |
174 |

Full Name

175 | setName(e.target.value)} 180 | /> 181 |
182 |
183 |

Email

184 | setEmail(e.target.value)} 189 | /> 190 |
191 | 192 |
193 |

Password

194 | setPassword(e.target.value)} 199 | /> 200 |
201 | 202 |
203 |

Confirm Password

204 | setConfirmPassword(e.target.value)} 209 | /> 210 |
211 | 212 | 215 | 216 |

or

217 | 224 |

setFlag(!flag)} 226 | style={{ 227 | textAlign: "center", 228 | marginBottom: 0, 229 | marginTop: "0.5rem", 230 | cursor: "pointer", 231 | }} 232 | > 233 | Or Have An Account Already? Click Here 234 |

235 | {/* */} 238 |
239 | )} 240 |
241 | 242 | ); 243 | }; 244 | 245 | export default SignUpSignIn; 246 | -------------------------------------------------------------------------------- /src/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Card, Row } from "antd"; 3 | import { Line, Pie } from "@ant-design/charts"; 4 | import moment from "moment"; 5 | import TransactionSearch from "./TransactionSearch"; 6 | import Header from "./Header"; 7 | import AddIncomeModal from "./Modals/AddIncome"; 8 | import AddExpenseModal from "./Modals/AddExpense"; 9 | import Cards from "./Cards"; 10 | import NoTransactions from "./NoTransactions"; 11 | import { useAuthState } from "react-firebase-hooks/auth"; 12 | import { auth, db } from "../firebase"; 13 | import { addDoc, collection, getDocs, query } from "firebase/firestore"; 14 | import Loader from "./Loader"; 15 | import { toast } from "react-toastify"; 16 | import { useNavigate } from "react-router-dom"; 17 | import { unparse } from "papaparse"; 18 | 19 | const Dashboard = () => { 20 | const [user] = useAuthState(auth); 21 | 22 | // const sampleTransactions = [ 23 | // { 24 | // name: "Pay day", 25 | // type: "income", 26 | // date: "2023-01-15", 27 | // amount: 2000, 28 | // tag: "salary", 29 | // }, 30 | // { 31 | // name: "Dinner", 32 | // type: "expense", 33 | // date: "2023-01-20", 34 | // amount: 500, 35 | // tag: "food", 36 | // }, 37 | // { 38 | // name: "Books", 39 | // type: "expense", 40 | // date: "2023-01-25", 41 | // amount: 300, 42 | // tag: "education", 43 | // }, 44 | // ]; 45 | const [isExpenseModalVisible, setIsExpenseModalVisible] = useState(false); 46 | const [isIncomeModalVisible, setIsIncomeModalVisible] = useState(false); 47 | const [transactions, setTransactions] = useState([]); 48 | const [loading, setLoading] = useState(false); 49 | const [currentBalance, setCurrentBalance] = useState(0); 50 | const [income, setIncome] = useState(0); 51 | const [expenses, setExpenses] = useState(0); 52 | 53 | const navigate = useNavigate(); 54 | 55 | const processChartData = () => { 56 | const balanceData = []; 57 | const spendingData = {}; 58 | 59 | transactions.forEach((transaction) => { 60 | const monthYear = moment(transaction.date).format("MMM YYYY"); 61 | const tag = transaction.tag; 62 | 63 | if (transaction.type === "income") { 64 | if (balanceData.some((data) => data.month === monthYear)) { 65 | balanceData.find((data) => data.month === monthYear).balance += 66 | transaction.amount; 67 | } else { 68 | balanceData.push({ month: monthYear, balance: transaction.amount }); 69 | } 70 | } else { 71 | if (balanceData.some((data) => data.month === monthYear)) { 72 | balanceData.find((data) => data.month === monthYear).balance -= 73 | transaction.amount; 74 | } else { 75 | balanceData.push({ month: monthYear, balance: -transaction.amount }); 76 | } 77 | 78 | if (spendingData[tag]) { 79 | spendingData[tag] += transaction.amount; 80 | } else { 81 | spendingData[tag] = transaction.amount; 82 | } 83 | } 84 | }); 85 | 86 | const spendingDataArray = Object.keys(spendingData).map((key) => ({ 87 | category: key, 88 | value: spendingData[key], 89 | })); 90 | 91 | return { balanceData, spendingDataArray }; 92 | }; 93 | 94 | const { balanceData, spendingDataArray } = processChartData(); 95 | const showExpenseModal = () => { 96 | setIsExpenseModalVisible(true); 97 | }; 98 | 99 | const showIncomeModal = () => { 100 | setIsIncomeModalVisible(true); 101 | }; 102 | 103 | const handleExpenseCancel = () => { 104 | setIsExpenseModalVisible(false); 105 | }; 106 | 107 | const handleIncomeCancel = () => { 108 | setIsIncomeModalVisible(false); 109 | }; 110 | 111 | useEffect(() => { 112 | fetchTransactions(); 113 | }, []); 114 | 115 | const onFinish = (values, type) => { 116 | const newTransaction = { 117 | type: type, 118 | date: moment(values.date).format("YYYY-MM-DD"), 119 | amount: parseFloat(values.amount), 120 | tag: values.tag, 121 | name: values.name, 122 | }; 123 | 124 | setTransactions([...transactions, newTransaction]); 125 | setIsExpenseModalVisible(false); 126 | setIsIncomeModalVisible(false); 127 | addTransaction(newTransaction); 128 | calculateBalance(); 129 | }; 130 | 131 | const calculateBalance = () => { 132 | let incomeTotal = 0; 133 | let expensesTotal = 0; 134 | 135 | transactions.forEach((transaction) => { 136 | if (transaction.type === "income") { 137 | incomeTotal += transaction.amount; 138 | } else { 139 | expensesTotal += transaction.amount; 140 | } 141 | }); 142 | 143 | setIncome(incomeTotal); 144 | setExpenses(expensesTotal); 145 | setCurrentBalance(incomeTotal - expensesTotal); 146 | }; 147 | 148 | // Calculate the initial balance, income, and expenses 149 | useEffect(() => { 150 | calculateBalance(); 151 | }, [transactions]); 152 | 153 | async function addTransaction(transaction, many) { 154 | try { 155 | const docRef = await addDoc( 156 | collection(db, `users/${user.uid}/transactions`), 157 | transaction 158 | ); 159 | console.log("Document written with ID: ", docRef.id); 160 | if (!many) { 161 | toast.success("Transaction Added!"); 162 | } 163 | } catch (e) { 164 | console.error("Error adding document: ", e); 165 | if (!many) { 166 | toast.error("Couldn't add transaction"); 167 | } 168 | } 169 | } 170 | 171 | async function fetchTransactions() { 172 | setLoading(true); 173 | if (user) { 174 | const q = query(collection(db, `users/${user.uid}/transactions`)); 175 | const querySnapshot = await getDocs(q); 176 | let transactionsArray = []; 177 | querySnapshot.forEach((doc) => { 178 | // doc.data() is never undefined for query doc snapshots 179 | transactionsArray.push(doc.data()); 180 | }); 181 | setTransactions(transactionsArray); 182 | toast.success("Transactions Fetched!"); 183 | } 184 | setLoading(false); 185 | } 186 | 187 | const balanceConfig = { 188 | data: balanceData, 189 | xField: "month", 190 | yField: "balance", 191 | }; 192 | 193 | const spendingConfig = { 194 | data: spendingDataArray, 195 | angleField: "value", 196 | colorField: "category", 197 | }; 198 | 199 | function reset() { 200 | console.log("resetting"); 201 | } 202 | const cardStyle = { 203 | boxShadow: "0px 0px 30px 8px rgba(227, 227, 227, 0.75)", 204 | margin: "2rem", 205 | borderRadius: "0.5rem", 206 | minWidth: "400px", 207 | flex: 1, 208 | }; 209 | 210 | function exportToCsv() { 211 | const csv = unparse(transactions, { 212 | fields: ["name", "type", "date", "amount", "tag"], 213 | }); 214 | const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); 215 | const url = URL.createObjectURL(blob); 216 | const link = document.createElement("a"); 217 | link.href = url; 218 | link.download = "transactions.csv"; 219 | document.body.appendChild(link); 220 | link.click(); 221 | document.body.removeChild(link); 222 | } 223 | 224 | return ( 225 |
226 |
227 | {loading ? ( 228 | 229 | ) : ( 230 | <> 231 | 240 | 241 | 246 | 251 | {transactions.length === 0 ? ( 252 | 253 | ) : ( 254 | <> 255 | 256 | 257 |

Financial Statistics

258 | 259 |
260 | 261 | 262 |

Total Spending

263 | {spendingDataArray.length == 0 ? ( 264 |

Seems like you haven't spent anything till now...

265 | ) : ( 266 | 267 | )} 268 |
269 |
270 | 271 | )} 272 | 278 | 279 | )} 280 |
281 | ); 282 | }; 283 | 284 | export default Dashboard; 285 | --------------------------------------------------------------------------------