feat(supporters): pay-for list + impact stats; scope zaps to the project
This commit is contained in:
parent
b88379a390
commit
87aca51248
4 changed files with 129 additions and 14 deletions
|
|
@ -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<ZapLeaderboard>({
|
||||
queryKey: ['zap-leaderboard', since ?? 'all'],
|
||||
queryKey: ['zap-leaderboard', since ?? 'all', recipients?.join(',') ?? ''],
|
||||
staleTime: 5 * 60 * 1000,
|
||||
queryFn: async (c): Promise<ZapLeaderboard> => {
|
||||
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 },
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<SupportSkateSpots className="mt-4" />
|
||||
</section>
|
||||
|
||||
{/* ── 2. Backers (Angor) ── */}
|
||||
{/* ── 2. What your support pays for (words only) ── */}
|
||||
<section>
|
||||
<SectionHeader
|
||||
icon={Heart}
|
||||
title={t('walletUi.supporters.paysFor')}
|
||||
subtitle={t('walletUi.supporters.paysForDesc')}
|
||||
color="#22d3ee"
|
||||
/>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
{PAYS_FOR.map(({ icon: Icon, key }) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center gap-3 rounded-xl border border-white/10 bg-black/60 px-4 py-3"
|
||||
>
|
||||
<Icon className="h-4 w-4 flex-shrink-0 text-[#22d3ee]" />
|
||||
<span className="text-sm font-medium text-white">{t(key)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── 3. Impact so far (what the community built) ── */}
|
||||
{impact.length > 0 && (
|
||||
<section>
|
||||
<SectionHeader
|
||||
icon={TrendingUp}
|
||||
title={t('walletUi.supporters.impact')}
|
||||
subtitle={t('walletUi.supporters.impactDesc')}
|
||||
color="#ccff00"
|
||||
/>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
{impact.map(({ key, value }) => (
|
||||
<div key={key} className="rounded-xl border border-white/10 bg-black/60 px-4 py-3 text-center">
|
||||
<div className="text-2xl font-black text-[#ccff00]">{value.toLocaleString()}</div>
|
||||
<div className="text-xs text-gray-400">{t(key)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ── 4. Supporters (Angor) ── */}
|
||||
<section>
|
||||
<SectionHeader
|
||||
icon={Zap}
|
||||
|
|
@ -439,7 +515,7 @@ export default function SupportersPage() {
|
|||
)}
|
||||
</section>
|
||||
|
||||
{/* ── 3. Top zappers (Lightning) ── */}
|
||||
{/* ── 5. Top zappers (Lightning) ── */}
|
||||
{topZappers.length > 0 && (
|
||||
<section>
|
||||
<SectionHeader
|
||||
|
|
@ -456,7 +532,7 @@ export default function SupportersPage() {
|
|||
</section>
|
||||
)}
|
||||
|
||||
{/* ── 4. Collaborators ── */}
|
||||
{/* ── 6. Collaborators ── */}
|
||||
<section>
|
||||
<SectionHeader
|
||||
icon={Users}
|
||||
|
|
@ -489,7 +565,7 @@ export default function SupportersPage() {
|
|||
)}
|
||||
</section>
|
||||
|
||||
{/* ── 3. Partners ── */}
|
||||
{/* ── 7. Partners ── */}
|
||||
<section>
|
||||
<SectionHeader
|
||||
icon={Handshake}
|
||||
|
|
@ -518,7 +594,7 @@ export default function SupportersPage() {
|
|||
)}
|
||||
</section>
|
||||
|
||||
{/* ── 4. Friends ── */}
|
||||
{/* ── 8. Friends ── */}
|
||||
<section>
|
||||
<SectionHeader
|
||||
icon={Star}
|
||||
|
|
|
|||
Loading…
Reference in a new issue