mirror of
https://github.com/mosh-hamedani/expense-tracker-starter.git
synced 2026-05-21 11:58:31 +02:00
Merge pull request #1 from mrtony/02-01-running-the-app
Fix bugs, extract components, and add delete feature
This commit is contained in:
commit
75ea9afd75
27
CLAUDE.md
Normal file
27
CLAUDE.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Expense tracker app from Mosh's [Claude Code course](https://codewithmosh.com/p/claude-code). It intentionally has bugs, poor UI, and messy code — all fixed throughout the course.
|
||||
|
||||
## Commands
|
||||
|
||||
- `npm run dev` — Start Vite dev server (http://localhost:5173)
|
||||
- `npm run build` — Production build
|
||||
- `npm run lint` — ESLint
|
||||
- `npm run preview` — Preview production build
|
||||
|
||||
## Architecture
|
||||
|
||||
React 19 app with Vite 7. No routing, no state library, no backend. Plain CSS styling via `App.css` and `index.css`.
|
||||
|
||||
- `src/App.jsx` — Root component, owns `transactions` state
|
||||
- `src/Summary.jsx` — Displays income/expenses/balance totals (computes from `transactions` prop)
|
||||
- `src/TransactionForm.jsx` — Add transaction form (owns form state, calls `onAddTransaction` callback)
|
||||
- `src/TransactionList.jsx` — Filtered transaction table (owns filter state, receives `transactions` prop)
|
||||
|
||||
## Known Intentional Issues
|
||||
|
||||
- "Freelance Work" is typed as "expense" instead of "income"
|
||||
154
src/App.jsx
154
src/App.jsx
|
|
@ -1,155 +1,37 @@
|
|||
import { useState } from 'react'
|
||||
import './App.css'
|
||||
import Summary from './Summary'
|
||||
import TransactionForm from './TransactionForm'
|
||||
import TransactionList from './TransactionList'
|
||||
|
||||
function App() {
|
||||
const [transactions, setTransactions] = useState([
|
||||
{ id: 1, description: "Salary", amount: "5000", type: "income", category: "salary", date: "2025-01-01" },
|
||||
{ id: 2, description: "Rent", amount: "1200", type: "expense", category: "housing", date: "2025-01-02" },
|
||||
{ id: 3, description: "Groceries", amount: "150", type: "expense", category: "food", date: "2025-01-03" },
|
||||
{ id: 4, description: "Freelance Work", amount: "800", type: "expense", category: "salary", date: "2025-01-05" },
|
||||
{ id: 5, description: "Electric Bill", amount: "95", type: "expense", category: "utilities", date: "2025-01-06" },
|
||||
{ id: 6, description: "Dinner Out", amount: "65", type: "expense", category: "food", date: "2025-01-07" },
|
||||
{ id: 7, description: "Gas", amount: "45", type: "expense", category: "transport", date: "2025-01-08" },
|
||||
{ id: 8, description: "Netflix", amount: "15", type: "expense", category: "entertainment", date: "2025-01-10" },
|
||||
{ id: 1, description: "Salary", amount: 5000, type: "income", category: "salary", date: "2025-01-01" },
|
||||
{ id: 2, description: "Rent", amount: 1200, type: "expense", category: "housing", date: "2025-01-02" },
|
||||
{ id: 3, description: "Groceries", amount: 150, type: "expense", category: "food", date: "2025-01-03" },
|
||||
{ id: 4, description: "Freelance Work", amount: 800, type: "expense", category: "salary", date: "2025-01-05" },
|
||||
{ id: 5, description: "Electric Bill", amount: 95, type: "expense", category: "utilities", date: "2025-01-06" },
|
||||
{ id: 6, description: "Dinner Out", amount: 65, type: "expense", category: "food", date: "2025-01-07" },
|
||||
{ id: 7, description: "Gas", amount: 45, type: "expense", category: "transport", date: "2025-01-08" },
|
||||
{ id: 8, description: "Netflix", amount: 15, type: "expense", category: "entertainment", date: "2025-01-10" },
|
||||
]);
|
||||
|
||||
const [description, setDescription] = useState("");
|
||||
const [amount, setAmount] = useState("");
|
||||
const [type, setType] = useState("expense");
|
||||
const [category, setCategory] = useState("food");
|
||||
const [filterType, setFilterType] = useState("all");
|
||||
const [filterCategory, setFilterCategory] = useState("all");
|
||||
|
||||
const categories = ["food", "housing", "utilities", "transport", "entertainment", "salary", "other"];
|
||||
|
||||
const totalIncome = transactions
|
||||
.filter(t => t.type === "income")
|
||||
.reduce((sum, t) => sum + t.amount, 0);
|
||||
|
||||
const totalExpenses = transactions
|
||||
.filter(t => t.type === "expense")
|
||||
.reduce((sum, t) => sum + t.amount, 0);
|
||||
|
||||
const balance = totalIncome - totalExpenses;
|
||||
|
||||
let filteredTransactions = transactions;
|
||||
if (filterType !== "all") {
|
||||
filteredTransactions = filteredTransactions.filter(t => t.type === filterType);
|
||||
}
|
||||
if (filterCategory !== "all") {
|
||||
filteredTransactions = filteredTransactions.filter(t => t.category === filterCategory);
|
||||
}
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!description || !amount) return;
|
||||
|
||||
const newTransaction = {
|
||||
id: Date.now(),
|
||||
description,
|
||||
amount,
|
||||
type,
|
||||
category,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
const handleAddTransaction = (transaction) => {
|
||||
setTransactions([...transactions, transaction]);
|
||||
};
|
||||
|
||||
setTransactions([...transactions, newTransaction]);
|
||||
setDescription("");
|
||||
setAmount("");
|
||||
setType("expense");
|
||||
setCategory("food");
|
||||
const handleDeleteTransaction = (id) => {
|
||||
setTransactions(transactions.filter(t => t.id !== id));
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<h1>Finance Tracker</h1>
|
||||
<p className="subtitle">Track your income and expenses</p>
|
||||
|
||||
<div className="summary">
|
||||
<div className="summary-card">
|
||||
<h3>Income</h3>
|
||||
<p className="income-amount">${totalIncome}</p>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<h3>Expenses</h3>
|
||||
<p className="expense-amount">${totalExpenses}</p>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<h3>Balance</h3>
|
||||
<p className="balance-amount">${balance}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="add-transaction">
|
||||
<h2>Add Transaction</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
/>
|
||||
<select value={type} onChange={(e) => setType(e.target.value)}>
|
||||
<option value="income">Income</option>
|
||||
<option value="expense">Expense</option>
|
||||
</select>
|
||||
<select value={category} onChange={(e) => setCategory(e.target.value)}>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="transactions">
|
||||
<h2>Transactions</h2>
|
||||
<div className="filters">
|
||||
<select value={filterType} onChange={(e) => setFilterType(e.target.value)}>
|
||||
<option value="all">All Types</option>
|
||||
<option value="income">Income</option>
|
||||
<option value="expense">Expense</option>
|
||||
</select>
|
||||
<select value={filterCategory} onChange={(e) => setFilterCategory(e.target.value)}>
|
||||
<option value="all">All Categories</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
<th>Category</th>
|
||||
<th>Amount</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredTransactions.map(t => (
|
||||
<tr key={t.id}>
|
||||
<td>{t.date}</td>
|
||||
<td>{t.description}</td>
|
||||
<td>{t.category}</td>
|
||||
<td className={t.type === "income" ? "income-amount" : "expense-amount"}>
|
||||
{t.type === "income" ? "+" : "-"}${t.amount}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Summary transactions={transactions} />
|
||||
<TransactionForm onAddTransaction={handleAddTransaction} />
|
||||
<TransactionList transactions={transactions} onDeleteTransaction={handleDeleteTransaction} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
30
src/Summary.jsx
Normal file
30
src/Summary.jsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
function Summary({ transactions }) {
|
||||
const totalIncome = transactions
|
||||
.filter(t => t.type === "income")
|
||||
.reduce((sum, t) => sum + t.amount, 0);
|
||||
|
||||
const totalExpenses = transactions
|
||||
.filter(t => t.type === "expense")
|
||||
.reduce((sum, t) => sum + t.amount, 0);
|
||||
|
||||
const balance = totalIncome - totalExpenses;
|
||||
|
||||
return (
|
||||
<div className="summary">
|
||||
<div className="summary-card">
|
||||
<h3>Income</h3>
|
||||
<p className="income-amount">${totalIncome}</p>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<h3>Expenses</h3>
|
||||
<p className="expense-amount">${totalExpenses}</p>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<h3>Balance</h3>
|
||||
<p className="balance-amount">${balance}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Summary;
|
||||
61
src/TransactionForm.jsx
Normal file
61
src/TransactionForm.jsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
const categories = ["food", "housing", "utilities", "transport", "entertainment", "salary", "other"];
|
||||
|
||||
function TransactionForm({ onAddTransaction }) {
|
||||
const [description, setDescription] = useState("");
|
||||
const [amount, setAmount] = useState("");
|
||||
const [type, setType] = useState("expense");
|
||||
const [category, setCategory] = useState("food");
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!description || !amount) return;
|
||||
|
||||
onAddTransaction({
|
||||
id: Date.now(),
|
||||
description,
|
||||
amount,
|
||||
type,
|
||||
category,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
});
|
||||
|
||||
setDescription("");
|
||||
setAmount("");
|
||||
setType("expense");
|
||||
setCategory("food");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="add-transaction">
|
||||
<h2>Add Transaction</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
/>
|
||||
<select value={type} onChange={(e) => setType(e.target.value)}>
|
||||
<option value="income">Income</option>
|
||||
<option value="expense">Expense</option>
|
||||
</select>
|
||||
<select value={category} onChange={(e) => setCategory(e.target.value)}>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TransactionForm;
|
||||
68
src/TransactionList.jsx
Normal file
68
src/TransactionList.jsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
const categories = ["food", "housing", "utilities", "transport", "entertainment", "salary", "other"];
|
||||
|
||||
function TransactionList({ transactions, onDeleteTransaction }) {
|
||||
const [filterType, setFilterType] = useState("all");
|
||||
const [filterCategory, setFilterCategory] = useState("all");
|
||||
|
||||
let filteredTransactions = transactions;
|
||||
if (filterType !== "all") {
|
||||
filteredTransactions = filteredTransactions.filter(t => t.type === filterType);
|
||||
}
|
||||
if (filterCategory !== "all") {
|
||||
filteredTransactions = filteredTransactions.filter(t => t.category === filterCategory);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="transactions">
|
||||
<h2>Transactions</h2>
|
||||
<div className="filters">
|
||||
<select value={filterType} onChange={(e) => setFilterType(e.target.value)}>
|
||||
<option value="all">All Types</option>
|
||||
<option value="income">Income</option>
|
||||
<option value="expense">Expense</option>
|
||||
</select>
|
||||
<select value={filterCategory} onChange={(e) => setFilterCategory(e.target.value)}>
|
||||
<option value="all">All Categories</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
<th>Category</th>
|
||||
<th>Amount</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredTransactions.map(t => (
|
||||
<tr key={t.id}>
|
||||
<td>{t.date}</td>
|
||||
<td>{t.description}</td>
|
||||
<td>{t.category}</td>
|
||||
<td className={t.type === "income" ? "income-amount" : "expense-amount"}>
|
||||
{t.type === "income" ? "+" : "-"}${t.amount}
|
||||
</td>
|
||||
<td>
|
||||
<button onClick={() => {
|
||||
if (window.confirm("Are you sure you want to delete this transaction?")) {
|
||||
onDeleteTransaction(t.id);
|
||||
}
|
||||
}}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TransactionList;
|
||||
Loading…
Reference in a new issue