From e1b3e9a3f3731590b7fb1d498e08fee56e84a2b2 Mon Sep 17 00:00:00 2001 From: nishtha-agarwal-211 Date: Wed, 17 Jun 2026 17:37:23 +0530 Subject: [PATCH] feat: port budget tracker to web app with local storage --- README.md | 2 +- web-app/assets/banners/budget-tracker.webp | Bin 0 -> 35237 bytes web-app/generate_banners.py | 1 + web-app/index.html | 8 + web-app/js/projects.js | 17 +- web-app/js/projects/budget-tracker.js | 636 +++++++++++++++++++++ web-app/utilities.html | 20 + 7 files changed, 681 insertions(+), 3 deletions(-) create mode 100644 web-app/assets/banners/budget-tracker.webp create mode 100644 web-app/js/projects/budget-tracker.js diff --git a/README.md b/README.md index 4b37fe88..a68f8083 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,7 @@ The browser app currently exposes 39 projects: ### Utilities - AI Resume Analyzer +- Budget Tracker - Color Palette Suggestor - Morse Code - Number Converter @@ -299,7 +300,6 @@ These standalone Python project files do not have a browser counterpart yet and - [math/Happy-Number/Happy-Number.py](math/Happy-Number/Happy-Number.py) - [math/Matrix-Calculator/Matrix-Calculator.py](math/Matrix-Calculator/Matrix-Calculator.py) - [math/Quadratic-Solver/Quadratic-Solver.py](math/Quadratic-Solver/Quadratic-Solver.py) -- [utilities/Budget-Tracker/budget_tracker.py](utilities/Budget-Tracker/budget_tracker.py) - [utilities/Caesar-Cipher/Caesar-Cipher.py](utilities/Caesar-Cipher/Caesar-Cipher.py) - [utilities/Fake-News-Headline-Generator/Fake-News-Headline-Generator.py](utilities/Fake-News-Headline-Generator/Fake-News-Headline-Generator.py) diff --git a/web-app/assets/banners/budget-tracker.webp b/web-app/assets/banners/budget-tracker.webp new file mode 100644 index 0000000000000000000000000000000000000000..d459128577b6827c782517db2d9f8a47f84f10cc GIT binary patch literal 35237 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 b7MzNyK1gBot@yqc{PhdFK0v{_e!uyDqil5q literal 0 HcmV?d00001 diff --git a/web-app/generate_banners.py b/web-app/generate_banners.py index 478743cc..f52470b2 100644 --- a/web-app/generate_banners.py +++ b/web-app/generate_banners.py @@ -573,6 +573,7 @@ def draw_o(ox, oy): ("AI Resume Analyzer", "utilities", "resume-analyzer.webp"), ("Caesar Cipher", "utilities", "caesar-cipher.webp"), ("Unit Converter", "utilities", "unit-converter.webp"), + ("Budget Tracker", "utilities", "budget-tracker.webp"), ] # Run generation diff --git a/web-app/index.html b/web-app/index.html index 239ee7fe..df5557e7 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -874,6 +874,7 @@

Legal

+ @@ -1122,6 +1123,13 @@

