SkateSpots/src/pages/SupportersPage.tsx

837 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useMemo, useState } from 'react';
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, Server, Globe, Bot, Code2, Briefcase, Megaphone, TrendingUp } from 'lucide-react';
import { SupportSkateSpots } from '@/components/SupportSkateSpots';
import { SilentPaymentsCard } from '@/components/SilentPaymentsCard';
import { AngorProjectCard, ANGOR_PROJECT_URL } from '@/components/AngorProjectCard';
import { WaysToSupport } from '@/components/WaysToSupport';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { BACKERS, BACKERS_TOTAL_SATS, tierOf } from '@/lib/backers';
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
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';
// Only count support received from this point (the project's public launch era).
// Zaps to the platform before this are ignored. NOTE: as of 2026-10-05 every
// platform zap is already ≥ 2026-01, so this doesn't currently trim the list —
// the visible cap below is what keeps it short.
const SUPPORTERS_SINCE = Math.floor(Date.UTC(2025, 11, 1) / 1000); // 2025-12-01
const VISIBLE_LIMIT = 21;
/** npub → hex pubkey (useAuthor queries relays by HEX authors). */
function npubToHex(npub?: string): string | undefined {
if (!npub) return undefined;
try {
const d = nip19.decode(npub);
return d.type === 'npub' ? d.data : undefined;
} catch {
return undefined;
}
}
// ─── Static data — edit these arrays to add/remove entries ──────────────────
interface Collaborator {
name: string;
role: string;
avatar?: string; // URL or leave blank for placeholder
npub?: string; // Nostr npub — links to their profile
url?: string; // fallback link
}
interface Partner {
name: string;
logo?: string; // URL to logo (SVG/PNG transparent preferred)
url: string;
description: string;
}
interface Friend {
name: string;
logo?: string; // URL to logo (SVG/PNG transparent preferred, min 200×200px)
invertLogo?: boolean; // true if logo is dark and needs inversion for dark backgrounds
url: string;
tagline?: string;
}
const COLLABORATORS: Collaborator[] = [
// Add collaborators here — mix of Nostr users and external people
// Example:
// { name: 'Alice', role: 'Developer', npub: 'npub1...', avatar: 'https://...' },
// { name: 'Bob', role: 'Relay Operator', url: 'https://...' },
];
const PARTNERS: Partner[] = [
{
name: 'SNT4EVER',
logo: 'https://blossom.ditto.pub/f6e1d75ed8123b860ca139ee2d43a0a1238b89a0bec24752d9fe48cc41aa5d4c.png',
url: 'https://www.snt4ever.cat/',
description: 'Bringing back up Sants Estació',
},
// Add partners here
// Format: { name: 'Example', logo: 'https://...', url: 'https://...', description: 'Short description' }
// Logo: PNG/WebP transparent background preferred
];
const FRIENDS: Friend[] = [
{
name: 'No Teeth Skateboarding',
logo: 'https://blossom.dreamith.to/61bb7055930c442dfe1dfe5b8a8be5417f9b62f68ed79dc4ffbbddce5d4f122c.png',
url: 'https://www.instagram.com/noteethskateboarding/',
tagline: 'Hand-made Long Lasting Skateboards',
},
{
name: 'SNT4EVER',
logo: 'https://blossom.ditto.pub/f6e1d75ed8123b860ca139ee2d43a0a1238b89a0bec24752d9fe48cc41aa5d4c.png',
invertLogo: true,
url: 'https://www.snt4ever.cat/',
tagline: 'Bringing back up Sants Estació',
},
{
name: 'RK69 DIY Skatepark',
logo: 'https://blossom.ditto.pub/0d1d3855e7922b4ad7c10332e14723a16525e7aa26d95c58ea1dc1ec75514c3c.png',
url: 'https://www.instagram.com/rk69diyskatepark',
tagline: 'DIY Skatepark 101%',
},
{
name: 'Suan Luang Backyard',
logo: 'https://blossom.ditto.pub/8ac40ffbf9db9c9bff20e9b34d627a31b9c2044fe4f534948292516319684523.jpeg',
url: 'https://www.slbskate.com/',
tagline: "Bangkok's most famous Backyard DIY Spot",
},
// Add more friends here
];
// ─── "What your support pays for" (words only — no figures) ──────────────────
const PAYS_FOR = [
{ icon: Server, key: 'walletUi.supporters.payRelay', desc: 'walletUi.supporters.payRelayDesc' },
{ icon: Globe, key: 'walletUi.supporters.payDomain', desc: 'walletUi.supporters.payDomainDesc' },
{ icon: Bot, key: 'walletUi.supporters.payAi', desc: 'walletUi.supporters.payAiDesc' },
{ icon: Code2, key: 'walletUi.supporters.payDev', desc: 'walletUi.supporters.payDevDesc' },
{ icon: Briefcase, key: 'walletUi.supporters.payOps', desc: 'walletUi.supporters.payOpsDesc' },
{ icon: Megaphone, key: 'walletUi.supporters.payMarketing', desc: 'walletUi.supporters.payMarketingDesc' },
] as const;
// ─── Silent-payments donation card (BIP-353 name → BIP-321 URI) ──────────────
// ─── Section header ───────────────────────────────────────────────────────────
function SectionHeader({
icon: Icon,
title,
subtitle,
color = '#ccff00',
}: {
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
title: string;
subtitle?: string;
color?: string;
}) {
return (
<div className="mb-6 md:mb-8">
<div className="flex items-center gap-3 mb-2">
<Icon className="h-5 w-5 flex-shrink-0" style={{ color }} />
<h2 className="text-2xl md:text-3xl font-black uppercase tracking-wide" style={{ color }}>
{title}
</h2>
<div className="flex-1 h-px" style={{ background: `linear-gradient(to right, ${color}40, transparent)` }} />
</div>
{subtitle && <p className="text-gray-400 text-sm ml-8">{subtitle}</p>}
</div>
);
}
// ─── Collaborator card ────────────────────────────────────────────────────────
function CollaboratorCard({ collab }: { collab: Collaborator }) {
const href = collab.npub
? `/${collab.npub}`
: collab.url;
const inner = (
<div className="flex items-center gap-3 p-4 rounded-xl bg-black/60 border border-white/10 hover:border-[#ff0040]/40 transition-all group">
<div className="relative flex-shrink-0">
{collab.avatar ? (
<img
src={collab.avatar}
alt={collab.name}
className="w-12 h-12 rounded-full object-cover border-2 border-white/10 group-hover:border-[#ff0040]/50 transition-all"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-[#ff0040]/20 to-[#ff6600]/20 border-2 border-white/10 flex items-center justify-center text-xl">
🛹
</div>
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-sm text-white truncate">{collab.name}</p>
<p className="text-xs text-[#ff0040]/80 font-medium">{collab.role}</p>
</div>
{href && <ExternalLink className="h-3.5 w-3.5 text-gray-600 group-hover:text-[#ff0040] flex-shrink-0 transition-colors" />}
</div>
);
if (!href) return inner;
if (collab.npub) return <Link to={`/${collab.npub}`}>{inner}</Link>;
return <a href={href} target="_blank" rel="noopener noreferrer">{inner}</a>;
}
// ─── Partner card ─────────────────────────────────────────────────────────────
function PartnerCard({ partner }: { partner: Partner }) {
return (
<a
href={partner.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 p-4 rounded-xl bg-black/60 border border-white/10 hover:border-[#ff6600]/50 hover:bg-[#ff6600]/5 transition-all group"
>
<div className="w-10 h-10 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center flex-shrink-0 overflow-hidden">
{partner.logo ? (
<img
src={partner.logo}
alt={partner.name}
className="w-7 h-7 object-contain brightness-0 invert opacity-70 group-hover:opacity-100 transition-opacity"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
) : (
<Building2 className="h-5 w-5 text-gray-600 group-hover:text-[#ff6600] transition-colors" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-sm text-white group-hover:text-[#ff6600] transition-colors">{partner.name}</p>
<p className="text-xs text-gray-500 truncate">{partner.description}</p>
</div>
<ExternalLink className="h-3.5 w-3.5 text-gray-600 group-hover:text-[#ff6600] flex-shrink-0 transition-colors" />
</a>
);
}
// ─── Friend card ──────────────────────────────────────────────────────────────
function FriendCard({ friend }: { friend: Friend }) {
return (
<a
href={friend.url}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center gap-3 p-5 rounded-xl bg-black/60 border border-white/10 hover:border-[#ccff00]/40 hover:bg-[#ccff00]/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-[#ccff00]/40 transition-all">
{friend.logo ? (
<img
src={friend.logo}
alt={friend.name}
className={`w-12 h-12 object-contain${friend.invertLogo ? ' invert brightness-200' : ''}`}
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-[#ccff00] transition-colors">{friend.name}</p>
{friend.tagline && <p className="text-xs text-gray-500 mt-0.5">{friend.tagline}</p>}
</div>
</a>
);
}
// ─── Supporter types + row (unified: Angor + Lightning) ──────────────────────
interface SupporterEntry {
key: string;
pubkey?: string;
npub?: string;
name?: string;
logo?: string;
url?: string;
sats: number;
angorSats: number;
angor: boolean;
zap: boolean;
txid?: string;
receiptId?: string;
note?: string;
anonymous?: boolean;
at?: number;
}
const TIER_META = {
supporter: { color: '#22d3ee', key: 'walletUi.supporters.tierSupporter', emoji: '🛹' },
patron: { color: '#f7931a', key: 'walletUi.supporters.tierPatron', emoji: '🔥' },
legend: { color: '#facc15', key: 'walletUi.supporters.tierLegend', emoji: '👑💎✨' },
} as const;
const TIER_ORDER = ['legend', 'patron', 'supporter'] as const;
const TIER_RANGE: Record<(typeof TIER_ORDER)[number], string> = {
legend: '1M+',
patron: '100k–1M',
supporter: '< 100k',
};
function MemberAvatar({ entry }: { entry: SupporterEntry }) {
const { data } = useAuthor(entry.anonymous ? undefined : entry.pubkey);
const meta = data?.metadata;
const name = entry.anonymous
? '★'
: entry.name || meta?.display_name || meta?.name || entry.npub || '';
const avatar = entry.anonymous ? undefined : (entry.logo || meta?.picture);
return (
<Avatar className="h-6 w-6 ring-1 ring-white/15" title={name}>
{avatar && <AvatarImage src={avatar} alt={name} />}
<AvatarFallback className="text-[10px] font-black text-white/70">
{entry.anonymous ? '🛹' : name.replace(/[^a-zA-Z0-9]/g, '').charAt(0).toUpperCase() || 'S'}
</AvatarFallback>
</Avatar>
);
}
function TierLadder({ supporters }: { supporters: SupporterEntry[] }) {
const { t } = useTranslation();
return (
<div className="mt-5 grid grid-cols-1 gap-3 sm:grid-cols-3">
{TIER_ORDER.map((tier) => {
const meta = TIER_META[tier];
const members = supporters.filter((s) => tierOf(s.sats) === tier);
return (
<div
key={tier}
className="rounded-xl border p-3"
style={{
borderColor: `${meta.color}55`,
background: `linear-gradient(135deg, ${meta.color}14, transparent)`,
boxShadow: tier === 'legend' ? `0 0 18px ${meta.color}33` : undefined,
}}
>
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-black uppercase tracking-wide" style={{ color: meta.color }}>
{meta.emoji} {t(meta.key)}
</span>
<span className="text-[10px] text-gray-500">{TIER_RANGE[tier]}</span>
</div>
{members.length > 0 ? (
<div className="mt-2 flex flex-wrap items-center gap-1">
{members.slice(0, 10).map((m) => (
<MemberAvatar key={m.key} entry={m} />
))}
{members.length > 10 && (
<span className="text-[10px] text-gray-500">+{members.length - 10}</span>
)}
</div>
) : (
<p className="mt-2 text-[10px] text-gray-600">{t('walletUi.supporters.tierEmpty')}</p>
)}
</div>
);
})}
</div>
);
}
function SupporterRow({ rank, entry }: { rank: number; entry: SupporterEntry }) {
const { t } = useTranslation();
const { data } = useAuthor(entry.anonymous ? undefined : entry.pubkey);
const meta = data?.metadata;
const name = entry.anonymous
? t('walletUi.supporters.anonymousBacker')
: entry.name || meta?.display_name || meta?.name
|| (entry.npub ? `${entry.npub.slice(0, 12)}…${entry.npub.slice(-6)}` : t('walletUi.supporters.anonymousBacker'));
const avatar = entry.anonymous ? undefined : (entry.logo || meta?.picture);
const tier = TIER_META[tierOf(entry.sats)];
const when = entry.at ? formatDistanceToNow(new Date(entry.at * 1000), { addSuffix: true }) : undefined;
const profileHref = !entry.anonymous && entry.npub ? `/${entry.npub}` : undefined;
const linkHref = profileHref || entry.url;
const proofUrl = entry.txid
? `https://mempool.space/tx/${entry.txid}`
: entry.receiptId
? `https://njump.me/${nip19.neventEncode({ id: entry.receiptId })}`
: undefined;
const nameEl = linkHref ? (
profileHref ? (
<Link to={linkHref} className="truncate text-sm font-medium text-white transition-colors hover:text-[#22d3ee]">{name}</Link>
) : (
<a href={linkHref} target="_blank" rel="noopener noreferrer" className="truncate text-sm font-medium text-white transition-colors hover:text-[#22d3ee]">{name}</a>
)
) : (
<span className="truncate text-sm font-medium text-white">{name}</span>
);
return (
<div className="flex items-center gap-3 rounded-xl border border-white/10 bg-black/60 px-3 py-2.5 transition-colors hover:border-[#22d3ee]/50">
<span className="w-6 shrink-0 text-center text-sm font-bold text-gray-500">#{rank}</span>
<Avatar className="h-9 w-9 shrink-0">
{avatar && <AvatarImage src={avatar} alt={name} />}
<AvatarFallback className="font-black text-[#22d3ee]">
{entry.anonymous ? '🛹' : name.replace(/[^a-zA-Z0-9]/g, '').charAt(0).toUpperCase() || 'S'}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-2">
{nameEl}
{entry.angor && (
<span className="shrink-0 rounded-full border border-[#22d3ee]/40 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide text-[#22d3ee]">
{t('walletUi.supporters.sourceAngor')}
</span>
)}
{entry.zap && <Zap className="h-3 w-3 shrink-0 text-[#f7931a]" />}
</div>
<div className="mt-0.5 flex items-center gap-2">
<span
className="rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide"
style={{
background: `${tier.color}22`,
color: tier.color,
border: `1px solid ${tier.color}55`,
boxShadow: entry.sats >= 1_000_000 ? `0 0 10px ${tier.color}66` : undefined,
}}
>
{tier.emoji} {t(tier.key)}
</span>
{entry.note && !entry.anonymous && <span className="text-[11px] text-gray-500">{entry.note}</span>}
{when && <span className="text-[11px] text-gray-600">· {when}</span>}
</div>
</div>
<div className="flex shrink-0 flex-col items-end gap-0.5">
<span className="font-bold text-[#ccff00]">
{entry.sats.toLocaleString()}<span className="ml-0.5 text-[10px] font-medium text-gray-500">sats</span>
</span>
{proofUrl && (
<a
href={proofUrl}
target="_blank"
rel="noopener noreferrer"
title={entry.txid || entry.receiptId}
className="inline-flex items-center gap-1 text-[10px] font-bold uppercase tracking-wide text-emerald-400/80 transition-colors hover:text-emerald-300"
>
<ShieldCheck className="h-3 w-3" />
{t('walletUi.supporters.verifiedOnChain')}
</a>
)}
</div>
</div>
);
}
// ─── Placeholder card (empty slot) ───────────────────────────────────────────
function PlaceholderCard({ label }: { label: string }) {
return (
<div className="flex flex-col items-center gap-3 p-5 rounded-xl border border-dashed border-white/10 text-center opacity-40">
<div className="w-16 h-16 rounded-2xl border border-dashed border-white/20 flex items-center justify-center">
<span className="text-2xl">?</span>
</div>
<p className="text-xs text-gray-600 font-medium">{label}</p>
</div>
);
}
// ─── Main page ────────────────────────────────────────────────────────────────
export default function SupportersPage() {
const { t } = useTranslation();
const { data: zaps } = useZapLeaderboard(SUPPORTERS_SINCE, PLATFORM_OWNER_PUBKEYS);
const zappers = useMemo(() => zaps?.senders ?? [], [zaps]);
const zapTotal = zappers.reduce((sum, z) => sum + z.sats, 0);
const allSats = BACKERS_TOTAL_SATS + zapTotal;
const totalUsd = useSatsToUsd(allSats);
const { data: stats } = useGrowthStats();
const [showAll, setShowAll] = useState(false);
// One ranked wall: Angor backers + Lightning zaps to the project, merged by npub.
const supporters = useMemo<SupporterEntry[]>(() => {
const map = new Map<string, SupporterEntry>();
BACKERS.forEach((b, i) => {
const npub = b.anonymous ? undefined : b.npub;
const key = (npub || b.url || b.name || `anon-${i}`).toLowerCase();
map.set(key, {
key,
pubkey: npub ? npubToHex(npub) : undefined,
npub,
name: b.anonymous ? undefined : b.name,
logo: b.anonymous ? undefined : b.logo,
url: b.url,
sats: b.amount ?? 0,
angorSats: b.amount ?? 0,
angor: true,
zap: false,
txid: b.txid,
note: b.note,
anonymous: b.anonymous,
at: b.at,
});
});
for (const z of zappers) {
const npub = nip19.npubEncode(z.pubkey);
const existing = map.get(npub);
if (existing) {
existing.sats += z.sats;
existing.zap = true;
existing.receiptId = z.receiptId;
if (z.createdAt && (!existing.at || z.createdAt > existing.at)) existing.at = z.createdAt;
} else {
map.set(npub, {
key: npub,
pubkey: z.pubkey,
npub,
sats: z.sats,
angorSats: 0,
angor: false,
zap: true,
receiptId: z.receiptId,
at: z.createdAt,
});
}
}
return [...map.values()].sort((a, b) => b.sats - a.sats);
}, [zappers]);
const latestAt = supporters.reduce((m, s) => Math.max(m, s.at ?? 0), 0);
// `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.',
ogTitle: 'Supporters — SkateSpots',
ogDescription: 'Built by skaters, funded by skaters. Back the map on Angor or zap it.',
ogImage: 'https://skate-spots.org/images/icon-1024.png',
});
return (
<>
<div className="container max-w-4xl px-4 py-12 md:py-16 space-y-16">
{/* ── Hero ── */}
<div className="text-center">
<div className="text-6xl mb-4">⚡</div>
<h1 className="text-5xl md:text-6xl font-black uppercase mb-3 leading-none">
<span className="neon-red">{t('walletUi.supporters.our')}</span>{' '}
<span className="neon-lime">{t('walletUi.supporters.community')}</span>
</h1>
<p className="text-gray-400 max-w-lg mx-auto text-base mb-8">
{t('walletUi.supporters.heroDesc')}
</p>
<a
href={ANGOR_PROJECT_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 rounded-full bg-gradient-to-r from-[#ff0040] to-[#ff6600] text-white font-black uppercase text-sm hover:opacity-90 transition-all shadow-lg shadow-[#ff0040]/30"
>
<Zap className="h-4 w-4" />
{t('walletUi.supporters.backAngor')}
<ExternalLink className="h-3.5 w-3.5 opacity-70" />
</a>
<Link
to="/donate"
className="mt-5 inline-flex items-center gap-1.5 text-xs font-bold text-[#ccff00]/80 transition-colors hover:text-[#ccff00]"
>
{t('walletUi.supporters.donatePageLink')} →
</Link>
</div>
{/* ── 1. Support ── */}
<section>
<SectionHeader
icon={Zap}
title={t('walletUi.supporters.support')}
subtitle={t('walletUi.supporters.supportDesc')}
color="#f7931a"
/>
<WaysToSupport className="mb-4" />
{/* Lightning rail — the in-app way to chip in (zap publicly or pay quietly). */}
<div id="support-lightning" className="scroll-mt-24">
<SupportSkateSpots className="mt-4" />
</div>
{/* Silent payments — private on-chain (and Lightning) via a BIP-353 name. */}
<div id="support-silent" className="scroll-mt-24">
<SilentPaymentsCard className="mt-4" />
</div>
<div id="support-angor" className="scroll-mt-24">
<AngorProjectCard count={supporters.length} className="mt-4" />
</div>
</section>
{/* ── 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, desc }) => (
<div
key={key}
className="flex items-start gap-3 rounded-xl border border-white/10 bg-black/60 px-4 py-3"
>
<Icon className="mt-0.5 h-4 w-4 flex-shrink-0 text-[#22d3ee]" />
<div className="min-w-0">
<span className="text-sm font-medium text-white">{t(key)}</span>
<p className="text-[11px] text-gray-500">{t(desc)}</p>
</div>
</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>
<p className="mt-4 text-center text-xs text-gray-500">
<Link to="/stats" className="text-[#ccff00]/70 underline transition-colors hover:text-[#ccff00]">
{t('walletUi.supporters.impactSeeStats')}
</Link>
{' · '}
{t('walletUi.supporters.impactAsOf')}
</p>
</section>
)}
{/* ── 4. Supporters (Angor + Lightning zaps) ── */}
<section>
<SectionHeader
icon={Zap}
title={t('walletUi.supporters.backers')}
subtitle={t('walletUi.supporters.backersDesc')}
color="#22d3ee"
/>
{supporters.length > 0 ? (
<>
<div className="flex flex-col gap-2">
{(showAll ? supporters : supporters.slice(0, VISIBLE_LIMIT)).map((entry, i) => (
<SupporterRow key={entry.key} rank={i + 1} entry={entry} />
))}
</div>
{supporters.length > VISIBLE_LIMIT && (
<div className="mt-3 text-center">
<button
onClick={() => setShowAll((v) => !v)}
className="min-h-0 min-w-0 text-xs font-bold uppercase tracking-wide text-[#22d3ee]/80 transition-colors hover:text-[#22d3ee]"
>
{showAll
? t('walletUi.supporters.showLess')
: t('walletUi.supporters.showAll', { count: supporters.length })}
</button>
</div>
)}
<p className="mt-4 text-center text-sm text-gray-400">
{t('walletUi.supporters.supportersTotals', {
count: supporters.length,
angor: BACKERS_TOTAL_SATS.toLocaleString(),
zap: zapTotal.toLocaleString(),
})}
{typeof totalUsd === 'number' && (
<span className="text-gray-500"> · ≈ ${totalUsd.toFixed(2)}</span>
)}
</p>
<p className="mt-2 text-center text-xs text-gray-600">
{t('walletUi.supporters.quietNote')}
</p>
{latestAt > 0 && (
<p className="mt-1 text-center text-[11px] text-gray-600">
{t('walletUi.supporters.updatedAt', {
when: formatDistanceToNow(new Date(latestAt * 1000), { addSuffix: true }),
})}
</p>
)}
<TierLadder supporters={supporters} />
</>
) : (
<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>
{/* ── 6. Collaborators ── */}
<section>
<SectionHeader
icon={Users}
title={t('walletUi.supporters.collaborators')}
subtitle={t('walletUi.supporters.collaboratorsDesc')}
color="#ff0040"
/>
{COLLABORATORS.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{COLLABORATORS.map((c) => (
<CollaboratorCard key={c.name} collab={c} />
))}
</div>
) : (
<div className="rounded-xl border border-dashed border-[#ff0040]/20 p-8 text-center">
<Users className="h-10 w-10 mx-auto mb-3 text-[#ff0040]/30" />
<p className="text-gray-500 text-sm mb-1">{t('walletUi.supporters.wantCollaborate')}</p>
<p className="text-xs text-gray-600 mb-4">{t('walletUi.supporters.wantCollaborateDesc')}</p>
<a
href="https://nostrhub.io/naddr1qvzqqqrhnypzqg88e9f588nx7wyz4wt0ura23w3yd4hmztqrkr95utwyvgachjepqyd8wumn8ghj7emfwsh8x6rpddjhxur9v9ex2tnyd9uj7qq22d4kzar92dcx7arnhyhdwn"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-xs font-bold text-[#ff0040]/70 hover:text-[#ff0040] transition-colors"
>
<ExternalLink className="h-3 w-3" />
{t('walletUi.supporters.viewNostrHub')}
</a>
</div>
)}
</section>
{/* ── 7. Partners ── */}
<section>
<SectionHeader
icon={Handshake}
title={t('walletUi.supporters.partners')}
subtitle={t('walletUi.supporters.partnersDesc')}
color="#ff6600"
/>
{PARTNERS.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{PARTNERS.map((p) => (
<PartnerCard key={p.name} partner={p} />
))}
</div>
) : (
<>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 mb-4">
{Array.from({ length: 4 }).map((_, i) => (
<PlaceholderCard key={i} label={t('walletUi.supporters.comingSoon')} />
))}
</div>
<div className="rounded-xl border border-dashed border-[#ff6600]/20 p-6 text-center">
<p className="text-gray-500 text-sm">{t('walletUi.supporters.wantPartner')}</p>
</div>
</>
)}
</section>
{/* ── 8. Friends ── */}
<section>
<SectionHeader
icon={Star}
title={t('walletUi.supporters.friends')}
subtitle={t('walletUi.supporters.friendsDesc')}
color="#ccff00"
/>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
{FRIENDS.map((f) => (
<FriendCard key={f.name} friend={f} />
))}
{/* Fill remaining slots up to 5 with placeholders */}
{FRIENDS.length < 5 && Array.from({ length: 5 - FRIENDS.length }).map((_, i) => (
<PlaceholderCard key={`ph-${i}`} label={t('walletUi.supporters.comingSoon')} />
))}
</div>
<div className="mt-4 rounded-xl border border-dashed border-[#ccff00]/20 p-5 text-center">
<p className="text-gray-500 text-sm mb-1">{t('walletUi.supporters.wantListed')}</p>
<p className="text-xs text-gray-600">
{t('walletUi.supporters.wantListedDesc')}
</p>
</div>
</section>
{/* ── Mission ── */}
<section>
<div className="relative rounded-2xl overflow-hidden border-2 border-[#ff0040]/30 bg-black/80 p-8 md:p-10">
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-[#ff0040] via-[#ff6600] to-[#ccff00]" />
<div className="flex justify-center mb-5">
<Heart className="h-10 w-10 text-[#ff0040]" />
</div>
<h2 className="text-2xl font-black uppercase text-center mb-3">
{t('walletUi.supporters.whyTitle1')} <span className="neon-red">{t('walletUi.supporters.whyTitle2')}</span>
</h2>
<p className="text-gray-400 text-sm text-center max-w-2xl mx-auto mb-6 leading-relaxed">
{t('walletUi.supporters.whyDesc')}
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ emoji: '🛹', title: t('walletUi.supporters.why1Title'), desc: t('walletUi.supporters.why1Desc') },
{ emoji: '⚡', title: t('walletUi.supporters.why2Title'), desc: t('walletUi.supporters.why2Desc') },
{ emoji: '🌍', title: t('walletUi.supporters.why3Title'), desc: t('walletUi.supporters.why3Desc') },
].map(({ emoji, title, desc }) => (
<div key={title} className="p-4 rounded-xl bg-white/5 border border-white/10 text-center">
<div className="text-3xl mb-2">{emoji}</div>
<p className="font-bold text-sm mb-1">{title}</p>
<p className="text-xs text-gray-500">{desc}</p>
</div>
))}
</div>
</div>
</section>
{/* ── Footer ── */}
<div className="border-t border-gray-800 pt-8 text-center space-y-2 text-sm text-gray-500">
<p>{t('walletUi.supporters.thankYou')}</p>
<p>
<Link to="/license" className="text-[#ccff00]/70 hover:text-[#ccff00] transition-colors underline">{t('walletUi.supporters.license')}</Link>
{' '}&middot;{' '}
<Link to="/about" className="text-[#ccff00]/70 hover:text-[#ccff00] transition-colors underline">{t('nav.about')}</Link>
{' '}&middot;{' '}
<Link to="/faq" className="text-[#ccff00]/70 hover:text-[#ccff00] transition-colors underline">{t('walletUi.supporters.faq')}</Link>
</p>
</div>
</div>
</>
);
}