├── src ├── styles │ ├── App.css │ ├── DoughnutChart.css │ ├── NoUser.css │ ├── TransactionForm.css │ ├── Navbar.css │ ├── ExpenseTracker.css │ ├── Transactions.css │ ├── Signup.css │ └── Login.css ├── images │ ├── default.webp │ └── default1.webp ├── index.js ├── icons │ ├── close.svg │ ├── plus.svg │ ├── others.svg │ ├── delete.svg │ ├── travel.svg │ ├── logout.svg │ ├── edit.svg │ ├── moon.svg │ ├── plus1.svg │ ├── profile.svg │ ├── food.svg │ ├── edit-small.svg │ ├── shopping.svg │ ├── bills.svg │ ├── camera.svg │ ├── cross.svg │ ├── ph_plus-fill.svg │ ├── sun.svg │ └── search.svg ├── ProblemStatement.md ├── components │ ├── TransactionLoading.js │ ├── NoUser.js │ ├── profile │ │ ├── Profile.js │ │ ├── PredefinedTransactions.css │ │ ├── PredefinedTransactionsCards.js │ │ ├── Sidebar.js │ │ ├── RightDashboard.js │ │ └── PredefinedTransactions.js │ ├── ErrorPage.js │ ├── ui │ │ └── drawer.js │ ├── App.js │ ├── DoughnutChart.js │ ├── ExpenseTracker.js │ ├── Login.js │ ├── Signup.js │ ├── Navbar.js │ ├── TransactionForm.js │ └── Transactions.js ├── firebaseConfig.js ├── context │ └── ThemeContext.jsx └── index.css ├── public ├── icon.png └── index.html ├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── tailwind.config.js ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── package.json ├── CODE_OF_CONDUCT.md ├── README.md └── CONTRIBUTING.md /src/styles/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ani1609/Spendwise/HEAD/public/icon.png -------------------------------------------------------------------------------- /src/images/default.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ani1609/Spendwise/HEAD/src/images/default.webp -------------------------------------------------------------------------------- /src/images/default1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ani1609/Spendwise/HEAD/src/images/default1.webp -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import "./index.css"; 3 | 4 | import App from "./components/App"; 5 | 6 | const rootElement = document.getElementById("root"); 7 | const root = createRoot(rootElement); 8 | 9 | root.render(); 10 | -------------------------------------------------------------------------------- /src/styles/DoughnutChart.css: -------------------------------------------------------------------------------- 1 | .chart_container { 2 | font-family: "Lato", sans-serif; 3 | letter-spacing: 0.5px; 4 | width: 100%; 5 | max-width: 360px; 6 | box-sizing: border-box; 7 | } 8 | 9 | .chart_container p { 10 | color: #777; 11 | margin: 0; 12 | font-size: 15px; 13 | text-align: center; 14 | } 15 | -------------------------------------------------------------------------------- /src/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ProblemStatement.md: -------------------------------------------------------------------------------- 1 | ### Delete the expense from firestore when the delete button is clicked. 2 | 3 | When the delete button is clicked on an expense use the appropriate firebase method to delete the expense from the database. 4 | 5 | Note: This is a follow-up question to the previous expense tracker project. So use the same firebase configuration in the solution code. 6 | -------------------------------------------------------------------------------- /src/icons/others.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | 4 | ## Reporting a Vulnerability 5 | 6 | If you find any security vulnerability on this platform, please send an email to 7 | 8 | 1. [ankitparallax@gmail.com](mailto:ankitparallax@gmail.com) ([@ani1609](https://github.com/ani1609)) 9 | 2. [niladrix719@gmail.com](mailto:niladrix719@gmail.com) ([@niladrix719](https://github.com/niladrix719)) 10 | 11 | -------------------------------------------------------------------------------- /src/icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/travel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/logout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | "primary-bg": "var(--primary-bg)", 8 | "secondary-bg": "var(--secondary-bg)", 9 | border: "var(--border)", 10 | text: "var(--text)", 11 | }, 12 | }, 13 | }, 14 | darkMode: "class", 15 | plugins: [], 16 | }; 17 | -------------------------------------------------------------------------------- /src/icons/moon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "requireConfigFile": false, 11 | "babelOptions": { 12 | "presets": ["@babel/preset-react"] 13 | }, 14 | "ecmaVersion": 12, 15 | "sourceType": "module", 16 | "ecmaFeatures": { 17 | "jsx": true 18 | } 19 | }, 20 | "plugins": ["react"], 21 | "rules": {} 22 | } 23 | -------------------------------------------------------------------------------- /src/icons/plus1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /src/icons/profile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Pull Request 2 | 3 | **Related Issues:** 4 | [List any related issues or reference them using the syntax `#issue_number`.] 5 | 6 | Fixes #(issue number) 7 | 8 | **Description:** 9 | [Provide a brief description of your changes.] 10 | 11 | **Checklist:** 12 | - [ ] I have tested my changes. 13 | - [ ] My code follows the project's coding standards. 14 | - [ ] I have updated the documentation (if applicable). 15 | 16 | **Screenshots:** 17 | [If applicable, include screenshots to help reviewers understand your changes.] 18 | -------------------------------------------------------------------------------- /src/icons/food.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/TransactionLoading.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ContentLoader from "react-content-loader"; 3 | 4 | const MyLoader = (props) => ( 5 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export default MyLoader; 21 | -------------------------------------------------------------------------------- /src/icons/edit-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/NoUser.js: -------------------------------------------------------------------------------- 1 | import "../styles/NoUser.css"; 2 | import "../index.css"; 3 | import Default from "../images/default.webp"; 4 | 5 | function NoUser(props) { 6 | return ( 7 |
8 | No User 9 | 15 |
16 | ); 17 | } 18 | 19 | export default NoUser; 20 | -------------------------------------------------------------------------------- /src/styles/NoUser.css: -------------------------------------------------------------------------------- 1 | .noUser_container { 2 | width: 100vw; 3 | height: calc(100vh - 80px); 4 | overflow: hidden; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | gap: 30px; 10 | } 11 | 12 | .noUser_container img { 13 | width: 50%; 14 | object-fit: cover; 15 | object-position: center; 16 | } 17 | 18 | /* --------responsiveness-------- */ 19 | 20 | @media only screen and (max-width: 1080px) { 21 | .noUser_container img { 22 | width: 70%; 23 | } 24 | } 25 | 26 | @media only screen and (max-width: 768px) { 27 | .noUser_container img { 28 | width: 90%; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/components/profile/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Sidebar from "./Sidebar"; 3 | import RightDashboard from "./RightDashboard"; 4 | import PredefinedTransactions from "./PredefinedTransactions"; 5 | 6 | const Profile = () => { 7 | const [content, setContent] = useState("profile"); 8 | 9 | const handleButtonClick = (selectedContent) => { 10 | setContent(selectedContent); 11 | }; 12 | return ( 13 |
14 | 15 | {content === "profile" && } 16 | {content === "predefinedTransactions" && } 17 |
18 | ); 19 | }; 20 | 21 | export default Profile; 22 | -------------------------------------------------------------------------------- /src/icons/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/firebaseConfig.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getFirestore, collection } from "firebase/firestore"; 3 | 4 | const firebaseConfig = 5 | { 6 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY, 7 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, 8 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, 9 | storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, 10 | messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, 11 | appId: process.env.REACT_APP_FIREBASE_APP_ID, 12 | measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID 13 | }; 14 | 15 | // Initialize Firebase 16 | const app = initializeApp(firebaseConfig); 17 | const db = getFirestore(app); 18 | const transactionsCollection = collection(db, "transactions"); 19 | const usersCollection = collection(db, "users"); 20 | 21 | export { db, transactionsCollection, usersCollection }; 22 | -------------------------------------------------------------------------------- /src/icons/bills.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/icons/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/profile/PredefinedTransactions.css: -------------------------------------------------------------------------------- 1 | /* A stylesheet just for 2 | i) Removing arrows from number input type. 3 | ii) Glassmorphism effect on the form. 4 | iii) Custom radio button styling when checked 5 | */ 6 | /* Chrome, Safari, Edge, Opera */ 7 | input::-webkit-outer-spin-button, 8 | input::-webkit-inner-spin-button { 9 | appearance: none; 10 | margin: 0; 11 | } 12 | 13 | /* Firefox */ 14 | input[type=number] { 15 | appearance: textfield; 16 | } 17 | .form,.form-container{ 18 | background: rgba( 255, 255, 255, 0.25 ); 19 | box-shadow: 0 8px 32px 0 rgba( 31, 38, 135, 0.37 ); 20 | backdrop-filter: blur( 10px ); 21 | border-radius: 10px; 22 | border: 1px solid rgba( 255, 255, 255, 0.18 ); 23 | } 24 | input[name="transactionType"]:checked{ 25 | background-color: #079DF2; 26 | position: relative; 27 | } 28 | input[name="transactionType"]:checked::before { 29 | content: ""; 30 | display: block; 31 | width: 4px; 32 | height: 4px; 33 | background-color: white; 34 | border-radius: 50%; 35 | position: absolute; 36 | top: 50%; 37 | left: 50%; 38 | transform: translate(-50%, -50%); 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ankit Kr Chowdhury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/ErrorPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function ErrorPage () { 4 | return ( 5 | <> 6 |
7 |
8 | 404 Error Illustration 13 |

404 Not Found

14 |

The page you are looking for might be under maintenance or doesn`'`t exist.

15 | 23 |
24 |
25 | 26 | ); 27 | } 28 | 29 | export default ErrorPage; 30 | -------------------------------------------------------------------------------- /src/context/ThemeContext.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createContext, useEffect, useState } from "react"; 4 | 5 | export const ThemeContext = createContext(); 6 | 7 | const getFromLocalStorage = () => { 8 | if (typeof window !== "undefined") { 9 | const value = localStorage.getItem("theme"); 10 | return value || "light"; 11 | } 12 | }; 13 | 14 | export const ThemeContextProvider = ({ children }) => { 15 | const [theme, setTheme] = useState(() => { 16 | return getFromLocalStorage(); 17 | }); 18 | 19 | const toggle = () => { 20 | setTheme(theme === "light" ? "dark" : "light"); 21 | }; 22 | 23 | useEffect(() => { 24 | localStorage.setItem("theme", theme); 25 | document.querySelector("html").classList.remove("dark", "light"); 26 | document.querySelector("html").classList.add(theme); 27 | if (theme === "dark") { 28 | document.querySelector("body").style.backgroundColor = "black"; 29 | } else { 30 | document.querySelector("body").style.backgroundColor = "white"; 31 | } 32 | }, [theme]); 33 | 34 | return ( 35 | 36 | {children} 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/icons/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/styles/TransactionForm.css: -------------------------------------------------------------------------------- 1 | .form_container { 2 | padding: 30px; 3 | height: 374px; 4 | width: 430px; 5 | } 6 | 7 | .form-title { 8 | display: flex; 9 | } 10 | .edittransaction { 11 | max-width: 50%; 12 | } 13 | 14 | #contactChoice1 { 15 | appearance: none; 16 | border-radius: 50%; 17 | width: 16px; 18 | height: 16px; 19 | transition: 0.2s all linear; 20 | margin-right: 5px; 21 | position: relative; 22 | top: 4px; 23 | } 24 | 25 | #contactChoice1:checked { 26 | border: 5px solid var(--border); 27 | } 28 | 29 | #contactChoice2 { 30 | appearance: none; 31 | border-radius: 50%; 32 | width: 16px; 33 | height: 16px; 34 | transition: 0.2s all linear; 35 | margin-right: 5px; 36 | position: relative; 37 | top: 4px; 38 | } 39 | 40 | #contactChoice2:checked { 41 | border: 5px solid var(--border); 42 | } 43 | 44 | #number { 45 | width: 100%; 46 | } 47 | 48 | .input[type="number"]::-webkit-inner-spin-button, 49 | input[type="number"]::-webkit-outer-spin-button { 50 | opacity: 1; 51 | width: 100%; 52 | } 53 | 54 | #text { 55 | width: 100%; 56 | } 57 | 58 | /* --------responsiveness--------- */ 59 | @media only screen and (max-width: 480px) { 60 | .form_container { 61 | width: 100%; 62 | padding: 20px; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/icons/ph_plus-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @import url("https://fonts.googleapis.com/css2?family=Lato:wght@100;300;400;700;900&display=swap"); 5 | @import url("https://fonts.googleapis.com/css2?family=Inter&family=Leckerli+One&display=swap"); 6 | 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | transition: all 0.5s; 11 | } 12 | 13 | ::-webkit-scrollbar { 14 | width: 5px; 15 | } 16 | 17 | ::-webkit-scrollbar-track { 18 | background: #f1f1f1; 19 | } 20 | 21 | ::-webkit-scrollbar-thumb { 22 | /* background: linear-gradient(transparent,#C599D8); */ 23 | background-color: var(--primary); 24 | } 25 | 26 | .dark > ::-webkit-scrollbar-track { 27 | background: #959595; 28 | } 29 | 30 | .dark > ::-webkit-scrollbar-thumb { 31 | background-color: rgba(19, 43, 57, 1); 32 | } 33 | 34 | ::-webkit-calendar-picker-indicator { 35 | transition: ease-in-out 500ms; 36 | filter: none; 37 | } 38 | 39 | .dark ::-webkit-calendar-picker-indicator { 40 | transition: ease-in-out 500ms; 41 | filter: invert(76%) sepia(28%) saturate(482%) hue-rotate(188deg) 42 | brightness(66%) contrast(98%); 43 | } 44 | 45 | @layer base { 46 | :root { 47 | --primary-bg: #6e9df7; 48 | --secondary-bg: #f3f7ff; 49 | --border: #3e81ff; 50 | --text: #000000; 51 | } 52 | .dark { 53 | --primary-bg: #335467; 54 | --secondary-bg: #011019; 55 | --border: #263f4e; 56 | --text: #ffffff; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | SpendWise 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-app-with-tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "custom-reporter": { 8 | "type": "module" 9 | }, 10 | "dependencies": { 11 | "@testing-library/jest-dom": "5.16.5", 12 | "@testing-library/react": "13.4.0", 13 | "@testing-library/user-event": "14.4.3", 14 | "apexcharts": "^3.44.2", 15 | "axios": "^1.6.2", 16 | "dotenv": "^16.3.1", 17 | "firebase": "^10.7.0", 18 | "lucide-react": "^0.424.0", 19 | "react": "18.2.0", 20 | "react-apexcharts": "^1.4.1", 21 | "react-content-loader": "^6.2.1", 22 | "react-dom": "18.2.0", 23 | "react-icons": "^4.12.0", 24 | "react-router-dom": "^6.21.0", 25 | "react-scripts": "5.0.1", 26 | "react-toastify": "^9.1.1", 27 | "vaul": "^0.9.1" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.25.2", 31 | "@babel/preset-env": "^7.25.3", 32 | "@babel/preset-react": "^7.24.7", 33 | "@babel/runtime": "7.13.8", 34 | "@stylistic/eslint-plugin-js": "^1.5.1", 35 | "babel-loader": "^9.1.3", 36 | "cross-env": "^7.0.3", 37 | "eslint": "^8.56.0", 38 | "eslint-config-standard": "^17.1.0", 39 | "eslint-plugin-import": "^2.29.1", 40 | "eslint-plugin-n": "^16.4.0", 41 | "eslint-plugin-promise": "^6.1.1", 42 | "eslint-plugin-react": "^7.33.2", 43 | "tailwindcss": "^3.3.5" 44 | }, 45 | "scripts": { 46 | "start": "react-scripts start", 47 | "build": "react-scripts build", 48 | "test": "react-scripts test --watchAll=false '--testResultsProcessor=/custom-reporter' '--silent' '--maxWorkers=2' ./", 49 | "eject": "react-scripts eject" 50 | }, 51 | "browserslist": [ 52 | ">0.2%", 53 | "not dead", 54 | "not ie <= 11", 55 | "not op_mini all" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /src/styles/Navbar.css: -------------------------------------------------------------------------------- 1 | .navbar_parent a span { 2 | font-family: "Barlow Condensed", sans-serif; 3 | letter-spacing: 0.5px; 4 | font-size: 35px; 5 | } 6 | 7 | .login_signup_container { 8 | display: flex; 9 | justify-content: space-between; 10 | align-items: center; 11 | gap: 20px; 12 | } 13 | 14 | .login_signup_container button { 15 | height: 35px; 16 | width: 90px; 17 | border: none; 18 | outline: none; 19 | border-radius: 0.25rem; 20 | cursor: pointer; 21 | font-family: "Lato", sans-serif; 22 | font-size: 17px; 23 | letter-spacing: 0.5px; 24 | background-color: white; 25 | color: var(--primary-bg); 26 | font-weight: 500; 27 | } 28 | 29 | .login_parent, 30 | .signup_parent { 31 | width: 100vw; 32 | height: calc(100vh - 70px); 33 | position: absolute; 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | top: 70px; 38 | left: 0; 39 | backdrop-filter: blur(3px); 40 | opacity: 0; 41 | animation-name: fadeIn; 42 | animation-duration: 0.3s; 43 | animation-fill-mode: both; 44 | z-index: 100; 45 | } 46 | 47 | @keyframes fadeIn { 48 | 0% { 49 | opacity: 0; 50 | } 51 | 52 | 100% { 53 | opacity: 1; 54 | } 55 | } 56 | 57 | .login_parent::before, 58 | .signup_parent::before { 59 | content: ""; 60 | position: absolute; 61 | height: 100%; 62 | width: 100%; 63 | background-color: rgba(24, 24, 24, 0.5); 64 | z-index: -1; 65 | } 66 | 67 | /* ---------responsiveness----- */ 68 | 69 | /* padding to 30px for screen widths<=768px */ 70 | 71 | @media only screen and (max-width: 768px) { 72 | .navbar_parent a span { 73 | font-size: 30px; 74 | } 75 | } 76 | 77 | @media only screen and (max-width: 480px) { 78 | .navbar_parent a span { 79 | font-size: 25px; 80 | } 81 | } 82 | 83 | @media only screen and (max-width: 400px) { 84 | .navbar_parent a span { 85 | font-size: 22px; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/icons/sun.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ui/drawer.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Drawer as DrawerPrimitive } from "vaul"; 3 | 4 | const Drawer = ({ shouldScaleBackground = true, ...props }) => ( 5 | 9 | ); 10 | Drawer.displayName = "Drawer"; 11 | 12 | const DrawerTrigger = DrawerPrimitive.Trigger; 13 | 14 | const DrawerPortal = DrawerPrimitive.Portal; 15 | 16 | const DrawerClose = DrawerPrimitive.Close; 17 | 18 | const DrawerOverlay = React.forwardRef(({ className, ...props }, ref) => ( 19 | 24 | )); 25 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; 26 | 27 | const DrawerContent = React.forwardRef( 28 | ({ className, children, ...props }, ref) => ( 29 | 30 | 31 | 32 |
33 | {children} 34 | 35 | 36 | ) 37 | ); 38 | DrawerContent.displayName = "DrawerContent"; 39 | 40 | const DrawerHeader = ({ className, ...props }) => ( 41 |
42 | ); 43 | DrawerHeader.displayName = "DrawerHeader"; 44 | 45 | const DrawerFooter = ({ className, ...props }) => ( 46 |
47 | ); 48 | DrawerFooter.displayName = "DrawerFooter"; 49 | 50 | const DrawerTitle = React.forwardRef(({ className, ...props }, ref) => ( 51 | 52 | )); 53 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName; 54 | 55 | const DrawerDescription = React.forwardRef(({ className, ...props }, ref) => ( 56 | 57 | )); 58 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName; 59 | 60 | export { 61 | Drawer, 62 | DrawerPortal, 63 | DrawerOverlay, 64 | DrawerTrigger, 65 | DrawerClose, 66 | DrawerContent, 67 | DrawerHeader, 68 | DrawerFooter, 69 | DrawerTitle, 70 | DrawerDescription, 71 | }; 72 | -------------------------------------------------------------------------------- /src/styles/ExpenseTracker.css: -------------------------------------------------------------------------------- 1 | .expenseTracker_parent { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | gap: 70px; 7 | font-family: "Lato", sans-serif; 8 | box-sizing: border-box; 9 | border-color: var(--border); 10 | } 11 | 12 | .balance_container { 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | gap: 8px; 18 | color: white; 19 | padding: 15px 30px; 20 | } 21 | 22 | .balance_container h3 { 23 | font-size: 16px; 24 | letter-spacing: 0.5px; 25 | } 26 | 27 | .balance_container h1 { 28 | font-size: 22px; 29 | letter-spacing: 0.5px; 30 | } 31 | 32 | .formTransactions_container { 33 | width: 100%; 34 | } 35 | 36 | .WalletDetails_container { 37 | width: 430px; 38 | } 39 | 40 | .incomeExpense_container { 41 | padding: 15px 30px; 42 | border-color: var(--border); 43 | } 44 | 45 | .incomeExpense_container h3 { 46 | font-size: 16px; 47 | letter-spacing: 0.5px; 48 | } 49 | 50 | .incomeExpense_container h3 span { 51 | font-size: 22px; 52 | margin-top: 8px; 53 | } 54 | 55 | .WalletDetails_container h4 { 56 | font-size: 16px; 57 | letter-spacing: 0.5px; 58 | margin-top: 20px; 59 | } 60 | 61 | .WalletDetails_container p { 62 | font-size: 14px; 63 | letter-spacing: 0.5px; 64 | margin-top: 5px; 65 | } 66 | 67 | .Toastify { 68 | position: absolute; 69 | left: 0; 70 | bottom: 0; 71 | } 72 | /* --------responsiveness--------- */ 73 | 74 | @media only screen and (max-width: 870px) { 75 | .formTransactions_container { 76 | flex-direction: column; 77 | align-items: center; 78 | justify-content: center; 79 | gap: 20px; 80 | } 81 | } 82 | 83 | @media only screen and (max-width: 768px) { 84 | .balance_container { 85 | color: black; 86 | } 87 | 88 | .dark .balance_container { 89 | color: #b6cefc; 90 | } 91 | } 92 | 93 | @media only screen and (max-width: 480px) { 94 | .expenseTracker_parent { 95 | padding: 0px 10px; 96 | } 97 | 98 | .WalletDetails_container { 99 | width: 100%; 100 | } 101 | 102 | .incomeExpense_container { 103 | justify-content: space-between; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/styles/Transactions.css: -------------------------------------------------------------------------------- 1 | .transactions_container { 2 | max-height: calc(100vh - 433px); 3 | overflow: auto; 4 | margin-top: 5px; 5 | } 6 | 7 | .income, 8 | .outcome { 9 | width: 100%; 10 | box-sizing: border-box; 11 | height: 70px; 12 | position: relative; 13 | } 14 | 15 | .income::after { 16 | content: ""; 17 | position: absolute; 18 | top: 0px; 19 | bottom: 0px; 20 | right: 0px; 21 | width: 8px; 22 | background-color: #1cd503; 23 | border-radius: 1px 2px 2px 1px; 24 | } 25 | 26 | .outcome::after { 27 | content: ""; 28 | position: absolute; 29 | top: 0px; 30 | bottom: 0px; 31 | right: 0px; 32 | width: 8px; 33 | background-color: #f53838; 34 | border-radius: 1px 2px 2px 1px; 35 | } 36 | 37 | .icon_container { 38 | padding: 12px; 39 | border-radius: 50%; 40 | } 41 | 42 | .icons { 43 | height: 29px; 44 | width: 29px; 45 | } 46 | 47 | .descDate_container { 48 | height: 100%; 49 | display: flex; 50 | flex-direction: column; 51 | justify-content: center; 52 | align-items: flex-start; 53 | gap: 5px; 54 | margin-right: auto; 55 | margin-left: 10px; 56 | } 57 | 58 | .descDate_container h4 { 59 | margin: 0; 60 | font-size: 16px; 61 | letter-spacing: 0.5px; 62 | font-weight: 500; 63 | } 64 | 65 | .descDate_container p { 66 | margin: 0; 67 | font-size: 10px; 68 | letter-spacing: 0.3px; 69 | } 70 | 71 | .income h1 { 72 | margin: 0; 73 | font-size: 16px; 74 | color: #1cd503; 75 | margin-right: 10px; 76 | } 77 | 78 | .outcome h1 { 79 | margin: 0; 80 | font-size: 16px; 81 | color: #f53838; 82 | margin-right: 10px; 83 | } 84 | 85 | .editDelete_container { 86 | display: flex; 87 | justify-content: center; 88 | align-items: center; 89 | gap: 10px; 90 | transition: all 0.3s ease-in-out; 91 | width: 0px; 92 | height: 100%; 93 | } 94 | 95 | .income:hover .editDelete_container, 96 | .outcome:hover .editDelete_container { 97 | width: 60px; 98 | } 99 | 100 | .edit_icon { 101 | fill: var(--primary); 102 | height: 25px; 103 | width: 25px; 104 | cursor: pointer; 105 | } 106 | 107 | .delete_icon { 108 | height: 25px; 109 | width: 25px; 110 | cursor: pointer; 111 | fill: #f53838; 112 | } 113 | 114 | .edit_icon { 115 | fill: #65c971; 116 | } 117 | 118 | .delete_icon { 119 | fill: #f53838; 120 | } 121 | 122 | .arrow { 123 | border-width: 0 2px 2px 0; 124 | float: right; 125 | padding: 2.4px; 126 | position: relative; 127 | top: 7px; 128 | } 129 | 130 | .down { 131 | transform: rotate(45deg); 132 | -webkit-transform: rotate(45deg); 133 | } 134 | 135 | /* --------responsiveness--------- */ 136 | @media only screen and (max-width: 480px) { 137 | .editDelete_container { 138 | width: 60px; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 3 | import "../styles/App.css"; 4 | import Navbar from "./Navbar"; 5 | import ExpenseTracker from "./ExpenseTracker"; 6 | import NoUser from "./NoUser"; 7 | import Profile from "./profile/Profile"; 8 | import ErrorPage from "./ErrorPage.js"; 9 | import { ThemeContextProvider, ThemeContext } from "../context/ThemeContext"; 10 | // Home route 11 | function Home({ 12 | userJWTToken, 13 | userFirebaseRefId, 14 | showSignupForm, 15 | setShowSignupForm, 16 | }) { 17 | const { theme } = useContext(ThemeContext); 18 | return ( 19 |
20 |
21 | { 22 | 27 | 32 | 33 | } 34 | 38 | {userJWTToken || userFirebaseRefId ? ( 39 | 40 | ) : ( 41 | 45 | )} 46 |
47 | ); 48 | } 49 | 50 | // Profile route 51 | function UserProfile() { 52 | return ( 53 |
54 | {/* */} 55 | 56 |
57 | ); 58 | } 59 | 60 | function App() { 61 | const [showSignupForm, setShowSignupForm] = useState(false); 62 | const userJWTToken = JSON.parse( 63 | localStorage.getItem("expenseTrackerUserJWTToken") 64 | ); 65 | const userFirebaseRefId = JSON.parse( 66 | localStorage.getItem("expenseTrackerUserFirebaseRefId") 67 | ); 68 | return ( 69 | 70 | 71 |
72 | 73 | 82 | } 83 | /> 84 | } /> 85 | } /> 86 | 87 |
88 |
89 |
90 | ); 91 | } 92 | 93 | export default App; 94 | -------------------------------------------------------------------------------- /src/components/profile/PredefinedTransactionsCards.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import editSmall from "../../icons/edit-small.svg"; 3 | import cross from "../../icons/cross.svg"; 4 | 5 | const PredefinedTransactionsCards = () => { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export const ExpenseCard = () => { 15 | return ( 16 | <> 17 |
18 |
19 |

Expense

20 |
21 | 22 | 23 |
24 |
25 |
26 |
27 |

Rent

28 |

Rs 10000

29 |
30 |
31 |
32 |

Category

33 |

Bill

34 |
35 |
36 |
37 |

Date

38 |

15/03/2024

39 |
40 |
41 |
42 | 43 | ); 44 | }; 45 | 46 | export const IncomeCard = () => { 47 | return ( 48 | <> 49 |
50 |
51 |

Income

52 |
53 | 54 | 55 |
56 |
57 |
58 |
59 |

Rent

60 |

Rs 20000

61 |
62 |
63 |
64 |

Category

65 |

Food

66 |
67 |
68 |
69 |

Date

70 |

15/03/2024

71 |
72 |
73 |
74 | 75 | ); 76 | }; 77 | 78 | export default PredefinedTransactionsCards; 79 | -------------------------------------------------------------------------------- /src/components/profile/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from "react"; 2 | import "../../styles/App.css"; 3 | import { 4 | FaUser, 5 | FaListAlt, 6 | FaSignOutAlt, 7 | FaTimes, 8 | FaBars, 9 | } from "react-icons/fa"; 10 | import { ThemeContext } from "../../context/ThemeContext"; 11 | 12 | const Sidebar = ({ onButtonClick }) => { 13 | // const sidebarClasses = `sidebar fixed left-0 top-0 h-screen w-60 text-black transition-width`; 14 | const [isSidebarVisible, setSidebarVisible] = useState(false); 15 | 16 | const toggleSidebar = () => { 17 | setSidebarVisible(!isSidebarVisible); 18 | }; 19 | 20 | const sidebarClasses = `transition-colors dark:text-white sidebar fixed left-0 top-0 h-screen w-60 text-black transform transition-transform ${ 21 | isSidebarVisible 22 | ? "translate-x-0 transition-transform duration-300 ease-in" 23 | : "-translate-x-full transition-transform duration-300 ease-out" 24 | }`; 25 | useEffect(() => { 26 | // Check screen width and set initial sidebar visibility 27 | const handleResize = () => { 28 | const screenWidth = window.innerWidth; 29 | setSidebarVisible(screenWidth >= 640); // Adjust the breakpoint as needed 30 | }; 31 | 32 | handleResize(); // Set initial state 33 | 34 | // Listen for window resize events 35 | window.addEventListener("resize", handleResize); 36 | 37 | // Cleanup event listener on component unmount 38 | return () => { 39 | window.removeEventListener("resize", handleResize); 40 | }; 41 | }, []); 42 | 43 | const handleLogOut = () => { 44 | localStorage.removeItem("expenseTrackerUserJWTToken"); 45 | localStorage.removeItem("expenseTrackerUserFirebaseRefId"); 46 | window.location.href = "/"; 47 | }; 48 | const { theme } = useContext(ThemeContext); 49 | const dynamicStyle = { 50 | backgroundColor: theme === "dark" ? "#011019" : "#EAF0FB", 51 | }; 52 | return ( 53 | <> 54 |
55 | 59 |
60 |
61 | 65 | 72 | 79 | 83 |
84 |
85 |
86 | 87 | ); 88 | }; 89 | 90 | export default Sidebar; 91 | -------------------------------------------------------------------------------- /src/components/DoughnutChart.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from "react"; 2 | import Chart from "react-apexcharts"; 3 | import { ThemeContext } from "../context/ThemeContext"; 4 | import "../styles/DoughnutChart.css"; 5 | import "../index.css"; 6 | 7 | function DoughnutChart (props) { 8 | const { transactions } = props; 9 | const [showChart, setShowChart] = useState(false); 10 | const [foodExpenses, setFoodExpenses] = useState(0); 11 | const [travelExpenses, setTravelExpenses] = useState(0); 12 | const [shoppingExpenses, setShoppingExpenses] = useState(0); 13 | const [billsExpenses, setBillsExpenses] = useState(0); 14 | const [othersExpenses, setOthersExpenses] = useState(0); 15 | const { theme } = useContext(ThemeContext); 16 | 17 | useEffect(() => { 18 | if (transactions) { 19 | let flag = false; 20 | setFoodExpenses(0); 21 | setTravelExpenses(0); 22 | setShoppingExpenses(0); 23 | setBillsExpenses(0); 24 | setOthersExpenses(0); 25 | 26 | transactions.forEach((transaction) => { 27 | if (transaction.transactionType === "Expense") { 28 | setShowChart(true); 29 | flag = true; 30 | switch (transaction.category) { 31 | case "Food": 32 | setFoodExpenses((prev) => prev + transaction.amount); 33 | break; 34 | case "Travel": 35 | setTravelExpenses((prev) => prev + transaction.amount); 36 | break; 37 | case "Shopping": 38 | setShoppingExpenses((prev) => prev + transaction.amount); 39 | break; 40 | case "Bills": 41 | setBillsExpenses((prev) => prev + transaction.amount); 42 | break; 43 | case "Others": 44 | setOthersExpenses((prev) => prev + transaction.amount); 45 | break; 46 | default: 47 | break; 48 | } 49 | } 50 | }); 51 | if (!flag) { 52 | setShowChart(false); 53 | } 54 | } 55 | }, [transactions]); 56 | 57 | return ( 58 | 59 |
60 | {showChart 61 | ? ( 83 | ) 84 | : ( 85 |

Add your expenses to see meaningful insights here!

86 | )} 87 |
88 |
89 | ); 90 | } 91 | 92 | export default DoughnutChart; 93 | -------------------------------------------------------------------------------- /src/styles/Signup.css: -------------------------------------------------------------------------------- 1 | .signup_form_container { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | align-items: center; 7 | gap: 40px; 8 | padding: 30px 50px; 9 | border-radius: 5px; 10 | transition: 0.5s ease-in-out; 11 | background-color: rgba(65, 65, 65, 0.833); 12 | } 13 | 14 | .close-icon { 15 | position: absolute; 16 | top: 0; 17 | right: 0; 18 | padding: 10px; 19 | } 20 | 21 | .signup_form_container h1, 22 | .close-icon { 23 | color: var(--primary); 24 | font-weight: 600; 25 | font-size: 31px; 26 | margin: 0; 27 | opacity: 0; 28 | animation-name: fadeInSlideIn; 29 | animation-duration: 0.5s; 30 | animation-fill-mode: forwards; 31 | animation-delay: 300ms; 32 | font-family: "Lato", sans-serif; 33 | letter-spacing: 1px; 34 | } 35 | 36 | .signup_form_container form { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: center; 40 | align-items: center; 41 | gap: 25px; 42 | width: 20rem; 43 | } 44 | 45 | .signup_form_container form input { 46 | font-size: 14px; 47 | font-family: "Montserrat", sans-serif; 48 | font-weight: 500; 49 | padding: 10px 15px; 50 | box-sizing: border-box; 51 | transition: 0.5s ease-in-out; 52 | border: 0; 53 | outline: 0; 54 | border-radius: 5px; 55 | transition: 0.5s ease-in-out; 56 | width: 100%; 57 | opacity: 0; 58 | background-color: rgb(230, 230, 230); 59 | color: #888888; 60 | } 61 | 62 | .signup_form_container form input::placeholder { 63 | color: #888888; 64 | } 65 | 66 | .signupBtn { 67 | color: black; 68 | background-color: white; 69 | cursor: pointer; 70 | font-size: 15px; 71 | padding: 9.25px 15px; 72 | border-radius: 5px; 73 | border: none; 74 | font-family: "Montserrat", sans-serif; 75 | font-weight: 500; 76 | opacity: 0; 77 | animation-name: fadeInSlideIn; 78 | animation-duration: 0.5s; 79 | animation-fill-mode: forwards; 80 | animation-delay: 1300ms; 81 | font-family: "Lato", sans-serif; 82 | letter-spacing: 0.5px; 83 | display: flex; 84 | justify-content: center; 85 | align-items: center; 86 | } 87 | 88 | .signupSeparator { 89 | opacity: 0; 90 | animation-name: fadeInSlideIn; 91 | animation-duration: 0.5s; 92 | animation-fill-mode: forwards; 93 | animation-delay: 1500ms; 94 | } 95 | 96 | .googleSignup { 97 | margin-top: -15px; 98 | color: black; 99 | background-color: white; 100 | cursor: pointer; 101 | font-size: 15px; 102 | padding: 9.25px 15px; 103 | width: 100%; 104 | border-radius: 5px; 105 | border: none; 106 | font-family: "Montserrat", sans-serif; 107 | font-weight: 500; 108 | opacity: 0; 109 | animation-name: fadeInSlideIn; 110 | animation-duration: 0.5s; 111 | animation-fill-mode: forwards; 112 | animation-delay: 1700ms; 113 | font-family: "Lato", sans-serif; 114 | letter-spacing: 0.5px; 115 | display: flex; 116 | justify-content: center; 117 | align-items: center; 118 | } 119 | 120 | .signup_form_container form input:nth-child(1) { 121 | animation-name: fadeInSlideIn; 122 | animation-duration: 0.5s; 123 | animation-fill-mode: forwards; 124 | animation-delay: 500ms; 125 | } 126 | 127 | .signup_form_container form input:nth-child(2) { 128 | animation-name: fadeInSlideIn; 129 | animation-duration: 0.5s; 130 | animation-fill-mode: forwards; 131 | animation-delay: 700ms; 132 | } 133 | 134 | .signup_form_container form input:nth-child(3) { 135 | animation-name: fadeInSlideIn; 136 | animation-duration: 0.5s; 137 | animation-fill-mode: forwards; 138 | animation-delay: 900ms; 139 | } 140 | 141 | .signup_form_container form input:nth-child(4) { 142 | animation-name: fadeInSlideIn; 143 | animation-duration: 0.5s; 144 | animation-fill-mode: forwards; 145 | animation-delay: 1100ms; 146 | } 147 | 148 | @keyframes fadeInSlideIn { 149 | 0% { 150 | opacity: 0; 151 | transform: translateY(30px); 152 | } 153 | 154 | 100% { 155 | opacity: 1; 156 | transform: translateY(0px); 157 | } 158 | } 159 | 160 | .error_message { 161 | color: red; 162 | font-size: 15px; 163 | font-family: "Lato", sans-serif; 164 | letter-spacing: 0.5px; 165 | margin: -12px auto 0 0; 166 | } 167 | 168 | /* Media query for screens below 420px */ 169 | @media (max-width: 420px) { 170 | .signup_form_container { 171 | padding: 20px; 172 | } 173 | 174 | .signup_form_container h1 { 175 | font-size: 28px; 176 | } 177 | 178 | .signup_form_container form input { 179 | font-size: 12px; 180 | } 181 | 182 | .signup_form_container button { 183 | font-size: 14px; 184 | } 185 | } 186 | 187 | .loading-spinner { 188 | border: 4px solid rgb(143, 142, 142); 189 | border-left: 4px solid #fff; 190 | border-radius: 50%; 191 | width: 22.5px; 192 | height: 22.5px; 193 | animation: spin 1s linear infinite; 194 | } 195 | 196 | @keyframes spin { 197 | 0% { 198 | transform: rotate(0deg); 199 | } 200 | 100% { 201 | transform: rotate(360deg); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/components/ExpenseTracker.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect, useState } from "react"; 2 | import "../styles/ExpenseTracker.css"; 3 | import "../index.css"; 4 | import { usersCollection } from "../firebaseConfig"; 5 | import { doc, getDoc } from "firebase/firestore"; 6 | import { ToastContainer } from "react-toastify"; 7 | import "react-toastify/dist/ReactToastify.css"; 8 | import DoughnutChart from "./DoughnutChart"; 9 | import Transactions from "./Transactions"; 10 | import TransactionForm from "./TransactionForm"; 11 | 12 | function ExpenseTracker() { 13 | const userJWTToken = JSON.parse( 14 | localStorage.getItem("expenseTrackerUserJWTToken") 15 | ); 16 | const userFirebaseRefId = JSON.parse( 17 | localStorage.getItem("expenseTrackerUserFirebaseRefId") 18 | ); 19 | const [user, setUser] = useState({}); 20 | const [transactions, setTransactions] = useState([]); 21 | const [formData, setFormData] = useState({ 22 | email: "", 23 | transactionType: "", 24 | category: "", 25 | date: "", 26 | amount: "", 27 | description: "", 28 | transactionId: "", 29 | }); 30 | const [balance, setBalance] = useState(0); 31 | const [incoming, setIncoming] = useState(0); 32 | const [outgoing, setOutgoing] = useState(0); 33 | const [editEnabled, setEditEnabled] = useState(false); 34 | const [descriptionChars, setDescriptionChars] = useState(0); 35 | 36 | const fetchUserFromFirebase = async (docrefId) => { 37 | try { 38 | const userDocRef = doc(usersCollection, docrefId); 39 | const userDocSnapshot = await getDoc(userDocRef); 40 | 41 | if (userDocSnapshot.exists()) { 42 | const userData = userDocSnapshot.data(); 43 | setUser(userData); 44 | } else { 45 | return null; 46 | } 47 | } catch (error) { 48 | console.error("Error fetching user from Firestore:", error); 49 | return null; 50 | } 51 | }; 52 | 53 | useEffect(() => { 54 | if (userJWTToken) { 55 | fetchDataFromProtectedAPI(userJWTToken); 56 | } 57 | if (userFirebaseRefId) { 58 | fetchUserFromFirebase(userFirebaseRefId); 59 | } 60 | }, [userJWTToken, userFirebaseRefId]); 61 | 62 | useEffect(() => { 63 | let incoming = 0; 64 | let outgoing = 0; 65 | transactions.forEach((transaction) => { 66 | if (transaction.transactionType === "Income") { 67 | incoming += transaction.amount; 68 | } else { 69 | outgoing += transaction.amount; 70 | } 71 | }); 72 | setIncoming(incoming); 73 | setOutgoing(outgoing); 74 | setBalance(incoming - outgoing); 75 | }, [transactions]); 76 | 77 | useLayoutEffect(() => { 78 | function updateSize() { 79 | const width = window.innerWidth; 80 | setDescriptionChars( 81 | width <= 320 82 | ? 5 83 | : width <= 375 84 | ? 8 85 | : width <= 480 86 | ? 12 87 | : width <= 768 88 | ? 20 89 | : 22 90 | ); 91 | } 92 | window.addEventListener("resize", updateSize); 93 | updateSize(); 94 | return () => window.removeEventListener("resize", updateSize); 95 | }, []); 96 | 97 | return ( 98 |
99 |
100 |

Your Balance-

101 |

₹{balance}

102 |
103 |
104 | 111 | 112 |
113 |
114 |

115 | Income 116 | 117 | + ₹{incoming} 118 | 119 |

120 |

121 | Expense 122 | - ₹{outgoing} 123 |

124 |
125 | 133 |
134 |
135 | {transactions.length > 0 && } 136 | 137 |
138 | ); 139 | } 140 | 141 | export default ExpenseTracker; 142 | -------------------------------------------------------------------------------- /src/styles/Login.css: -------------------------------------------------------------------------------- 1 | .login_form_container { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | align-items: center; 7 | gap: 40px; 8 | padding: 30px 50px; 9 | border-radius: 5px; 10 | transition: 0.5s ease-in-out; 11 | background-color: rgba(65, 65, 65, 0.833); 12 | } 13 | 14 | .close-icon { 15 | position: absolute; 16 | top: 0; 17 | right: 0; 18 | padding: 10px; 19 | } 20 | 21 | .login_form_container h1, 22 | .close-icon { 23 | color: var(--primary); 24 | font-weight: 600; 25 | font-size: 35px; 26 | margin: 0; 27 | opacity: 0; 28 | animation-name: fadeInSlideIn; 29 | animation-duration: 0.5s; 30 | animation-fill-mode: forwards; 31 | animation-delay: 300ms; 32 | font-family: "Lato", sans-serif; 33 | letter-spacing: 1px; 34 | } 35 | 36 | .login_form_container form { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: center; 40 | align-items: center; 41 | gap: 25px; 42 | width: 20rem; 43 | } 44 | 45 | .login_form_container form input { 46 | font-size: 14px; 47 | font-family: "Montserrat", sans-serif; 48 | font-weight: 500; 49 | padding: 10px 15px; 50 | box-sizing: border-box; 51 | transition: 0.5s ease-in-out; 52 | border: 0; 53 | outline: 0; 54 | border-radius: 5px; 55 | transition: 0.5s ease-in-out; 56 | width: 100%; 57 | opacity: 0; 58 | background-color: rgb(230, 230, 230); 59 | color: #888888; 60 | } 61 | 62 | .login_form_container form .input_div { 63 | font-size: 14px; 64 | font-family: "Montserrat", sans-serif; 65 | font-weight: 500; 66 | /* padding: 10px 15px; */ 67 | box-sizing: border-box; 68 | transition: 0.5s ease-in-out; 69 | border: 0; 70 | outline: 0; 71 | border-radius: 5px; 72 | transition: 0.5s ease-in-out; 73 | width: 100%; 74 | opacity: 0; 75 | background-color: rgb(230, 230, 230); 76 | color: #888888; 77 | } 78 | 79 | .login_form_container form input::placeholder { 80 | color: #888888; 81 | } 82 | 83 | .loginBtn { 84 | color: black; 85 | background-color: white; 86 | cursor: pointer; 87 | font-size: 15px; 88 | padding: 9.25px 15px; 89 | border-radius: 5px; 90 | border: none; 91 | font-family: "Montserrat", sans-serif; 92 | font-weight: 500; 93 | opacity: 0; 94 | animation-name: fadeInSlideIn; 95 | animation-duration: 0.5s; 96 | animation-fill-mode: forwards; 97 | animation-delay: 900ms; 98 | font-family: "Lato", sans-serif; 99 | letter-spacing: 0.5px; 100 | display: flex; 101 | justify-content: center; 102 | align-items: center; 103 | } 104 | 105 | .loginSeparator { 106 | opacity: 0; 107 | animation-name: fadeInSlideIn; 108 | animation-duration: 0.5s; 109 | animation-fill-mode: forwards; 110 | animation-delay: 1100ms; 111 | } 112 | 113 | .googleLogin { 114 | margin-top: -15px; 115 | color: black; 116 | background-color: white; 117 | cursor: pointer; 118 | font-size: 15px; 119 | padding: 9.25px 15px; 120 | width: 100%; 121 | border-radius: 5px; 122 | border: none; 123 | font-family: "Montserrat", sans-serif; 124 | font-weight: 500; 125 | opacity: 0; 126 | animation-name: fadeInSlideIn; 127 | animation-duration: 0.5s; 128 | animation-fill-mode: forwards; 129 | animation-delay: 1300ms; 130 | font-family: "Lato", sans-serif; 131 | letter-spacing: 0.5px; 132 | display: flex; 133 | justify-content: center; 134 | align-items: center; 135 | } 136 | 137 | .login_form_container form input:nth-child(1) { 138 | animation-name: fadeInSlideIn; 139 | animation-duration: 0.5s; 140 | animation-fill-mode: forwards; 141 | animation-delay: 500ms; 142 | } 143 | 144 | .login_form_container form .input_div { 145 | animation-name: fadeInSlideIn; 146 | animation-duration: 0.5s; 147 | animation-fill-mode: forwards; 148 | animation-delay: 700ms; 149 | } 150 | 151 | @keyframes fadeInSlideIn { 152 | 0% { 153 | opacity: 0; 154 | transform: translateY(30px); 155 | } 156 | 157 | 100% { 158 | opacity: 1; 159 | transform: translateY(0px); 160 | } 161 | } 162 | 163 | .login_form_container p { 164 | color: red; 165 | font-size: 15px; 166 | font-family: "Lato", "sans-serif"; 167 | letter-spacing: 0.5px; 168 | margin: -12px auto 0 0; 169 | } 170 | 171 | @media (max-width: 420px) { 172 | .login_form_container { 173 | padding: 20px; 174 | } 175 | 176 | .login_form_container h1 { 177 | font-size: 28px; 178 | } 179 | 180 | .login_form_container form input { 181 | font-size: 12px; 182 | } 183 | 184 | .login_form_container button { 185 | font-size: 14px; 186 | } 187 | } 188 | 189 | .login_form_container h4 { 190 | align-items: center; 191 | margin: 0; 192 | opacity: 0; 193 | animation-name: fadeInSlideIn; 194 | animation-duration: 0.5s; 195 | animation-fill-mode: forwards; 196 | animation-delay: 1300ms; 197 | font-family: "Lato", "sans-serif"; 198 | } 199 | 200 | .loading-spinner { 201 | border: 4px solid rgb(143, 142, 142); 202 | border-left: 4px solid #fff; 203 | border-radius: 50%; 204 | width: 22.5px; 205 | height: 22.5px; 206 | animation: spin 1s linear infinite; 207 | } 208 | 209 | @keyframes spin { 210 | 0% { 211 | transform: rotate(0deg); 212 | } 213 | 100% { 214 | transform: rotate(360deg); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 |

Spendwise

5 | 6 |

7 | Your User-Friendly Expense Tracker Empowering Your Finances. 8 |
9 | Kharagpur Winter of Code 10 | . 11 | Report Bug 12 | . 13 | Request Feature 14 |

15 |

16 | 17 |
18 | 19 | ![Contributors](https://img.shields.io/github/contributors/ani1609/Spendwise?color=dark-green) ![Forks](https://img.shields.io/github/forks/ani1609/Spendwise?style=social) ![Stargazers](https://img.shields.io/github/stars/ani1609/Spendwise?style=social) ![Issues](https://img.shields.io/github/issues/ani1609/Spendwise) ![License](https://img.shields.io/github/license/ani1609/Spendwise) 20 |
21 | 22 | ## Community 23 | We would love to hear from you! We communicate on Slack: 24 | 25 | [![Slack](https://img.shields.io/badge/chat-on_slack-purple.svg?style=for-the-badge&logo=slack)](https://join.slack.com/t/spendwisegroup/shared_invite/zt-28g7vaeb4-ZZbfrM8cpb6j~EEoWvWR2A) 26 | 27 | ## Table Of Contents 28 | 29 | * [About the Project](#about-the-project) 30 | * [Built With](#built-with) 31 | * [Getting Started](#getting-started) 32 | * [Prerequisites](#prerequisites) 33 | * [Installation](#installation) 34 | * [Ideas List](#ideas-list) 35 | * [Authors](#authors) 36 | * [Code Contributors](#code-contributors) 37 | 38 | ## About The Project 39 | 40 | ![spendwiseSS](https://github.com/ani1609/Spendwise/assets/89239354/9297e584-f8e7-4dd6-aec2-01841bda01f4) 41 | 42 | 43 | Spendwise: Your intuitive, user-friendly expense tracker. Seamlessly manage and understand your finances with simplicity and precision. Track, analyze, and take control of your expenses effortlessly. 44 |

(back to top)

45 | 46 | ## Built With 47 | 48 | Spendwise is powered by a modern tech stack: 49 | 50 | * [![REACT][React.com]][React-url] : Dynamic user interfaces. 51 | 52 | * [![NODEJS][nodejs.com]][nodejs-url] : Efficient server-side scripting. 53 | 54 | * [![EXPRESS][express.com]][express-url] : Streamlined API development. 55 | 56 | * [![MONGODB][mongodb.com]][mongodb-url] : User profile and authentication. 57 | 58 | * [![FIREBASE][firebase.com]][firebase-url] : Real-time handling of the transactions of the users. 59 | 60 | This combination ensures a seamless and powerful expense tracking experience. 61 |

(back to top)

62 | 63 | 64 | ## Getting Started 65 | 66 | ### Prerequisites 67 | 68 | This is an example of how to list things you need to use the software and how to install them. 69 | 70 | * npm 71 | 72 | ```sh 73 | npm install npm@latest -g 74 | ``` 75 | 76 | ### Installation 77 | 78 | 1. Fork the repository 79 | 80 | 2. Clone Your Forked Repository: 81 | 82 | ```sh 83 | git clone https://github.com//Spendwise.git 84 | ``` 85 | 86 | **Client (Frontend)** 87 | 88 | 3. Navigate to the Client Directory: 89 | 90 | ```sh 91 | cd Spendwise/client 92 | ``` 93 | 94 | 4. Install Dependencies: 95 | 96 | ```sh 97 | npm install 98 | ``` 99 | 100 | 5. Start the Application: 101 | 102 | ```sh 103 | npm run dev 104 | ``` 105 | 106 | The client will be running on http://localhost:4000 in your browser. 107 | 108 | 109 | **Server (Backend)** 110 | 111 | 1. Navigate to the Server Directory: 112 | 113 | ```sh 114 | cd Spendwise/server 115 | ``` 116 | 117 | 2. Install Dependencies: 118 | 119 | ```sh 120 | npm install 121 | ``` 122 | 123 | 3. Start the server 124 | 125 | ```sh 126 | npm run dev 127 | ``` 128 | 129 | The server will be running on http://localhost:3000 130 | 131 |

(back to top)

132 | 133 | ## Ideas List 134 | 135 | 1. **Development Simplification:** 136 | - Make the project easily deployable through Docker for streamlined development. 137 | - Enable compatibility with Codespaces and Gitpod to facilitate collaborative coding. 138 | 139 | 3. **User-Friendly Interface:** 140 | - Improve the current user interface for simplicity and clarity. 141 | - Implement basic user feedback mechanisms. 142 | 143 | 4. **Enhance Expense Categorisation:** 144 | - Extend the categorisation system with subcategories or tags. 145 | - Explore ways to improve the visibility and usability of categories. 146 | 147 | 5. **Implement Filters and Search:** 148 | - Add basic filtering options for expenses based on categories, dates, or amounts. 149 | - Implement a search feature to easily locate specific transactions. 150 | 151 | 6. **Codebase Refinement and Documentation:** 152 | - Implement consistent coding standards and practices for improved readability and maintainability. 153 | - Create thorough documentation covering project architecture, API endpoints, and key code modules. 154 | 155 |

(back to top)

156 | ## Authors 157 | 158 | * [Ankit Kumar Chowdhury](https://github.com/ani1609) - *Organisation Admin* 159 | * [Niladri Sekhar Adhikary](https://github.com/niladrix719) - *Organisation Admin* 160 | 161 | ### Code Contributors 162 | 163 | 164 | 165 | 166 |

(back to top)

167 | 168 | 169 | [React.com]:https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB 170 | [React-url]:https://react.dev/ 171 | [nodejs.com]:https://img.shields.io/badge/Node.js-43853D?style=for-the-badge&logo=node.js&logoColor=white 172 | [nodejs-url]:https://nodejs.org/en 173 | [express.com]:https://img.shields.io/badge/Express.js-404D59?style=for-the-badge 174 | [express-url]:https://expressjs.com/ 175 | [mongodb.com]:https://img.shields.io/badge/MongoDB-4EA94B?style=for-the-badge&logo=mongodb&logoColor=white 176 | [mongodb-url]:https://www.mongodb.com/ 177 | [firebase.com]:https://img.shields.io/badge/Firebase-039BE5?style=for-the-badge&logo=Firebase&logoColor=white 178 | [firebase-url]:https://firebase.google.com/ 179 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Circuitverse 2 | 3 | - We expect contributors to abide by our underlying [Code of Conduct](https://github.com/ani1609/Spendwise/blob/master/code-of-conduct.md) . All discussions about this project must be respectful and harassment-free. 4 | - Remember that communication is the lifeblood of any Open Source project. We are all working on this together, and we are all benefiting from this software. 5 | - It's very easy to misunderstand one another in asynchronous, text-based conversations. When in doubt, assume everyone has the best intentions. 6 | - If you feel anyone has violated our Code of Conduct, you should anonymously contact the team with our abuse report form via [Slack](https://join.slack.com/t/spendwisegroup/shared_invite/zt-28g7vaeb4-ZZbfrM8cpb6j~EEoWvWR2A), necessary action will be taken by the team. 7 | 8 | ## Issue label 9 | 10 | Please note: If you wanted to work on an issue, let us know by leaving a comment on the issue. If someone is already assigned or working on the issue, do not try to start working without asking in a thread. Also let us know later if you are no longer working on it. 11 | 12 | - `maintainers` label are internal tasks that will be completed by a Circuitverse core team member. 13 | - [good first issue](https://github.com/CircuitVerse/CircuitVerse/labels/good%20first%20issue) labeled issues are meant for newer developers. 14 | - [feature](https://github.com/CircuitVerse/CircuitVerse/labels/%F0%9F%8C%9F%20feature) labeled issues are meant to propose new features. 15 | - [bugs](https://github.com/CircuitVerse/CircuitVerse/labels/%F0%9F%90%9E%20bug) labeled issues are meant to have errors in existing code base. 16 | - [documentation](https://github.com/CircuitVerse/CircuitVerse/labels/documentation) labeled issues are meant to have typo errors in documentation. 17 | - `difficulty: easy` issues are usually confined to isolated areas of existing code. 18 | - `difficulty: medium` issues sometimes entail new features and might affect a significant area of the codebase, but aren't overly complex. 19 | - `difficulty: hard` issues are typically far-reaching, and might need architecture decisions during implementation. This label might also denote highly complex issues. 20 | - `duplicate` labeled issues are meant to be already existing issue in the repository. 21 | - `priority: less` labeled issues are meant to have priority comparatavily lesser than other issues. 22 | - `priority: medium` labeled issues are meant to have priority comparatively intermediate than other issues. 23 | - `priority: high` labeled issues are meant to have the highest priority and need to fix as soon as possible. 24 | - `help wanted` labeled issues signify that the contributor requires help with something specific in the issue and your help is very much appreciated. 25 | 26 | ## Creating an issue. 27 | 28 | - Check if the issue you are going to propose is not duplicate of another issue. 29 | - Open a new issue according to type i.e., if issue is a bug open a new issue by clicking on `Get Started` in the scope of `Bug Report`. 30 | - Give a precise and meaningful name of the issue. 31 | - Describe your issue as good as possible that may ease the process of issue-reviewing by a community member. 32 | 33 | ## How to contribute 34 | 35 | 1. Fork the project and clone it to your local machine. Follow the [setup](https://github.com/ani1609/Spendwise#getting-started) guideline. 36 | 2. Always take a pull from the remote repository to your master branch to keep it at par with the main project(updated repository). 37 | 38 | git pull upstream main 39 | 40 | 3. Create a branch. For example, if you are going to work on issue number #44 (which is, say, a new feature for ‘forgot password’ management): 41 | 42 | git checkout -b forgot-password#44 43 | 44 | This both creates and checks out that branch in one command. 45 | The feature name should provide a (short) description of the issue. 46 | 47 | 4. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. 48 | 5. Commit your changes and push it to your fork of the repository. 49 | 6. Create Pull Request (PR). Make sure to comment the issue that your PR is supposed to solve. 50 | 51 | ## Create a pull request 52 | 53 | - Try to keep the pull requests small. A pull request should try its very best to address only a single concern. 54 | - For work in progress pull requests, please use the Draft PR feature. 55 | - Make sure all tests pass and add additional tests for the code you submit. 56 | - Document your reasoning behind the changes. Explain why you wrote the code in the way you did. The code should explain what it does. 57 | - If there's an existing issue, reference to it by adding something like `References/Closes/Fixes/Resolves #123`, where 123 is the issue number. 58 | - Please fill out the PR Template when making a PR. 59 | 60 | > Please note: maintainers may close your PR if it has gone stale or if we don't plan to merge the code. 61 | 62 | ## Pull request reviews 63 | 64 | - Requested changes must be resolved (with code or discussion) before merging. 65 | - If you make changes to a PR, be sure to re-request a review. 66 | - Don't repeadetely tag someone(may be it is not the right time to review your PR), be patient. 67 | - Do not 'resolve conversation' unnecessary raised by a community member or any workflow tools(codeclimate or hound) as they may have some purpose, try to resolve the request changes and if any help wanted tag a community member to give views about that. 68 | 69 | #### PR Labels 70 | 71 | - `under-review` labeled PRs are under review by core team. 72 | - `waiting for contributor` labeled PRs are meant to waiting for contributor to respond. 73 | - `waiting for design` labeled PRs are meant to waiting for review from UI/UX core team. 74 | - `no activity` labeled PRs are meant to have no activity in the PR from since a while. 75 | - `blocked` labeled PRs are meant not to go ahead for review. 76 | - `do not merge` labeled PRs are meant not to merge the PR right now(may be later). 77 | - [awaiting-approval](https://github.com/CircuitVerse/CircuitVerse/labels/awaiting-approval) labeled PRs are meant to be waiting for other communtiy members. 78 | 79 | 80 | ### Code Contributors 81 | 82 | This project exists because of all the people who have [contributed]((CONTRIBUTING.md)). 83 | 84 | 85 | ## The bottom line 86 | 87 | We are all humans trying to work together to improve the community. Always be kind and appreciate the need for tradeoffs. ❤️ 88 | -------------------------------------------------------------------------------- /src/components/profile/RightDashboard.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import cameraIcon from "../../icons/camera.svg"; 3 | 4 | const RightDashboard = () => { 5 | const [triangleCount, setTriangleCount] = useState(0); 6 | const [firstName, setFirstName] = useState(""); 7 | const [lastName, setLastName] = useState(""); 8 | 9 | const dynamicWidth = "calc(100% - 240px)"; 10 | 11 | const handleFirstNameChange = (event) => { 12 | setFirstName(event.target.value); 13 | }; 14 | const handleLastNameChange = (event) => { 15 | setLastName(event.target.value); 16 | }; 17 | 18 | // for calculating no of triangles on two rows according to viewport width 19 | useEffect(() => { 20 | const calculateTriangles = () => { 21 | const containerWidth = 22 | document.getElementById("triangle-row")?.offsetWidth || 0; 23 | const triangleWidth = 82; 24 | const trianglesPerRow = Math.floor(containerWidth / triangleWidth); 25 | const totalTriangles = trianglesPerRow * 2; 26 | setTriangleCount(totalTriangles); 27 | }; 28 | // Initial calculation 29 | calculateTriangles(); 30 | // Recalculate on window resize 31 | window.addEventListener("resize", calculateTriangles); 32 | // Cleanup on component unmount 33 | return () => { 34 | window.removeEventListener("resize", calculateTriangles); 35 | }; 36 | }, []); 37 | 38 | return ( 39 | <> 40 |
41 | {/* Profile Heading */} 42 |
43 |

44 | Profile 45 |

46 |
47 | {/* Background Containing Triangles on two rows */} 48 |
49 |
50 |
54 | {[...Array(triangleCount)].map((_, index) => ( 55 |
60 | ))} 61 |
62 |
63 |
64 | 65 | 66 | Change Cover 67 | 68 |
69 |
70 |
71 |
72 | 73 | {/* Profile Container - includes info and settings */} 74 |
75 |
76 |
77 | 82 |
83 |
84 |
85 | 86 |
87 |
88 |
89 |

90 | Your Name 91 |

92 |
93 |
94 | 95 | {/* Profile Settings */} 96 |
97 |

98 | Account Settings 99 |

100 |
101 |
102 | 108 | 115 |
116 |
117 | 123 | 130 |
131 |
132 | 135 |
136 |
137 |
138 |
139 |
140 | 141 | ); 142 | }; 143 | 144 | export default RightDashboard; 145 | -------------------------------------------------------------------------------- /src/components/profile/PredefinedTransactions.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { RxCross2 } from "react-icons/rx"; 3 | import { IoIosArrowUp, IoIosArrowDown } from "react-icons/io"; 4 | import plusFill from "../../icons/ph_plus-fill.svg"; 5 | import PredefinedTransactionsCards from "./PredefinedTransactionsCards"; 6 | import "./PredefinedTransactions.css"; 7 | 8 | const PredefinedTransactions = () => { 9 | // const dynamicWidth = "calc(100% - 240px)"; 10 | 11 | const [transactionType, setTransactionType] = useState("Expense"); 12 | const [category, setCategory] = useState("food"); 13 | const [date, setDate] = useState(""); 14 | const [amount, setAmount] = useState(""); 15 | const [description, setDescription] = useState(""); 16 | const [formVisible, setFormVisible] = useState(false); 17 | 18 | const handleAddNewTransactions = () => { 19 | setFormVisible(true); 20 | }; 21 | 22 | const handleCloseClick = (e) => { 23 | e.preventDefault(); 24 | setFormVisible(false); 25 | }; 26 | 27 | console.log(transactionType); 28 | return ( 29 | <> 30 |
31 |
32 |

33 | Predefined Transactions 34 |

35 |

36 | Income/Expense 37 |

38 |
39 |
40 | 41 | 44 |
45 | 46 |
47 |
48 |
49 |
50 | {/* Main Form */} 51 |
52 | 55 | 56 | {/* Left side of the form */} 57 |
58 |
59 |

Transaction Type

60 |
61 | setTransactionType("Income")} 68 | className="mr-2 appearance-none w-3 h-3 bg-slate-300 rounded-full" 69 | /> 70 | 71 |
72 |
73 | setTransactionType("Expense")} 80 | className="mr-2 appearance-none w-3 h-3 bg-slate-300 rounded-full" 81 | /> 82 | 83 |
84 |
85 |
86 | 87 |
88 | 100 | 101 | 102 | 103 | 104 |
105 |
106 |
107 | 108 | setDate(e.target.value)} 114 | className="w-[10.25rem] h-[2.2rem] border border-[#6E9DF7] border-opacity-50 px-2"/> 115 |
116 |
117 | 118 | {/* Right side of the form */} 119 |
120 |
121 | 122 | setAmount(e.target.value)} 128 | className="w-[10.25rem] h-[2.2rem] border border-[#6E9DF7] border-opacity-50 px-2 appearance-none"/> 129 |
130 |
131 | 132 | setDescription(e.target.value)} 138 | className="w-[10.25rem] h-[2.2rem] border border-[#6E9DF7] border-opacity-50 px-2"/> 139 |
140 |
141 | 142 |
143 |
144 |
145 |
146 | 147 | ); 148 | }; 149 | 150 | export default PredefinedTransactions; 151 | -------------------------------------------------------------------------------- /src/components/Login.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import "../index.css"; 3 | import "../styles/Login.css"; 4 | import { ReactComponent as Close } from "../icons/close.svg"; 5 | import { 6 | getAuth, 7 | signInWithPopup, 8 | GoogleAuthProvider, 9 | signInWithEmailAndPassword, 10 | } from "firebase/auth"; 11 | import { addDoc, getDocs, query, where } from "firebase/firestore"; 12 | import { usersCollection } from "../firebaseConfig"; 13 | import { FaEye, FaEyeSlash } from "react-icons/fa"; 14 | 15 | // import search from "../icons/search.svg"; 16 | 17 | function Login({ setShowLoginForm }) { 18 | const [invalidEmailPassword, setInvalidEmailPassword] = useState(false); 19 | const [emailNotVerified, setEmailNotVerified] = useState(false); 20 | const [passwordShow, setPasswordShow] = useState(false); 21 | const [loginData, setLoginData] = useState({ 22 | email: "", 23 | password: "", 24 | }); 25 | 26 | const passwordShowToggle = () => { 27 | setPasswordShow(!passwordShow); 28 | }; 29 | 30 | const [loading, setLoading] = useState(false); 31 | 32 | const handleLogin = async (e) => { 33 | e.preventDefault(); 34 | setLoading(true); 35 | try { 36 | const auth = getAuth(); 37 | const response = await signInWithEmailAndPassword( 38 | auth, 39 | loginData.email, 40 | loginData.password 41 | ); 42 | console.log("User logged in: ", response); 43 | if (!response.user.emailVerified) { 44 | setLoading(false); 45 | setInvalidEmailPassword(false); 46 | setEmailNotVerified(true); 47 | } else { 48 | const userObject = { 49 | email: response.user.email, 50 | password: loginData.password, 51 | }; 52 | const existingUserQuery = query( 53 | usersCollection, 54 | where("email", "==", userObject.email) 55 | ); 56 | const existingUserSnapshot = await getDocs(existingUserQuery); 57 | if (existingUserSnapshot.size === 0) { 58 | const addedUser = await addDoc(usersCollection, userObject); 59 | console.log("New User added"); 60 | localStorage.setItem( 61 | "expenseTrackerUserFirebaseRefId", 62 | JSON.stringify(addedUser.id) 63 | ); 64 | window.location.reload(); 65 | } else { 66 | existingUserSnapshot.forEach((doc) => { 67 | console.log("User already exists: ", doc.id); 68 | localStorage.setItem( 69 | "expenseTrackerUserFirebaseRefId", 70 | JSON.stringify(doc.id) 71 | ); 72 | window.location.reload(); 73 | }); 74 | } 75 | } 76 | } catch (error) { 77 | setLoading(false); 78 | if (error.code === "auth/invalid-credential") { 79 | setEmailNotVerified(false); 80 | setInvalidEmailPassword(true); 81 | } else { 82 | console.error(error); 83 | } 84 | } finally { 85 | setLoading(false); 86 | } 87 | }; 88 | 89 | const handleGoogleSignIn = async () => { 90 | try { 91 | const provider = new GoogleAuthProvider(); 92 | const auth = getAuth(); 93 | const response = await signInWithPopup(auth, provider); 94 | const userObject = { 95 | name: response.user.displayName, 96 | email: response.user.email, 97 | profilePicture: response.user.photoURL, 98 | }; 99 | const existingUserQuery = query( 100 | usersCollection, 101 | where("email", "==", userObject.email) 102 | ); 103 | const existingUserSnapshot = await getDocs(existingUserQuery); 104 | if (existingUserSnapshot.size === 0) { 105 | const addedUser = await addDoc(usersCollection, userObject); 106 | console.log("New User added"); 107 | localStorage.setItem( 108 | "expenseTrackerUserFirebaseRefId", 109 | JSON.stringify(addedUser.id) 110 | ); 111 | window.location.reload(); 112 | } else { 113 | existingUserSnapshot.forEach((doc) => { 114 | console.log("User already exists: ", doc.id); 115 | localStorage.setItem( 116 | "expenseTrackerUserFirebaseRefId", 117 | JSON.stringify(doc.id) 118 | ); 119 | window.location.reload(); 120 | }); 121 | } 122 | } catch (error) { 123 | console.log("Some error occurred", error); 124 | } 125 | }; 126 | 127 | return ( 128 |
e.stopPropagation()}> 129 |

