diff --git a/src/i18n/en.ts b/src/i18n/en.ts index eb49678..40e3ace 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -6502,6 +6502,11 @@ walletUi: { backersTotal: '{{count}} supporters · {{amount}} sats raised', verifiedOnChain: 'On-chain verified', supportedBy: 'Supported by {{count}}', + tierSupporter: 'Supporter', + tierPatron: 'Patron', + tierLegend: 'Legend', + topZappers: 'Top zappers', + topZappersDesc: 'Skaters who zapped the map — Lightning support, all-time.', support: 'Support', supportDesc: 'Fund SkateSpots on Angor — decentralized, on-chain Bitcoin crowdfunding. No middleman, no KYC.', collaborators: 'Collaborators', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index b21dc69..dadbde6 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -6502,6 +6502,11 @@ walletUi: { backersTotal: '{{count}} patrocinadores · {{amount}} sats recaudados', verifiedOnChain: 'Verificado on-chain', supportedBy: 'Apoyado por {{count}}', + tierSupporter: 'Colaborador', + tierPatron: 'Patrón', + tierLegend: 'Leyenda', + topZappers: 'Principales zapeadores', + topZappersDesc: 'Patinadores que han zapeado el mapa — apoyo por Lightning, histórico.', support: 'Apoyo', supportDesc: 'Financia SkateSpots en Angor — crowdfunding descentralizado on-chain en Bitcoin. Sin intermediarios, sin KYC.', collaborators: 'Colaboradores', diff --git a/src/lib/backers.ts b/src/lib/backers.ts index b579d58..32d2270 100644 --- a/src/lib/backers.ts +++ b/src/lib/backers.ts @@ -50,3 +50,16 @@ export const BACKERS: Backer[] = [ /** Total raised across the listed supporters, in satoshis. */ export const BACKERS_TOTAL_SATS = BACKERS.reduce((sum, b) => sum + (b.amount ?? 0), 0); + +export type SupporterTier = 'supporter' | 'patron' | 'legend'; + +/** + * Tier by amount. The 100,000-sat boundary is the project's Angor penalty + * threshold: at/below it a contribution is donation-like, above it's an + * investment with a recovery path — so the tiers mirror the two rails honestly. + */ +export function tierOf(sats: number | undefined): SupporterTier { + if (!sats || sats < 100_000) return 'supporter'; + if (sats < 1_000_000) return 'patron'; + return 'legend'; +} diff --git a/src/pages/SupportersPage.tsx b/src/pages/SupportersPage.tsx index 1f8bece..b97a902 100644 --- a/src/pages/SupportersPage.tsx +++ b/src/pages/SupportersPage.tsx @@ -1,10 +1,14 @@ import { useSeoMeta } from '@unhead/react'; import { Link } from 'react-router-dom'; +import { nip19 } from 'nostr-tools'; import { ExternalLink, Zap, Heart, Users, Handshake, Building2, Star, ShieldCheck } from 'lucide-react'; import { SupportSkateSpots } from '@/components/SupportSkateSpots'; -import { BACKERS, BACKERS_TOTAL_SATS, type Backer } from '@/lib/backers'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { BACKERS, BACKERS_TOTAL_SATS, tierOf, type Backer } from '@/lib/backers'; +import { genUserName } from '@/lib/genUserName'; import { useAuthor } from '@/hooks/useAuthor'; import { useSatsToUsd } from '@/hooks/useExchangeRate'; +import { useZapLeaderboard } from '@/hooks/useZapLeaderboard'; import { useTranslation } from '@/contexts/language'; const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z'; @@ -207,6 +211,12 @@ function FriendCard({ friend }: { friend: Friend }) { // ─── Backer card (Angor backers) ───────────────────────────────────────────── +const TIER_META = { + supporter: { color: '#22d3ee', key: 'walletUi.supporters.tierSupporter' }, + patron: { color: '#f7931a', key: 'walletUi.supporters.tierPatron' }, + legend: { color: '#facc15', key: 'walletUi.supporters.tierLegend' }, +} as const; + function BackerCard({ backer }: { backer: Backer }) { const { t } = useTranslation(); const { data } = useAuthor(backer.npub); @@ -216,6 +226,7 @@ function BackerCard({ backer }: { backer: Backer }) { const display = name || (backer.npub ? `${backer.npub.slice(0, 12)}…${backer.npub.slice(-6)}` : t('walletUi.supporters.anonymousBacker')); const initial = display.replace(/[^a-zA-Z0-9]/g, '').charAt(0).toUpperCase() || 'S'; + const tier = TIER_META[tierOf(backer.amount)]; const href = backer.npub ? `/${backer.npub}` : backer.url; return ( @@ -247,6 +258,12 @@ function BackerCard({ backer }: { backer: Backer }) { {typeof backer.amount === 'number' && (

{backer.amount.toLocaleString()} sats

)} + + {t(tier.key)} + {backer.note &&

{backer.note}

} {backer.txid && ( + #{rank} + + + {name.slice(0, 1).toUpperCase()} + + {name} + + + {sats.toLocaleString()} + + + ); +} + // ─── Placeholder card (empty slot) ─────────────────────────────────────────── function PlaceholderCard({ label }: { label: string }) { @@ -283,6 +325,8 @@ function PlaceholderCard({ label }: { label: string }) { export default function SupportersPage() { const { t } = useTranslation(); const totalUsd = useSatsToUsd(BACKERS_TOTAL_SATS); + const { data: zaps } = useZapLeaderboard(); + const topZappers = (zaps?.senders ?? []).slice(0, 10); useSeoMeta({ title: 'Supporters - SkateSpots', description: 'The community keeping SkateSpots alive — supporters, collaborators, partners and friends.', @@ -395,7 +439,24 @@ export default function SupportersPage() { )} - {/* ── 3. Collaborators ── */} + {/* ── 3. Top zappers (Lightning) ── */} + {topZappers.length > 0 && ( +
+ +
+ {topZappers.map((z, i) => ( + + ))} +
+
+ )} + + {/* ── 4. Collaborators ── */}