Legal

}, // UTILITIES (10+) + { + project: "budget-tracker", + title: "Budget Tracker", + category: "utilities", + desc: "Track income and expenses, view categories, and persist data locally", + tags: "utility,finance,tracker,money" + }, { project: "morse-code", title: "Morse Code", diff --git a/web-app/js/projects.js b/web-app/js/projects.js index d6813ebc..1088c12b 100644 --- a/web-app/js/projects.js +++ b/web-app/js/projects.js @@ -1604,6 +1604,8 @@ const projectInstructions = { "The player with the higher card rank wins the round and gets a point.", "Ace is the highest, 2 is the lowest.", "Play continues until all cards are drawn. The player with the most points wins!" + ] + }, "number-sliding-puzzle": { title: "🧩 How to Play Number Sliding Puzzle", steps: [ @@ -1881,6 +1883,16 @@ const projectInstructions = { "Click any color to copy its hex code" ] }, + "budget-tracker": { + title: "💰 How to Use Budget Tracker", + steps: [ + "Select the transaction type (Income or Expense) from the dropdown.", + "Enter a category, description (optional), and positive amount value.", + "Click 'Add Transaction' to log it in your dashboard.", + "Analyze spending behavior through dynamic category-wise progress bars.", + "Clear individual records using the delete action or wipe out all history securely." + ] + }, "morse-code": { title: "📻 How to Use Morse Code", steps: [ @@ -3176,8 +3188,9 @@ function initializeProject(projectName) { "math-quiz": "initMathQuiz", "resume-analyzer": "initResumeAnalyzer", "caesar-cipher": "initCaesarCipher", - "war-card-game": "initWarCardGame" - "number-sliding-puzzle": "initNumberSlidingPuzzle" + "war-card-game": "initWarCardGame", + "number-sliding-puzzle": "initNumberSlidingPuzzle", + "budget-tracker": "initBudgetTracker" }; const initializerName = initializers[projectName]; diff --git a/web-app/js/projects/budget-tracker.js b/web-app/js/projects/budget-tracker.js new file mode 100644 index 00000000..e2eebb6b --- /dev/null +++ b/web-app/js/projects/budget-tracker.js @@ -0,0 +1,636 @@ +function getBudgetTrackerHTML() { + return ` +
+

💰 Budget Tracker

+
+ + +
+
+
📈
+
+ Total Income + ₹0.00 +
+
+
+
📉
+
+ Total Expenses + ₹0.00 +
+
+
+
⚖️
+
+ Net Balance + ₹0.00 +
+
+
+ +
+ +
+

Add New Transaction

+
+
+ + +
+ +
+ + + + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + +
+

Expense Analytics

+
+

No expense data to analyze yet.

+
+
+
+ + +
+
+

Recent Transactions

