├── backend ├── public │ └── temp │ │ └── .gitkeep ├── .gitignore ├── src │ ├── constants.js │ ├── utils │ │ ├── asyncHandler.js │ │ ├── apiResponse.js │ │ ├── apiError.js │ │ └── cloudinary.js │ ├── middlewares │ │ ├── multer.middleware.js │ │ └── auth.middleware.js │ ├── routes │ │ ├── demo.routes.js │ │ ├── feedback.routes.js │ │ ├── stats.routes.js │ │ ├── payment.routes.js │ │ ├── goal.routes.js │ │ ├── transaction.routes.js │ │ └── user.routes.js │ ├── index.js │ ├── db │ │ └── db.js │ ├── models │ │ ├── feedback.model.js │ │ ├── goal.model.js │ │ ├── transaction.model.js │ │ └── user.model.js │ ├── controllers │ │ ├── feedback.controller.js │ │ ├── demo.controller.js │ │ ├── payment.controller.js │ │ └── stats.controller.js │ └── app.js ├── .gcloudignore └── package.json ├── frontend ├── src │ ├── vite-env.d.ts │ ├── utils │ │ └── formatter.ts │ ├── components │ │ ├── Navbar.tsx │ │ ├── ui │ │ │ ├── skeleton.tsx │ │ │ ├── label.tsx │ │ │ ├── textarea.tsx │ │ │ ├── progress.tsx │ │ │ ├── input.tsx │ │ │ ├── toaster.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── badge.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── popover.tsx │ │ │ ├── avatar.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── button.tsx │ │ │ ├── tabs.tsx │ │ │ ├── card.tsx │ │ │ ├── calendar.tsx │ │ │ ├── table.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dialog.tsx │ │ │ ├── use-toast.ts │ │ │ ├── sheet.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── toast.tsx │ │ │ └── command.tsx │ │ ├── AccountBalance.tsx │ │ ├── TransactionSkeleton.tsx │ │ ├── GoalsSkeleton.tsx │ │ ├── LogoutButton.tsx │ │ ├── NotFound.tsx │ │ ├── CheckoutButton.tsx │ │ ├── DemoLoginButton.tsx │ │ ├── mode-toggle.tsx │ │ ├── payment │ │ │ └── PaymentSuccess.tsx │ │ ├── stats │ │ │ ├── TimeRangeIncomeAndExpense.tsx │ │ │ ├── HiddenStats.tsx │ │ │ └── FixedIncomeExpenseGraph.tsx │ │ ├── IncomeAndExpense.tsx │ │ ├── SingleTransactionSkeleton.tsx │ │ ├── theme-provider.tsx │ │ ├── SidebarPreferences.tsx │ │ ├── UpdatePassword.tsx │ │ ├── UpdateCurrency.tsx │ │ ├── UpdateAccountBalance.tsx │ │ ├── AddMoneyToGoal.tsx │ │ ├── InitialDeposit.tsx │ │ ├── MoreAccountOptions.tsx │ │ ├── DatePicker.tsx │ │ ├── Command.tsx │ │ ├── Feedback.tsx │ │ ├── AddIncomeAndExpense.tsx │ │ ├── UpdateIncomeAndExpense.tsx │ │ ├── GoalsDisplay.tsx │ │ ├── TransactionDisplay.tsx │ │ ├── FirstGoal.tsx │ │ ├── AddNewGoal.tsx │ │ ├── SingularGoalView.tsx │ │ └── RecentTransactions.tsx │ ├── lib │ │ └── utils.ts │ ├── hooks │ │ └── useTitle.tsx │ ├── main.tsx │ ├── pages │ │ ├── Landing.tsx │ │ ├── Settings.tsx │ │ ├── Goals.tsx │ │ ├── ResetDemo.tsx │ │ ├── Transactions.tsx │ │ ├── NewTransaction.tsx │ │ ├── Overview.tsx │ │ ├── Statistics.tsx │ │ ├── Dashboard.tsx │ │ └── Login.tsx │ ├── Routing.tsx │ └── App.tsx ├── postcss.config.js ├── vercel.json ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── components.json ├── .eslintrc.cjs ├── index.html ├── tsconfig.json ├── package.json └── tailwind.config.js ├── LICENSE ├── README.md └── todo.md /backend/public/temp/.gitkeep: -------------------------------------------------------------------------------- 1 | keep 2 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | app.yaml -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/utils/formatter.ts: -------------------------------------------------------------------------------- 1 | export const formatter = new Intl.NumberFormat("en-US"); 2 | -------------------------------------------------------------------------------- /backend/src/constants.js: -------------------------------------------------------------------------------- 1 | export const DB_NAME = "expense-tracker"; 2 | export const PORT = process.env.PORT || 3000; 3 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | function Navbar() { 2 | return ( 3 |
::Navbar Goes Here::
4 | ) 5 | } 6 | 7 | export default Navbar 8 | -------------------------------------------------------------------------------- /frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/hooks/useTitle.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | function useTitle(title: string) { 4 | useEffect(() => { 5 | document.title = `${title} - SpendSync`; 6 | }, [title]); 7 | } 8 | export default useTitle; 9 | -------------------------------------------------------------------------------- /backend/src/utils/asyncHandler.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = (requestHandler) => { 2 | return (req, res, next) => { 3 | Promise.resolve(requestHandler(req, res, next)).catch((err) => next(err)); 4 | }; 5 | }; 6 | 7 | export { asyncHandler }; 8 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/utils/apiResponse.js: -------------------------------------------------------------------------------- 1 | class ApiResponse { 2 | constructor(statusCode, data, message = "success") { 3 | this.statusCode = statusCode; 4 | this.data = data; 5 | this.message = message; 6 | this.success = statusCode < 400; 7 | } 8 | } 9 | 10 | export { ApiResponse }; 11 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | react() 8 | ], 9 | resolve: { 10 | alias: { 11 | "@": path.resolve(__dirname, "./src"), 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /backend/src/middlewares/multer.middleware.js: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | 3 | const storage = multer.diskStorage({ 4 | destination: function (req, file, cb) { 5 | cb(null, "./public/temp"); 6 | }, 7 | filename: function (req, file, cb) { 8 | cb(null, file.originalname); 9 | }, 10 | }); 11 | 12 | export const upload = multer({ storage }); 13 | -------------------------------------------------------------------------------- /backend/src/routes/demo.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { verifyJWT } from "../middlewares/auth.middleware.js"; 3 | import { resetDemoUser } from "../controllers/demo.controller.js"; 4 | 5 | const router = Router(); 6 | 7 | // secure routes 8 | router.route("/reset-demo-user").post(verifyJWT, resetDemoUser); 9 | 10 | export default router 11 | -------------------------------------------------------------------------------- /frontend/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /backend/src/routes/feedback.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { verifyJWT } from "../middlewares/auth.middleware.js"; 3 | import { createFeedback } from "../controllers/feedback.controller.js"; 4 | 5 | const router = Router(); 6 | 7 | // secure routes 8 | router.route("/create-feedback").post(verifyJWT, createFeedback); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .env 11 | 12 | node_modules 13 | dist 14 | dist-ssr 15 | *.local 16 | 17 | .unlighthouse 18 | 19 | # Editor directories and files 20 | .vscode/* 21 | !.vscode/extensions.json 22 | .idea 23 | .DS_Store 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /backend/src/routes/stats.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { verifyJWT } from "../middlewares/auth.middleware.js"; 3 | import { getIncomeAndExpenseByTimeRange } from "../controllers/stats.controller.js"; 4 | 5 | const router = Router(); 6 | 7 | // secure routes 8 | router 9 | .route("/get-income-expense-by-time-range") 10 | .get(verifyJWT, getIncomeAndExpenseByTimeRange); 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/index.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import connectDB from "./db/db.js"; 3 | import app from "./app.js"; 4 | import { PORT } from "./constants.js"; 5 | 6 | dotenv.config({ 7 | path: "../.env", 8 | }); 9 | 10 | connectDB() 11 | .then(() => { 12 | app.listen(PORT, () => { 13 | console.log(`Server Running on Port: ${PORT}`); 14 | }); 15 | }) 16 | .catch((err) => { 17 | console.error("MongoDB Connection failed !!!", err); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/routes/payment.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { verifyJWT } from "../middlewares/auth.middleware.js"; 3 | import { 4 | confirmPayment, 5 | createCheckout, 6 | } from "../controllers/payment.controller.js"; 7 | 8 | const router = Router(); 9 | 10 | // secure routes 11 | router.route("/create-checkout").post(verifyJWT, createCheckout); 12 | router.route("/confirm-payment").post(verifyJWT, confirmPayment); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /frontend/src/pages/Landing.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import Navbar from "../components/Navbar"; 3 | import { Button } from "@/components/ui/button"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | function Landing() { 7 | const navigate = useNavigate(); 8 | 9 | useEffect(() => { 10 | navigate("/register"); 11 | }, []); 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export default Landing; 22 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | SpendSync 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /backend/src/utils/apiError.js: -------------------------------------------------------------------------------- 1 | class ApiError extends Error { 2 | constructor( 3 | statusCode, 4 | message = "Something went wrong", 5 | errors = [], 6 | stack = "", 7 | ) { 8 | super(message); 9 | this.statusCode = statusCode; 10 | this.data = null; 11 | this.message = message; 12 | this.success = false; 13 | this.errors = errors; 14 | 15 | if (stack) { 16 | this.stack = stack; 17 | } else { 18 | Error.captureStackTrace(this, this.constructor); 19 | } 20 | } 21 | } 22 | 23 | export { ApiError }; 24 | -------------------------------------------------------------------------------- /backend/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /backend/src/db/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { DB_NAME } from "../constants.js"; 3 | 4 | async function connectDB() { 5 | try { 6 | const connectionInstance = await mongoose.connect( 7 | `${process.env.MONGO_URL}/${DB_NAME}`, 8 | { 9 | writeConcern: { w: "majority" }, 10 | }, 11 | ); 12 | console.log( 13 | `\nMongoDB connected !! DB HOST: ${connectionInstance.connection.host}`, 14 | ); 15 | } catch (error) { 16 | console.error("MONGODB connection error: ", error); 17 | throw error; 18 | } 19 | } 20 | 21 | export default connectDB; 22 | -------------------------------------------------------------------------------- /backend/src/models/feedback.model.js: -------------------------------------------------------------------------------- 1 | import { Schema } from "mongoose"; 2 | import mongoose from "mongoose"; 3 | 4 | const feedbackSchema = mongoose.Schema( 5 | { 6 | madeBy: { 7 | type: Schema.Types.ObjectId, 8 | ref: "User", 9 | required: true, 10 | }, 11 | rating: { 12 | type: String, 13 | required: true, 14 | enum: ["1-star", "2-star", "3-star", "4-star", "5-star"], 15 | }, 16 | description: { 17 | type: String, 18 | }, 19 | }, 20 | { timestamps: true } 21 | ); 22 | 23 | export const Feedback = mongoose.model("Feedback", feedbackSchema); 24 | -------------------------------------------------------------------------------- /frontend/src/pages/Settings.tsx: -------------------------------------------------------------------------------- 1 | import UpdatePassword from "@/components/UpdatePassword"; 2 | import UpdateCurrency from "@/components/UpdateCurrency"; 3 | import useTitle from "@/hooks/useTitle"; 4 | 5 | function Settings() { 6 | useTitle("Settings"); 7 | return ( 8 |
9 |

Settings

10 | 11 |
12 | 13 | 14 |
15 |
16 | ); 17 | } 18 | 19 | export default Settings; 20 | -------------------------------------------------------------------------------- /backend/src/routes/goal.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { verifyJWT } from "../middlewares/auth.middleware.js"; 3 | import { 4 | createGoal, 5 | getGoals, 6 | updateGoal, 7 | addMoneyToGoal, 8 | deleteGoal, 9 | } from "../controllers/goal.controller.js"; 10 | 11 | const router = Router(); 12 | 13 | // secure routes 14 | router.route("/create-goal").post(verifyJWT, createGoal); 15 | router.route("/get-goals").get(verifyJWT, getGoals); 16 | router.route("/update-goal").post(verifyJWT, updateGoal); 17 | router.route("/add-money-to-goal").post(verifyJWT, addMoneyToGoal); 18 | router.route("/delete-goal").post(verifyJWT, deleteGoal); 19 | 20 | export default router; 21 | -------------------------------------------------------------------------------- /frontend/src/pages/Goals.tsx: -------------------------------------------------------------------------------- 1 | import FirstGoal from "@/components/FirstGoal"; 2 | import { useContext } from "react"; 3 | import { AppContext } from "@/App"; 4 | import GoalsDisplay from "@/components/GoalsDisplay"; 5 | import useTitle from "@/hooks/useTitle"; 6 | 7 | function Goals() { 8 | useTitle("Goals"); 9 | const { userData } = useContext(AppContext); 10 | 11 | return ( 12 |
13 |

