SkateSpots/src/pages/SupportersPage.tsx
shakespeare.diy 805edd86ca fix: remove Lightning address card from /supporters, keep only Geyser
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-16 11:15:41 +07:00

285 lines
12 KiB
TypeScript

import { useSeoMeta } from '@unhead/react';
import { useQuery } from '@tanstack/react-query';
import { Layout } from '@/components/Layout';
import { Link } from 'react-router-dom';
import { ExternalLink, Zap, Heart, Users } from 'lucide-react';
const GEYSER_PROJECT_URL = 'https://geyser.fund/project/skatespots?hero=satsnfacts';
const GEYSER_LEADERBOARD_URL = 'https://geyser.fund/project/skatespots/leaderboard';
const GEYSER_API_URL = 'https://api.geyser.fund/graphql';
interface GeyserContributor {
user: {
id: string;
username: string;
imageUrl?: string;
heroLink?: string;
};
amount: number; // sats
funder?: boolean;
}
function formatSats(sats: number): string {
if (sats >= 1_000_000) return `${(sats / 1_000_000).toFixed(2)}M`;
if (sats >= 1_000) return `${Math.round(sats / 1_000).toLocaleString()}k`;
return sats.toLocaleString();
}
function useGeyserLeaderboard() {
return useQuery<GeyserContributor[]>({
queryKey: ['geyser-leaderboard'],
queryFn: async () => {
try {
const res = await fetch(GEYSER_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `{
projectGet(where: { name: "skatespots" }) {
funders(input: { orderBy: { amountFunded: desc }, pagination: { take: 100 } }) {
amountFunded
user {
id
username
imageUrl
heroLink
}
}
}
}`,
}),
});
const json = await res.json();
const funders = json?.data?.projectGet?.funders ?? [];
return funders.map((f: { amountFunded: number; user: GeyserContributor['user'] }) => ({
user: f.user,
amount: f.amountFunded,
}));
} catch {
return [];
}
},
staleTime: 5 * 60 * 1000, // 5 min cache
retry: false,
});
}
function SupporterCard({ contributor }: { contributor: GeyserContributor }) {
const isAnon = !contributor.user.username || contributor.user.username.toLowerCase().startsWith('anon');
const profileUrl = contributor.user.heroLink || `https://geyser.fund/user/${contributor.user.id}`;
const card = (
<div className="flex flex-col items-center gap-2 p-4 rounded-xl bg-black/60 border border-white/10 hover:border-[#ccff00]/40 transition-all group min-w-0">
<div className="relative">
{contributor.user.imageUrl ? (
<img
src={contributor.user.imageUrl}
alt={contributor.user.username || 'Anon'}
className="w-14 h-14 rounded-full object-cover border-2 border-white/10 group-hover:border-[#ccff00]/50 transition-all"
/>
) : (
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-[#ff0040]/20 to-[#ccff00]/20 border-2 border-white/10 flex items-center justify-center text-2xl">
🛹
</div>
)}
<div className="absolute -bottom-1 -right-1 bg-[#f7931a] rounded-full w-5 h-5 flex items-center justify-center">
<Zap className="h-2.5 w-2.5 text-white" />
</div>
</div>
<p className="text-xs font-bold text-center text-white truncate max-w-full">
{isAnon ? 'Anon 🛹' : contributor.user.username}
</p>
<p className="text-[10px] font-bold text-[#f7931a]">
{formatSats(contributor.amount)} sats
</p>
</div>
);
if (isAnon) return card;
return (
<a href={profileUrl} target="_blank" rel="noopener noreferrer">
{card}
</a>
);
}
export default function SupportersPage() {
useSeoMeta({
title: 'Supporters - SkateSpots',
description: 'The community keeping SkateSpots alive. Powered by Geyser and Bitcoin Lightning.',
});
const { data: contributors = [], isLoading } = useGeyserLeaderboard();
return (
<Layout>
<div className="container max-w-4xl px-4 py-12 md:py-16">
{/* Hero */}
<div className="text-center mb-14">
<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">OUR</span>{' '}
<span className="neon-lime">SUPPORTERS</span>
</h1>
<p className="text-gray-400 max-w-lg mx-auto text-base mb-6">
SkateSpots is community-powered. These skaters and Bitcoiners help keep the project free, decentralized, and built for the streets.
</p>
<a
href={GEYSER_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" />
Become a Supporter
<ExternalLink className="h-3.5 w-3.5 opacity-70" />
</a>
</div>
{/* Individuals section */}
<section className="mb-16">
<div className="flex items-center gap-3 mb-6">
<Users className="h-5 w-5 text-[#ccff00]" />
<h2 className="text-2xl font-black uppercase">
<span className="neon-lime">Plebs</span>
</h2>
<div className="flex-1 h-px bg-gradient-to-r from-[#ccff00]/30 to-transparent" />
</div>
<p className="text-gray-400 text-sm mb-6">
Every sat counts — ordered by cumulative contributions.
</p>
{isLoading ? (
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="flex flex-col items-center gap-2 p-4 rounded-xl bg-black/40 border border-white/5 animate-pulse">
<div className="w-14 h-14 rounded-full bg-white/10" />
<div className="h-3 w-16 bg-white/10 rounded" />
<div className="h-3 w-12 bg-white/5 rounded" />
</div>
))}
</div>
) : contributors.length > 0 ? (
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3">
{contributors.map((c, i) => (
<SupporterCard key={`${c.user.id}-${i}`} contributor={c} />
))}
</div>
) : (
<div className="text-center py-12 rounded-2xl border border-dashed border-white/10">
<div className="text-4xl mb-3">🛹</div>
<p className="text-gray-500 mb-2">Be the first supporter!</p>
<p className="text-sm text-gray-600">
The leaderboard will update once contributions come in.
</p>
</div>
)}
{/* Geyser leaderboard link */}
<div className="mt-6 text-center">
<a
href={GEYSER_LEADERBOARD_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border-2 border-[#ccff00]/30 text-[#ccff00] font-bold text-sm hover:bg-[#ccff00]/10 transition-all"
>
<ExternalLink className="h-4 w-4" />
Full Leaderboard on Geyser
</a>
</div>
{/* Powered by Geyser */}
<div className="mt-8 flex items-center justify-center gap-3">
<span className="text-xs text-gray-500">Powered by</span>
<a
href={GEYSER_PROJECT_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
>
<img
src="https://geyser.fund/logo-brand.svg"
alt="Geyser"
className="h-5 brightness-0 invert opacity-60 hover:opacity-90 transition-opacity"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
<span className="text-sm font-bold text-gray-400 hover:text-white transition-colors">Geyser</span>
</a>
</div>
</section>
{/* Mission section */}
<section className="mb-16">
<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="absolute -top-16 -right-16 w-48 h-48 bg-[#ff0040] rounded-full opacity-5 blur-3xl pointer-events-none" />
<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">
Why We Need <span className="neon-red">Support</span>
</h2>
<p className="text-gray-400 text-sm text-center max-w-2xl mx-auto mb-6 leading-relaxed">
SkateSpots is 100% free, open-source, and built on Nostr — a decentralized protocol with no central server to shut it down.
We don't run ads, sell your data, or answer to investors. Keeping the lights on and building new features relies entirely on the skate community.
Every sat you contribute goes directly toward development, relay hosting, and keeping this tool free for every skater on earth.
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-6">
{[
{ emoji: '🛹', title: 'Nostr-native', desc: 'No central server. Your data lives on relays you choose.' },
{ emoji: '⚡', title: 'Bitcoin powered', desc: 'Earn sats for mapping spots. Spend them at local shops.' },
{ emoji: '🌍', title: 'Free forever', desc: 'Open source, no paywalls, no tracking, no VC funding.' },
].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>
{/* Payment options */}
<section className="mb-12">
<h2 className="text-xl font-black uppercase mb-5 flex items-center gap-2">
<Zap className="h-5 w-5 text-[#f7931a]" />
<span>Send Sats</span>
</h2>
<div className="p-5 rounded-xl border-2 border-[#ff0040]/20 bg-black/60 flex flex-col justify-between gap-3">
<p className="text-xs font-bold uppercase text-gray-400 tracking-widest">Via Geyser</p>
<p className="text-sm text-gray-400">
Contribute via the Geyser campaign — your name goes on the leaderboard above.
</p>
<a
href={GEYSER_PROJECT_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm font-bold text-[#ccff00] hover:text-[#ccff00]/80 transition-colors"
>
Open campaign <ExternalLink className="h-3.5 w-3.5" />
</a>
</div>
</section>
{/* Footer links */}
<div className="border-t border-gray-800 pt-8 text-center space-y-2 text-sm text-gray-500">
<p>Thank you. Every sat keeps the spots on the map. 🛹⚡</p>
<p>
<Link to="/license" className="text-[#ccff00]/70 hover:text-[#ccff00] transition-colors underline">License</Link>
{' '}&middot;{' '}
<Link to="/about" className="text-[#ccff00]/70 hover:text-[#ccff00] transition-colors underline">About</Link>
{' '}&middot;{' '}
<Link to="/faq" className="text-[#ccff00]/70 hover:text-[#ccff00] transition-colors underline">FAQ</Link>
</p>
</div>
</div>
</Layout>
);
}