import { useMemo, useState } from 'react'; import { useSeoMeta } from '@unhead/react'; import { Link } from 'react-router-dom'; 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 { RegionBackfillCard } from '@/components/RegionBackfillCard'; import { useDeriveRegions } from '@/hooks/useDeriveRegions'; import { useTranslation } from '@/contexts/language'; import { cn } from '@/lib/utils'; import { formatAge } from '@/lib/routeCache'; 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, Truck, Shirt, ShoppingBag, Tag, Handshake, Globe, RefreshCw } 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; totalTransport: number; totalDesigners: 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 + (s.totalTransport ?? 0) + (s.totalDesigners ?? 0) ); } const CHART_TOOLTIP_STYLE = { backgroundColor: '#0d0d0d', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '12px', }; // ─── Hero stat card ────────────────────────────────────────────────────────── /** Compact number for large values (1234567 → "1.2M"); exact value in the title. */ function compactNumber(n: number): string { return new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 }).format(n); } function StatCard({ icon: Icon, value, label, sub, trustedValue, color, loading, thisMonthDelta, to, compact, }: { 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; /** Abbreviate big numbers (e.g. sats) so they don't overflow the card. */ compact?: boolean; }) { const { t } = useTranslation(); const clickable = !!to && !loading && value > 0; const card = (

{label}

{loading ? ( ) : (

{compact ? compactNumber(value) : value.toLocaleString()}

{typeof thisMonthDelta === 'number' && thisMonthDelta > 0 && ( {t('walletUi.stats.thisMonth', { count: thisMonthDelta })} )}
)} {!loading && trustedValue !== undefined && (

🛡 {t('walletUi.stats.fromActive', { count: trustedValue.toLocaleString() })}

)} {sub &&

{sub}

} {clickable && (

{t('walletUi.stats.viewAll')}

)}
); return clickable ? {card} : card; } // ─── Top mapper row ────────────────────────────────────────────────────────── function MapperRow({ pubkey, rank, count, unit }: { pubkey: string; rank: number; count: number; unit: 'spots' | 'shops' }) { const { t } = useTranslation(); 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} {t(unit === 'shops' ? 'walletUi.stats.shopCount' : 'walletUi.stats.spotCount', { count })}
); } // ─── Skeleton helpers ──────────────────────────────────────────────────────── function ChartSkeleton() { return (
{Array.from({ length: 6 }).map((_, i) => ( ))}
); } // ─── Page ──────────────────────────────────────────────────────────────────── export default function CommunityStatsPage() { const { t } = useTranslation(); useSeoMeta({ title: t('seoUi.communityStatsTitle'), description: t('seoUi.communityStatsDesc'), }); const { data: stats, isLoading, isError, isFetching, refetch, dataUpdatedAt } = useGrowthStats(); // Resolve a region for untagged pins (cached) so the region card matches the map. const { pending: derivingPending } = useDeriveRegions(); // Collapse the 13 Lab craft cards on mobile (they were ~7 rows of scrolling // before the first chart); always expanded from lg up. const [showAllLab, setShowAllLab] = useState(false); // Content mix for donut chart const contentMix = useMemo(() => { if (!stats) return []; return [ { name: t('walletUi.stats.spots'), value: stats.totalSpots, color: '#ff0040' }, { name: t('walletUi.stats.events'), value: stats.totalEvents, color: '#ccff00' }, { name: t('walletUi.stats.lab'), value: labCraftTotal(stats), color: '#ff6600' }, { name: t('walletUi.stats.shops'), value: stats.totalShops, color: '#4ade80' }, { name: t('walletUi.stats.tours'), value: stats.totalTours, color: '#c084fc' }, { name: t('walletUi.stats.crews'), value: stats.totalCrews, color: '#2dd4bf' }, ].filter(d => d.value > 0); }, [stats, t]); // 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?.length ? stats.monthly[stats.monthly.length - 1] : undefined), [stats], ); // Total across ALL Lab creator/service crafts (matches what /lab lists) const labTotal = useMemo(() => (stats ? labCraftTotal(stats) : 0), [stats]); // Defensive reads: a persisted cache saved before these fields existed would // lack them, and `stats.topCountries.length` would throw. (The persist buster // was bumped too, but never trust a cache blindly.) const tradesByMethod = stats?.tradesByMethod ?? []; const topCountries = stats?.topCountries ?? []; const topCities = stats?.topCities ?? []; const otherCountries = stats?.otherCountries ?? 0; const otherCities = stats?.otherCities ?? 0; const uncategorised = stats?.uncategorised ?? 0; const locationsTotal = stats?.locationsTotal ?? 0; return ( <>
{/* ── Header ── */}

Community
Stats

{t('walletUi.stats.subtitle')}

{/* Data source badge + freshness + manual refresh */}
{t('walletUi.stats.dataSource')}  {SKATE_RELAY.replace('wss://', '')} {dataUpdatedAt > 0 && ( <>·{t('walletUi.stats.updated', { age: formatAge(Date.now() - dataUpdatedAt, t) })} )}
{/* Honesty banner: a relay outage must not read as real zeros. */} {stats?.partial && (

{t('walletUi.stats.partial')}

)}
{/* ── Error state ── */} {isError && (
{t('walletUi.stats.error')}
)} {/* ── Section 1: Hero numbers ── */}

