diff --git a/src/hooks/useZapLeaderboard.ts b/src/hooks/useZapLeaderboard.ts index e1149e8..f9278ae 100644 --- a/src/hooks/useZapLeaderboard.ts +++ b/src/hooks/useZapLeaderboard.ts @@ -30,17 +30,24 @@ export interface ZapLeaderboard { * - **senders** — total sats sent per zapper (taken from the embedded zap * request, since a receipt's own pubkey is the LNURL server, not the sender). */ -export function useZapLeaderboard(since?: number) { +export function useZapLeaderboard(since?: number, recipients?: string[]) { const { nostr } = useNostr(); return useQuery({ - queryKey: ['zap-leaderboard', since ?? 'all'], + queryKey: ['zap-leaderboard', since ?? 'all', recipients?.join(',') ?? ''], staleTime: 5 * 60 * 1000, queryFn: async (c): Promise => { const signal = AbortSignal.any([c.signal, AbortSignal.timeout(12000)]); const receipts: NostrEvent[] = await nostr.query( - [{ kinds: [9735], ...(since ? { since } : {}), limit: 1000 }], + [ + { + kinds: [9735], + ...(since ? { since } : {}), + ...(recipients?.length ? { '#p': recipients } : {}), + limit: 1000, + }, + ], { signal }, ); diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 40e3ace..b4b33b5 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -6505,8 +6505,24 @@ walletUi: { tierSupporter: 'Supporter', tierPatron: 'Patron', tierLegend: 'Legend', - topZappers: 'Top zappers', - topZappersDesc: 'Skaters who zapped the map — Lightning support, all-time.', + topZappers: 'Zapped SkateSpots', + topZappersDesc: 'Skaters who sent a public zap straight to the project. Quiet payments aren’t listed.', + paysFor: 'What your support pays for', + paysForDesc: 'Where every sat goes — no salaries, no offices, no ads.', + payRelay: 'Relay hosting', + payDomain: 'Domain & TLS', + payAi: 'AI / ContextVM node', + payDev: 'Development time', + payOps: 'Operations', + payMarketing: 'Marketing', + impact: 'Impact so far', + impactDesc: 'What the community has built — funded by skaters.', + impactSpots: 'Spots', + impactCrews: 'Crews', + impactEvents: 'Events', + impactLab: 'Lab pros', + impactShops: 'Shops', + impactMappers: 'Mappers', 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 dadbde6..22f1302 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -6505,8 +6505,24 @@ walletUi: { tierSupporter: 'Colaborador', tierPatron: 'Patrón', tierLegend: 'Leyenda', - topZappers: 'Principales zapeadores', - topZappersDesc: 'Patinadores que han zapeado el mapa — apoyo por Lightning, histórico.', + topZappers: 'Zapearon SkateSpots', + topZappersDesc: 'Patinadores que enviaron un zap público directamente al proyecto. Los pagos silenciosos no se listan.', + paysFor: 'En qué se invierte tu apoyo', + paysForDesc: 'A dónde va cada sat — sin sueldos, sin oficinas, sin anuncios.', + payRelay: 'Alojamiento de relés', + payDomain: 'Dominio y TLS', + payAi: 'Nodo de IA / ContextVM', + payDev: 'Tiempo de desarrollo', + payOps: 'Operaciones', + payMarketing: 'Marketing', + impact: 'Impacto hasta ahora', + impactDesc: 'Lo que ha construido la comunidad — financiado por patinadores.', + impactSpots: 'Spots', + impactCrews: 'Crews', + impactEvents: 'Eventos', + impactLab: 'Pros del Lab', + impactShops: 'Tiendas', + impactMappers: 'Mapeadores', support: 'Apoyo', supportDesc: 'Financia SkateSpots en Angor — crowdfunding descentralizado on-chain en Bitcoin. Sin intermediarios, sin KYC.', collaborators: 'Colaboradores', diff --git a/src/pages/SupportersPage.tsx b/src/pages/SupportersPage.tsx index b97a902..8d40f5d 100644 --- a/src/pages/SupportersPage.tsx +++ b/src/pages/SupportersPage.tsx @@ -1,12 +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 { ExternalLink, Zap, Heart, Users, Handshake, Building2, Star, ShieldCheck, Server, Globe, Bot, Code2, Briefcase, Megaphone, TrendingUp } from 'lucide-react'; import { SupportSkateSpots } from '@/components/SupportSkateSpots'; 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 { useGrowthStats } from '@/hooks/useGrowthStats'; +import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions'; import { useSatsToUsd } from '@/hooks/useExchangeRate'; import { useZapLeaderboard } from '@/hooks/useZapLeaderboard'; import { useTranslation } from '@/contexts/language'; @@ -86,6 +88,17 @@ const FRIENDS: Friend[] = [ // Add more friends here ]; +// ─── "What your support pays for" (words only — no figures) ────────────────── + +const PAYS_FOR = [ + { icon: Server, key: 'walletUi.supporters.payRelay' }, + { icon: Globe, key: 'walletUi.supporters.payDomain' }, + { icon: Bot, key: 'walletUi.supporters.payAi' }, + { icon: Code2, key: 'walletUi.supporters.payDev' }, + { icon: Briefcase, key: 'walletUi.supporters.payOps' }, + { icon: Megaphone, key: 'walletUi.supporters.payMarketing' }, +] as const; + // ─── Section header ─────────────────────────────────────────────────────────── function SectionHeader({ @@ -325,8 +338,30 @@ function PlaceholderCard({ label }: { label: string }) { export default function SupportersPage() { const { t } = useTranslation(); const totalUsd = useSatsToUsd(BACKERS_TOTAL_SATS); - const { data: zaps } = useZapLeaderboard(); + const { data: zaps } = useZapLeaderboard(undefined, PLATFORM_OWNER_PUBKEYS); const topZappers = (zaps?.senders ?? []).slice(0, 10); + const { data: stats } = useGrowthStats(); + + // `growth-stats` is persisted, so an older cache may lack newer fields — guard every read. + const num = (v: number | undefined) => v ?? 0; + const labTotal = stats + ? num(stats.totalCoaches) + num(stats.totalFilmers) + num(stats.totalEditors) + num(stats.totalPhotographers) + + num(stats.totalBuilders) + num(stats.totalArtists) + num(stats.totalGraffiti) + num(stats.totalTattoo) + + num(stats.totalTrainers) + num(stats.totalMusicians) + num(stats.totalJudges) + num(stats.totalOrganizers) + + num(stats.totalBarbers) + : 0; + + const impact = stats + ? [ + { key: 'walletUi.supporters.impactSpots', value: num(stats.totalSpots) }, + { key: 'walletUi.supporters.impactCrews', value: num(stats.totalCrews) }, + { key: 'walletUi.supporters.impactEvents', value: num(stats.totalEvents) }, + { key: 'walletUi.supporters.impactLab', value: labTotal }, + { key: 'walletUi.supporters.impactShops', value: num(stats.totalShops) }, + { key: 'walletUi.supporters.impactMappers', value: num(stats.totalMappers) }, + ].filter((s) => s.value > 0) + : []; + useSeoMeta({ title: 'Supporters - SkateSpots', description: 'The community keeping SkateSpots alive — supporters, collaborators, partners and friends.', @@ -396,7 +431,48 @@ export default function SupportersPage() { - {/* ── 2. Backers (Angor) ── */} + {/* ── 2. What your support pays for (words only) ── */} +
+ +
+ {PAYS_FOR.map(({ icon: Icon, key }) => ( +
+ + {t(key)} +
+ ))} +
+
+ + {/* ── 3. Impact so far (what the community built) ── */} + {impact.length > 0 && ( +
+ +
+ {impact.map(({ key, value }) => ( +
+
{value.toLocaleString()}
+
{t(key)}
+
+ ))} +
+
+ )} + + {/* ── 4. Supporters (Angor) ── */}
- {/* ── 3. Top zappers (Lightning) ── */} + {/* ── 5. Top zappers (Lightning) ── */} {topZappers.length > 0 && (
)} - {/* ── 4. Collaborators ── */} + {/* ── 6. Collaborators ── */}
- {/* ── 3. Partners ── */} + {/* ── 7. Partners ── */}
- {/* ── 4. Friends ── */} + {/* ── 8. Friends ── */}