Welcome Back

130 |
{ 133 | setShowLoginForm(false); 134 | }} 135 | > 136 | 137 |
138 |
139 | 144 | setLoginData({ ...loginData, email: e.target.value }) 145 | } 146 | required 147 | /> 148 | {/* setLoginData({ ...loginData, password: e.target.value })} 153 | required 154 | /> */} 155 |
156 | 162 | setLoginData({ ...loginData, password: e.target.value }) 163 | } 164 | required 165 | /> 166 | {!passwordShow ? ( 167 | 171 | ) : ( 172 | 176 | )} 177 |
178 | {invalidEmailPassword && ( 179 |

Invalid email or password

180 | )} 181 | {emailNotVerified && ( 182 |

Email not verified

183 | )} 184 | 185 | 200 |
204 |
  or  {" "} 205 |
206 |
207 |
208 | 220 |
221 | ); 222 | } 223 | 224 | export default Login; 225 | -------------------------------------------------------------------------------- /src/components/Signup.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import "../index.css"; 3 | import "../styles/Signup.css"; 4 | import { ReactComponent as Close } from "../icons/close.svg"; 5 | import { 6 | createUserWithEmailAndPassword, 7 | sendEmailVerification, 8 | getAuth, 9 | GoogleAuthProvider, 10 | signInWithPopup, 11 | } from "firebase/auth"; 12 | import { addDoc, getDocs, query, where } from "firebase/firestore"; 13 | import { usersCollection } from "../firebaseConfig"; 14 | 15 | function Signup({ setShowSignupForm }) { 16 | const [invalidEmailFOrmat, setInvalidEmailFormat] = useState(false); 17 | const [userExists, setUserExists] = useState(false); 18 | const [passwordUnmatched, setPasswordUnmatched] = useState(false); 19 | const [invalidName, setInvalidName] = useState(false); 20 | const [invalidPassword, setInvalidPassword] = useState(false); 21 | const [emailVerificationSent, setEmailVerificationSent] = useState(false); 22 | const [signupData, setSignupData] = useState({ 23 | name: "", 24 | email: "", 25 | password: "", 26 | confirmPassword: "", 27 | }); 28 | const [loading, setLoading] = useState(false); 29 | 30 | const handleNameChange = (e) => { 31 | const isValidName = /^[a-zA-Z\s]*$/.test(e.target.value); 32 | 33 | if (isValidName || e.target.value === "") { 34 | setSignupData({ ...signupData, name: e.target.value }); 35 | setInvalidName(false); 36 | } else { 37 | setInvalidName(true); 38 | } 39 | }; 40 | 41 | const handleSignup = async (e) => { 42 | e.preventDefault(); 43 | setLoading(true); 44 | if (signupData.password !== signupData.confirmPassword) { 45 | setUserExists(false); 46 | setInvalidEmailFormat(false); 47 | setInvalidPassword(false); 48 | setInvalidName(false); 49 | setEmailVerificationSent(false); 50 | setPasswordUnmatched(true); 51 | setLoading(false); 52 | return; 53 | } 54 | 55 | if (signupData.password.length < 8) { 56 | setUserExists(false); 57 | setPasswordUnmatched(false); 58 | setInvalidEmailFormat(false); 59 | setInvalidName(false); 60 | setEmailVerificationSent(false); 61 | setInvalidPassword(true); 62 | setLoading(false); 63 | return; 64 | } else { 65 | setInvalidPassword(false); 66 | } 67 | try { 68 | const auth = getAuth(); 69 | const response = await createUserWithEmailAndPassword( 70 | auth, 71 | signupData.email, 72 | signupData.password 73 | ); 74 | // Send email verification 75 | await sendEmailVerification(response.user); 76 | setEmailVerificationSent(true); 77 | } catch (error) { 78 | if (error.code === "auth/email-already-in-use") { 79 | setPasswordUnmatched(false); 80 | setInvalidEmailFormat(false); 81 | setInvalidPassword(false); 82 | setInvalidName(false); 83 | setEmailVerificationSent(false); 84 | setUserExists(true); 85 | } else if (error.code === "auth/invalid-email") { 86 | setUserExists(false); 87 | setPasswordUnmatched(false); 88 | setInvalidPassword(false); 89 | setInvalidName(false); 90 | setEmailVerificationSent(false); 91 | setInvalidEmailFormat(true); 92 | } else { 93 | console.error(error); 94 | } 95 | } finally { 96 | setLoading(false); 97 | } 98 | }; 99 | 100 | const handleGoogleSignIn = async () => { 101 | try { 102 | const provider = new GoogleAuthProvider(); 103 | const auth = getAuth(); 104 | const response = await signInWithPopup(auth, provider); 105 | const userObject = { 106 | name: response.user.displayName, 107 | email: response.user.email, 108 | profilePicture: response.user.photoURL, 109 | }; 110 | const existingUserQuery = query( 111 | usersCollection, 112 | where("email", "==", userObject.email) 113 | ); 114 | const existingUserSnapshot = await getDocs(existingUserQuery); 115 | if (existingUserSnapshot.size === 0) { 116 | const addedUser = await addDoc(usersCollection, userObject); 117 | localStorage.setItem( 118 | "expenseTrackerUserFirebaseRefId", 119 | JSON.stringify(addedUser.id) 120 | ); 121 | window.location.reload(); 122 | } else { 123 | existingUserSnapshot.forEach((doc) => { 124 | localStorage.setItem( 125 | "expenseTrackerUserFirebaseRefId", 126 | JSON.stringify(doc.id) 127 | ); 128 | window.location.reload(); 129 | }); 130 | } 131 | } catch (error) { 132 | console.log("Some error occurred", error); 133 | } 134 | }; 135 | 136 | return ( 137 |
e.stopPropagation()}> 138 |

Create Your Account

139 |
{ 142 | setShowSignupForm(false); 143 | }} 144 | > 145 | 146 |
147 |
148 | 155 | 160 | setSignupData({ ...signupData, email: e.target.value }) 161 | } 162 | required 163 | /> 164 | 169 | setSignupData({ ...signupData, password: e.target.value }) 170 | } 171 | required 172 | /> 173 | 178 | setSignupData({ ...signupData, confirmPassword: e.target.value }) 179 | } 180 | required 181 | /> 182 | {passwordUnmatched && ( 183 |

Passwords do not match

184 | )} 185 | {userExists &&

User already exists

} 186 | {invalidEmailFOrmat && ( 187 |

Invalid email format

188 | )} 189 | {invalidName &&

Invalid name format

} 190 | {invalidPassword && ( 191 |

Invalid Password format

192 | )} 193 | {emailVerificationSent && ( 194 |

195 | Email verification sent. Please verify. 196 |

197 | )} 198 | 209 |
213 |
  or  {" "} 214 |