{t('walletUi.stats.atAGlance')}

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

{t('walletUi.stats.labHeader')}

{!isLoading && labTotal > 0 && ( {t('walletUi.stats.total', { count: labTotal.toLocaleString() })} )}
{/* Extra crafts: collapsed on mobile (they were ~7 rows of scroll before the first chart), always visible from `lg` up. The `contents` wrapper keeps its children as real grid items. */}
{/* ── Section 1c: Community — tours & crews ── */}

{t('walletUi.stats.community')}

{/* ── Section 1d: Marketplace ── */}

{t('walletUi.stats.marketplace')}

{tradesByMethod.length > 0 && (
{tradesByMethod.map((m) => ( {m.method} · {m.count} ))}
)}

{stats?.tradedCount ?? 0} {t('walletUi.stats.tradesNote1')}{' '} {stats?.tradedTraders ?? 0} {t('walletUi.stats.tradesNote2')}{' '} {t('walletUi.stats.mutual')} {t('walletUi.stats.tradesNote3')}{' '} {(stats?.tradedSatsClaimed ?? 0).toLocaleString()}{' '} {t('walletUi.stats.tradesNote4')} {t('walletUi.stats.not')} {t('walletUi.stats.tradesNote5')}

{/* ── Section 1e: Regions ── */} {(topCountries.length > 0 || topCities.length > 0) && (

{t('walletUi.stats.byRegion')}

{derivingPending > 0 && (

{t('walletUi.stats.resolving', { count: derivingPending })}

)}
{topCountries.length > 0 && ( {t('walletUi.stats.topCountries')} {topCountries.map((c) => (
{c.name}
{c.spots}
))} {otherCountries > 0 && (
{t('walletUi.stats.other')}
{otherCountries}
)} {uncategorised > 0 && (
{t('walletUi.stats.uncategorised')}
{uncategorised}
)} )} {topCities.length > 0 && ( {t('walletUi.stats.topCities')} {topCities.map((c) => (
{c.name}
{c.spots}
))} {otherCities > 0 && (
{t('walletUi.stats.other')}
{otherCities}
)} {uncategorised > 0 && (
{t('walletUi.stats.uncategorised')}
{uncategorised}
)} )}

{t('walletUi.stats.regionNote1')} {t('walletUi.stats.spotsAndShops')}{t('walletUi.stats.regionNote2')} {locationsTotal > 0 && ( <> {t('walletUi.stats.ofTotal', { count: uncategorised, total: locationsTotal })} {t('walletUi.stats.regionNote3')} )}

{/* Admin-only (renders null otherwise): fix the gap right where it shows up — re-derive region tags for the viewer's own legacy pins. */} {uncategorised > 0 &&
}
)} {/* ── Section 2: Growth charts ── */}

{t('walletUi.stats.growth')}

{/* Cumulative spots — area chart */} {t('walletUi.stats.chartCumulativeTitle')}

{t('walletUi.stats.chartCumulativeDesc')}

{isLoading ? : ( )}
{/* Monthly new content — stacked bar */} {t('walletUi.stats.chartMonthlyTitle')}

{t('walletUi.stats.chartMonthlyDesc')}

{isLoading ? : ( )}
{/* Active mappers per month — bar */} {t('walletUi.stats.chartMappersTitle')}

{t('walletUi.stats.chartMappersDesc')}

{isLoading ? : ( {recentMonthly.map((_, i) => ( ))} )}
{/* Content mix — donut */} {t('walletUi.stats.chartMixTitle')}

{t('walletUi.stats.chartMixDesc')}

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

{t('walletUi.stats.noData')}

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

{t('walletUi.stats.topMappers')} {t('walletUi.stats.mostSpots')}

{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) => ( ))} ) : (
{t('walletUi.stats.noMapperData')}
)}
{/* ── Section 3b: Top shops ── */}

{t('walletUi.stats.topShops')} {t('walletUi.stats.mostShops')}

{isLoading ? ( <> {[0, 1].map((c) => ( {Array.from({ length: 5 }).map((_, i) => (
))}
))} ) : stats && (stats.topShops ?? []).length > 0 ? ( <> {(stats.topShops ?? []).slice(0, 5).map((m, i) => ( ))} {(stats.topShops ?? []).slice(5, 10).map((m, i) => ( ))} ) : (
{t('walletUi.stats.noShopData')}
)}
{/* ── Section 4: Supporters ── */}

{t('walletUi.stats.supporters')}

{t('walletUi.stats.fundedTitle')}

{t('walletUi.stats.fundedDesc1')}
{t('walletUi.stats.fundedDesc2')}

{t('walletUi.stats.backAngor')} {t('walletUi.stats.communityLink')}
{/* ── Footer / verifiability note ── */}

{t('walletUi.stats.footer1')}{' '} {SKATE_RELAY} {' '}{t('walletUi.stats.footer2')}{' '} {t('walletUi.stats.footer3')}{' '} {t('walletUi.stats.footer4')}

); }