Implement expense tracking foundation for v1

This commit is contained in:
2026-03-23 12:32:36 -04:00
parent 5d7e25c015
commit 905af75cd8
39 changed files with 9923 additions and 9 deletions

View File

@@ -0,0 +1,121 @@
"use client";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { getCategoryLabel, type CategoryValue } from "@/lib/categories";
import { getCurrentMonthKey, getMonthLabel, isDateInMonth } from "@/lib/date";
import { formatCurrencyFromCents } from "@/lib/money";
type ExpenseRecord = {
id: string;
title: string;
amountCents: number;
date: string;
category: CategoryValue;
};
export function HomeDashboard() {
const [expenses, setExpenses] = useState<ExpenseRecord[]>([]);
useEffect(() => {
async function loadExpenses() {
const response = await fetch("/expenses", { cache: "no-store" });
const payload = (await response.json()) as { expenses?: ExpenseRecord[] };
setExpenses(payload.expenses ?? []);
}
void loadExpenses();
}, []);
const month = getCurrentMonthKey();
const monthExpenses = useMemo(
() => expenses.filter((expense) => isDateInMonth(expense.date, month)),
[expenses, month],
);
const recentExpenses = useMemo(() => monthExpenses.slice(0, 6), [monthExpenses]);
const totalSpentCents = useMemo(
() => monthExpenses.reduce((sum, expense) => sum + expense.amountCents, 0),
[monthExpenses],
);
return (
<div className="space-y-10">
<section className="grid gap-6 rounded-[2rem] border border-stone-200 bg-[radial-gradient(circle_at_top_left,_rgba(251,191,36,0.26),_transparent_32%),linear-gradient(135deg,#fffaf2,#f3efe7)] p-8 shadow-[0_28px_70px_rgba(120,90,50,0.10)] lg:grid-cols-[1.2fr_0.8fr]">
<div className="space-y-5">
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-amber-700">Monthly Expense Tracker</p>
<h1 className="max-w-3xl text-5xl font-semibold leading-tight text-stone-950">
A calm local-first home for everyday spending.
</h1>
<p className="max-w-2xl text-lg leading-8 text-stone-600">
The dashboard is starting with the expense-tracking slice first: fast entry, visible history, and a live pulse on the current month.
</p>
<div className="flex flex-wrap gap-3">
<Link
href="/add-expense"
className="rounded-full bg-stone-950 px-5 py-3 text-sm font-semibold text-white transition hover:bg-stone-800"
>
Add an expense
</Link>
<Link
href="/income"
className="rounded-full border border-stone-300 bg-white px-5 py-3 text-sm font-semibold text-stone-800 transition hover:border-stone-900"
>
View paycheck plan
</Link>
</div>
</div>
<div className="rounded-[1.75rem] border border-white/80 bg-white/90 p-6">
<p className="text-sm font-semibold uppercase tracking-[0.24em] text-stone-500">This month</p>
<h2 className="mt-2 text-3xl font-semibold text-stone-950">{getMonthLabel(month)}</h2>
<div className="mt-6 grid gap-4 sm:grid-cols-2">
<article className="rounded-3xl bg-stone-950 px-4 py-5 text-white">
<p className="text-xs uppercase tracking-[0.2em] text-stone-300">Total spent</p>
<p className="mt-3 text-3xl font-semibold">{formatCurrencyFromCents(totalSpentCents)}</p>
</article>
<article className="rounded-3xl bg-amber-50 px-4 py-5 text-stone-950">
<p className="text-xs uppercase tracking-[0.2em] text-amber-700">Entries logged</p>
<p className="mt-3 text-3xl font-semibold">{monthExpenses.length}</p>
</article>
</div>
</div>
</section>
<section className="rounded-[2rem] border border-stone-200 bg-white p-8 shadow-[0_24px_60px_rgba(120,90,50,0.08)]">
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.24em] text-stone-500">Recent expense pulse</p>
<h2 className="mt-2 text-3xl font-semibold text-stone-950">Latest entries</h2>
</div>
<Link href="/add-expense" className="text-sm font-semibold text-amber-800 transition hover:text-stone-950">
Manage expenses
</Link>
</div>
<div className="mt-6 grid gap-3">
{recentExpenses.length === 0 ? (
<div className="rounded-3xl border border-dashed border-stone-300 px-4 py-10 text-center text-stone-600">
No expenses recorded yet. Start with one quick entry.
</div>
) : (
recentExpenses.map((expense) => (
<article
key={expense.id}
className="flex flex-wrap items-center justify-between gap-3 rounded-3xl border border-stone-200 bg-[#fffcf7] px-4 py-4"
>
<div>
<p className="font-semibold text-stone-950">{expense.title}</p>
<p className="mt-1 text-sm text-stone-600">
{expense.date} · {getCategoryLabel(expense.category)}
</p>
</div>
<p className="font-semibold text-stone-950">{formatCurrencyFromCents(expense.amountCents)}</p>
</article>
))
)}
</div>
</section>
</div>
);
}