Build an offline budget tracker app (PWA)
An installable app that records income and expenses, works offline, shows a monthly chart and exports to CSV, with no server and no account.
1 Friday evening: screens and data
Sketch three screens on paper: Add (amount, category, date, note), This month (total in, total out, balance, chart by category), History (list with delete).
Define one record:
{ id: crypto.randomUUID(), type: "expense", amount: 1250, category: "Food", date: "2026-11-03", note: "" }
Store money as whole cents (1250 = 12.50) so sums never drift.
Done when: you can describe every field and every screen in one paragraph.
2 Saturday morning: the app shell and storage
One index.html, app.css, app.js. Use IndexedDB through a tiny wrapper so the data survives reloads and is not limited like localStorage:
const dbp = new Promise((res, rej) => {
const r = indexedDB.open("budget", 1);
r.onupgradeneeded = () => r.result.createObjectStore("tx", { keyPath: "id" });
r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error);
});
async function tx(mode, fn) {
const db = await dbp;
return new Promise((res, rej) => {
const t = db.transaction("tx", mode), store = t.objectStore("tx"), out = fn(store);
t.oncomplete = () => res(out?.result ?? out); t.onerror = () => rej(t.error);
});
}
const addTx = (rec) => tx("readwrite", (s) => s.put(rec));
const allTx = () => tx("readonly", (s) => s.getAll());
Done when: adding a record, reloading the page and seeing it again works.
3 Saturday afternoon: the month view
Group by category for the selected month and draw a doughnut with Chart.js (pinned version from a CDN). Format money with Intl.NumberFormat(undefined, { style: "currency", currency: "USD" }).
Done when: the totals match a calculator for 10 test records, including a refund (negative expense).
4 Sunday morning: offline and installable
Add manifest.webmanifest (name, icons 192/512, display: "standalone", start_url: "/") and a service worker that caches the app shell:
// sw.js
const CACHE = "budget-v1";
const SHELL = ["/", "/index.html", "/app.css", "/app.js", "/manifest.webmanifest"];
self.addEventListener("install", (e) => e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL))));
self.addEventListener("fetch", (e) => e.respondWith(caches.match(e.request).then((r) => r || fetch(e.request))));
Bump CACHE on every release so users get the new files.
Done when: the app opens with Wi-Fi off and "Install app" appears in Chrome.
5 Sunday afternoon: export and ship
- Export CSV (
Blob+URL.createObjectURL) and Import CSV for backups; the data lives only on the device. - Deploy to Vercel or Netlify (static files, HTTPS is required for service workers).
- Test on a real phone: install, add a record offline, reopen.
Done when: a friend installs it on their phone and records a week of spending.
Approfondir avec ces parcours
Plus de guides
- schedule_sendAutomate a daily report with Python, cron and emailA script that pulls data every morning, builds a short summary with a chart, and emails it to you, running unattended on a small Linux server.
- event_availableBuild an online booking system for a small businessCustomers pick a free time slot, book it and get an email confirmation; the owner sees and cancels bookings in a simple admin page.
- monitor_heartBuild an uptime monitor that alerts youA small service that checks your websites every minute, records response times, and emails you when a site goes down and when it comes back.