diff --git a/src/plays/expense-tracker-pro/ExpenseTrackerPro.jsx b/src/plays/expense-tracker-pro/ExpenseTrackerPro.jsx
new file mode 100644
index 000000000..84ad12b93
--- /dev/null
+++ b/src/plays/expense-tracker-pro/ExpenseTrackerPro.jsx
@@ -0,0 +1,59 @@
+import PlayHeader from 'common/playlists/PlayHeader';
+import useTransactions from './hooks/useTransaction';
+import SummaryCards from './components/SummaryCards';
+import TransactionForm from './components/TransactionForm';
+import TransactionList from './components/TransactionList';
+import Charts from './components/Charts';
+
+function ExpenseTrackerPro(props) {
+ const {
+ filtered,
+ summary,
+ chartData,
+ monthlyData,
+ availableMonths,
+ filterType,
+ setFilterType,
+ filterMonth,
+ setFilterMonth,
+ addTransaction,
+ deleteTransaction,
+ clearAll
+ } = useTransactions();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {filtered.length > 0 && (
+
+
+ Clear All Transactions
+
+
+ )}
+
+
+
+ );
+}
+
+export default ExpenseTrackerPro;
diff --git a/src/plays/expense-tracker-pro/Readme.md b/src/plays/expense-tracker-pro/Readme.md
new file mode 100644
index 000000000..37585355a
--- /dev/null
+++ b/src/plays/expense-tracker-pro/Readme.md
@@ -0,0 +1,20 @@
+@'
+## Expense Tracker Pro
+
+A full-featured expense tracker built with React that helps you manage income and expenses.
+
+### Features
+- Track income and expenses with categories
+- Summary cards showing balance, total income, total expenses
+- Visual charts โ expenses by category (bar) and monthly overview
+- Filter by type (income/expense) and month
+- localStorage persistence โ data saved between sessions
+- Delete individual transactions or clear all
+
+### Concepts Used
+- useState, useEffect, useMemo
+- Custom hook (useTransactions)
+- localStorage for data persistence
+- CSS-based charts (no external chart library)
+- Tailwind CSS for styling
+'@ | Out-File -FilePath src/plays/expense-tracker-pro/Readme.md -Encoding utf8
\ No newline at end of file
diff --git a/src/plays/expense-tracker-pro/components/Charts.jsx b/src/plays/expense-tracker-pro/components/Charts.jsx
new file mode 100644
index 000000000..25d44cbe7
--- /dev/null
+++ b/src/plays/expense-tracker-pro/components/Charts.jsx
@@ -0,0 +1,95 @@
+import { getCategoryMeta, CHART_COLORS } from '../constants/categories';
+import { formatCurrency, getMonthLabel } from '../utils/format';
+
+const ExpenseChart = ({ data }) => {
+ if (!data.length)
+ return (
+
+ No expense data yet
+
+ );
+ const total = data.reduce((sum, d) => sum + d.amount, 0);
+
+ return (
+
+ {data.map((item, i) => {
+ const meta = getCategoryMeta(item.category);
+ const pct = total ? (item.amount / total) * 100 : 0;
+
+ return (
+
+
+
+ {meta.icon} {meta.label}
+
+
+ {formatCurrency(item.amount)} ({pct.toFixed(0)}%)
+
+
+
+
+ );
+ })}
+
+ );
+};
+
+const MonthlyChart = ({ data }) => {
+ if (!data.length)
+ return (
+
+ No monthly data yet
+
+ );
+ const max = Math.max(...data.flatMap((d) => [d.income, d.expense]));
+
+ return (
+
+ {data.map((item) => (
+
+
+
+ {getMonthLabel(item.month).split(' ')[0]}
+
+
+ ))}
+
+ );
+};
+
+const Charts = ({ chartData, monthlyData }) => (
+
+
+
Expenses by Category
+
+
+
+
Monthly Overview
+
+
+ Income
+
+
+ Expense
+
+
+
+
+
+);
+
+export default Charts;
diff --git a/src/plays/expense-tracker-pro/components/SummaryCards.jsx b/src/plays/expense-tracker-pro/components/SummaryCards.jsx
new file mode 100644
index 000000000..07afe5dba
--- /dev/null
+++ b/src/plays/expense-tracker-pro/components/SummaryCards.jsx
@@ -0,0 +1,28 @@
+import { formatCurrency } from '../utils/format';
+
+const SummaryCards = ({ summary }) => {
+ const { income, expense, balance } = summary;
+
+ return (
+
+
= 0 ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}
+ >
+
Balance
+
= 0 ? 'text-green-600' : 'text-red-600'}`}>
+ {formatCurrency(balance)}
+
+
+
+
Income
+
{formatCurrency(income)}
+
+
+
Expenses
+
{formatCurrency(expense)}
+
+
+ );
+};
+
+export default SummaryCards;
diff --git a/src/plays/expense-tracker-pro/components/TransactionForm.jsx b/src/plays/expense-tracker-pro/components/TransactionForm.jsx
new file mode 100644
index 000000000..1bf801366
--- /dev/null
+++ b/src/plays/expense-tracker-pro/components/TransactionForm.jsx
@@ -0,0 +1,129 @@
+import { useState } from 'react';
+import { CATEGORIES } from '../constants/categories';
+
+const defaultForm = {
+ type: 'expense',
+ amount: '',
+ category: '',
+ description: '',
+ date: new Date().toISOString().split('T')[0]
+};
+
+const TransactionForm = ({ onAdd }) => {
+ const [form, setForm] = useState(defaultForm);
+ const [error, setError] = useState('');
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setForm((prev) => ({
+ ...prev,
+ [name]: value,
+ ...(name === 'type' ? { category: '' } : {})
+ }));
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ if (!form.amount || isNaN(form.amount) || parseFloat(form.amount) <= 0)
+ return setError('Please enter a valid amount.');
+ if (!form.category) return setError('Please select a category.');
+ if (!form.description.trim()) return setError('Please enter a description.');
+ setError('');
+ onAdd(form);
+ setForm({ ...defaultForm, type: form.type });
+ };
+
+ const currentCategories = CATEGORIES[form.type];
+ const inputClass =
+ 'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm outline-none focus:border-blue-400 transition-colors';
+
+ return (
+
+
Add Transaction
+
+ {['expense', 'income'].map((type) => (
+ setForm({ ...defaultForm, type })}
+ >
+ {type === 'expense' ? 'Expense' : 'Income'}
+
+ ))}
+
+
+
+ );
+};
+
+export default TransactionForm;
diff --git a/src/plays/expense-tracker-pro/components/TransactionList.jsx b/src/plays/expense-tracker-pro/components/TransactionList.jsx
new file mode 100644
index 000000000..1a6c9094f
--- /dev/null
+++ b/src/plays/expense-tracker-pro/components/TransactionList.jsx
@@ -0,0 +1,87 @@
+import { formatCurrency, formatDate } from '../utils/format';
+import { getCategoryMeta } from '../constants/categories';
+
+const TransactionList = ({
+ transactions,
+ onDelete,
+ filterType,
+ setFilterType,
+ filterMonth,
+ setFilterMonth,
+ availableMonths
+}) => {
+ const selectClass =
+ 'border border-gray-200 text-sm rounded-lg px-2 py-1.5 outline-none focus:border-blue-400 bg-white';
+
+ return (
+
+
+
Transactions ({transactions.length})
+
+ setFilterType(e.target.value)}
+ >
+ All Types
+ Income
+ Expense
+
+ setFilterMonth(e.target.value)}
+ >
+ All Months
+ {availableMonths.map((m) => (
+
+ {m}
+
+ ))}
+
+
+
+ {transactions.length === 0 ? (
+
+
๐ญ
+
No transactions yet. Add one above!
+
+ ) : (
+
+ {transactions.map((t) => {
+ const meta = getCategoryMeta(t.category);
+
+ return (
+
+
{meta.icon}
+
+
{t.description}
+
+ {meta.label} ยท {formatDate(t.date)}
+
+
+
+ {t.type === 'income' ? '+' : '-'}
+ {formatCurrency(t.amount)}
+
+
onDelete(t.id)}
+ >
+ x
+
+
+ );
+ })}
+
+ )}
+
+ );
+};
+
+export default TransactionList;
diff --git a/src/plays/expense-tracker-pro/constants/categories.js b/src/plays/expense-tracker-pro/constants/categories.js
new file mode 100644
index 000000000..25d44cbe7
--- /dev/null
+++ b/src/plays/expense-tracker-pro/constants/categories.js
@@ -0,0 +1,95 @@
+import { getCategoryMeta, CHART_COLORS } from '../constants/categories';
+import { formatCurrency, getMonthLabel } from '../utils/format';
+
+const ExpenseChart = ({ data }) => {
+ if (!data.length)
+ return (
+
+ No expense data yet
+
+ );
+ const total = data.reduce((sum, d) => sum + d.amount, 0);
+
+ return (
+
+ {data.map((item, i) => {
+ const meta = getCategoryMeta(item.category);
+ const pct = total ? (item.amount / total) * 100 : 0;
+
+ return (
+
+
+
+ {meta.icon} {meta.label}
+
+
+ {formatCurrency(item.amount)} ({pct.toFixed(0)}%)
+
+
+
+
+ );
+ })}
+
+ );
+};
+
+const MonthlyChart = ({ data }) => {
+ if (!data.length)
+ return (
+
+ No monthly data yet
+
+ );
+ const max = Math.max(...data.flatMap((d) => [d.income, d.expense]));
+
+ return (
+
+ {data.map((item) => (
+
+
+
+ {getMonthLabel(item.month).split(' ')[0]}
+
+
+ ))}
+
+ );
+};
+
+const Charts = ({ chartData, monthlyData }) => (
+
+
+
Expenses by Category
+
+
+
+
Monthly Overview
+
+
+ Income
+
+
+ Expense
+
+
+
+
+
+);
+
+export default Charts;
diff --git a/src/plays/expense-tracker-pro/hooks/useTransaction.js b/src/plays/expense-tracker-pro/hooks/useTransaction.js
new file mode 100644
index 000000000..e7f971c50
--- /dev/null
+++ b/src/plays/expense-tracker-pro/hooks/useTransaction.js
@@ -0,0 +1,111 @@
+import { useState, useEffect, useMemo } from 'react';
+
+const STORAGE_KEY = 'expense_tracker_pro_transactions';
+
+const generateId = () => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+
+const loadFromStorage = () => {
+ try {
+ const data = localStorage.getItem(STORAGE_KEY);
+
+ return data ? JSON.parse(data) : [];
+ } catch {
+ return [];
+ }
+};
+
+const useTransactions = () => {
+ const [transactions, setTransactions] = useState(loadFromStorage);
+ const [filterType, setFilterType] = useState('all');
+ const [filterMonth, setFilterMonth] = useState('all');
+
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(transactions));
+ }, [transactions]);
+
+ const addTransaction = (data) => {
+ const newTransaction = {
+ id: generateId(),
+ ...data,
+ amount: parseFloat(data.amount),
+ date: data.date || new Date().toISOString().split('T')[0],
+ createdAt: new Date().toISOString()
+ };
+ setTransactions((prev) => [newTransaction, ...prev]);
+ };
+
+ const deleteTransaction = (id) => {
+ setTransactions((prev) => prev.filter((t) => t.id !== id));
+ };
+
+ const clearAll = () => setTransactions([]);
+
+ const summary = useMemo(() => {
+ const income = transactions
+ .filter((t) => t.type === 'income')
+ .reduce((sum, t) => sum + t.amount, 0);
+ const expense = transactions
+ .filter((t) => t.type === 'expense')
+ .reduce((sum, t) => sum + t.amount, 0);
+
+ return { income, expense, balance: income - expense };
+ }, [transactions]);
+
+ const filtered = useMemo(() => {
+ return transactions.filter((t) => {
+ const matchType = filterType === 'all' || t.type === filterType;
+ const matchMonth = filterMonth === 'all' || t.date.startsWith(filterMonth);
+
+ return matchType && matchMonth;
+ });
+ }, [transactions, filterType, filterMonth]);
+
+ const chartData = useMemo(() => {
+ const expenseMap = {};
+ transactions
+ .filter((t) => t.type === 'expense')
+ .forEach((t) => {
+ expenseMap[t.category] = (expenseMap[t.category] || 0) + t.amount;
+ });
+
+ return Object.entries(expenseMap).map(([category, amount]) => ({
+ category,
+ amount: parseFloat(amount.toFixed(2))
+ }));
+ }, [transactions]);
+
+ const monthlyData = useMemo(() => {
+ const map = {};
+ transactions.forEach((t) => {
+ const month = t.date.slice(0, 7);
+ if (!map[month]) map[month] = { month, income: 0, expense: 0 };
+ map[month][t.type] += t.amount;
+ });
+
+ return Object.values(map).sort((a, b) => a.month.localeCompare(b.month));
+ }, [transactions]);
+
+ const availableMonths = useMemo(() => {
+ const months = [...new Set(transactions.map((t) => t.date.slice(0, 7)))];
+
+ return months.sort((a, b) => b.localeCompare(a));
+ }, [transactions]);
+
+ return {
+ transactions,
+ filtered,
+ summary,
+ chartData,
+ monthlyData,
+ availableMonths,
+ filterType,
+ setFilterType,
+ filterMonth,
+ setFilterMonth,
+ addTransaction,
+ deleteTransaction,
+ clearAll
+ };
+};
+
+export default useTransactions;
diff --git a/src/plays/expense-tracker-pro/utils/format.js b/src/plays/expense-tracker-pro/utils/format.js
new file mode 100644
index 000000000..cc706c07c
--- /dev/null
+++ b/src/plays/expense-tracker-pro/utils/format.js
@@ -0,0 +1,22 @@
+export const formatCurrency = (amount) =>
+ new Intl.NumberFormat('en-PH', {
+ style: 'currency',
+ currency: 'PHP',
+ minimumFractionDigits: 2
+ }).format(amount);
+
+export const formatDate = (dateStr) =>
+ new Date(dateStr).toLocaleDateString('en-PH', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ });
+
+export const getMonthLabel = (monthStr) => {
+ const [year, month] = monthStr.split('-');
+
+ return new Date(year, month - 1).toLocaleDateString('en-PH', {
+ month: 'long',
+ year: 'numeric'
+ });
+};