feat(supporters): show Angor supporters with amounts on /supporters
This commit is contained in:
parent
3d7fc9af4f
commit
3f94d9be30
5 changed files with 142 additions and 1 deletions
|
|
@ -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
|
`angor:members`, `angor:media`). Live: displayName "Skate Spots", NIP-05
|
||||||
`owner@skate-spots.org`, lud16 `skatespots@minibits.cash` (single-sourced in
|
`owner@skate-spots.org`, lud16 `skatespots@minibits.cash` (single-sourced in
|
||||||
`src/lib/platform.ts`), picture on `blossom.angor.io`.
|
`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
|
### Tooling — machine-local, deliberately NOT in the repo
|
||||||
`~/.config/skatespots/angor/` → `angor-metadata.mjs` + `blossom-upload.mjs` (own
|
`~/.config/skatespots/angor/` → `angor-metadata.mjs` + `blossom-upload.mjs` (own
|
||||||
|
|
|
||||||
|
|
@ -6495,6 +6495,11 @@ walletUi: {
|
||||||
community: 'COMMUNITY',
|
community: 'COMMUNITY',
|
||||||
heroDesc: 'SkateSpots is built by and for the community. Supporters, collaborators, partners and friends keeping this free and decentralized.',
|
heroDesc: 'SkateSpots is built by and for the community. Supporters, collaborators, partners and friends keeping this free and decentralized.',
|
||||||
backAngor: 'Back us on Angor',
|
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',
|
support: 'Support',
|
||||||
supportDesc: 'Fund SkateSpots on Angor — decentralized, on-chain Bitcoin crowdfunding. No middleman, no KYC.',
|
supportDesc: 'Fund SkateSpots on Angor — decentralized, on-chain Bitcoin crowdfunding. No middleman, no KYC.',
|
||||||
collaborators: 'Collaborators',
|
collaborators: 'Collaborators',
|
||||||
|
|
|
||||||
|
|
@ -6495,6 +6495,11 @@ walletUi: {
|
||||||
community: 'COMUNIDAD',
|
community: 'COMUNIDAD',
|
||||||
heroDesc: 'SkateSpots lo construye y lo mantiene la comunidad. Patrocinadores, colaboradores, socios y amigos que lo mantienen libre y descentralizado.',
|
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',
|
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',
|
support: 'Apoyo',
|
||||||
supportDesc: 'Financia SkateSpots en Angor — crowdfunding descentralizado on-chain en Bitcoin. Sin intermediarios, sin KYC.',
|
supportDesc: 'Financia SkateSpots en Angor — crowdfunding descentralizado on-chain en Bitcoin. Sin intermediarios, sin KYC.',
|
||||||
collaborators: 'Colaboradores',
|
collaborators: 'Colaboradores',
|
||||||
|
|
|
||||||
48
src/lib/backers.ts
Normal file
48
src/lib/backers.ts
Normal 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);
|
||||||
|
|
@ -2,6 +2,7 @@ import { useSeoMeta } from '@unhead/react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ExternalLink, Zap, Heart, Users, Handshake, Building2, Star } from 'lucide-react';
|
import { ExternalLink, Zap, Heart, Users, Handshake, Building2, Star } from 'lucide-react';
|
||||||
import { SupportSkateSpots } from '@/components/SupportSkateSpots';
|
import { SupportSkateSpots } from '@/components/SupportSkateSpots';
|
||||||
|
import { BACKERS, BACKERS_TOTAL_SATS, type Backer } from '@/lib/backers';
|
||||||
import { useTranslation } from '@/contexts/language';
|
import { useTranslation } from '@/contexts/language';
|
||||||
|
|
||||||
const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z';
|
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) ───────────────────────────────────────────
|
// ─── Placeholder card (empty slot) ───────────────────────────────────────────
|
||||||
|
|
||||||
function PlaceholderCard({ label }: { label: string }) {
|
function PlaceholderCard({ label }: { label: string }) {
|
||||||
|
|
@ -288,7 +326,47 @@ export default function SupportersPage() {
|
||||||
<SupportSkateSpots className="mt-4" />
|
<SupportSkateSpots className="mt-4" />
|
||||||
</section>
|
</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>
|
<section>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
icon={Users}
|
icon={Users}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue