mirror of
https://github.com/mosh-hamedani/expense-tracker-starter.git
synced 2026-05-21 11:58:31 +02:00
refactor(app): extract Summary, TransactionForm, and TransactionList components
Split monolithic App.jsx into focused child components, each owning their relevant state (form fields, filters, totals calculation). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
84a35eeadc
commit
7df89909bc
|
|
@ -11,12 +11,17 @@ Expense tracker app from Mosh's [Claude Code course](https://codewithmosh.com/p/
|
||||||
- `npm run dev` — Start Vite dev server (http://localhost:5173)
|
- `npm run dev` — Start Vite dev server (http://localhost:5173)
|
||||||
- `npm run build` — Production build
|
- `npm run build` — Production build
|
||||||
- `npm run lint` — ESLint
|
- `npm run lint` — ESLint
|
||||||
|
- `npm run preview` — Preview production build
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Single-component React 19 app with Vite 7. All state and UI live in `src/App.jsx` — no routing, no state library, no backend. Plain CSS styling via `App.css` and `index.css`.
|
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
|
## Known Intentional Issues
|
||||||
|
|
||||||
- Transaction amounts stored as strings, so `reduce()` concatenates instead of summing
|
|
||||||
- "Freelance Work" is typed as "expense" instead of "income"
|
- "Freelance Work" is typed as "expense" instead of "income"
|
||||||
|
|
|
||||||
138
src/App.jsx
138
src/App.jsx
|
|
@ -1,5 +1,8 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
import Summary from './Summary'
|
||||||
|
import TransactionForm from './TransactionForm'
|
||||||
|
import TransactionList from './TransactionList'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [transactions, setTransactions] = useState([
|
const [transactions, setTransactions] = useState([
|
||||||
|
|
@ -13,143 +16,18 @@ function App() {
|
||||||
{ id: 8, description: "Netflix", amount: 15, type: "expense", category: "entertainment", date: "2025-01-10" },
|
{ id: 8, description: "Netflix", amount: 15, type: "expense", category: "entertainment", date: "2025-01-10" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [description, setDescription] = useState("");
|
const handleAddTransaction = (transaction) => {
|
||||||
const [amount, setAmount] = useState("");
|
setTransactions([...transactions, transaction]);
|
||||||
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],
|
|
||||||
};
|
|
||||||
|
|
||||||
setTransactions([...transactions, newTransaction]);
|
|
||||||
setDescription("");
|
|
||||||
setAmount("");
|
|
||||||
setType("expense");
|
|
||||||
setCategory("food");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<h1>Finance Tracker</h1>
|
<h1>Finance Tracker</h1>
|
||||||
<p className="subtitle">Track your income and expenses</p>
|
<p className="subtitle">Track your income and expenses</p>
|
||||||
|
|
||||||
<div className="summary">
|
<Summary transactions={transactions} />
|
||||||
<div className="summary-card">
|
<TransactionForm onAddTransaction={handleAddTransaction} />
|
||||||
<h3>Income</h3>
|
<TransactionList transactions={transactions} />
|
||||||
<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>
|
|
||||||
</div>
|
</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;
|
||||||
60
src/TransactionList.jsx
Normal file
60
src/TransactionList.jsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const categories = ["food", "housing", "utilities", "transport", "entertainment", "salary", "other"];
|
||||||
|
|
||||||
|
function TransactionList({ transactions }) {
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransactionList;
|
||||||
Loading…
Reference in a new issue