SkateSpots/src/components/WhosSkating.tsx
SkateSpots d1f8870379 chore(lint): remove all explicit any + sweep dead imports
- Fix all 12 no-explicit-any (NPool, NostrEvent[], LabProfile, structural
  Cluster type, explicit SkateSpot cast) -> 0 remaining
- Remove unused imports/vars across 48 files (manual + a verified codemod for
  unused import specifiers only); tsc + tests + boot smoke all green
- Warning backlog: 182 -> 76 (remaining: ~55 no-unused-vars locals,
  14 react-hooks/exhaustive-deps, 7 react-refresh)
- AGENTS.md baseline updated
2026-09-26 21:40:42 +07:00

141 lines
5.3 KiB
TypeScript

import { Link, useNavigate } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { useWhosSkatingAtSpot, SKATING_STATUSES } from '@/hooks/useUserStatus';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { Users, MessageCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
interface WhosSkatingProps {
spotPubkey: string;
spotIdentifier: string;
className?: string;
}
function SkaterCard({ pubkey, status, emoji }: { pubkey: string; status: string; emoji?: string }) {
const navigate = useNavigate();
const { data: authorData, isLoading } = useAuthor(pubkey);
const metadata = authorData?.metadata;
const displayName = metadata?.name ?? genUserName(pubkey);
const npub = nip19.npubEncode(pubkey);
if (isLoading) {
return (
<div className="flex items-center gap-3 p-3 rounded-lg bg-black/50">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="flex-1">
<Skeleton className="h-4 w-24 mb-1" />
<Skeleton className="h-3 w-16" />
</div>
</div>
);
}
return (
<Link
to={`/${npub}`}
className="block"
>
<div className="flex items-center gap-3 p-3 rounded-lg bg-black/50 hover:bg-black/70 transition-colors border border-transparent hover:border-[#ccff00]/30">
<div className="relative">
<Avatar className="h-10 w-10 border-2 border-green-500/50">
<AvatarImage src={metadata?.picture} alt={displayName} />
<AvatarFallback className="bg-green-500/20 text-green-400 font-bold">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
{/* Online indicator */}
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-3.5 w-3.5 bg-green-500 border-2 border-black" />
</span>
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-white text-sm truncate">{displayName}</p>
<p className="text-xs text-green-400 truncate">
{emoji} {status}
</p>
</div>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-gray-400 hover:text-[#ccff00]"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
// Client-side navigation (HashRouter). Using window.location.href
// here caused a full reload to a non-hash path the router couldn't
// match — a broken jump. useNavigate keeps it in-app and instant.
navigate(`/messages/${npub}`);
}}
>
<MessageCircle className="h-4 w-4" />
</Button>
</div>
</Link>
);
}
export function WhosSkating({ spotPubkey, spotIdentifier, className }: WhosSkatingProps) {
const { data: skatersAtSpot, isLoading } = useWhosSkatingAtSpot(spotPubkey, spotIdentifier);
// Don't show if no one is skating
if (!isLoading && (!skatersAtSpot || skatersAtSpot.length === 0)) {
return null;
}
return (
<Card className={cn('bg-gradient-to-br from-green-500/10 to-[#ccff00]/10 border-2 border-green-500/30', className)}>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<span className="relative flex">
<span className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500" />
</span>
<Users className="h-5 w-5 text-green-400" />
Who's Skating
{skatersAtSpot && skatersAtSpot.length > 0 && (
<Badge className="bg-green-500/20 text-green-400 border-green-500/50">
{skatersAtSpot.length}
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{isLoading ? (
<>
<Skeleton className="h-16 w-full rounded-lg" />
<Skeleton className="h-16 w-full rounded-lg" />
</>
) : skatersAtSpot && skatersAtSpot.length > 0 ? (
<>
{skatersAtSpot.slice(0, 5).map((skater) => {
const preset = skater.statusId
? SKATING_STATUSES.find(s => s.id === skater.statusId)
: null;
return (
<SkaterCard
key={skater.pubkey}
pubkey={skater.pubkey}
status={preset?.label || skater.status}
emoji={skater.emoji || preset?.emoji || '🛹'}
/>
);
})}
{skatersAtSpot.length > 5 && (
<p className="text-xs text-gray-400 text-center pt-2">
+{skatersAtSpot.length - 5} more
</p>
)}
</>
) : null}
</CardContent>
</Card>
);
}