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:
mrtony 2026-03-25 16:26:40 +08:00
parent 84a35eeadc
commit 7df89909bc
5 changed files with 166 additions and 132 deletions

View file

@ -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 build` — Production build
- `npm run lint` — ESLint
- `npm run preview` — Preview production build
## 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
- Transaction amounts stored as strings, so `reduce()` concatenates instead of summing
- "Freelance Work" is typed as "expense" instead of "income"

View file

@ -1,5 +1,8 @@
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([
@ -13,143 +16,18 @@ function App() {
{ 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],
};
setTransactions([...transactions, newTransaction]);
setDescription("");
setAmount("");
setType("expense");
setCategory("food");
const handleAddTransaction = (transaction) => {
setTransactions([...transactions, transaction]);
};
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} />
</div>
);
}

30
src/Summary.jsx Normal file
View 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
View 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
View 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;