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, Handshake, Building2, Star } 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'; // ─── 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) 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: 'Geyser', logo: 'https://geyser.fund/logo-brand.svg', url: 'https://geyser.fund/project/skatespots', description: 'Bitcoin Lightning crowdfunding', }, { name: 'Angor', logo: '', url: 'https://angor.io/', description: 'Bitcoin crowdfunding platform', }, { name: 'Zapstore', logo: '', url: 'https://zapstore.dev/apps/org.skatespots.app', description: 'Open-source app distribution', }, { name: 'ContextVM', logo: '', url: 'https://contextvm.org/s/e3564bb877adb0c99a7e4973ccb22e516d7d9eca0027d0bd62a04174ed670530', description: 'Nostr AI infrastructure', }, { name: 'MKStack', logo: '', url: 'https://soapbox.pub/mkstack', description: 'Nostr client framework', }, ]; const FRIENDS: Friend[] = [ // Add friends here once logos are ready // Format: { name: 'Example', logo: 'https://example.com/logo.png', url: 'https://example.com', tagline: 'Short description' } // Logo requirements: PNG/WebP with transparent background, min 200×200px square preferred ]; // ─── Helpers ───────────────────────────────────────────────────────────────── interface GeyserContributor { user: { id: string; username: string; imageUrl?: string; heroLink?: string; }; amount: number; } 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({ 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, retry: false, }); } // ─── Section header ─────────────────────────────────────────────────────────── function SectionHeader({ icon: Icon, title, subtitle, color = '#ccff00', }: { icon: React.ComponentType<{ className?: string }>; title: string; subtitle?: string; color?: string; }) { return (

{title}

{subtitle &&

{subtitle}

}
); } // ─── Supporter card (Geyser contributors) ──────────────────────────────────── 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 = (
{contributor.user.imageUrl ? ( {contributor.user.username ) : (
🛹
)}

{isAnon ? 'Anon 🛹' : contributor.user.username}

{formatSats(contributor.amount)} sats

); if (isAnon) return card; return {card}; } // ─── Collaborator card ──────────────────────────────────────────────────────── function CollaboratorCard({ collab }: { collab: Collaborator }) { const href = collab.npub ? `/${collab.npub}` : collab.url; const inner = (
{collab.avatar ? ( {collab.name} ) : (
🛹
)}

{collab.name}

{collab.role}

{href && }
); if (!href) return inner; if (collab.npub) return {inner}; return {inner}; } // ─── Partner card ───────────────────────────────────────────────────────────── function PartnerCard({ partner }: { partner: Partner }) { return (
{partner.logo ? ( {partner.name} { (e.target as HTMLImageElement).style.display = 'none'; }} /> ) : ( )}

{partner.name}

{partner.description}

); } // ─── Friend card ────────────────────────────────────────────────────────────── function FriendCard({ friend }: { friend: Friend }) { return (
{friend.logo ? ( {friend.name} { (e.target as HTMLImageElement).style.display = 'none'; }} /> ) : ( 🛹 )}

{friend.name}

{friend.tagline &&

{friend.tagline}

}
); } // ─── Placeholder card (empty slot) ─────────────────────────────────────────── function PlaceholderCard({ label }: { label: string }) { return (
?

{label}

); } // ─── Main page ──────────────────────────────────────────────────────────────── export default function SupportersPage() { useSeoMeta({ title: 'Supporters - SkateSpots', description: 'The community keeping SkateSpots alive — supporters, collaborators, partners and friends.', }); const { data: contributors = [], isLoading } = useGeyserLeaderboard(); return (
{/* ── Hero ── */}
⚡

OUR{' '} COMMUNITY

SkateSpots is built by and for the community. Supporters, collaborators, partners and friends keeping this free and decentralized.

Become a Supporter
{/* ── 1. Supporters ── */}
{isLoading ? (
{Array.from({ length: 12 }).map((_, i) => (
))}
) : contributors.length > 0 ? (
{contributors.map((c, i) => ( ))}
) : (
🛹

Be the first supporter!

The leaderboard updates once contributions come in.

)}
{/* ── 2. Collaborators ── */}
{COLLABORATORS.length > 0 ? (
{COLLABORATORS.map((c) => ( ))}
) : (

Want to collaborate?

Developers, designers, skaters with skills — reach out.

View on GitHub
)}
{/* ── 3. Partners ── */}
{PARTNERS.map((p) => ( ))}
{/* ── 4. Friends ── */}
{FRIENDS.length > 0 ? (
{FRIENDS.map((f) => ( ))}
) : ( <> {/* Placeholder grid — remove once real entries are added */}
{Array.from({ length: 5 }).map((_, i) => ( ))}

Want to be listed here?

Send a logo (PNG/WebP, transparent background, min 200×200px) and your URL via DM on Nostr.

)}
{/* ── Mission ── */}

Why We Need Support

SkateSpots is 100% free, open-source, and built on Nostr — a decentralized protocol with no central server to shut it down. No ads, no data selling, no investors. Every sat goes directly toward development, relay hosting, and keeping this free for every skater on earth.

{[ { 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 }) => (
{emoji}

{title}

{desc}

))}
{/* ── Footer ── */}

Thank you. Every sat keeps the spots on the map. 🛹⚡

License {' '}·{' '} About {' '}·{' '} FAQ

); }