import { useState } from 'react'; import { Link } from 'react-router-dom'; import { Trophy, Users, Hash, Zap, MessageSquare, HandHeart, Flame, } from 'lucide-react'; import type { NostrMetadata } from '@nostrify/nostrify'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useAuthor } from '@/hooks/useAuthor'; import { useProfileUrl } from '@/hooks/useProfileUrl'; import { useTrustedCountryStats } from '@/hooks/useTrustedCountryStats'; import { useTrustedGlobalStats } from '@/hooks/useTrustedGlobalStats'; import type { StatsTimeframe, TopAction, TopContributor, TopDonor, TopPoster, TrendingHashtag, TrustedCountryStats, } from '@/lib/statsParser'; import { genUserName } from '@/lib/genUserName'; import { getDisplayName } from '@/lib/getDisplayName'; import { cn } from '@/lib/utils'; const TIMEFRAMES: { value: StatsTimeframe; label: string }[] = [ { value: '7d', label: '7d' }, { value: '30d', label: '30d' }, { value: '90d', label: '90d' }, { value: 'all', label: 'All' }, ]; interface CommunityStatsPanelProps { /** ISO 3166 country code. Omit to render the global (`iso3166:ZZ`) snapshot. */ countryCode?: string; className?: string; /** * Render at narrow widths (e.g. inside a 360px sidebar): 2-column tile grid * with full labels, single-column leaderboards. Defaults to `false` (the * roomy multi-column layout used on country pages). */ compact?: boolean; } /** * Compact community-stats panel for a country (or the global aggregate when * `countryCode` is omitted). Reads pre-computed kind 30385 snapshots — see * NIP.md → Kind 30385 for the trust model and tag schema. * * Renders nothing when no trusted snapshot is available, so it can be safely * dropped into any page without producing empty placeholders. */ export function CommunityStatsPanel({ countryCode, className, compact = false }: CommunityStatsPanelProps) { const isGlobal = !countryCode; const country = useTrustedCountryStats(isGlobal ? undefined : countryCode); const global = useTrustedGlobalStats(); const { data: stats, isLoading } = isGlobal ? global : country; const [tf, setTf] = useState('7d'); if (isLoading && !stats) return ; if (!stats) return null; return (
); } // ── Header ─────────────────────────────────────────────────────────────────── function PanelHeader({ stats, timeframe, onTimeframeChange, }: { stats: TrustedCountryStats; timeframe: StatsTimeframe; onTimeframeChange: (tf: StatsTimeframe) => void; }) { return (

Community stats updated {formatRelative(stats.updatedAt)}

onTimeframeChange(v as StatsTimeframe)}> {TIMEFRAMES.map((t) => ( {t.label} ))}
); } // ── Aggregate counts ───────────────────────────────────────────────────────── function AggregateCounts({ stats, timeframe, compact, }: { stats: TrustedCountryStats; timeframe: StatsTimeframe; compact: boolean; }) { const c = stats.counts; return (
); } function CountTile({ icon: Icon, label, value, }: { icon: React.ComponentType<{ className?: string }>; label: string; value: number; }) { return (
{label}
{formatCompact(value)}
); } // ── Leaderboards ───────────────────────────────────────────────────────────── function Leaderboards({ stats, timeframe, compact, }: { stats: TrustedCountryStats; timeframe: StatsTimeframe; compact: boolean; }) { const tfData = stats.byTimeframe[timeframe]; return (
); } function SectionHeader({ icon: Icon, title }: { icon: React.ComponentType<{ className?: string }>; title: string }) { return (

{title}

); } function EmptyRow({ label }: { label: string }) { return

No {label} yet.

; } function TopActionsList({ actions }: { actions: TopAction[] }) { if (!actions.length) { return (
); } return (
    {actions.slice(0, 5).map((a, i) => { const parts = a.aTag.split(':'); const naddrPath = parts.length === 3 ? `/actions` : `/actions`; return (
  • {a.title}
    {a.submissions} submissions · {formatCompact(a.zapAmount)} sats zapped
  • ); })}
); } function TopPostersList({ posters }: { posters: TopPoster[] }) { if (!posters.length) { return (
); } return (
    {posters.slice(0, 5).map((p, i) => ( ))}
); } function TopZappedList({ contributors }: { contributors: TopContributor[] }) { if (!contributors.length) { return (
); } return (
    {contributors.slice(0, 5).map((c, i) => ( ))}
); } function TopDonorsList({ donors }: { donors: TopDonor[] }) { if (!donors.length) { return (
); } return (
    {donors.slice(0, 5).map((d, i) => ( ))}
); } function TrendingHashtagsList({ tags, className }: { tags: TrendingHashtag[]; className?: string }) { if (!tags.length) { return (
); } return (
{tags.slice(0, 16).map((t) => ( {t.tag} {t.count} ))}
); } // ── Generic pubkey row ─────────────────────────────────────────────────────── function PubkeyRow({ rank, pubkey, primary, secondary, }: { rank: number; pubkey: string; primary: string; secondary?: string; }) { const author = useAuthor(pubkey); const metadata: NostrMetadata | undefined = author.data?.metadata; const displayName = getDisplayName(metadata, pubkey) || genUserName(pubkey); const url = useProfileUrl(pubkey, metadata); return (
  • {displayName.slice(0, 2).toUpperCase()}
    {displayName}
    {primary} {secondary && · {secondary}}
  • ); } function RankBadge({ rank }: { rank: number }) { return ( {rank} ); } // ── Skeleton ───────────────────────────────────────────────────────────────── function PanelSkeleton({ className, compact }: { className?: string; compact?: boolean }) { return (
    {Array.from({ length: 5 }).map((_, i) => ( ))}
    {Array.from({ length: 4 }).map((_, i) => ( ))}
    ); } // ── Formatting ─────────────────────────────────────────────────────────────── const compactFormatter = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 }); function formatCompact(value: number): string { if (!value) return '0'; return compactFormatter.format(value); } function formatRelative(unixSeconds: number): string { if (!unixSeconds) return 'never'; const diffSec = Math.max(0, Math.floor(Date.now() / 1000 - unixSeconds)); if (diffSec < 60) return 'just now'; const diffMin = Math.floor(diffSec / 60); if (diffMin < 60) return `${diffMin}m ago`; const diffHr = Math.floor(diffMin / 60); if (diffHr < 24) return `${diffHr}h ago`; const diffDay = Math.floor(diffHr / 24); return `${diffDay}d ago`; }