929 lines
43 KiB
TypeScript
929 lines
43 KiB
TypeScript
import { useMemo, useState } from 'react';
|
||
import { useSeoMeta } from '@unhead/react';
|
||
import { Link, useNavigate } 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, Info, Lock, EyeOff } 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';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Textarea } from '@/components/ui/textarea';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||
|
||
|
||
// 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 = 12;
|
||
|
||
/** 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) ──────────────
|
||
|
||
|
||
// ─── Bitcoin grantmaker inquiry ──────────────────────────────────────────────
|
||
|
||
function GrantmakerForm() {
|
||
const { t } = useTranslation();
|
||
const navigate = useNavigate();
|
||
const owner = PLATFORM_OWNER_PUBKEYS[0];
|
||
const [org, setOrg] = useState('');
|
||
const [contact, setContact] = useState('');
|
||
const [website, setWebsite] = useState('');
|
||
const [program, setProgram] = useState('');
|
||
const [grantSize, setGrantSize] = useState('');
|
||
const [kyc, setKyc] = useState('');
|
||
const [fund, setFund] = useState('');
|
||
const [ack, setAck] = useState(false);
|
||
|
||
const canSend = !!org.trim() && !!contact.trim() && ack && !!owner;
|
||
const message = [
|
||
'Bitcoin grantmaker inquiry — SkateSpots',
|
||
'',
|
||
`Organization: ${org.trim()}`,
|
||
`Contact: ${contact.trim()}`,
|
||
website.trim() ? `Website: ${website.trim()}` : '',
|
||
program.trim() ? `Program / round: ${program.trim()}` : '',
|
||
grantSize.trim() ? `Grant size: ${grantSize.trim()}` : '',
|
||
kyc.trim() ? `KYC / KYB process: ${kyc.trim()}` : '',
|
||
fund.trim() ? `What we would like to fund: ${fund.trim()}` : '',
|
||
'',
|
||
'I confirm this is a grantmaker inquiry and we KYC/KYB our grantees.',
|
||
].filter(Boolean).join('\n');
|
||
|
||
return (
|
||
<div className="rounded-2xl border-2 border-[#f7931a]/30 bg-gradient-to-br from-[#f7931a]/10 via-black to-transparent p-4 sm:p-6">
|
||
<div className="flex items-start gap-3">
|
||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#f7931a]/15 text-[#f7931a]">
|
||
<Handshake className="h-5 w-5" />
|
||
</div>
|
||
<div className="min-w-0">
|
||
<h3 className="text-lg font-black uppercase text-white">{t('walletUi.supporters.grantsTitle')}</h3>
|
||
<p className="mt-1 text-sm font-bold text-[#f7931a]">{t('walletUi.supporters.grantsDisclaimer')}</p>
|
||
<p className="mt-1 text-xs leading-relaxed text-gray-500">{t('walletUi.supporters.grantsDisclaimerDesc')}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||
<div className="space-y-1.5">
|
||
<Label htmlFor="grant-org">{t('walletUi.supporters.grantsOrg')} *</Label>
|
||
<Input id="grant-org" value={org} onChange={(e) => setOrg(e.target.value)} placeholder="e.g. OpenSats" />
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<Label htmlFor="grant-contact">{t('walletUi.supporters.grantsContact')} *</Label>
|
||
<Input id="grant-contact" value={contact} onChange={(e) => setContact(e.target.value)} placeholder="npub, email or @handle" />
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<Label htmlFor="grant-website">{t('walletUi.supporters.grantsWebsite')}</Label>
|
||
<Input id="grant-website" value={website} onChange={(e) => setWebsite(e.target.value)} placeholder="https://…" />
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<Label htmlFor="grant-program">{t('walletUi.supporters.grantsProgram')}</Label>
|
||
<Input id="grant-program" value={program} onChange={(e) => setProgram(e.target.value)} placeholder="e.g. Q1 2027 grants" />
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<Label htmlFor="grant-size">{t('walletUi.supporters.grantsSize')}</Label>
|
||
<Input id="grant-size" value={grantSize} onChange={(e) => setGrantSize(e.target.value)} placeholder="e.g. 5–25M sats" />
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<Label htmlFor="grant-kyc">{t('walletUi.supporters.grantsKyc')}</Label>
|
||
<Input id="grant-kyc" value={kyc} onChange={(e) => setKyc(e.target.value)} placeholder="e.g. KYB + grantee KYC" />
|
||
</div>
|
||
<div className="space-y-1.5 sm:col-span-2">
|
||
<Label htmlFor="grant-fund">{t('walletUi.supporters.grantsFund')}</Label>
|
||
<Textarea id="grant-fund" value={fund} onChange={(e) => setFund(e.target.value)} rows={3} placeholder="Relay, app development, translation, design, events…" />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||
{[
|
||
{ icon: ShieldCheck, title: t('walletUi.supporters.grantsNoKycTitle'), body: t('walletUi.supporters.grantsNoKycDesc') },
|
||
{ icon: EyeOff, title: t('walletUi.supporters.grantsNoTrackingTitle'), body: t('walletUi.supporters.grantsNoTrackingDesc') },
|
||
{ icon: Lock, title: t('walletUi.supporters.grantsPrivateTitle'), body: t('walletUi.supporters.grantsPrivateDesc') },
|
||
].map(({ icon: Icon, title, body }) => (
|
||
<div key={title} className="flex items-start gap-2 rounded-xl border border-white/10 bg-black/30 p-2.5">
|
||
<Icon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#f7931a]" />
|
||
<div className="min-w-0">
|
||
<p className="text-[11px] font-bold text-white">{title}</p>
|
||
<p className="mt-0.5 text-[10px] leading-relaxed text-gray-500">{body}</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<p className="mt-3 rounded-xl border border-[#f7931a]/15 bg-[#f7931a]/[0.04] px-3 py-2 text-[11px] leading-relaxed text-gray-500">
|
||
{t('walletUi.supporters.grantsEthos')}
|
||
</p>
|
||
|
||
<label className="mt-4 flex cursor-pointer items-start gap-2.5 rounded-xl border border-[#f7931a]/20 bg-black/30 p-3 text-xs text-gray-400">
|
||
<input type="checkbox" checked={ack} onChange={(e) => setAck(e.target.checked)} className="mt-0.5 h-4 w-4 accent-[#f7931a]" />
|
||
<span>{t('walletUi.supporters.grantsAck')}</span>
|
||
</label>
|
||
|
||
<div className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-center">
|
||
<Button
|
||
disabled={!canSend}
|
||
onClick={() => {
|
||
if (!canSend || !owner) return;
|
||
navigate(`/messages/${nip19.npubEncode(owner)}?message=${encodeURIComponent(message)}`);
|
||
}}
|
||
className="w-full justify-center bg-[#f7931a] font-bold text-black hover:bg-[#ffb347] sm:w-auto"
|
||
>
|
||
<Handshake className="mr-2 h-4 w-4" /> {t('walletUi.supporters.grantsSend')}
|
||
</Button>
|
||
<p className="flex items-center gap-1.5 text-[11px] text-gray-600">
|
||
<Info className="h-3 w-3 shrink-0" />
|
||
{t('walletUi.supporters.grantsNote')}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── 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-3 md:mb-4">
|
||
<div className="mb-1.5 flex items-center gap-2.5">
|
||
<Icon className="h-4 w-4 flex-shrink-0" style={{ color }} />
|
||
<h2 className="text-lg font-black uppercase tracking-wide md:text-xl" style={{ color }}>
|
||
{title}
|
||
</h2>
|
||
<div className="h-px flex-1" style={{ background: `linear-gradient(to right, ${color}40, transparent)` }} />
|
||
</div>
|
||
{subtitle && <p className="ml-7 text-xs text-gray-400">{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>
|
||
);
|
||
}
|
||
|
||
// ─── 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);
|
||
const [tab, setTab] = useState<'support' | 'backers' | 'community' | 'grants'>('support');
|
||
const [rail, setRail] = useState<'support-lightning' | 'support-silent' | 'support-angor'>('support-lightning');
|
||
|
||
// 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) + num(stats.totalTransport) + num(stats.totalDesigners)
|
||
: 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: t('seoUi.supportersTitle'),
|
||
description: t('seoUi.supportersDesc'),
|
||
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 space-y-6 px-4 py-8 md:py-10">
|
||
|
||
{/* ── Hero ── */}
|
||
<div className="text-center">
|
||
<h1 className="text-4xl font-black uppercase leading-none md:text-5xl">
|
||
<span className="neon-red">{t('walletUi.supporters.our')}</span>{' '}
|
||
<span className="neon-lime">{t('walletUi.supporters.community')}</span>
|
||
</h1>
|
||
<p className="mx-auto mt-2 max-w-xl text-sm text-gray-400">
|
||
{t('walletUi.supporters.heroDesc')}
|
||
</p>
|
||
<div className="mt-5 flex flex-wrap items-center justify-center gap-2">
|
||
<a
|
||
href={ANGOR_PROJECT_URL}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="inline-flex items-center gap-2 rounded-full bg-gradient-to-r from-[#ff0040] to-[#ff6600] px-5 py-2.5 text-xs font-black uppercase text-white shadow-lg shadow-[#ff0040]/25 transition-opacity hover:opacity-90"
|
||
>
|
||
<Zap className="h-4 w-4" />
|
||
{t('walletUi.supporters.backAngor')}
|
||
<ExternalLink className="h-3 w-3 opacity-70" />
|
||
</a>
|
||
<Link
|
||
to="/donate"
|
||
className="inline-flex items-center gap-1.5 rounded-full border border-white/15 px-4 py-2.5 text-xs font-bold text-[#ccff00]/80 transition-colors hover:border-[#ccff00]/40 hover:text-[#ccff00]"
|
||
>
|
||
{t('walletUi.supporters.donatePageLink')} →
|
||
</Link>
|
||
</div>
|
||
<div className="mt-4 flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-[11px] text-gray-500">
|
||
<span>{supporters.length.toLocaleString()} {t('walletUi.supporters.backers')}</span>
|
||
<span aria-hidden="true">·</span>
|
||
<span>{allSats.toLocaleString()} sats</span>
|
||
{typeof totalUsd === 'number' && (
|
||
<>
|
||
<span aria-hidden="true">·</span>
|
||
<span>≈ ${totalUsd.toFixed(2)}</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Compact navigation ── */}
|
||
<Tabs
|
||
value={tab}
|
||
onValueChange={(value) => setTab(value as 'support' | 'backers' | 'community' | 'grants')}
|
||
>
|
||
<TabsList className="grid h-auto w-full grid-cols-4 gap-1 rounded-2xl border border-white/10 bg-black/70 p-1">
|
||
{[
|
||
{ value: 'support', label: t('walletUi.supporters.support') },
|
||
{ value: 'backers', label: t('walletUi.supporters.backers') },
|
||
{ value: 'community', label: t('walletUi.supporters.community') },
|
||
{ value: 'grants', label: t('walletUi.supporters.grantsSection') },
|
||
].map((item) => (
|
||
<TabsTrigger
|
||
key={item.value}
|
||
value={item.value}
|
||
className="h-auto min-h-10 whitespace-normal rounded-xl px-1.5 py-2 text-center text-[9px] font-black uppercase leading-tight tracking-wide data-[state=active]:bg-[#ccff00] data-[state=active]:text-black data-[state=active]:shadow-none sm:px-2 sm:text-[11px]"
|
||
>
|
||
{item.label}
|
||
</TabsTrigger>
|
||
))}
|
||
</TabsList>
|
||
|
||
{/* ── Support rails ── */}
|
||
<TabsContent value="support" className="space-y-4 pt-4">
|
||
<div>
|
||
<h2 className="text-xl font-black uppercase text-white">{t('walletUi.supporters.support')}</h2>
|
||
<p className="mt-0.5 text-sm text-gray-400">{t('walletUi.supporters.donateDesc')}</p>
|
||
</div>
|
||
<WaysToSupport active={rail} onSelect={(id) => setRail(id as typeof rail)} />
|
||
<div id={rail} className="scroll-mt-24">
|
||
{rail === 'support-lightning' && <SupportSkateSpots />}
|
||
{rail === 'support-silent' && <SilentPaymentsCard />}
|
||
{rail === 'support-angor' && <AngorProjectCard count={supporters.length} />}
|
||
</div>
|
||
|
||
<section className="pt-1">
|
||
<SectionHeader
|
||
icon={Heart}
|
||
title={t('walletUi.supporters.paysFor')}
|
||
subtitle={t('walletUi.supporters.paysForDesc')}
|
||
color="#22d3ee"
|
||
/>
|
||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||
{PAYS_FOR.map(({ icon: Icon, key, desc }) => (
|
||
<div key={key} className="flex items-start gap-2 rounded-xl border border-white/10 bg-black/60 px-3 py-2">
|
||
<Icon className="mt-0.5 h-3.5 w-3.5 flex-shrink-0 text-[#22d3ee]" />
|
||
<div className="min-w-0">
|
||
<span className="text-xs font-medium text-white">{t(key)}</span>
|
||
<p className="text-[10px] leading-snug text-gray-500">{t(desc)}</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</TabsContent>
|
||
|
||
{/* ── Backers, impact and tiers ── */}
|
||
<TabsContent value="backers" className="space-y-5 pt-4">
|
||
{impact.length > 0 && (
|
||
<section>
|
||
<SectionHeader
|
||
icon={TrendingUp}
|
||
title={t('walletUi.supporters.impact')}
|
||
subtitle={t('walletUi.supporters.impactDesc')}
|
||
color="#ccff00"
|
||
/>
|
||
<div className="grid grid-cols-3 gap-2 sm:grid-cols-6">
|
||
{impact.map(({ key, value }) => (
|
||
<div key={key} className="rounded-xl border border-white/10 bg-black/60 px-2 py-2.5 text-center">
|
||
<div className="text-lg font-black text-[#ccff00]">{value.toLocaleString()}</div>
|
||
<div className="truncate text-[10px] text-gray-400">{t(key)}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<p className="mt-2 text-center text-[11px] text-gray-500">
|
||
<Link to="/stats" className="text-[#ccff00]/70 underline underline-offset-2 hover:text-[#ccff00]">
|
||
{t('walletUi.supporters.impactSeeStats')}
|
||
</Link>
|
||
{' · '}
|
||
{t('walletUi.supporters.impactAsOf')}
|
||
</p>
|
||
</section>
|
||
)}
|
||
|
||
<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-1.5">
|
||
{(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-2 text-center">
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowAll((v) => !v)}
|
||
className="min-h-0 min-w-0 text-[11px] 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-3 text-center text-xs 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-1 text-center text-[11px] text-gray-600">{t('walletUi.supporters.quietNote')}</p>
|
||
{latestAt > 0 && (
|
||
<p className="mt-0.5 text-center text-[10px] 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-6 text-center">
|
||
<Zap className="mx-auto mb-2 h-8 w-8 text-[#22d3ee]/30" />
|
||
<p className="mb-1 text-sm text-gray-500">{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]">
|
||
<ExternalLink className="h-3 w-3" /> {t('walletUi.supporters.backAngor')}
|
||
</a>
|
||
</div>
|
||
)}
|
||
</section>
|
||
</TabsContent>
|
||
|
||
{/* ── Community, mission, collaborators, partners, friends ── */}
|
||
<TabsContent value="community" className="space-y-6 pt-4">
|
||
<section className="rounded-2xl border border-[#ff0040]/25 bg-black/70 p-5">
|
||
<div className="flex items-center justify-center gap-2">
|
||
<Heart className="h-5 w-5 text-[#ff0040]" />
|
||
<h2 className="text-center text-lg font-black uppercase text-white">
|
||
{t('walletUi.supporters.whyTitle1')}{' '}
|
||
<span className="neon-red">{t('walletUi.supporters.whyTitle2')}</span>
|
||
</h2>
|
||
</div>
|
||
<p className="mx-auto mt-2 max-w-2xl text-center text-xs leading-relaxed text-gray-400">
|
||
{t('walletUi.supporters.whyDesc')}
|
||
</p>
|
||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||
{[
|
||
{ 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="rounded-xl border border-white/10 bg-white/[0.03] p-3 text-center">
|
||
<div className="text-2xl">{emoji}</div>
|
||
<p className="mt-1 text-xs font-bold">{title}</p>
|
||
<p className="mt-0.5 text-[11px] text-gray-500">{desc}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
<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 gap-2 sm:grid-cols-2">
|
||
{COLLABORATORS.map((c) => <CollaboratorCard key={c.name} collab={c} />)}
|
||
</div>
|
||
) : (
|
||
<div className="rounded-xl border border-dashed border-[#ff0040]/20 p-5 text-center">
|
||
<Users className="mx-auto mb-2 h-8 w-8 text-[#ff0040]/30" />
|
||
<p className="mb-1 text-sm text-gray-500">{t('walletUi.supporters.wantCollaborate')}</p>
|
||
<p className="mb-3 text-xs text-gray-600">{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]">
|
||
<ExternalLink className="h-3 w-3" /> {t('walletUi.supporters.viewNostrHub')}
|
||
</a>
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<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 gap-2 sm:grid-cols-2">
|
||
{PARTNERS.map((p) => <PartnerCard key={p.name} partner={p} />)}
|
||
</div>
|
||
) : (
|
||
<div className="rounded-xl border border-dashed border-[#ff6600]/20 p-5 text-center text-sm text-gray-500">
|
||
{t('walletUi.supporters.wantPartner')}
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<section>
|
||
<SectionHeader icon={Star} title={t('walletUi.supporters.friends')} subtitle={t('walletUi.supporters.friendsDesc')} color="#ccff00" />
|
||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
|
||
{FRIENDS.map((f) => <FriendCard key={f.name} friend={f} />)}
|
||
</div>
|
||
<div className="mt-3 rounded-xl border border-dashed border-[#ccff00]/20 p-4 text-center">
|
||
<p className="text-xs text-gray-500">{t('walletUi.supporters.wantListed')}</p>
|
||
<p className="mt-0.5 text-[11px] text-gray-600">{t('walletUi.supporters.wantListedDesc')}</p>
|
||
</div>
|
||
</section>
|
||
</TabsContent>
|
||
|
||
{/* ── Grantmakers ── */}
|
||
<TabsContent value="grants" className="space-y-4 pt-4">
|
||
<SectionHeader
|
||
icon={Handshake}
|
||
title={t('walletUi.supporters.grantsSection')}
|
||
subtitle={t('walletUi.supporters.grantsSectionDesc')}
|
||
color="#f7931a"
|
||
/>
|
||
<GrantmakerForm />
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
{/* ── Footer ── */}
|
||
<div className="border-t border-gray-800 pt-5 text-center text-xs text-gray-500">
|
||
<p>{t('walletUi.supporters.thankYou')}</p>
|
||
<p className="mt-1.5">
|
||
<Link to="/license" className="text-[#ccff00]/70 underline transition-colors hover:text-[#ccff00]">{t('walletUi.supporters.license')}</Link>
|
||
{' '}·{' '}
|
||
<Link to="/about" className="text-[#ccff00]/70 underline transition-colors hover:text-[#ccff00]">{t('nav.about')}</Link>
|
||
{' '}·{' '}
|
||
<Link to="/faq" className="text-[#ccff00]/70 underline transition-colors hover:text-[#ccff00]">{t('walletUi.supporters.faq')}</Link>
|
||
</p>
|
||
</div>
|
||
|
||
</div>
|
||
</>
|
||
);
|
||
}
|