Goals

14 | {userData.user.goals.length === 0 ? : } 15 |
16 | ); 17 | } 18 | 19 | export default Goals; 20 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "server.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "nodemon -r dotenv/config src/index", 10 | "start": "node -r dotenv/config src/index" 11 | }, 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "bcrypt": "^5.1.1", 16 | "cloudinary": "^2.1.0", 17 | "cookie-parser": "^1.4.6", 18 | "cors": "^2.8.5", 19 | "dotenv": "^16.4.5", 20 | "express": "^4.19.2", 21 | "jsonwebtoken": "^9.0.2", 22 | "mongoose": "^8.3.1", 23 | "multer": "2.0.2", 24 | "stripe": "^14.25.0" 25 | }, 26 | "devDependencies": { 27 | "nodemon": "^3.1.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | }, 13 | 14 | /* Bundler mode */ 15 | "moduleResolution": "bundler", 16 | "allowImportingTsExtensions": true, 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | 22 | /* Linting */ 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true 27 | }, 28 | "include": ["src"], 29 | "references": [{ "path": "./tsconfig.node.json" }], 30 | "noImplicitAny": false 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/models/goal.model.js: -------------------------------------------------------------------------------- 1 | import { Schema } from "mongoose"; 2 | import mongoose from "mongoose"; 3 | 4 | const goalSchema = mongoose.Schema( 5 | { 6 | finalAmount: { 7 | type: Number, 8 | required: true, 9 | min: 0, 10 | }, 11 | currentAmount: { 12 | type: Number, 13 | default: 0, 14 | min: 0, 15 | }, 16 | madeBy: { 17 | type: Schema.Types.ObjectId, 18 | ref: "User", 19 | required: true, 20 | }, 21 | category: { 22 | type: String, 23 | default: "Savings", 24 | }, 25 | title: { 26 | type: String, 27 | required: true, 28 | trim: true, 29 | }, 30 | description: { 31 | type: String, 32 | trim: true, 33 | }, 34 | }, 35 | { timestamps: true }, 36 | ); 37 | 38 | export const Goal = mongoose.model("Goal", goalSchema); 39 | -------------------------------------------------------------------------------- /frontend/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |