Loading...
Loading...
Data-driven UI. Manage transactions, visualize spending, and track budgets.
Module 11 · JavaScript Projects
Theory becomes skill through building. These guided projects — calculator, todo app, weather app, and more — combine everything you've learned into real applications you can extend, share, and put in a portfolio.
Learn Expense Tracker in JavaScript — covering Data Model, Transactions, Summaries, Visualization with interactive visual examples and step-by-step explanations.
Reading about JavaScript is not the same as building with it. These guided projects combine variables, functions, DOM manipulation, async fetch, and state management into applications you can run, extend, and showcase.
Each project walks through architecture decisions, common pitfalls, and incremental steps. By the end you will have portfolio-ready apps and the confidence to start your own projects from scratch.
Understanding expense tracker is essential for every JavaScript developer. It shows up in frontend UI code, backend APIs, and framework internals — and getting it wrong leads to bugs that are hard to trace. This chapter builds intuition with visuals so the behavior sticks.
Transactions with amounts, categories, and dates.
// Transaction shape: // { // id: "uuid", // type: "expense" | "income", // amount: 45.99, // category: "food", // description: "Lunch with team", // date: "2024-01-15", // createdAt: 1705305600000, // } // App state: let transactions = []; let currentMonth = new Date().toISOString().slice(0, 7); // "2024-01" // Categories with icons and colors: const CATEGORIES = { food: { icon: "🍕", color: "#f97316", label: "Food & Dining" }, transport: { icon: "🚗", color: "#3b82f6", label: "Transport" }, housing: { icon: "🏠", color: "#8b5cf6", label: "Housing" }, utilities: { icon: "💡", color: "#eab308", label: "Utilities" }, entertainment: { icon: "🎬", color: "#ec4899", label: "Entertainment" }, shopping: { icon: "🛒", color: "#14b8a6", label: "Shopping" }, health: { icon: "💊", color: "#ef4444", label: "Health" }, salary: { icon: "💰", color: "#22c55e", label: "Salary" }, freelance: { icon: "💻", color: "#06b6d4", label: "Freelance" }, other: { icon: "📦", color: "#6b7280", label: "Other" }, }; // Budget limits per category (monthly): let budgets = { food: 500, transport: 200, entertainment: 150, shopping: 300, };
Add, edit, and delete income and expenses.
// Add transaction form: // <form class="transaction-form"> // <select name="type"><option>expense</option><option>income</option></select> // <input name="amount" type="number" step="0.01" min="0" required> // <select name="category">...categories...</select> // <input name="description" type="text" placeholder="Description"> // <input name="date" type="date"> // <button type="submit">Add</button> // </form> const form = document.querySelector(".transaction-form"); form.addEventListener("submit", (e) => { e.preventDefault(); const formData = new FormData(form); const transaction = { id: crypto.randomUUID(), type: formData.get("type"), amount: parseFloat(formData.get("amount")), category: formData.get("category"), description: formData.get("description").trim(), date: formData.get("date") || new Date().toISOString().slice(0, 10), createdAt: Date.now(), }; // Validate: if (transaction.amount <= 0 || isNaN(transaction.amount)) { showError("Enter a valid amount"); return; } transactions.push(transaction); form.reset(); save(); render(); });
FormData extracts all form values cleanly. Validate server-side too in a real app.
Calculate totals, balances, and budget status.
// Get transactions for a specific month: function getMonthTransactions(month) { return transactions.filter(t => t.date.startsWith(month)); } // Calculate summaries: function getSummary(month) { const monthTx = getMonthTransactions(month); const income = monthTx .filter(t => t.type === "income") .reduce((sum, t) => sum + t.amount, 0); const expenses = monthTx .filter(t => t.type === "expense") .reduce((sum, t) => sum + t.amount, 0); return { income, expenses, balance: income - expenses, transactionCount: monthTx.length, }; } // Spending by category: function getCategoryBreakdown(month) { const monthTx = getMonthTransactions(month) .filter(t => t.type === "expense"); const breakdown = {}; for (const t of monthTx) { breakdown[t.category] = (breakdown[t.category] || 0) + t.amount; } // Sort by amount descending: return Object.entries(breakdown) .map(([category, amount]) => ({ category, amount })) .sort((a, b) => b.amount - a.amount); } // Budget alerts: function getBudgetStatus(month) { const breakdown = getCategoryBreakdown(month); return Object.entries(budgets).map(([category, limit]) => { const spent = breakdown.find(b => b.category === category)?.amount || 0; const percentage = Math.round((spent / limit) * 100); return { category, limit, spent, percentage, status: percentage >= 100 ? "over" : percentage >= 80 ? "warning" : "ok", }; }); } // Render summary cards: function renderSummary() { const { income, expenses, balance } = getSummary(currentMonth); document.querySelector(".summary").innerHTML = ` <div class="card income"> <h3>Income</h3> <p>+$${income.toFixed(2)}</p> </div> <div class="card expenses"> <h3>Expenses</h3> <p>-$${expenses.toFixed(2)}</p> </div> <div class="card balance ${balance >= 0 ? "positive" : "negative"}"> <h3>Balance</h3> <p>${balance >= 0 ? "+" : ""}$${balance.toFixed(2)}</p> </div> `; }
Charts with pure CSS and Canvas — no libraries needed.
// CSS-only bar chart: function renderBarChart(data) { const max = Math.max(...data.map(d => d.amount)); const chartEl = document.querySelector(".bar-chart"); chartEl.innerHTML = data.map(d => { const cat = CATEGORIES[d.category]; const height = (d.amount / max) * 100; return ` <div class="bar-group"> <div class="bar" style="height:${height}%; background:${cat.color}"> <span class="bar-label">$${d.amount.toFixed(0)}</span> </div> <span class="bar-category">${cat.icon}</span> </div> `; }).join(""); } // Donut chart with Canvas: function renderDonutChart(data) { const canvas = document.querySelector(".donut-chart"); const ctx = canvas.getContext("2d"); const total = data.reduce((s, d) => s + d.amount, 0); const centerX = canvas.width / 2; const centerY = canvas.height / 2; const radius = 80; let startAngle = -Math.PI / 2; // start from top data.forEach(d => { const sliceAngle = (d.amount / total) * Math.PI * 2; const cat = CATEGORIES[d.category]; ctx.beginPath(); ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle); ctx.arc(centerX, centerY, radius * 0.6, startAngle + sliceAngle, startAngle, true); ctx.closePath(); ctx.fillStyle = cat.color; ctx.fill(); startAngle += sliceAngle; }); // Center text: ctx.fillStyle = "#fff"; ctx.font = "bold 20px system-ui"; ctx.textAlign = "center"; ctx.fillText(`$${total.toFixed(0)}`, centerX, centerY + 7); } // Budget progress bars: function renderBudgets() { const statuses = getBudgetStatus(currentMonth); const budgetEl = document.querySelector(".budgets"); budgetEl.innerHTML = statuses.map(b => { const cat = CATEGORIES[b.category]; const width = Math.min(b.percentage, 100); return ` <div class="budget-item ${b.status}"> <div class="budget-header"> <span>${cat.icon} ${cat.label}</span> <span>$${b.spent.toFixed(0)} / $${b.limit}</span> </div> <div class="budget-bar"> <div class="budget-fill" style="width:${width}%"></div> </div> </div> `; }).join(""); }
Download data as CSV or JSON. Import from file.
// Export as CSV: function exportCSV() { const headers = "Date,Type,Category,Description,Amount\n"; const rows = transactions.map(t => `${t.date},${t.type},${t.category},"${t.description.replace(/"/g, '""')}",${t.amount}` ).join("\n"); downloadFile(headers + rows, "expenses.csv", "text/csv"); } // Export as JSON: function exportJSON() { const data = JSON.stringify({ transactions, budgets }, null, 2); downloadFile(data, "expenses.json", "application/json"); } function downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } // Import from JSON file: const importInput = document.querySelector("#import-file"); importInput.addEventListener("change", async (e) => { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); const data = JSON.parse(text); // Validate structure: if (!Array.isArray(data.transactions)) { throw new Error("Invalid file format"); } // Merge or replace: const action = confirm("Merge with existing data? (Cancel to replace)"); if (action) { // Merge — avoid duplicates by id: const existingIds = new Set(transactions.map(t => t.id)); const newTx = data.transactions.filter(t => !existingIds.has(t.id)); transactions.push(...newTx); } else { transactions = data.transactions; } save(); render(); } catch (error) { showError("Failed to import: " + error.message); } importInput.value = ""; // reset file input }); // Key patterns in this project: // ✓ Complex data aggregation (reduce, filter, group) // ✓ Date-based filtering and formatting // ✓ Canvas API for charts // ✓ CSS-only charts (bar, progress) // ✓ File export (Blob + download) // ✓ File import (FileReader + validation) // ✓ Budget tracking with alerts // ✓ FormData API for clean form handling
Common questions about expense tracker.