feat: redesign /supporters with 4 sections + footer label update

- Supporters: existing Geyser leaderboard (unchanged)
- Collaborators: new section, empty placeholder with GitHub CTA
- Partners: Geyser, Angor, Zapstore, ContextVM, MKStack
- Friends: placeholder grid (5 slots) + logo spec instructions
- Footer: "Supporters" → "Collaborators & Friends"
- Static COLLABORATORS/PARTNERS/FRIENDS arrays at top of file for easy editing

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-06-27 17:51:11 +07:00
parent 5dffa5cac3
commit baf4c5e10d
2 changed files with 336 additions and 99 deletions

View file

@ -796,7 +796,7 @@ export function Layout({ children }: LayoutProps) {
to="/supporters"
className="hover:text-[#ccff00] transition-colors font-medium"
>
Supporters
Collaborators &amp; Friends
</Link>
<span className="flex items-center gap-1.5">
<span>Not sponsored by</span>

View file

@ -2,12 +2,84 @@ 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';
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;
@ -15,8 +87,7 @@ interface GeyserContributor {
imageUrl?: string;
heroLink?: string;
};
amount: number; // sats
funder?: boolean;
amount: number;
}
function formatSats(sats: number): string {
@ -38,12 +109,7 @@ function useGeyserLeaderboard() {
projectGet(where: { name: "skatespots" }) {
funders(input: { orderBy: { amountFunded: desc }, pagination: { take: 100 } }) {
amountFunded
user {
id
username
imageUrl
heroLink
}
user { id username imageUrl heroLink }
}
}
}`,
@ -59,34 +125,63 @@ function useGeyserLeaderboard() {
return [];
}
},
staleTime: 5 * 60 * 1000, // 5 min cache
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 (
<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>
);
}
// ─── 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 = (
<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="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">
<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"
className="w-12 h-12 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 className="w-12 h-12 rounded-full bg-gradient-to-br from-[#ff0040]/20 to-[#ccff00]/20 border-2 border-white/10 flex items-center justify-center text-xl">
🛹
</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 className="absolute -bottom-1 -right-1 bg-[#f7931a] rounded-full w-4 h-4 flex items-center justify-center">
<Zap className="h-2 w-2 text-white" />
</div>
</div>
<p className="text-xs font-bold text-center text-white truncate max-w-full">
<p className="text-xs font-bold text-center text-white truncate max-w-full w-full text-center">
{isAnon ? 'Anon 🛹' : contributor.user.username}
</p>
<p className="text-[10px] font-bold text-[#f7931a]">
@ -96,35 +191,141 @@ function SupporterCard({ contributor }: { contributor: GeyserContributor }) {
);
if (isAnon) return card;
return <a href={profileUrl} target="_blank" rel="noopener noreferrer">{card}</a>;
}
// ─── 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={profileUrl} target="_blank" rel="noopener noreferrer">
{card}
<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"
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>
);
}
// ─── 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() {
useSeoMeta({
title: 'Supporters - SkateSpots',
description: 'The community keeping SkateSpots alive. Powered by Geyser and Bitcoin Lightning.',
description: 'The community keeping SkateSpots alive — supporters, collaborators, partners and friends.',
});
const { data: contributors = [], isLoading } = useGeyserLeaderboard();
return (
<Layout>
<div className="container max-w-4xl px-4 py-12 md:py-16">
<div className="container max-w-4xl px-4 py-12 md:py-16 space-y-16">
{/* Hero */}
<div className="text-center mb-14">
{/* ── 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">OUR</span>{' '}
<span className="neon-lime">SUPPORTERS</span>
<span className="neon-lime">COMMUNITY</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 className="text-gray-400 max-w-lg mx-auto text-base mb-8">
SkateSpots is built by and for the community. Supporters, collaborators, partners and friends keeping this free and decentralized.
</p>
<a
href={GEYSER_PROJECT_URL}
@ -138,26 +339,22 @@ export default function SupportersPage() {
</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>
{/* ── 1. Supporters ── */}
<section>
<SectionHeader
icon={Zap}
title="Supporters"
subtitle="Bitcoin contributors via Geyser — ordered by cumulative sats."
color="#f7931a"
/>
{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 className="w-12 h-12 rounded-full bg-white/10" />
<div className="h-3 w-14 bg-white/10 rounded" />
<div className="h-3 w-10 bg-white/5 rounded" />
</div>
))}
</div>
@ -171,53 +368,116 @@ export default function SupportersPage() {
<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>
<p className="text-sm text-gray-600">The leaderboard updates once contributions come in.</p>
</div>
)}
{/* Geyser leaderboard link */}
<div className="mt-6 text-center">
<div className="mt-6 flex flex-wrap items-center justify-between gap-4">
<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"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border-2 border-[#f7931a]/30 text-[#f7931a] font-bold text-sm hover:bg-[#f7931a]/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 className="flex items-center gap-2 text-xs text-gray-500">
<span>Powered by</span>
<a href={GEYSER_PROJECT_URL} target="_blank" rel="noopener noreferrer"
className="font-bold text-gray-400 hover:text-white transition-colors">
Geyser ⚡
</a>
</div>
</div>
</section>
{/* Mission section */}
<section className="mb-16">
{/* ── 2. Collaborators ── */}
<section>
<SectionHeader
icon={Users}
title="Collaborators"
subtitle="People actively building SkateSpots — developers, designers, relay operators."
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">Want to collaborate?</p>
<p className="text-xs text-gray-600 mb-4">Developers, designers, skaters with skills — reach out.</p>
<a
href="https://github.com/soapbox-pub/mkstack"
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" />
View on GitHub
</a>
</div>
)}
</section>
{/* ── 3. Partners ── */}
<section>
<SectionHeader
icon={Handshake}
title="Partners"
subtitle="Organizations and projects we work closely with."
color="#ff6600"
/>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{PARTNERS.map((p) => (
<PartnerCard key={p.name} partner={p} />
))}
</div>
</section>
{/* ── 4. Friends ── */}
<section>
<SectionHeader
icon={Star}
title="Friends"
subtitle="Aligned projects and people who share our values."
color="#ccff00"
/>
{FRIENDS.length > 0 ? (
<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} />
))}
</div>
) : (
<>
{/* Placeholder grid — remove once real entries are added */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3 mb-4">
{Array.from({ length: 5 }).map((_, i) => (
<PlaceholderCard key={i} label="Coming soon" />
))}
</div>
<div className="rounded-xl border border-dashed border-[#ccff00]/20 p-6 text-center">
<p className="text-gray-500 text-sm mb-1">Want to be listed here?</p>
<p className="text-xs text-gray-600">
Send a logo (PNG/WebP, transparent background, min 200×200px) and your URL via DM on Nostr.
</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="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>
@ -226,11 +486,9 @@ export default function SupportersPage() {
</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.
No ads, no data selling, no investors. Every sat goes directly toward development, relay hosting, and keeping this free for every skater on earth.
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-6">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ 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.' },
@ -246,29 +504,7 @@ export default function SupportersPage() {
</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 */}
{/* ── Footer ── */}
<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>
@ -279,6 +515,7 @@ export default function SupportersPage() {
<Link to="/faq" className="text-[#ccff00]/70 hover:text-[#ccff00] transition-colors underline">FAQ</Link>
</p>
</div>
</div>
</Layout>
);