import { useMemo } from 'react'; import { useSeoMeta } from '@unhead/react'; import { Link } from 'react-router-dom'; import { Layout } from '@/components/Layout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { useAuthor } from '@/hooks/useAuthor'; import { genUserName } from '@/lib/genUserName'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { useGrowthStats } from '@/hooks/useGrowthStats'; import { cn } from '@/lib/utils'; const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z'; import { SKATE_RELAY } from '@/lib/types'; import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts'; import { MapPin, Store, Calendar, GraduationCap, Video, Scissors, Route, Users, Users2, Camera, Hammer, TrendingUp, Radio, Zap, ExternalLink, Trophy, FlaskConical, Palette, SprayCan, PenTool, Dumbbell, Music, Gavel, Tent, Sparkles } from 'lucide-react'; // ─── Helpers ───────────────────────────────────────────────────────────────── /** Sum of every Lab craft count — keeps the "total" badge and donut in sync. */ function labCraftTotal(s: { totalCoaches: number; totalFilmers: number; totalEditors: number; totalPhotographers: number; totalBuilders: number; totalArtists: number; totalGraffiti: number; totalTattoo: number; totalTrainers: number; totalMusicians: number; totalJudges: number; totalOrganizers: number; totalBarbers: number; }): number { return ( s.totalCoaches + s.totalFilmers + s.totalEditors + s.totalPhotographers + s.totalBuilders + s.totalArtists + s.totalGraffiti + s.totalTattoo + s.totalTrainers + s.totalMusicians + s.totalJudges + s.totalOrganizers + s.totalBarbers ); } const CHART_TOOLTIP_STYLE = { backgroundColor: '#0d0d0d', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '12px', }; // ─── Hero stat card ────────────────────────────────────────────────────────── function StatCard({ icon: Icon, value, label, sub, trustedValue, color, loading, thisMonthDelta, to, }: { icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>; value: number; label: string; sub?: string; /** If set, shows a secondary "X trusted" line below the main count */ trustedValue?: number; color: string; loading: boolean; /** How many new items were added this month */ thisMonthDelta?: number; /** If set, the whole card becomes a link to this route (view all items). */ to?: string; }) { const clickable = !!to && !loading && value > 0; const card = (

{label}

{loading ? ( ) : (

{value.toLocaleString()}

{typeof thisMonthDelta === 'number' && thisMonthDelta > 0 && ( +{thisMonthDelta} this month )}
)} {!loading && trustedValue !== undefined && (

🛡 {trustedValue.toLocaleString()} from active accounts

)} {sub &&

{sub}

} {clickable && (

View all

)}
); return clickable ? {card} : card; } // ─── Top mapper row ────────────────────────────────────────────────────────── function MapperRow({ pubkey, rank, spotCount }: { pubkey: string; rank: number; spotCount: number }) { const author = useAuthor(pubkey); const meta = author.data?.metadata; const name = meta?.name ?? genUserName(pubkey); const rankColors: Record = { 1: '#f7931a', 2: '#c0c0c0', 3: '#cd7f32' }; return (
{rank <= 3 ? ['🥇','🥈','🥉'][rank - 1] : rank} {name.slice(0, 2).toUpperCase()} {name} {spotCount} {spotCount === 1 ? 'spot' : 'spots'}
); } // ─── Skeleton helpers ──────────────────────────────────────────────────────── function ChartSkeleton() { return (
{Array.from({ length: 6 }).map((_, i) => ( ))}
); } // ─── Page ──────────────────────────────────────────────────────────────────── export default function CommunityStatsPage() { useSeoMeta({ title: 'Community Stats — SkateSpots', description: 'Live community metrics for SkateSpots — spots, events, coaches, filmers, editors, tours and growth over time. Pulled directly from Nostr.', }); const { data: stats, isLoading, isError } = useGrowthStats(); // Content mix for donut chart const contentMix = useMemo(() => { if (!stats) return []; return [ { name: 'Spots', value: stats.totalSpots, color: '#ff0040' }, { name: 'Events', value: stats.totalEvents, color: '#ccff00' }, { name: 'Lab', value: labCraftTotal(stats), color: '#ff6600' }, { name: 'Shops', value: stats.totalShops, color: '#4ade80' }, { name: 'Tours', value: stats.totalTours, color: '#c084fc' }, { name: 'Crews', value: stats.totalCrews, color: '#2dd4bf' }, ].filter(d => d.value > 0); }, [stats]); // Only show last 12 months on charts to keep them readable const recentMonthly = useMemo(() => stats?.monthly.slice(-12) ?? [], [stats]); const recentCumulative = useMemo(() => stats?.cumulativeSpots.slice(-12) ?? [], [stats]); // Current month delta — how many items were added this month const thisMonth = useMemo(() => stats?.monthly[stats.monthly.length - 1], [stats]); // Total across ALL Lab creator/service crafts (matches what /lab lists) const labTotal = useMemo(() => (stats ? labCraftTotal(stats) : 0), [stats]); return (
{/* ── Header ── */}

Community
Stats

Live metrics pulled directly from the Nostr network. Transparent, decentralized, verifiable by anyone.

{/* Data source badge */}
Data source:  {SKATE_RELAY.replace('wss://', '')}  ·  cached 15 min
{/* ── Error state ── */} {isError && (
Could not reach the relay right now. Stats will load once the connection is restored.
)} {/* ── Section 1: Hero numbers ── */}

Community at a Glance

{/* ── Section 1b: The Lab — creators & services ── */}

The Lab · Creators & Services

{!isLoading && labTotal > 0 && ( {labTotal.toLocaleString()} total )}
{/* ── Section 1c: Community — tours & crews ── */}

Community

{/* ── Section 2: Growth charts ── */}

Growth Over Time

{/* Cumulative spots — area chart */} Spots Mapped (Cumulative)

Total spots added to the map over time

{isLoading ? : ( )}
{/* Monthly new content — stacked bar */} Monthly Activity

New content published each month

{isLoading ? : ( )}
{/* Active mappers per month — bar */} Active Mappers / Month

Unique contributors publishing each month

{isLoading ? : ( {recentMonthly.map((_, i) => ( ))} )}
{/* Content mix — donut */} Content Mix

Breakdown by content type (all time)

{isLoading ? (
) : contentMix.length > 0 ? ( {contentMix.map((entry, i) => ( ))} ) : (

No data yet

)}
{/* ── Section 3: Top mappers ── */}

Top Mappers — most spots added

{isLoading ? ( <> {Array.from({ length: 5 }).map((_, i) => (
))}
{Array.from({ length: 5 }).map((_, i) => (
))}
) : stats && stats.topMappers.length > 0 ? ( <> {stats.topMappers.slice(0, 5).map((m, i) => ( ))} {stats.topMappers.slice(5, 10).map((m, i) => ( ))} ) : (
No mapper data yet
)}
{/* ── Section 4: Supporters ── */}

Community Supporters

Funded by the Community

Decentralized Bitcoin crowdfunding via Angor.
No ads, no investors, no data selling.

Back us on Angor Community →
{/* ── Footer / verifiability note ── */}

All data is pulled live from{' '} {SKATE_RELAY} {' '}— a public Nostr relay.{' '} Every event is signed by its creator and cryptographically verifiable.{' '} No numbers can be faked.

); }