215 |
216 |
217 | 229 |
230 | ); 231 | } 232 | 233 | export default Signup; 234 | -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from "react"; 2 | import "../styles/Navbar.css"; 3 | import "../index.css"; 4 | import Login from "./Login"; 5 | import Signup from "./Signup"; 6 | import { usersCollection } from "../firebaseConfig"; 7 | import { doc, getDoc } from "firebase/firestore"; 8 | import { ReactComponent as Moon } from "../icons/moon.svg"; 9 | import { ReactComponent as Sun } from "../icons/sun.svg"; 10 | import { SiMoneygram } from "react-icons/si"; 11 | import { Link } from "react-router-dom"; 12 | import { ThemeContext } from "../context/ThemeContext"; 13 | import { LogOut, Menu, X } from "lucide-react"; 14 | import { 15 | Drawer, 16 | DrawerClose, 17 | DrawerContent, 18 | DrawerDescription, 19 | DrawerHeader, 20 | DrawerTrigger, 21 | } from "./ui/drawer"; 22 | 23 | function Navbar(props) { 24 | const userJWTToken = JSON.parse( 25 | localStorage.getItem("expenseTrackerUserJWTToken") 26 | ); 27 | const userFirebaseRefId = JSON.parse( 28 | localStorage.getItem("expenseTrackerUserFirebaseRefId") 29 | ); 30 | const [showLoginForm, setShowLoginForm] = useState(false); 31 | const [user, setUser] = useState({}); 32 | const { toggle, theme } = useContext(ThemeContext); 33 | 34 | const fetchUserFromFirebase = async (docrefId) => { 35 | try { 36 | const userDocRef = doc(usersCollection, docrefId); 37 | const userDocSnapshot = await getDoc(userDocRef); 38 | 39 | if (userDocSnapshot.exists()) { 40 | const userData = userDocSnapshot.data(); 41 | console.log("User data from Firestore:", userData); 42 | setUser(userData); 43 | } else { 44 | return null; 45 | } 46 | } catch (error) { 47 | console.error("Error fetching user from Firestore:", error); 48 | return null; 49 | } 50 | }; 51 | 52 | useEffect(() => { 53 | if (userFirebaseRefId) { 54 | fetchUserFromFirebase(userFirebaseRefId); 55 | } 56 | }, [userJWTToken]); 57 | 58 | const handleLogOut = () => { 59 | localStorage.removeItem("expenseTrackerUserFirebaseRefId"); 60 | window.location.href = "/"; 61 | }; 62 | 63 | return ( 64 |
65 | 69 | 70 | SPENDWISE 71 | 72 |
73 | {userFirebaseRefId && ( 74 | 80 | 93 | 94 | 95 | 96 | 97 | )} 98 | {userFirebaseRefId && ( 99 |
100 | {theme === "dark" ? ( 101 | <> 102 | 103 | 104 | ) : ( 105 | <> 106 | 107 | 108 | )} 109 |
110 | )} 111 | {userFirebaseRefId && ( 112 |
113 | 114 |
115 | )} 116 | {userFirebaseRefId ? ( 117 |
118 | {/* */} 119 | {/* Link to the /profile route */} 120 |
121 | profile 126 |
127 | {/* */} 128 |
129 | ) : ( 130 |
131 | 139 | 147 |
148 | )} 149 |
150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | SPENDWISE 159 | 160 | 161 | 162 | 163 | 164 | 170 | 183 | 184 | 185 | 186 | 187 | {!userFirebaseRefId && ( 188 | 189 |
190 | 198 |
199 |
200 | )} 201 | {!userFirebaseRefId && ( 202 | 203 |
204 | 212 |
213 |
214 | )} 215 | {userFirebaseRefId && ( 216 | <> 217 |
218 | {theme === "dark" ? ( 219 | <> 220 | 221 | 222 | ) : ( 223 | <> 224 | 225 | 226 | )} 227 |
228 |
229 | 230 |
231 | 232 | )} 233 | {userFirebaseRefId && ( 234 |
235 | profile 240 |
241 | )} 242 |
243 |
244 | 245 | 246 | {showLoginForm && ( 247 |
248 | 249 |
250 | )} 251 | 252 | {props.showSignupForm && ( 253 |
254 | 255 |
256 | )} 257 |
258 | ); 259 | } 260 | 261 | export default Navbar; 262 | -------------------------------------------------------------------------------- /src/components/TransactionForm.js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { addDoc, query, where, updateDoc, getDocs } from "firebase/firestore"; 3 | import { transactionsCollection } from "../firebaseConfig"; 4 | import { toast } from "react-toastify"; 5 | import "react-toastify/dist/ReactToastify.css"; 6 | import "../styles/ExpenseTracker.css"; 7 | import "../styles/TransactionForm.css"; 8 | import "../index.css"; 9 | import { ThemeContext } from "../context/ThemeContext"; 10 | 11 | function TransactionForm({ 12 | user, 13 | editEnabled, 14 | setEditEnabled, 15 | formData, 16 | setFormData, 17 | }) { 18 | const newTransaction = () => { 19 | setEditEnabled(false); 20 | const addedTransaction = { 21 | transactionType: "", 22 | category: "", 23 | date: "", 24 | description: "", 25 | amount: "", 26 | }; 27 | setFormData(addedTransaction); 28 | }; 29 | 30 | const generateTransactionId = () => { 31 | const characters = 32 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 33 | let transactionId = ""; 34 | for (let i = 0; i < 10; i++) { 35 | const randomIndex = Math.floor(Math.random() * characters.length); 36 | transactionId += characters.charAt(randomIndex); 37 | } 38 | return transactionId; 39 | }; 40 | 41 | const handleChange = (e) => { 42 | const targetValue = 43 | e.target.name === "amount" ? parseFloat(e.target.value) : e.target.value; 44 | setFormData({ ...formData, [e.target.name]: targetValue }); 45 | }; 46 | 47 | const handleSubmit = async (e) => { 48 | e.preventDefault(); 49 | const today = new Date(); 50 | if (formData.transactionType === "") { 51 | toast.error("Please select a transaction type."); 52 | return; 53 | } 54 | if (formData.transactionType === "Income" && formData.category !== "NULL") { 55 | setFormData({ ...formData, category: "NULL" }); 56 | } 57 | if (formData.transactionType === "Expense" && formData.category === "") { 58 | toast.error("Please select a category."); 59 | return; 60 | } 61 | if (formData.date === "") { 62 | toast.error("Please select a date."); 63 | return; 64 | } 65 | const selectedDate = new Date(formData.date); 66 | if (selectedDate > today) { 67 | toast.error("Please select a date in the past or present."); 68 | return; 69 | } 70 | if (formData.amount === "") { 71 | toast.error("Please enter an amount."); 72 | return; 73 | } 74 | if (formData.description === "") { 75 | toast.error("Please enter a description."); 76 | return; 77 | } 78 | if (editEnabled) { 79 | try { 80 | const querySnapshot = await getDocs( 81 | query( 82 | transactionsCollection, 83 | where("transactionId", "==", formData.transactionId) 84 | ) 85 | ); 86 | if (querySnapshot.empty) { 87 | toast.error("Transaction not found."); 88 | } else { 89 | const transactionDoc = querySnapshot.docs[0].ref; 90 | const transactionObject = { 91 | transactionType: formData.transactionType, 92 | category: formData.category, 93 | date: formData.date, 94 | amount: formData.amount, 95 | description: formData.description, 96 | }; 97 | await updateDoc(transactionDoc, transactionObject); 98 | setFormData({ 99 | transactionType: "", 100 | category: "", 101 | date: "", 102 | amount: "", 103 | description: "", 104 | }); 105 | setEditEnabled(false); 106 | toast.success("Transaction edited successfully."); 107 | } 108 | } catch (error) { 109 | toast.error("Error editing transaction."); 110 | } 111 | } else { 112 | try { 113 | const transactionObject = { 114 | email: user.email, 115 | transactionType: formData.transactionType, 116 | category: formData.category, 117 | date: formData.date, 118 | amount: formData.amount, 119 | description: formData.description, 120 | transactionId: generateTransactionId(), 121 | created_at: new Date(), 122 | }; 123 | await addDoc(transactionsCollection, transactionObject); 124 | setFormData({ 125 | transactionType: "", 126 | category: "", 127 | date: "", 128 | amount: "", 129 | description: "", 130 | }); 131 | toast.success("Transaction added successfully."); 132 | } catch (error) { 133 | toast.error("Error adding transaction."); 134 | } 135 | } 136 | }; 137 | 138 | useEffect(() => { 139 | if (formData.transactionType === "Income") { 140 | setFormData({ ...formData, category: "NULL" }); 141 | } 142 | }, [formData.transactionType]); 143 | 144 | return ( 145 |
146 |
147 | {editEnabled ? ( 148 | <> 149 |

Edit transaction

150 | 157 | 158 | ) : ( 159 | <> 160 |

Add new transaction

161 | 162 | )} 163 |
164 | 165 |
169 |
170 |
171 | Transaction Type 172 | 185 | 198 |
199 |
203 | 204 | 253 |
254 |
258 | 259 | 268 |
269 |
270 | 271 |
272 |
273 | 274 | 284 |
285 |
289 | 290 | 300 |
301 | 308 |
309 |
310 |
311 | ); 312 | } 313 | 314 | export default TransactionForm; 315 | -------------------------------------------------------------------------------- /src/components/Transactions.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { 3 | query, 4 | where, 5 | onSnapshot, 6 | orderBy, 7 | getDocs, 8 | deleteDoc, 9 | } from "firebase/firestore"; 10 | import { transactionsCollection } from "../firebaseConfig"; 11 | import { toast } from "react-toastify"; 12 | import "react-toastify/dist/ReactToastify.css"; 13 | import { ReactComponent as Edit } from "../icons/edit.svg"; 14 | import { ReactComponent as Delete } from "../icons/delete.svg"; 15 | import { ReactComponent as Food } from "../icons/food.svg"; 16 | import { ReactComponent as Travel } from "../icons/travel.svg"; 17 | import { ReactComponent as Shopping } from "../icons/shopping.svg"; 18 | import { ReactComponent as Bills } from "../icons/bills.svg"; 19 | import { ReactComponent as Others } from "../icons/others.svg"; 20 | import { ReactComponent as Plus } from "../icons/plus1.svg"; 21 | import MyLoader from "./TransactionLoading"; 22 | import "../styles/Transactions.css"; 23 | import "../index.css"; 24 | 25 | function Transactions({ 26 | transactions, 27 | setTransactions, 28 | user, 29 | setEditEnabled, 30 | setFormData, 31 | descriptionChars, 32 | }) { 33 | const [dateFillter, setDateFilter] = useState(""); 34 | const [transactionFilter, setTransactionFilter] = useState([]); 35 | const [transactionType, setTransactionType] = useState(""); 36 | const [categoryFilter, setCategoryFilter] = useState(""); 37 | const [isDropdownOpen, setDropdownOpen] = useState(false); 38 | const [isCategoryDropdownOpen, setCategoryDropdownOpen] = useState(false); 39 | const [transactionsLoading, setTransactionsLoading] = useState(true); 40 | 41 | useEffect(() => { 42 | if (user && user.email) { 43 | const q = query( 44 | transactionsCollection, 45 | where("email", "==", user.email), 46 | orderBy("created_at", "asc") // or 'asc' for ascending order 47 | ); 48 | 49 | console.log("the user is", user); 50 | console.log("the transactions are", q); 51 | 52 | return onSnapshot(q, (snapshot) => { 53 | let updatedTransactions = snapshot.docs.map((doc) => doc.data()); 54 | setTransactions(updatedTransactions); 55 | const transactiontype = localStorage.getItem("transactionType") 56 | ? localStorage.getItem("transactionType") 57 | : ""; 58 | const datefiter = localStorage.getItem("dateFilter") 59 | ? localStorage.getItem("dateFilter") 60 | : ""; 61 | 62 | if (transactiontype && transactiontype !== "All") { 63 | updatedTransactions = updatedTransactions.filter( 64 | (item) => item.transactionType === transactiontype 65 | ); 66 | } 67 | if (categoryFilter && categoryFilter !== "All") { 68 | updatedTransactions = updatedTransactions.filter( 69 | (item) => item.category === categoryFilter 70 | ); 71 | } 72 | if (datefiter) { 73 | updatedTransactions = updatedTransactions.filter( 74 | (item) => item.date === datefiter 75 | ); 76 | } 77 | 78 | setTransactionFilter(updatedTransactions); 79 | 80 | if (transactionsLoading) { 81 | setTransactionsLoading(false); 82 | } 83 | }); 84 | } 85 | }, [user, transactionType, categoryFilter, dateFillter]); 86 | 87 | useEffect(() => { 88 | localStorage.removeItem("transactionType"); 89 | localStorage.removeItem("dateFilter"); 90 | }, []); 91 | 92 | const toggleDropdown = () => { 93 | setDropdownOpen(!isDropdownOpen); 94 | }; 95 | 96 | const closeDropdown = () => { 97 | setDropdownOpen(false); 98 | }; 99 | 100 | const toggleCategoryDropdown = () => { 101 | setCategoryDropdownOpen(!isCategoryDropdownOpen); 102 | }; 103 | 104 | const closeCategoryDropdown = () => { 105 | setCategoryDropdownOpen(false); 106 | }; 107 | 108 | const handleEdit = (transactionId, index) => { 109 | setEditEnabled(true); 110 | const editedTransaction = { 111 | ...transactions.filter((e) => e.transactionId === transactionId)[0], 112 | }; 113 | const date = new Date(editedTransaction.date); 114 | const formattedDate = date.toISOString().split("T")[0]; 115 | editedTransaction.date = formattedDate; 116 | setFormData(editedTransaction); 117 | }; 118 | 119 | const handleDelete = async (transactionId, index) => { 120 | try { 121 | const querySnapshot = await getDocs( 122 | query( 123 | transactionsCollection, 124 | where("transactionId", "==", transactionId) 125 | ) 126 | ); 127 | 128 | if (querySnapshot.empty) { 129 | toast.error("Transaction not found."); 130 | } else { 131 | const transactionDoc = querySnapshot.docs[0].ref; 132 | await deleteDoc(transactionDoc); 133 | toast.success("Transaction deleted successfully."); 134 | } 135 | } catch (error) { 136 | toast.error("Error deleting transaction."); 137 | } 138 | }; 139 | 140 | const TransactionTypeChange = (e) => { 141 | if (e.target.value === "All") { 142 | let transactionnews = transactions; 143 | if (dateFillter) { 144 | transactionnews = transactionnews.filter( 145 | (item) => item.date === dateFillter 146 | ); 147 | setTransactionFilter(transactionnews); 148 | } 149 | } else { 150 | let transactionnews = transactions.filter( 151 | (item) => item.transactionType === e.target.value 152 | ); 153 | if (dateFillter) { 154 | transactionnews = transactionnews.filter( 155 | (item) => item.date === dateFillter 156 | ); 157 | setTransactionFilter(transactionnews); 158 | } 159 | } 160 | if (e.target.value) { 161 | localStorage.setItem("transactionType", e.target.value); 162 | } else localStorage.removeItem("transactionType"); 163 | setTransactionType(e.target.value); 164 | setCategoryFilter(""); 165 | closeDropdown(); 166 | }; 167 | 168 | const CategoryChange = (e) => { 169 | setCategoryFilter(e.target.value); 170 | closeCategoryDropdown(); 171 | }; 172 | 173 | const changeDateFilter = (e) => { 174 | if (e.target.value) { 175 | let transactionsfilter = transactions.filter( 176 | (item) => item.date === e.target.value 177 | ); 178 | if (transactionType && transactionType !== "All") { 179 | transactionsfilter = transactionsfilter.filter( 180 | (item) => item.transactionType === transactionType 181 | ); 182 | setTransactionFilter(transactionsfilter); 183 | } 184 | localStorage.setItem("dateFilter", e.target.value); 185 | } else { 186 | let transactionsfilter = transactions; 187 | if (transactionType && transactionType !== "All") { 188 | transactionsfilter = transactionsfilter.filter( 189 | (item) => item.transactionType === transactionType 190 | ); 191 | setTransactionFilter(transactionsfilter); 192 | } 193 | localStorage.removeItem("dateFilter"); 194 | } 195 | setDateFilter(e.target.value); 196 | }; 197 | 198 | return ( 199 |
200 |
201 |

Transactions

202 |
203 |
204 |
205 |
210 |
214 | {transactionType || "Type"} 215 | 216 |
217 | {isDropdownOpen && ( 218 |
219 |
221 | TransactionTypeChange({ target: { value: "All" } }) 222 | } 223 | className="option bg-secondary-bg text-start cursor-pointer px-4 py-[2.5px] hover:!bg-primary-bg hover:text-[#ffffff]" 224 | > 225 | All 226 |
227 |
229 | TransactionTypeChange({ target: { value: "Income" } }) 230 | } 231 | className="option bg-secondary-bg text-start cursor-pointer px-4 py-[2.5px] hover:!bg-primary-bg hover:text-[#ffffff]" 232 | > 233 | Income 234 |
235 |
237 | TransactionTypeChange({ target: { value: "Expense" } }) 238 | } 239 | className="option bg-secondary-bg text-start cursor-pointer px-4 py-[2.5px] hover:!bg-primary-bg hover:text-[#ffffff]" 240 | > 241 | Expense 242 |
243 |
244 | )} 245 |
246 |
247 |
248 |
249 |
254 |
258 | {categoryFilter || "Category"} 259 | 260 |
261 | {isCategoryDropdownOpen && ( 262 |
263 |
265 | CategoryChange({ target: { value: "All" } }) 266 | } 267 | className="option bg-secondary-bg text-start cursor-pointer px-4 py-[2.5px] hover:!bg-primary-bg hover:text-[#ffffff]" 268 | > 269 | All 270 |
271 |
273 | CategoryChange({ target: { value: "Food" } }) 274 | } 275 | className="option bg-secondary-bg text-start cursor-pointer px-4 py-[2.5px] hover:!bg-primary-bg hover:text-[#ffffff]" 276 | > 277 | Food 278 |
279 |
281 | CategoryChange({ target: { value: "Travel" } }) 282 | } 283 | className="option bg-secondary-bg text-start cursor-pointer px-4 py-[2.5px] hover:!bg-primary-bg hover:text-[#ffffff]" 284 | > 285 | Travel 286 |
287 |
289 | CategoryChange({ target: { value: "Shopping" } }) 290 | } 291 | className="option bg-secondary-bg text-start cursor-pointer px-4 py-[2.5px] hover:!bg-primary-bg hover:text-[#ffffff]" 292 | > 293 | Shopping 294 |
295 |
297 | CategoryChange({ target: { value: "Bills" } }) 298 | } 299 | className="option bg-secondary-bg text-start cursor-pointer px-4 py-[2.5px] hover:!bg-primary-bg hover:text-[#ffffff]" 300 | > 301 | Bills 302 |
303 |
305 | CategoryChange({ target: { value: "Others" } }) 306 | } 307 | className="option bg-secondary-bg text-start cursor-pointer px-4 py-[2.5px] hover:!bg-primary-bg hover:text-[#ffffff]" 308 | > 309 | Others 310 |
311 |
312 | )} 313 |
314 |
315 |
316 | changeDateFilter(e)} 321 | placeholder="Date" 322 | required 323 | className="selected-value cursor-pointer bg-secondary-bg border border-border rounded h-[26px] px-1 text-text max-sm:w-[130px] outline-none" 324 | /> 325 |
326 |
327 | 328 | {transactionFilter.length > 0 ? ( 329 |
    330 | {transactionFilter.map((transaction, index) => 331 | transaction.transactionType === "Income" ? ( 332 |
  • 336 |
    337 | 338 |
    339 |
    340 | {transaction.description.length > descriptionChars ? ( 341 |

    342 | {transaction.description.substring(0, descriptionChars) + 343 | "..."} 344 |

    345 | ) : ( 346 |

    {transaction.description}

    347 | )} 348 |

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

    349 |
    350 |

    +₹{transaction.amount}

    351 |
    352 | handleEdit(transaction.transactionId, index)} 355 | /> 356 | 359 | handleDelete(transaction.transactionId, index) 360 | } 361 | /> 362 |
    363 |
  • 364 | ) : ( 365 |
  • 369 |
    370 | {transaction.category === "Food" ? ( 371 | 372 | ) : null} 373 | {transaction.category === "Travel" ? ( 374 | 375 | ) : null} 376 | {transaction.category === "Shopping" ? ( 377 | 378 | ) : null} 379 | {transaction.category === "Bills" ? ( 380 | 381 | ) : null} 382 | {transaction.category === "Others" ? ( 383 | 384 | ) : null} 385 |
    386 |
    387 | {transaction.description.length > descriptionChars ? ( 388 |

    389 | {transaction.description.substring(0, descriptionChars) + 390 | "..."} 391 |

    392 | ) : ( 393 |

    {transaction.description}

    394 | )} 395 |

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

    396 |
    397 |

    -₹{transaction.amount}

    398 |
    399 | handleEdit(transaction.transactionId, index)} 402 | /> 403 | 406 | handleDelete(transaction.transactionId, index) 407 | } 408 | /> 409 |
    410 |
  • 411 | ) 412 | )} 413 |
414 | ) : transactionsLoading ? ( 415 | 416 | ) : ( 417 |

No transactions added yet.

418 | )} 419 |
420 | ); 421 | } 422 | 423 | export default Transactions; 424 | -------------------------------------------------------------------------------- /src/icons/search.svg: -------------------------------------------------------------------------------- 1 | 3 | 16 | 75 | 112 | 142 | 163 | 175 | 188 | 201 | 215 | 228 | 243 | 253 | 265 | 279 | 291 | 301 | 308 | 316 | 326 | 333 | 340 | 347 | 367 | 379 | 389 | 399 | 407 | 424 | 441 | 457 | 472 | 483 | 493 | 503 | 516 | 529 | 538 | 554 | 578 | 591 | 603 | 617 | 627 | 643 | 662 | 676 | 688 | 697 | 711 | 731 | 742 | 754 | 764 | 773 | 786 | 805 | 822 | 835 | 845 | 858 | 873 | 892 | 902 | 908 | 917 | --------------------------------------------------------------------------------