+ +
+
+ + + + + + + + + + + + + + +
DateTypeCategoryDescriptionAmountAction
+
+
+ +
+
+ + + + + + `; +} + +function initBudgetTracker() { + const form = document.getElementById('transactionForm'); + const transType = document.getElementById('transType'); + const transCategory = document.getElementById('transCategory'); + const transDescription = document.getElementById('transDescription'); + const transAmount = document.getElementById('transAmount'); + + const totalIncomeEl = document.getElementById('totalIncome'); + const totalExpensesEl = document.getElementById('totalExpenses'); + const netBalanceEl = document.getElementById('netBalance'); + const balanceCard = document.getElementById('balanceCard'); + const balanceIcon = document.getElementById('balanceIcon'); + + const analyticsContainer = document.getElementById('analyticsContainer'); + const transactionList = document.getElementById('transactionList'); + + // Confirmation Modal Elements + const clearAllBtn = document.getElementById('clearAllBtn'); + const budgetConfirmModal = document.getElementById('budgetConfirmModal'); + const cancelClearBtn = document.getElementById('cancelClearBtn'); + const confirmClearBtn = document.getElementById('confirmClearBtn'); + + // Load Initial Transactions + let transactions = JSON.parse(localStorage.getItem('budget_transactions') || '[]'); + + function saveAndRender() { + localStorage.setItem('budget_transactions', JSON.stringify(transactions)); + renderUI(); + } + + function renderUI() { + // 1. Calculate Sums + let totalIncome = 0; + let totalExpenses = 0; + const expenseByCategory = {}; + + transactions.forEach(t => { + if (t.type === 'income') { + totalIncome += t.amount; + } else { + totalExpenses += t.amount; + expenseByCategory[t.category] = (expenseByCategory[t.category] || 0) + t.amount; + } + }); + + const netBalance = totalIncome - totalExpenses; + + // 2. Update Summary Cards + totalIncomeEl.textContent = `₹${totalIncome.toFixed(2)}`; + totalExpensesEl.textContent = `₹${totalExpenses.toFixed(2)}`; + netBalanceEl.textContent = `${netBalance >= 0 ? '' : '-'}₹${Math.abs(netBalance).toFixed(2)}`; + + // Net Balance Styling + if (netBalance >= 0) { + balanceCard.className = 'summary-card balance positive'; + balanceIcon.textContent = '✅'; + } else { + balanceCard.className = 'summary-card balance negative'; + balanceIcon.textContent = '⚠️'; + } + + // 3. Update Category Analytics + analyticsContainer.innerHTML = ''; + const categories = Object.keys(expenseByCategory); + if (categories.length === 0) { + analyticsContainer.innerHTML = '

No expense data to analyze yet.

'; + } else { + const wrapper = document.createElement('div'); + wrapper.className = 'analytics-container'; + + // Sort categories by expenditure (descending) + categories + .sort((a, b) => expenseByCategory[b] - expenseByCategory[a]) + .forEach(cat => { + const amount = expenseByCategory[cat]; + const percentage = totalExpenses > 0 ? (amount / totalExpenses) * 100 : 0; + + const item = document.createElement('div'); + item.className = 'analytics-item'; + item.innerHTML = ` +
+ ${cat} + ₹${amount.toFixed(2)} (${percentage.toFixed(0)}%) +
+
+
+
+ `; + wrapper.appendChild(item); + }); + analyticsContainer.appendChild(wrapper); + } + + // 4. Update Transaction List Table + transactionList.innerHTML = ''; + if (transactions.length === 0) { + transactionList.innerHTML = ` + + No transactions found. Add some above! + + `; + } else { + // Show newest first + [...transactions].reverse().forEach(t => { + const row = document.createElement('tr'); + const badgeClass = t.type === 'income' ? 'badge income' : 'badge expense'; + const symbol = t.type === 'income' ? '+' : '-'; + + row.innerHTML = ` + ${t.date} + ${t.type} + ${t.category} + ${t.description || '-'} + + ${symbol}₹${t.amount.toFixed(2)} + + + + + `; + transactionList.appendChild(row); + }); + + // Hook up delete buttons + document.querySelectorAll('.btn-delete').forEach(btn => { + btn.addEventListener('click', (e) => { + const id = parseInt(btn.getAttribute('data-id')); + transactions = transactions.filter(t => t.id !== id); + saveAndRender(); + }); + }); + } + } + + // Handle Form Submission + form.addEventListener('submit', (e) => { + e.preventDefault(); + + const category = transCategory.value.trim(); + const amount = parseFloat(transAmount.value); + if (!category || isNaN(amount) || amount <= 0) return; + + const newTransaction = { + id: Date.now(), + date: new Date().toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric' + }), + type: transType.value, + category: category.charAt(0).toUpperCase() + category.slice(1), + description: transDescription.value.trim(), + amount: parseFloat(amount.toFixed(2)) + }; + + transactions.push(newTransaction); + saveAndRender(); + form.reset(); + }); + + // Handle Custom Confirmation Modal Overlay for Clear All + clearAllBtn.addEventListener('click', () => { + budgetConfirmModal.style.display = 'flex'; + }); + + cancelClearBtn.addEventListener('click', () => { + budgetConfirmModal.style.display = 'none'; + }); + + confirmClearBtn.addEventListener('click', () => { + transactions = []; + saveAndRender(); + budgetConfirmModal.style.display = 'none'; + }); + + // Render initial list + renderUI(); +} diff --git a/web-app/utilities.html b/web-app/utilities.html index ca8a9ff5..9448347f 100644 --- a/web-app/utilities.html +++ b/web-app/utilities.html @@ -348,6 +348,25 @@

Caesar Cipher

Encrypt & decrypt with a shift!

+
+ Budget Tracker +
+ +
+ +

Budget Tracker

+

Track income and expenses, view categories, and persist data locally

+
+
Typing Speed Tester +