Skip to content

feat(supporters): redesign Support Us as tiered Supporters page#913

Merged
dadofsambonzuki merged 71 commits into
mainfrom
feat/supporters-page
May 2, 2026
Merged

feat(supporters): redesign Support Us as tiered Supporters page#913
dadofsambonzuki merged 71 commits into
mainfrom
feat/supporters-page

Conversation

@dadofsambonzuki

@dadofsambonzuki dadofsambonzuki commented Apr 13, 2026

Copy link
Copy Markdown
Member

Summary

Redesigns the former Support Us page into a fully-featured Supporters page at /supporters, with tiered org sponsors, a live individual supporters section, donation options, and a lightning node row.


Routing & Navigation

  • Renamed /support-us/supporters with a 301 redirect from the old URL to preserve inbound links
  • Updated nav label, map attribution link, and all i18n keys from supportUssupporters across all 8 locales (en, de, fr, es, nl, pt-BR, ru, bg)

Tiered Org Sponsors

  • New sponsors.tsx config defines 5 tiers: Explorer, Wayfinder, Cartographer, Navigator, Pioneer
  • Each tier renders via a SupportSection component with per-tier colour schemes, sponsor logo cards, and an always-visible "Become a {tier}" CTA card
  • Explorer and Wayfinder sections display side-by-side in a 2-column layout with compact cards; higher tiers are full-width 3-column
  • Added sponsors: JAN3 (Explorer), Bittylicious (Explorer), Coinos (Wayfinder), BTC Curacao (Cartographer), Wallet of Satoshi (Navigator)
  • Added SVG logos (light + dark variants) for JAN3; PNG for Bittylicious

Individual Supporters (Pleb Tier)

  • Live leaderboard fetched server-side from the Geyser GraphQL API on every page load
  • Named funders: avatar validated via HEAD request; falls back to a Robohash robot seeded by user ID
  • Anonymous funders: each donation shown as a separate "Anon" entry with a Robohash seeded by createdAt timestamp
  • Platform accounts (BTC Map, Geyser) excluded via an exceptions list
  • Sorted by cumulative sats descending
  • Avatar tooltip (name + sats) on hover/tap; first tap shows tooltip, second tap follows the Geyser profile link
  • "Powered by Geyser" footer linking to the leaderboard
  • Entries with no URL render a <div> rather than a broken <a href="#">

Donation Options

  • On-chain, Lightning (donations@btcmap.org), and Lightning Node rows using a shared DonationOption component
  • QR modals now use the common Modal component (same backdrop, slide-up animation, Escape key, focus management, and scroll lock as language/apps modals)
  • Node QR encodes the raw pubkey; clicking it opens the Amboss page
  • Node row uses a bolt icon instead of the LN logo
  • Removed hardcoded truncation from donation values; replaced with CSS truncate

Lightning Address

  • Added /.well-known/lnurlp/donations SvelteKit server route that proxies btcmap@rizful.com and rewrites the text/identifier to donations@btcmap.org

Lightning Node

  • Updated pubkey to 038f4d8a…
  • Updated Amboss footer link to /node/038f4d8a…

i18n

  • All previously hardcoded English strings on the page are now keyed and translated across all 8 locales:
    • supporters.individual.heading, description, cta
    • supporters.sponsors.description
    • supporters.node.scanOrClick

Security / Accessibility

  • All target="_blank" links use rel="noopener noreferrer"
  • Anon pleb entries with no URL render a <div> instead of a broken <a href="#">

Bug Fixes

  • Restored SavedPlace type accidentally dropped during rebase conflict resolution

Summary by CodeRabbit

  • New Features

    • Added a full "Supporters" page with sponsorship tiers, supporter lists (named + anon) and sponsor galleries.
    • Added "Node" as a donation option alongside On-chain and Lightning; QR modal and copy actions for each.
  • Navigation & Routes

    • Renamed "Support Us" → "Supporters" in the UI; legacy /support-us now redirects to /supporters.
  • Updates

    • Updated translations across languages and enhanced donation UI with Lightning address + scan-or-click prompts.
    • Live funder data integrated for supporters lists.

@netlify

netlify Bot commented Apr 13, 2026

Copy link
Copy Markdown

Deploy Preview for btcmap ready!

