boymath
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data β all from the browser, and deployed in milliseconds.
Viewing readonly version of main branch: v21View latest version
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.
- Backend: Hono on Deno (Val Town serverless)
- Frontend: React 18 + TailwindCSS (via cdn.twind.style)
- Storage: SQLite via
https://esm.town/v/stevekrouse/sqlite - Charts: Recharts via
https://esm.sh/recharts@2.8.0?deps=react@18.2.0,react-dom@18.2.0
backend/
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 |
- Top section: Input 1 (would-have-spent) and Input 2 (actually-spent), right-aligned, large gray placeholder text. A minus (β) between them. Horizontal divider line beneath.
- Amount Saved display: Large bold "$0.00" centered below divider β tappable for direct-input mode.
- Submit button: Large gray circle with "$" sign.
- Dashed divider
- 8 preset buttons in 2 rows:
$1 $2 $3 $4/5Β’ 10Β’ 20Β’ 50Β’β yellow (#FFCF48) circles. - Dashed divider
- History icon (bar chart icon) bottom-right β navigates to History page.
All modes write to a single amountCents: number state. Switching modes resets the previous mode's inputs.
Mode A β Subtraction (Input 1 & Input 2)
- Two
<input inputMode="numeric">fields (triggers iOS number pad) - User types digits only; displayed as
$X.XX(e.g. typing "300" β shows "$3.00") - Raw integer treated as cents: 300 β 300 cents β $3.00
amountCents = input1Cents - input2Cents- Activates when user focuses Input 1 or Input 2
- Resets button accumulator and direct-input state
Mode B β Direct input
- Tap the large Amount Saved display to focus it
- Same digit-only formatting (300 β $3.00)
- Activates when user taps the Amount Saved display
- Resets Input 1/2 and button accumulator
Mode C β Preset buttons
- Each button press adds its value to
amountCents - Pressing $1 ten times β $10.00; buttons do not hold state, they're additive triggers
- Activates on first button press; resets Input 1/2 and direct-input
// 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)}`;
}
- If
amountCents < 0: full background transitions to red, Submit button disabled + grayed out - Amount Saved display still shows the negative value (e.g. "-$1.50")
- If
amountCents > 0: the Submit button turns #FFBC00.
amountCents > 0: POST/api/savingswith{ amount_cents: amountCents }- On success: trigger
<CoinExplosion>animation, then reset all state to $0.00
- On trigger, render ~20 coin elements (πͺ emoji) absolutely positioned at the Submit button's center
- Each coin animates outward with a unique random angle and distance using CSS
@keyframes - Scale up β scale down + fade out over ~800ms
- After animation ends, unmount the component
- Back button (β) top-left to return to main page
- Period selector: Three pill buttons β
1 Day,1 Week,1 Month - Total saved: Large display (same style as Amount Saved on main page)
- Chart: Recharts
ComposedChartβ bars for daily totals + line overlay for 7-day rolling average- X-axis: dates; Y-axis: amount ($)
- Bars: yellow-tinted; Line: accent yellow (#FFCF48)
- On mount and period change:
GET /api/savings?period=...β updates total display - Chart always loads last 30 days:
GET /api/savings/chart
getDailyChart() 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.
- iOS Safari number pad:
<input type="text" inputMode="numeric" pattern="[0-9]*">β numeric keypad on iOS without decimal/negative keys - React pinning: All React imports use
?deps=react@18.2.0,react-dom@18.2.0. First line of every.tsx:/** @jsxImportSource https://esm.sh/react@18.2.0 */ - No Hono serveStatic: Use
readFile/serveFilefromhttps://esm.town/v/std/utils/index.ts - Routing: Hash-based β
window.location.hash === '#/history'switches view - Hono error unwrapping:
app.onError((err) => Promise.reject(err))at top level - Entry point export:
export default app.fetch
- Open Val URL in iOS Safari β confirm numeric keypad appears for all inputs
- Test Mode A: type in Input 1 and Input 2 β Amount Saved shows correct difference
- Test Mode B: tap Amount Saved display β type digits β formats correctly
- Test Mode C: tap preset buttons multiple times β amount accumulates correctly
- Test negative amount: make Input 2 > Input 1 β background turns red, Submit disabled
- Test Submit: valid amount β coin explosion fires, everything resets to $0.00, record saved
- Open History page β toggle 1 Day / 1 Week / 1 Month β totals update correctly
- Verify chart renders with bars + line overlay