A single-page donation tracker backed by a Google Sheet. No backend, no build step, no database. You edit a spreadsheet; the page updates within minutes.
Built for صدقة جارية campaigns run by a few friends in a WhatsApp group — the money is pooled, spent on one project at a time, and everyone can see where it went.
- Live total, share count, progress bar toward the current goal
- Milestones with real delivery states — funds collected is not work done
- Photos of past examples while collecting, receipts and proof once finished
- InstaPay, e-wallets and bank transfer, each tap-to-copy
- Arabic, RTL, mobile first, dark
Create a Google Sheet with five tabs. The names below are just labels for you — the page finds each tab by its gid, so call them whatever you like:
| Tab | Columns | What it is |
|---|---|---|
donations |
date · channel · amount |
one row per donation received |
milestones |
name · target · note · status · link · images · proof |
the projects, in spending order |
payments |
type · name · provider · number · iban · link |
how people can give |
content |
text · style |
the paragraphs at the top of the page |
settings |
key · value |
numbers and wording |
Starter CSVs for all five are in examples/. Create each tab, then
File → Import → Upload → Replace current sheet for that tab.
Only the header row matters — column order is free, and extra columns are ignored.
content and settings are optional: leave their gids as PASTE-GID and the
page runs on its built-in Arabic wording. The donations and milestones tabs
are what make it useful.
Only amount is read. 1,250, ١٢٥٠, ج.م ٣٠٠ and 1 500 all parse.
Do not add a totals row; it would be counted twice.
Order is spending order. Money is one pool: a project is only funded once everything above it is paid for.
-
target— cost in your currency. Leave empty if not confirmed yet; the page then showsnoteinstead of a number and skips it in the arithmetic. -
status— leave empty while collecting. Then, by hand, as things happen:value badge effect (empty) الهدف الحالي / التالي / لاحقًا / اكتمل جمع المبلغ worked out from the money transferredتم تحويل المبلغ إلى الجهة subtracted from the headline figure workingجارٍ التنفيذ subtracted done✓ تم بحمد الله subtracted The headline number is money still in hand — collected minus everything already handed over. Only mark a milestone once the transfer has really happened.
-
link— an outward link, shown as اعرف المزيد. -
images— photos of comparable past work, shown while still collecting. -
proof— receipts and photos of the finished work, shown only whenstatusisdone, replacingimages.
Separate multiple images with commas: img/a.jpg, img/b.jpg.
One row per method. type groups them under a heading:
| type | uses | shows |
|---|---|---|
instapay |
name, number, link |
a transfer button beside a tap-to-copy username |
ewallet |
name, provider, number |
provider name and a tap-to-copy number |
bank |
name, provider, number, iban |
account number and IBAN, both tap-to-copy |
Any other type gets its own section too — add label_<type> in settings to
title it. Adding a wallet is a row, never a code change.
Each row is a paragraph at the top of the page, in order. style is text,
quote (muted, for a hadith or citation) or prayer. Alt+Enter makes a line
break inside a cell.
key / value. Everything here is optional — each key has a sensible Arabic
default, so an empty tab still renders a correct page. Common ones:
title العنوان أعلى الصفحة
currency جنيه مصري
share_value 200 ← one "سهم" is worth this much
fallback_total 0 ← shown only if the donations tab cannot be read
projects_note a line above the milestone list
Every visible string can be overridden: label_current, label_done,
label_instapay, label_copy, label_offline … see examples/settings.csv
and DEFAULTS in src/model.js for the full list.
File → Share → Publish to web → Entire document → Comma-separated values (.csv) → Publish.
Make sure "Automatically republish when changes are made" is ticked, or your edits will never reach the page.
Google gives you a URL containing a long id starting 2PACX-. Copy that id.
Then click each tab and read its gid from the address bar:
https://docs.google.com/spreadsheets/d/…/edit#gid=1295809989
└─ this
Put both into config.js — the only file you edit. You can paste the whole
published URL or just the id, and either the bare gid or the whole address-bar
URL; both are recognised:
export const SHEET = {
publishedId: "2PACX-1vS…",
tabs: {
donations: "0",
milestones: "1295809989",
payments: "…",
content: "…",
settings: "…",
},
};Published CSV is the only Google endpoint that sends CORS headers, which is why this needs gids rather than tab names.
Edits appear on the page within a few minutes — Google's publish cache lags a little behind the editor.
If the sheet cannot be reached at all, the page says so rather than showing a zero. Open the browser console for the specific tab that failed.
Dashboard — Workers & Pages → Create → Pages → Upload assets → drag the whole folder in. Or:
npx wrangler pages deploy . --project-name=my-sadaqaYou get https://my-sadaqa.pages.dev. Then, once, open index.html and
replace YOUR-PROJECT.pages.dev in the four og: / twitter: meta tags with
your real domain, and deploy again — chat apps need absolute URLs or the link
preview shows nothing.
Test the preview at developers.facebook.com/tools/debug → Scrape Again before sharing. WhatsApp caches a link's preview the first time it is sent and will not refresh it.
Keep the preview text and og.png free of amounts and goals — they are static
and would go stale. The live numbers are on the page.
Put them in img/ and deploy them with the site, then reference them relatively:
milestone images → img/well-1.jpg, img/well-2.jpg
Facebook and Google Drive links do not work as images — they block hotlinking and the photo silently disappears. Files must be uploaded with the site, or served from somewhere that allows direct linking.
Adding photos therefore needs a redeploy. Everything else — text, amounts, milestones, statuses, payment methods — is sheet-only.
Regenerate og.png after editing og-template.html:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--headless --disable-gpu --window-size=1200,630 \
--screenshot=og.png --virtual-time-budget=4000 og-template.htmlpython3 -m http.server 8000then open http://localhost:8000. Opening index.html by double-clicking will
not work: browsers block both ES modules and cross-origin fetches on
file://, so the page falls back to its offline state.
index.html markup only
styles.css all styling; change --accent to re-skin
config.js your sheet id and gids — the only file you must edit
src/csv.js CSV, number and URL parsing
src/sheet.js fetching tabs, turning rows into objects
src/model.js the arithmetic and rules; DEFAULTS lives here
src/render.js everything that touches the DOM
src/main.js wiring
examples/ starter CSV for each tab
Plain ES modules, no bundler, no dependencies except a Google font.
Safety. The sheet is treated as untrusted input: text is inserted as
textContent, never HTML, and any URL whose scheme is not http(s) is dropped,
so a javascript: link in a cell cannot execute. Several people usually share
edit access — this matters.
Never renders zero or a broken page. If the donations tab fails, times out
(8s), or returns nothing usable, the page shows fallback_total with a quiet
note. Any tab can fail independently without taking the others down.
MIT — see LICENSE.