Name Link
🔨 Latest commit 2d965b8
🔍 Latest deploy log https://app.netlify.com/projects/btcmap/deploys/69f5d08fe76c23000829bd09
😎 Deploy Preview https://deploy-preview-913--btcmap.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 57 (🔴 down 35 from production)
Accessibility: 97 (no change from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 96 (no change from production)
PWA: 90 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@dadofsambonzuki has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 32 minutes and 44 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 104c47bc-eb77-4fb5-82eb-cc55d9fd59b9

📥 Commits

Reviewing files that changed from the base of the PR and between 23a1bcd and 2d965b8.

📒 Files selected for processing (4)
  • src/routes/.well-known/lnurlp/donations/+server.ts
  • src/routes/supporters/+page.server.ts
  • src/routes/supporters/components/PlebSection.svelte
  • src/routes/supporters/components/SupportSection.svelte
📝 Walkthrough

Walkthrough

Renames and migrates the legacy /support-us support flow to a new /supporters experience: i18n keys renamed across locales, legacy support page/components removed, a new supporters page and server load fetch Geyser funder data, new sponsor/pleb components and types added, LNURLP donations proxy endpoint added, routing and UI links updated, and DonationType extended with "Node".

Changes

Supporters migration (single DAG)

Layer / File(s) Summary
Data Shape / Types / i18n
src/lib/types.ts, src/lib/i18n/locales/*.json
Adds DonationType union member "Node"; renames top-level i18n namespace supportUssupporters and updates nav and media.assetNames keys across locales; introduces new supporters schema (donate.lightningAddress, node.scanOrClick, individual, sponsors, tiers).
Server / API
src/routes/supporters/+page.server.ts, src/routes/.well-known/lnurlp/donations/+server.ts
Adds page server load that queries Geyser GraphQL (funders + contributions), validates image URLs, builds pleb list (named + anon) and sorts by sats; adds CORS-enabled LNURLP GET proxy that rewrites metadata identifiers to donations@btcmap.org with timeout/error handling.
Core Page & Components
src/routes/supporters/+page.svelte, src/routes/supporters/components/*, src/routes/supporters/sponsors.tsx
Implements new /supporters page, QR modal and dynamic qrcode rendering, donation option component, PlebSection and SupportSection components, sponsorship tier and sponsors data/types.
Routing / Wiring / Redirects
src/routes/support-us/+page.server.ts, src/routes/support-us/+page.svelte (deleted)
Adds 301 redirect for /support-us/supporters; removes legacy /support-us page implementation and its components.
UI Links / Map / Store
src/components/layout/Header.svelte, src/lib/map/setup.ts, src/lib/store.ts
Updates header nav entry id/href to /supporters; map attribution link target changed to /supporters; updates socials.amboss URL.
Tooling / Config
tsconfig.json
Enables jsx: "preserve" in TypeScript compiler options.

Sequence Diagram

sequenceDiagram
    participant User
    participant Page as Supporters Page
    participant Server as Page Server
    participant Geyser as Geyser GraphQL API
    participant ImageCheck as Image Validator
    User->>Page: Visit /supporters
    Page->>Server: load() request
    Server->>Geyser: Query funders & contributions
    Geyser-->>Server: Return data
    Server->>ImageCheck: Validate image URLs (allowlist)
    alt image allowed
        ImageCheck-->>Server: keep imageUrl
    else
        ImageCheck-->>Server: reject -> use robohash
    end
    Server->>Server: Build named + anon plebs, sort by sats
    Server-->>Page: Return { plebs }
    User->>Page: Click donation option
    Page->>Page: Load qrcode module, generate QR on canvas
    Page-->>User: Show QR modal / link
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Review effort 4/5

Suggested reviewers

  • escapedcat

Poem

🐰 A hop and a nibble, the routes rearranged,
From support-us to supporters the names have changed.
Tiers, plebs and sponsors, QR codes in a hop,
New pages and endpoints — let generosity pop! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly summarizes the main change: redesigning the Support Us page into a tiered Supporters page.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering routing, tier design, individual supporters, donations, i18n updates, and security considerations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/supporters-page

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 32 minutes and 44 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR redesigns the former Support Us page into a new tiered Supporters page at /supporters, updates navigation and attribution links accordingly, and migrates i18n keys from supportUssupporters across locales.

Changes:

  • Replace /support-us with /supporters in navigation and map attribution, and introduce a tiered supporters layout (including an Individual Supporters / “Pleb” section).
  • Add a sponsors/tiers config (sponsors.tsx) and new Svelte components (SupportSection, PlebSection) to render the new page.
  • Update i18n locale files and various components to use the new supporters.* keys; add new supporter logo assets.

Reviewed changes

Copilot reviewed 17 out of 23 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tsconfig.json Enables JSX preservation to support a .tsx config file.
static/images/supporters/square.svg Adds Square logo asset (light).
static/images/supporters/square-dark.svg Adds Square logo asset (dark).
static/images/supporters/spiral.svg Adds Spiral logo asset.
static/images/supporters/plebs/nathan-day.jpg Adds individual supporter avatar.
static/images/supporters/opensats.png Adds OpenSats logo asset.
src/routes/supporters/sponsors.tsx Introduces tiers + sponsors + plebs configuration.
src/routes/supporters/components/SupportSection.svelte New tier card component for org sponsors.
src/routes/supporters/components/PlebSection.svelte New section component for individual supporters + CTA.
src/routes/supporters/components/DonationOption.svelte Updates translation keys from supportUs.* to supporters.*.
src/routes/supporters/+page.svelte New Supporters page layout with pleb + tiered org sponsors + donation UI.
src/routes/support-us/components/SupportSection.svelte Removes old Support Us SupportSection component.
src/lib/map/setup.ts Updates Leaflet attribution support link to /supporters.
src/lib/i18n/locales/{en,de,fr,es,nl,pt-BR,ru,bg}.json Renames supportUs keys to supporters and updates nav labels.
src/components/layout/Header.svelte Updates nav link label/id/url to Supporters.
src/components/InvoicePayment.svelte Updates error key to supporters.qrLoadError.
Comments suppressed due to low confidence (1)

src/routes/supporters/+page.svelte:121

  • These sections introduce new user-facing copy (headings, paragraphs, CTA label) as hardcoded English strings rather than i18n keys. Since the route supports multiple locales, this will cause non-English pages to show English text; consider moving this copy into the supporters locale entries and rendering via t(...).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/layout/Header.svelte
Comment thread src/routes/supporters/components/PlebSection.svelte
Comment thread src/routes/supporters/sponsors.tsx Outdated
Comment thread src/routes/supporters/components/PlebSection.svelte Outdated
Comment thread src/routes/supporters/components/SupportSection.svelte Outdated
- Rename route /support-us -> /supporters and nav label to 'Supporters'
- Rename i18n key supportUs -> supporters across all 8 locales
- Add sponsors.tsx config with Explorer/Wayfinder/Cartographer/Navigator/Pioneer tiers
- Add Pleb tier with individual supporter avatars (Nathan Day)
- Add new PlebSection component with circular avatar grid and always-visible CTA
- Add SupportSection component with per-tier colour accents and sponsor logo cards
- Add sponsor logos: Spiral, Square (black/white SVG), OpenSats
- Split page into Individual Supporters (top) and Industry Sponsors (bottom) sections
- Add AppDownloadModal, Modal, SaveButton components and session API routes
- Remove verbose intro copy and replace with tight marketing text + button CTAs
- Add JSX support to tsconfig for .tsx config files

🤖 Generated with [opencode](https://opencode.ai)
🤖 Generated with [opencode](https://opencode.ai)
… by cumulative contributions

🤖 Generated with [opencode](https://opencode.ai)
@dadofsambonzuki dadofsambonzuki marked this pull request as ready for review May 1, 2026 11:10
@dadofsambonzuki dadofsambonzuki requested a review from escapedcat May 1, 2026 11:10
@qodo-code-review

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Redesign Support Us as tiered Supporters page with live Geyser leaderboard

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Redesign Support Us page into tiered Supporters page with org sponsors and individual supporters
• Add server-side Geyser GraphQL integration to fetch live individual supporter leaderboard
• Implement 5-tier sponsorship system (Explorer, Wayfinder, Cartographer, Navigator, Pioneer) with
  color-coded sections
• Add Lightning node QR support and refactor donation modals to use common Modal component
• Update all i18n keys from supportUs to supporters across 8 locales and add new
  individual/sponsors descriptions
Diagram
flowchart LR
  A["Old /support-us page"] -- "301 redirect" --> B["/supporters route"]
  B --> C["Individual Supporters<br/>Geyser leaderboard"]
  B --> D["Tiered Org Sponsors<br/>5 sponsorship levels"]
  B --> E["Donation Options<br/>On-chain, Lightning, Node"]
  C -- "Server-side fetch" --> F["Geyser GraphQL API"]
  D --> G["SupportSection component<br/>per-tier styling"]
  C --> H["PlebSection component<br/>avatar grid + CTA"]
  E --> I["Modal component<br/>QR codes"]
Loading

Grey Divider

File Changes

1. src/routes/supporters/+page.svelte ✨ Enhancement +225/-0

New Supporters page with tiered layout

src/routes/supporters/+page.svelte


2. src/routes/supporters/+page.server.ts ✨ Enhancement +153/-0

Server-side Geyser GraphQL leaderboard fetching

src/routes/supporters/+page.server.ts


3. src/routes/supporters/sponsors.tsx ⚙️ Configuration changes +104/-0

Sponsorship tiers and sponsor data config

src/routes/supporters/sponsors.tsx


View more (22)
4. src/routes/supporters/components/PlebSection.svelte ✨ Enhancement +136/-0

Individual supporters circular avatar grid

src/routes/supporters/components/PlebSection.svelte


5. src/routes/supporters/components/SupportSection.svelte ✨ Enhancement +72/-0

Tiered sponsor section with color schemes

src/routes/supporters/components/SupportSection.svelte


6. src/routes/supporters/components/DonationOption.svelte ✨ Enhancement +61/-0

Refactored donation option with Node support

src/routes/supporters/components/DonationOption.svelte


7. src/routes/support-us/+page.server.ts ⚙️ Configuration changes +7/-0

301 redirect from old support-us route

src/routes/support-us/+page.server.ts


8. src/routes/.well-known/lnurlp/donations/+server.ts ✨ Enhancement +32/-0

Lightning address proxy server route

src/routes/.well-known/lnurlp/donations/+server.ts


9. src/components/layout/Header.svelte ⚙️ Configuration changes +3/-3

Update nav link from supportUs to supporters

src/components/layout/Header.svelte


10. src/components/InvoicePayment.svelte ⚙️ Configuration changes +1/-1

Update i18n key to supporters.qrLoadError

src/components/InvoicePayment.svelte


11. src/lib/map/setup.ts ⚙️ Configuration changes +2/-2

Update map attribution link to /supporters

src/lib/map/setup.ts


12. src/lib/store.ts ⚙️ Configuration changes +2/-1

Update Amboss link to Lightning node pubkey

src/lib/store.ts


13. src/lib/types.ts ✨ Enhancement +1/-1

Add Node type to DonationType union

src/lib/types.ts


14. src/lib/i18n/locales/en.json 📝 Documentation +17/-9

Translate supporters page keys to English

src/lib/i18n/locales/en.json


15. src/lib/i18n/locales/de.json 📝 Documentation +16/-8

Translate supporters page keys to German

src/lib/i18n/locales/de.json


16. src/lib/i18n/locales/fr.json 📝 Documentation +16/-8

Translate supporters page keys to French

src/lib/i18n/locales/fr.json


17. src/lib/i18n/locales/es.json 📝 Documentation +16/-8

Translate supporters page keys to Spanish

src/lib/i18n/locales/es.json


18. src/lib/i18n/locales/nl.json 📝 Documentation +16/-8

Translate supporters page keys to Dutch

src/lib/i18n/locales/nl.json


19. src/lib/i18n/locales/pt-BR.json 📝 Documentation +16/-8

Translate supporters page keys to Portuguese

src/lib/i18n/locales/pt-BR.json


20. src/lib/i18n/locales/ru.json 📝 Documentation +16/-8

Translate supporters page keys to Russian

src/lib/i18n/locales/ru.json


21. src/lib/i18n/locales/bg.json 📝 Documentation +16/-8

Translate supporters page keys to Bulgarian

src/lib/i18n/locales/bg.json


22. tsconfig.json ⚙️ Configuration changes +1/-0

Enable JSX preserve for .tsx config files

tsconfig.json


23. src/routes/support-us/+page.svelte Additional files +0/-175

...

src/routes/support-us/+page.svelte


24. src/routes/support-us/components/DonationOption.svelte Additional files +0/-51

...

src/routes/support-us/components/DonationOption.svelte


25. src/routes/support-us/components/SupportSection.svelte Additional files +0/-48

...

src/routes/support-us/components/SupportSection.svelte


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Avatar URL SSRF fetch🐞 Bug ⛨ Security
Description
The Supporters page server load performs server-side HEAD requests to each funder’s imageUrl
returned by Geyser; since that URL is user-controlled, this enables SSRF from your server. It also
risks slow/stalled renders because it does an unbounded number of external requests on every page
load.
Code

src/routes/supporters/+page.server.ts[R69-125]

+async function isImageUrl(
+	url: string,
+	fetch: typeof globalThis.fetch,
+): Promise<boolean> {
+	try {
+		const res = await fetch(url, { method: "HEAD" });
+		const contentType = res.headers.get("content-type") ?? "";
+		return res.ok && contentType.startsWith("image/");
+	} catch {
+		return false;
+	}
+}
+
+async function gql<T>(
+	query: string,
+	fetch: typeof globalThis.fetch,
+): Promise<T | null> {
+	const res = await fetch(GEYSER_API, {
+		method: "POST",
+		headers: { "Content-Type": "application/json" },
+		body: JSON.stringify({ query }),
+	});
+	if (!res.ok) return null;
+	return res.json();
+}
+
+export const load: PageServerLoad = async ({ fetch }) => {
+	try {
+		const [fundersJson, contributionsJson] = await Promise.all([
+			gql<FundersResponse>(FUNDERS_QUERY, fetch),
+			gql<ContributionsResponse>(CONTRIBUTIONS_QUERY, fetch),
+		]);
+
+		if (!fundersJson || !contributionsJson) {
+			return { plebs: [] };
+		}
+
+		const funders = fundersJson.data?.projectGet?.funders ?? [];
+		const contributions =
+			contributionsJson.data?.projectGet?.contributions ?? [];
+
+		// Named funders (pre-aggregated), excluding platform accounts
+		const namedFunders = funders
+			.filter(
+				(f): f is GeyserFunder & { user: NonNullable<GeyserFunder["user"]> } =>
+					f.user !== null && !EXCLUDED_USER_IDS.has(f.user.id),
+			)
+			.sort((a, b) => b.amountFunded - a.amountFunded);
+
+		// Validate avatar URLs in parallel; fall back to robohash for missing ones
+		const avatars = await Promise.all(
+			namedFunders.map((f) =>
+				f.user.imageUrl
+					? isImageUrl(f.user.imageUrl, fetch)
+					: Promise.resolve(false),
+			),
+		);
Evidence
imageUrl is read from the Geyser GraphQL response and then used directly as the URL for a
server-side fetch(..., { method: "HEAD" }) with no allowlist or network-range blocking, and is
executed for every named funder via Promise.all.

src/routes/supporters/+page.server.ts[9-18]
src/routes/supporters/+page.server.ts[69-80]
src/routes/supporters/+page.server.ts[118-125]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`src/routes/supporters/+page.server.ts` performs server-side `HEAD` requests to `f.user.imageUrl` values returned by Geyser. Because `imageUrl` is user-controlled, this creates an SSRF vector and can also slow down/stall page loads (unbounded parallel outbound requests).
### Issue Context
The code is attempting to validate that `imageUrl` points to an actual image. This validation should not require the server to fetch arbitrary user-provided URLs.
### Fix Focus Areas
- src/routes/supporters/+page.server.ts[69-80]
- src/routes/supporters/+page.server.ts[118-125]
### Suggested fix approach
Choose one (prefer options that avoid server-side fetching of untrusted URLs):
1. **Remove server-side URL validation**: always return the `imageUrl` as-is and let the client handle fallback using `<img on:error={...}>` (or a CSS/background fallback), OR always use RoboHash unless the URL matches a strict allowlist.
2. **Strict allowlist**: only `HEAD` URLs on an allowlisted host set (e.g., `geyser.fund`/their CDN), enforce `https:` scheme, and reject private/loopback/link-local IP ranges.
3. **Add safeguards** if you must fetch:
- cap number of avatar validations (e.g., top N funders)
- add a timeout/abort controller
- rate limit / cache results
Implement the chosen approach and ensure malicious URLs (localhost, 169.254.169.254, internal RFC1918 ranges) are never fetched server-side.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Upstream JSON parse crash🐞 Bug ☼ Reliability
Description
The LNURLP proxy route calls res.json() without checking res.ok or guarding against non-JSON
upstream responses, so upstream errors/outages can throw and return 500s. This can break wallet
LNURL discovery for donations@btcmap.org.
Code

src/routes/.well-known/lnurlp/donations/+server.ts[R7-10]

+export const GET: RequestHandler = async ({ fetch }) => {
+	const res = await fetch(UPSTREAM);
+	const data = await res.json();
+
Evidence
The handler always attempts to parse JSON immediately after fetch; any non-2xx response or invalid
body will throw before any error handling runs.

src/routes/.well-known/lnurlp/donations/+server.ts[7-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`/.well-known/lnurlp/donations` parses upstream JSON without checking status or catching parse errors. If the upstream is down or returns HTML/non-JSON, the route will throw and respond 500.
### Issue Context
This endpoint is used by wallets to resolve the LNURL-pay parameters. It should degrade gracefully (e.g., return 502) instead of crashing.
### Fix Focus Areas
- src/routes/.well-known/lnurlp/donations/+server.ts[7-10]
### Suggested fix approach
- Check `res.ok` and return a controlled error (e.g., `502 Bad Gateway`) when upstream fails.
- Wrap `await res.json()` in try/catch and return `502` on parse errors.
- Optionally forward/cache relevant headers (or add a short cache) to reduce upstream dependence.
Example sketch:
- If `!res.ok`, return `json({ error: 'Upstream unavailable' }, { status: 502, headers: { ... } })`
- `let data; try { data = await res.json(); } catch { return json({ error: 'Invalid upstream response' }, { status: 502, headers: { ... } }) }`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Anon tooltip state collision🐞 Bug ≡ Correctness
Description
PlebSection uses pleb.name to key/toggle tooltip state, but anonymous entries are all created
with the name "Anon", causing tooltip state collisions. On touch devices, tapping one anon entry can
show/hide tooltips for multiple anon entries instead of only the tapped one.
Code

src/routes/supporters/components/PlebSection.svelte[R16-26]

+let activeTooltip: string | null = null;
+
+function onTouchEnd(pleb: Pleb, e: TouchEvent) {
+	if (activeTooltip === pleb.name) {
+		// Already showing — let the tap through to follow the link
+		activeTooltip = null;
+	} else {
+		// First tap — show tooltip, block navigation
+		e.preventDefault();
+		activeTooltip = pleb.name;
+	}
Evidence
The server constructs every anonymous supporter with name: "Anon", while the client uses
activeTooltip === pleb.name for tooltip visibility and tap toggling, so all anon entries share the
same state key.

src/routes/supporters/+page.server.ts[136-143]
src/routes/supporters/components/PlebSection.svelte[16-26]
src/routes/supporters/components/PlebSection.svelte[66-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PlebSection` tracks the active tooltip by `pleb.name`, but anonymous entries all share the same name ("Anon"), which causes tooltip state collisions.
### Issue Context
The server creates anon entries per contribution, so there are often multiple "Anon" items.
### Fix Focus Areas
- src/routes/supporters/+page.server.ts[136-143]
- src/routes/supporters/components/PlebSection.svelte[16-26]
### Suggested fix approach
- Add a stable unique identifier to `Pleb` (e.g., `id`), and set it server-side:
- for named: use `user.id`
- for anon: use `createdAt` (or `anon-${createdAt}`)
- In `PlebSection`, use `activeTooltip: string | null` keyed by `pleb.id` instead of `pleb.name`.
- Optionally also change the `{#each}` key from `(i)` to `(pleb.id)` to keep DOM identity stable across sorts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/routes/supporters/+page.server.ts Outdated
Comment thread src/routes/.well-known/lnurlp/donations/+server.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/routes/.well-known/lnurlp/donations/`+server.ts:
- Around line 8-10: The route currently assumes fetch(UPSTREAM) always succeeds
and calls await res.json() directly, which can throw or hang; wrap the upstream
calls (the fetch(UPSTREAM) invocation and the other fetch block around lines
27-31) with a timeout-enabled fetch or AbortController, check res.ok before
parsing, and guard await res.json() in try/catch; on any timeout/non-OK/non-JSON
error return a controlled LNURL error payload (e.g., status/error message)
instead of throwing so the donation flow receives a proper LNURL error response.

In `@src/routes/supporters/`+page.server.ts:
- Around line 69-80: The isImageUrl function performs server-side HEAD requests
to arbitrary user-supplied URLs (SSRF risk); remove/stop using server-side fetch
in isImageUrl and any call sites (e.g., the usages around lines 119-133) and
instead validate avatar URLs locally: parse with URL to ensure scheme is http or
https, reject non-absolute URLs, disallow IPs/hostnames that resolve to
private/loopback addresses, and only accept URLs matching a configured allowlist
or pass the URL to a trusted image-proxy service for server-side fetching;
update any code that relied on isImageUrl (function name: isImageUrl) to either
rely on the local validation or the proxied/whitelisted fetch rather than
performing direct HEAD requests from the server.
- Around line 82-90: The gql function performs fetches to GEYSER_API without
timeouts; update gql to create an AbortController, pass controller.signal into
fetch, and set a timer (e.g., via setTimeout) to call controller.abort() after a
configurable timeout (e.g., 5s); ensure you clear the timer on success/failure
and handle AbortError by returning null or a clear error from gql so upstream
code can handle timeouts. Reference the gql function and GEYSER_API when adding
the AbortController and timeout logic.

In `@src/routes/supporters/`+page.svelte:
- Around line 105-115: Server and client are rendering different trees causing
hydration mismatch: change the conditional that currently checks typeof window
to use an onMount-controlled boolean (e.g., declare let mounted = false; set
mounted = true inside Svelte's onMount) so SSR outputs the same
<HeaderPlaceholder /> and the <h1> is only rendered after mounted is true on the
client; also update the Tailwind important modifier on the <h1> class from the
deprecated "!leading-tight" to the V4 postfix "leading-tight!" so replace
occurrences in the class string used by the <h1> element.

In `@src/routes/supporters/components/DonationOption.svelte`:
- Around line 49-54: The QR trigger button in DonationOption.svelte is icon-only
and missing an accessible name and explicit type; update the button (the element
that calls showQrToggle(network)) to include type="button" and provide an
accessible label (for example via aria-label="Show QR code" or a visually hidden
text node) so screen readers can announce its purpose while preserving the
existing on:click handler.

In `@src/routes/supporters/components/PlebSection.svelte`:
- Around line 16-26: The tooltip open state currently keys off pleb.name
(activeTooltip) which causes all "Anon" entries to share state; change the state
to store a unique supporter identifier (e.g., pleb.id or a generated unique key)
and update every place that reads/sets activeTooltip to use that identifier
instead of pleb.name (notably the onTouchEnd handler and the other tooltip
toggle/clear logic that compare or assign activeTooltip). If Plebs don't have a
stable id, derive one from a unique property or the list index when rendering,
ensure the same unique key is used wherever activeTooltip is compared/assigned,
and leave null behavior unchanged.

In `@src/routes/supporters/components/SupportSection.svelte`:
- Around line 28-29: The headings and CTA in SupportSection.svelte currently
concatenate raw tier.level values ("{tier.level}s" and "Become a {tier.level}")
which bypasses the app's i18n; replace these with calls to the project's
translation helper (the same function used elsewhere in the app) using
translation keys that accept the tier level as an interpolated parameter and
support pluralization where needed (e.g., a "tier.label" key for singular/plural
forms and a "support.cta.become" key that interpolates tier). Update the
references around the tier.level usage in the template so they call the
translator with the variable (not string concatenation) to ensure proper
localization.

In `@src/routes/supporters/sponsors.tsx`:
- Around line 29-43: The current sponsorshipTiers array hardcodes English
display strings in the headline property; change the data model so
SponsorshipTier uses a headlineKey (e.g., replace headline: string with
headlineKey: string) and update the sponsorshipTiers entries to provide
translation keys instead of English text, then update any Svelte components that
read sponsorshipTiers (referencing sponsorshipTiers and SponsorshipTier) to call
the i18n translator (t(...)) with tier.headlineKey when rendering; finally, add
the corresponding keys and translations to each locale file so non-English
locales show localized tier headlines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 33c6a309-f668-4606-acfe-c489c3e30621

📥 Commits

Reviewing files that changed from the base of the PR and between a2639ab and 5d1ada7.

⛔ Files ignored due to path filters (8)
  • static/images/supporters/bittylicious.png is excluded by !**/*.png
  • static/images/supporters/jan3-dark.svg is excluded by !**/*.svg
  • static/images/supporters/jan3.svg is excluded by !**/*.svg
  • static/images/supporters/opensats.png is excluded by !**/*.png
  • static/images/supporters/plebs/nathan-day.jpg is excluded by !**/*.jpg
  • static/images/supporters/spiral.svg is excluded by !**/*.svg
  • static/images/supporters/square-dark.svg is excluded by !**/*.svg
  • static/images/supporters/square.svg is excluded by !**/*.svg
📒 Files selected for processing (25)
  • src/components/InvoicePayment.svelte
  • src/components/layout/Header.svelte
  • src/lib/i18n/locales/bg.json
  • src/lib/i18n/locales/de.json
  • src/lib/i18n/locales/en.json
  • src/lib/i18n/locales/es.json
  • src/lib/i18n/locales/fr.json
  • src/lib/i18n/locales/nl.json
  • src/lib/i18n/locales/pt-BR.json
  • src/lib/i18n/locales/ru.json
  • src/lib/map/setup.ts
  • src/lib/store.ts
  • src/lib/types.ts
  • src/routes/.well-known/lnurlp/donations/+server.ts
  • src/routes/support-us/+page.server.ts
  • src/routes/support-us/+page.svelte
  • src/routes/support-us/components/DonationOption.svelte
  • src/routes/support-us/components/SupportSection.svelte
  • src/routes/supporters/+page.server.ts
  • src/routes/supporters/+page.svelte
  • src/routes/supporters/components/DonationOption.svelte
  • src/routes/supporters/components/PlebSection.svelte
  • src/routes/supporters/components/SupportSection.svelte
  • src/routes/supporters/sponsors.tsx
  • tsconfig.json
💤 Files with no reviewable changes (3)
  • src/routes/support-us/components/SupportSection.svelte
  • src/routes/support-us/components/DonationOption.svelte
  • src/routes/support-us/+page.svelte

Comment thread src/routes/.well-known/lnurlp/donations/+server.ts Outdated
Comment thread src/routes/supporters/+page.server.ts Outdated
Comment thread src/routes/supporters/+page.server.ts Outdated
Comment thread src/routes/supporters/+page.svelte Outdated
Comment thread src/routes/supporters/components/DonationOption.svelte
Comment thread src/routes/supporters/components/PlebSection.svelte
Comment thread src/routes/supporters/components/SupportSection.svelte Outdated
Comment thread src/routes/supporters/sponsors.tsx
@escapedcat

Copy link
Copy Markdown
Contributor

Code looks fine, not sure about the colors in general but can be adjusted later as well.

This is "red tier"? Might look like a warning somehow
image

…/cyan palette

- Sponsor tiers (Explorer→Pioneer) now use brand teal/cyan/navy pastels
  instead of generic emerald/sky/amber/indigo/rose Tailwind colors
- Pleb tier uses brand bitcoin orange (#F7931A) instead of generic orange-*
- Remove initials fallback divs; robohash is always used when avatar is absent

🤖 Generated with [opencode](https://opencode.ai)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/i18n/locales/nl.json`:
- Around line 984-985: The locale key was renamed to "supporters" but consumers
still use "media.assetNames.supportUs"; to maintain backward compatibility add
the old key back in nl.json (e.g., "supportUs": "Steun ons") or provide both
keys so src/routes/media/+page.svelte can continue using
media.assetNames.supportUs until all references are migrated to "supporters";
update nl.json's entry near "supporters"/"topCommunities" to include the legacy
"supportUs" key with identical value.

In `@src/routes/.well-known/lnurlp/donations/`+server.ts:
- Around line 33-45: The code assumes "data" is an object before reading
data.metadata, which can throw if res.json() returns null/primitive/array;
update the logic after data = await res.json() to first verify the JSON shape
(e.g., ensure data is non-null, typeof data === "object", and not
Array.isArray(data)) and if that check fails return the same 502/json error,
then proceed to the existing typeof data.metadata === "string" branch; apply
this validation around the metadata access to prevent the unhandled TypeError
and preserve the existing error response behavior.

In `@src/routes/supporters/`+page.server.ts:
- Around line 151-155: The anonPlebs mapping uses id: `anon-${c.createdAt}`
which can collide for same-timestamp contributions; update the mapping in the
anonPlebs creation to produce guaranteed-unique IDs (for example combine a
stable unique from the contribution such as c.id or the array index, or call
crypto.randomUUID()/uuidv4 if available) so each generated Pleb.id is unique and
prevents key/tooltip collisions in PlebSection.svelte; modify the map callback
that builds anonPlebs to use that combined or UUID-based id instead of only
createdAt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f6aa1bab-c522-4a03-8396-04bf7ef8d79c

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1ada7 and 23a1bcd.

📒 Files selected for processing (15)
  • src/lib/i18n/locales/bg.json
  • src/lib/i18n/locales/de.json
  • src/lib/i18n/locales/en.json
  • src/lib/i18n/locales/es.json
  • src/lib/i18n/locales/fr.json
  • src/lib/i18n/locales/nl.json
  • src/lib/i18n/locales/pt-BR.json
  • src/lib/i18n/locales/ru.json
  • src/routes/.well-known/lnurlp/donations/+server.ts
  • src/routes/supporters/+page.server.ts
  • src/routes/supporters/+page.svelte
  • src/routes/supporters/components/DonationOption.svelte
  • src/routes/supporters/components/PlebSection.svelte
  • src/routes/supporters/components/SupportSection.svelte
  • src/routes/supporters/sponsors.tsx
✅ Files skipped from review due to trivial changes (3)
  • src/routes/supporters/components/DonationOption.svelte
  • src/routes/supporters/components/SupportSection.svelte
  • src/lib/i18n/locales/pt-BR.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/lib/i18n/locales/fr.json
  • src/lib/i18n/locales/de.json
  • src/lib/i18n/locales/en.json
  • src/lib/i18n/locales/bg.json
  • src/lib/i18n/locales/ru.json
  • src/routes/supporters/+page.svelte

Comment thread src/lib/i18n/locales/nl.json
Comment thread src/routes/.well-known/lnurlp/donations/+server.ts
Comment thread src/routes/supporters/+page.server.ts
…palette

- Explorer/Wayfinder blend with page bg, Cartographer/Navigator/Pioneer
  use ascending teal richness (#0099AF → #095D73) with increasing opacity
- Pleb dark mode bg bumped to 8% bitcoin orange for visible warm tint

🤖 Generated with [opencode](https://opencode.ai)
@dadofsambonzuki

Copy link
Copy Markdown
Member Author

Code looks fine, not sure about the colors in general but can be adjusted later as well.

Yup, bringing colours on-brand now.

- Validate upstream JSON is a non-null, non-array object before accessing
  data.metadata to prevent unhandled TypeError on unexpected shapes
- Append array index to anon pleb IDs to prevent collisions when multiple
  contributions share the same createdAt timestamp

🤖 Generated with [opencode](https://opencode.ai)
@dadofsambonzuki dadofsambonzuki merged commit b8ad6d2 into main May 2, 2026
12 checks passed
escapedcat added a commit that referenced this pull request May 4, 2026
Renames legacy `supportUs` namespace to `supporters` (top-level,
nav.*, media.assetNames.*) and adds Dutch translations for the new
keys introduced in #913: tiers (explorer/wayfinder/cartographer/
navigator/pioneer/pleb), individual, sponsors.description,
donate.lightningAddress, and node.scanOrClick.

Drops obsolete supportUs.intro and supportUs.appreciate (no longer
present in en.json). NL now has full key parity with EN (987 keys).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
escapedcat added a commit that referenced this pull request May 4, 2026
* i18n(nl): update Dutch translations

Squashes the original nl.json update with subsequent JSON syntax fixes
(missing commas, trailing comma) and rebases onto main, picking up the
H1 hero trailing-period removals from #962.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(i18n/nl): restore HTML entities in 5 garbled strings

Replace ASCII-space placeholders with the correct HTML entity encoding
used elsewhere in nl.json:
- backup.copyFailed: "Kopi ren" -> "Kopi&euml;ren"
- errors.mapView / mapViewCachedCoords: "co rdinaten" -> "co&ouml;rdinaten"
- addLocation.advancedHint: "co rdinaten" -> "co&ouml;rdinaten"
- supertaggerDescription: " berhaupt" -> "&uuml;berhaupt"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* i18n(nl): align nl.json keys with en.json after #913 supporters rename

Renames legacy `supportUs` namespace to `supporters` (top-level,
nav.*, media.assetNames.*) and adds Dutch translations for the new
keys introduced in #913: tiers (explorer/wayfinder/cartographer/
navigator/pioneer/pleb), individual, sponsors.description,
donate.lightningAddress, and node.scanOrClick.

Drops obsolete supportUs.intro and supportUs.appreciate (no longer
present in en.json). NL now has full key parity with EN (987 keys).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(i18n/nl): keep proper names and handles untranslated #956

- secondl1ght: revert "tweedelig" -> "secondl1ght" (username, not a phrase)
- nathanDay: revert "Nathan Dag" -> "Nathan Day" (surname, not "day")

Matches en.json and fr.json behavior. Flagged in code review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(agents): add i18n guidelines about names, key parity, entities #956

Captures lessons from PR #956 review:
- never translate proper names or handles
- keep keys aligned with en.json across all locales
- match the locale file's existing HTML-entity style for diacritics

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: escapedcat <github@htmlcss.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants