Three places used window.location.href to navigate to bare paths (/signers, /messages/:npub, /settings). Under HashRouter those trigger a full page reload to a non-hash path the router can't match — the user got a blank/404 flash instead of the target screen. - WhosSkating: DM button now uses useNavigate() instead of a reload. - StashPage: "Learn About Signers" now uses <Link to="/signers"> (asChild). - BackupReminder: "Back up now" now uses navigate() AND corrects the deep-link param from ?tab=security to ?section=security (SettingsPage reads ?section), which previously dropped the user on the settings root instead of Security. Also closes the reminder sheet on navigate. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
141 lines
5.4 KiB
TypeScript
141 lines
5.4 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 { formatDistanceToNow } from 'date-fns';
|
|
import { Users, MessageCircle, Zap } 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>
|
|
);
|
|
}
|