feat(supporters): show Angor supporters with amounts on /supporters

This commit is contained in:
SkateSpots 2026-10-05 17:37:00 +07:00
parent 3d7fc9af4f
commit 3f94d9be30
5 changed files with 142 additions and 1 deletions

View file

@ -1777,6 +1777,11 @@ Nostr events signed by a **project identity** (derived from the founder's wallet
`angor:members`, `angor:media`). Live: displayName "Skate Spots", NIP-05
`owner@skate-spots.org`, lud16 `skatespots@minibits.cash` (single-sourced in
`src/lib/platform.ts`), picture on `blossom.angor.io`.
- **In-app display:** `/supporters` has a **Supporters** section (curated) fed by
**`src/lib/backers.ts`** — `{ npub, amount (sats), note }` or `{ name, logo, url }`.
Amounts come from the investment txs' Taproot stage outputs. Discovery helper:
`~/.config/skatespots/angor/angor-backers.mjs` (decrypts the founder's investment DMs →
`work/backers.json`). ⚠️ Don't list a backer without consent.
### Tooling — machine-local, deliberately NOT in the repo
`~/.config/skatespots/angor/` → `angor-metadata.mjs` + `blossom-upload.mjs` (own

View file

@ -6495,6 +6495,11 @@ walletUi: {
community: 'COMMUNITY',
heroDesc: 'SkateSpots is built by and for the community. Supporters, collaborators, partners and friends keeping this free and decentralized.',
backAngor: 'Back us on Angor',
backers: 'Supporters',
backersDesc: 'People who backed SkateSpots on Angor — decentralized, on-chain Bitcoin. Thank you.',
anonymousBacker: 'Anonymous supporter',
beFirstBacker: 'No public supporters yet — be the first.',
backersTotal: '{{count}} supporters · {{amount}} sats raised',
support: 'Support',
supportDesc: 'Fund SkateSpots on Angor — decentralized, on-chain Bitcoin crowdfunding. No middleman, no KYC.',
collaborators: 'Collaborators',

View file

@ -6495,6 +6495,11 @@ walletUi: {
community: 'COMUNIDAD',
heroDesc: 'SkateSpots lo construye y lo mantiene la comunidad. Patrocinadores, colaboradores, socios y amigos que lo mantienen libre y descentralizado.',
backAngor: 'Apóyanos en Angor',
backers: 'Patrocinadores',
backersDesc: 'Personas que han apoyado SkateSpots en Angor — Bitcoin descentralizado on-chain. Gracias.',
anonymousBacker: 'Patrocinador anónimo',
beFirstBacker: 'Aún no hay patrocinadores públicos — sé el primero.',
backersTotal: '{{count}} patrocinadores · {{amount}} sats recaudados',
support: 'Apoyo',
supportDesc: 'Financia SkateSpots en Angor — crowdfunding descentralizado on-chain en Bitcoin. Sin intermediarios, sin KYC.',
collaborators: 'Colaboradores',

48
src/lib/backers.ts Normal file
View file

@ -0,0 +1,48 @@
/**
* Curated list of Angor supporters shown on /supporters.
*
* Deliberately opt-in: only list people/organisations who are happy to be
* shown. Discover the raw list (privately) with:
* node ~/.config/skatespots/angor/angor-backers.mjs
* → writes ~/.config/skatespots/angor/work/backers.json (npub + txid).
* The amount = the sum of the investment transaction's Taproot stage outputs
* (per-backer `amount` = 99,000 sats here; total matches the app's "0.00198 BTC").
*
* Entries:
* - people: { npub, amount } → links to their profile; add `name` to override
* - shops / orgs: { name, logo, url, amount } (logo: PNG/WebP, transparent, ≥200px)
* - hide identity: { anonymous: true, note }
*
* ⚠️ Don't add someone without their consent — a backer's npub + "backed this
* project" is new, linkable information even though the key is public.
*/
export interface Backer {
/** Display name; falls back to "Anonymous supporter". */
name?: string;
/** Nostr profile — renders a skate placeholder + links to /<npub>. */
npub?: string;
/** Logo for a shop/brand/org (shown instead of the placeholder). */
logo?: string;
/** External link (used when there is no npub). */
url?: string;
/** Amount contributed, in satoshis. */
amount?: number;
/** Small line under the name, e.g. "Founding backer". */
note?: string;
}
export const BACKERS: Backer[] = [
{
npub: 'npub15q3ekfxdvzuc9nhhyh2yv7lwtnfagurllc9rkkrypt40d303jsjsdfujeu',
amount: 99000,
note: 'Founding supporter',
},
{
npub: 'npub1t5yjluyvxatlmmcxccv7tdppmp4ae0j850k63323t7j0fe4lsqlsp7mfyl',
amount: 99000,
note: 'Founding supporter',
},
];
/** Total raised across the listed supporters, in satoshis. */
export const BACKERS_TOTAL_SATS = BACKERS.reduce((sum, b) => sum + (b.amount ?? 0), 0);

View file

@ -2,6 +2,7 @@ import { useSeoMeta } from '@unhead/react';
import { Link } from 'react-router-dom';
import { ExternalLink, Zap, Heart, Users, Handshake, Building2, Star } from 'lucide-react';
import { SupportSkateSpots } from '@/components/SupportSkateSpots';
import { BACKERS, BACKERS_TOTAL_SATS, type Backer } from '@/lib/backers';
import { useTranslation } from '@/contexts/language';
const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z';
@ -202,6 +203,43 @@ function FriendCard({ friend }: { friend: Friend }) {
);
}
// ─── Backer card (Angor backers) ─────────────────────────────────────────────
function BackerCard({ backer }: { backer: Backer }) {
const { t } = useTranslation();
const href = backer.npub ? `/${backer.npub}` : backer.url;
const inner = (
<div className="flex flex-col items-center gap-3 p-5 rounded-xl bg-black/60 border border-white/10 hover:border-[#22d3ee]/40 hover:bg-[#22d3ee]/5 transition-all group text-center">
<div className="w-16 h-16 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center overflow-hidden group-hover:border-[#22d3ee]/40 transition-all">
{backer.logo ? (
<img
src={backer.logo}
alt={backer.name || 'Backer'}
className="w-12 h-12 object-contain"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
) : (
<span className="text-2xl">🛹</span>
)}
</div>
<div>
<p className="font-bold text-sm text-white group-hover:text-[#22d3ee] transition-colors">
{backer.name || t('walletUi.supporters.anonymousBacker')}
</p>
{typeof backer.amount === 'number' && (
<p className="text-xs font-mono mt-0.5 text-[#22d3ee]/80">{backer.amount.toLocaleString()} sats</p>
)}
{backer.note && <p className="text-xs text-gray-500 mt-0.5">{backer.note}</p>}
</div>
</div>
);
if (!href) return inner;
if (backer.npub) return <Link to={`/${backer.npub}`}>{inner}</Link>;
return <a href={href} target="_blank" rel="noopener noreferrer">{inner}</a>;
}
// ─── Placeholder card (empty slot) ───────────────────────────────────────────
function PlaceholderCard({ label }: { label: string }) {
@ -288,7 +326,47 @@ export default function SupportersPage() {
<SupportSkateSpots className="mt-4" />
</section>
{/* ── 2. Collaborators ── */}
{/* ── 2. Backers (Angor) ── */}
<section>
<SectionHeader
icon={Zap}
title={t('walletUi.supporters.backers')}
subtitle={t('walletUi.supporters.backersDesc')}
color="#22d3ee"
/>
{BACKERS.length > 0 ? (
<>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
{BACKERS.map((b) => (
<BackerCard key={b.npub || b.name || b.url} backer={b} />
))}
</div>
<p className="mt-4 text-center text-sm text-gray-400">
{t('walletUi.supporters.backersTotal', {
count: BACKERS.length,
amount: BACKERS_TOTAL_SATS.toLocaleString(),
})}
</p>
</>
) : (
<div className="rounded-xl border border-dashed border-[#22d3ee]/20 p-8 text-center">
<Zap className="h-10 w-10 mx-auto mb-3 text-[#22d3ee]/30" />
<p className="text-gray-500 text-sm mb-1">{t('walletUi.supporters.beFirstBacker')}</p>
<a
href={ANGOR_PROJECT_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-xs font-bold text-[#22d3ee]/80 hover:text-[#22d3ee] transition-colors"
>
<ExternalLink className="h-3 w-3" />
{t('walletUi.supporters.backAngor')}
</a>
</div>
)}
</section>
{/* ── 3. Collaborators ── */}
<section>
<SectionHeader
icon={Users}