Personal savings tracker Val on Val Town. Helps track money saved throughout the day by comparing would-have-spent vs. actually-spent, direct amount entry, or quick-tap preset buttons. Records to SQLite; history page shows totals + chart.
https://esm.town/v/stevekrouse/sqlitehttps://esm.sh/recharts@2.8.0?deps=react@18.2.0,react-dom@18.2.0backend/
index.ts ← Hono entry point
database/
migrations.ts ← CREATE TABLE
queries.ts ← insertSaving, getSavings, getDailyChart
routes/
savings.ts ← POST /api/savings, GET /api/savings, GET /api/savings/chart
static.ts ← serve frontend/ files
frontend/
index.html
index.tsx ← React root, hash router (#/ vs #/history)
components/
App.tsx ← top-level router
MainPage.tsx ← main input page
HistoryPage.tsx ← history + chart page
CoinExplosion.tsx ← coin burst animation
shared/
types.ts ← SavingsRecord, ChartDataPoint types
Table name: boymath_savings_1
CREATE TABLE IF NOT EXISTS boymath_savings_1 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
amount_cents INTEGER NOT NULL, -- always positive (UI blocks negative submission)
recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
)
| Method | Path | Description |
|---|---|---|
| POST | /api/savings | Body: { amount_cents: number }. Insert record. Returns the inserted row. |
| GET | /api/savings?period=day|week|month | Returns { total_cents, records[] } for the period |
| GET | /api/savings/chart | Returns { days: ChartDataPoint[] } — last 30 days of daily totals + 7-day rolling avg |
$1 $2 $3 $4 / 5¢ 10¢ 20¢ 50¢ — yellow (#FFCF48) circles.All modes write to a single amountCents: number state. Switching modes resets the previous mode's inputs.
Mode A — Subtraction (Input 1 & Input 2)
<input inputMode="numeric"> fields (triggers iOS number pad)$X.XX (e.g. typing "300" → shows "$3.00")amountCents = input1Cents - input2CentsMode B — Direct input
Mode C — Preset buttons
amountCents// Raw integer treated as cents: 300 → $3.00, 1250 → $12.50
function centsFromDigits(digits: string): number {
return parseInt(digits.replace(/\D/g, '') || '0', 10);
}
function formatCents(cents: number): string {
const abs = Math.abs(cents);
const sign = cents < 0 ? '-' : '';
return `${sign}$${(abs / 100).toFixed(2)}`;
}
amountCents < 0: full background transitions to red, Submit button disabled + grayed outamountCents > 0: the Submit button turns #FFBC00.amountCents > 0: POST /api/savings with { amount_cents: amountCents }<CoinExplosion> animation, then reset all state to $0.00@keyframes1 Day, 1 Week, 1 MonthComposedChart — bars for daily totals + line overlay for 7-day rolling average
GET /api/savings?period=... → updates total displayGET /api/savings/chartgetDailyChart() in queries.ts:
SELECT date(recorded_at) as day, SUM(amount_cents) as total_cents
FROM boymath_savings_1
WHERE recorded_at >= date('now', '-30 days')
GROUP BY date(recorded_at)
ORDER BY day ASC
7-day rolling average computed in TypeScript after fetching.
<input type="text" inputMode="numeric" pattern="[0-9]*"> — numeric keypad on iOS without decimal/negative keys?deps=react@18.2.0,react-dom@18.2.0. First line of every .tsx: /** @jsxImportSource https://esm.sh/react@18.2.0 */readFile/serveFile from https://esm.town/v/std/utils/index.tswindow.location.hash === '#/history' switches viewapp.onError((err) => Promise.reject(err)) at top levelexport default app.fetch