- 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
141 lines
5.3 KiB
TypeScript
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>
|
|
);
|
|
}
|