import { useMemo, useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { LazyMount } from '@/components/LazyMount';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Skeleton } from '@/components/ui/skeleton';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { useSkaters, useUserSkaterProfile } from '@/hooks/useSkaters';
import { useSkaterVerifications } from '@/hooks/useSkaterVerifications';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { useTranslation } from '@/contexts/language';
import {
SKATER_STYLES,
SKATER_STANCES,
SKATER_LEVELS,
type Skater,
type SkaterStyle,
} from '@/lib/types';
import { cn } from '@/lib/utils';
import { PersonStanding, Search, MapPin, Check, Sparkles, Users, ShieldCheck, X } from 'lucide-react';
// ─── Skater card ────────────────────────────────────────────────────────────
function SkaterCard({ skater, verified }: { skater: Skater; verified?: boolean }) {
const { t } = useTranslation();
const author = useAuthor(skater.pubkey);
const metadata = author.data?.metadata;
const name = metadata?.name ?? skater.title ?? genUserName(skater.pubkey);
const avatar = metadata?.picture ?? skater.image;
const npub = nip19.npubEncode(skater.pubkey);
const level = skater.level ? SKATER_LEVELS[skater.level] : undefined;
return (
{/* accent glow */}
{name.slice(0, 2).toUpperCase()}
{level ? (
{level.emoji} {level.label}
) : skater.customLevel ? (
✏️ {skater.customLevel}
) : null}
{name}
{verified && (
)}
{skater.location && (
{skater.location}
)}
{skater.stance && (
{SKATER_STANCES[skater.stance].emoji} {SKATER_STANCES[skater.stance].label}
{skater.yearsSkating ? ` · ${skater.yearsSkating}y` : ''}
)}
{/* Styles */}
{skater.styles.length > 0 && (
{skater.styles.slice(0, 4).map((s) => (
{SKATER_STYLES[s].emoji} {SKATER_STYLES[s].label}
))}
{skater.styles.length > 4 && (
+{skater.styles.length - 4}
)}
)}
{skater.bio && (
{skater.bio}
)}
);
}
function SkaterCardSkeleton() {
return (
);
}
// ─── Page ─────────────────────────────────────────────────────────────────
export default function SkatersPage() {
const { t } = useTranslation();
useSeoMeta({
title: 'Skaters - SkateSpots',
description: 'Discover skaters in the SkateSpots community. Browse by style, stance, level and location.',
});
const { data: skaters, isLoading } = useSkaters();
const { user } = useCurrentUser();
const { data: myProfile } = useUserSkaterProfile(user?.pubkey);
const [query, setQuery] = useState('');
const [styleFilter, setStyleFilter] = useState('all');
const [verifiedOnly, setVerifiedOnly] = useState(false);
// Soft NIP-05 verification for the whole list (never a gate — powers the
// "Verified only" toggle and the ✓ badge, so trust is opt-in, not enforced).
const skaterPubkeys = useMemo(() => (skaters ?? []).map((s) => s.pubkey), [skaters]);
const { data: verifiedSet, isLoading: verifying } = useSkaterVerifications(skaterPubkeys);
const verifiedCount = verifiedSet?.size ?? 0;
// Which styles actually appear (to avoid empty filter chips).
const availableStyles = useMemo(() => {
const set = new Set();
for (const s of skaters ?? []) for (const st of s.styles) set.add(st);
return (Object.keys(SKATER_STYLES) as SkaterStyle[]).filter((s) => set.has(s));
}, [skaters]);
const filtered = useMemo(() => {
let list = skaters ?? [];
if (verifiedOnly) list = list.filter((s) => verifiedSet?.has(s.pubkey));
if (styleFilter !== 'all') list = list.filter((s) => s.styles.includes(styleFilter));
if (query.trim()) {
const q = query.trim().toLowerCase();
list = list.filter(
(s) =>
s.title.toLowerCase().includes(q) ||
s.location?.toLowerCase().includes(q) ||
s.bio.toLowerCase().includes(q) ||
nip19.npubEncode(s.pubkey).toLowerCase().includes(q),
);
}
return list;
}, [skaters, styleFilter, query, verifiedOnly, verifiedSet]);
const total = skaters?.length ?? 0;
return (
<>
{/* ── Hero header ── */}
{t('labUi.skaters.community')}
Skaters
{t('labUi.skaters.heroDesc')}
{!myProfile ? (
) : (
)}
{total > 0 && (
{total} {t(total === 1 ? 'labUi.skaters.countOne' : 'labUi.skaters.countMany', { count: total })}
)}
{/* ── Search + filters ── */}
setQuery(e.target.value)}
className="pl-9 h-10 bg-white/5 border-white/10"
/>
{query && (
)}
{/* Style chips + verified toggle */}
{(availableStyles.length > 0 || verifiedCount > 0) && (
{/* Verified only — soft trust filter, not a gate */}
{availableStyles.map((s) => (
))}
)}
{/* ── Grid ── */}
{isLoading ? (
{Array.from({ length: 6 }).map((_, i) => )}
) : (() => {
const shown = filtered;
return shown.length === 0 ? (
{total === 0 ? (
<>
{t('labUi.skaters.noneTitle')}
{t('labUi.skaters.noneDesc')}
>
) : (
<>
{verifiedOnly ? t('labUi.skaters.noVerifiedMatch') : t('labUi.skaters.noMatches')}
{verifiedOnly
? t('labUi.skaters.tryTurnOffVerified')
: t('labUi.skaters.tryDifferentSearch')}
>
)}
) : (
{shown.map((s) => (
))}
);
})()}
>
);
}