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.

22 h intermediate HTMLCSSJavaScriptIndexedDBService WorkerChart.js

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.

Projet terminé ? Envoyez les liens pour relecture. Envoyer pour relecture (50 points)

Plus de guides