285 lines
12 KiB
TypeScript
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>
|
|
{' '}·{' '}
|
|
<Link to="/about" className="text-[#ccff00]/70 hover:text-[#ccff00] transition-colors underline">About</Link>
|
|
{' '}·{' '}
|
|
<Link to="/faq" className="text-[#ccff00]/70 hover:text-[#ccff00] transition-colors underline">FAQ</Link>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|