import React, { useState, useEffect, useRef } from "react"; import { Link } from "react-router-dom"; import Avatar from "../ui/Avatar"; import RichText from "./RichText"; import { getProfile } from "../../api/client"; import type { UserProfile } from "../../types"; import { Loader2 } from "lucide-react"; interface ProfileHoverCardProps { did?: string; handle?: string; children: React.ReactNode; className?: string; } export default function ProfileHoverCard({ did, handle, children, className, }: ProfileHoverCardProps) { const [isOpen, setIsOpen] = useState(false); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(false); const timeoutRef = useRef | null>(null); const closeTimeoutRef = useRef | null>(null); const cardRef = useRef(null); const handleMouseEnter = () => { timeoutRef.current = setTimeout(async () => { setIsOpen(true); if (!profile && (did || handle)) { setLoading(true); try { const identifier = did || handle || ""; const [marginData, bskyData] = await Promise.all([ getProfile(identifier).catch(() => null), fetch( `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(identifier)}`, ) .then((res) => (res.ok ? res.json() : null)) .catch(() => null), ]); const merged: UserProfile = { did: marginData?.did || bskyData?.did || identifier, handle: marginData?.handle || bskyData?.handle || "", displayName: marginData?.displayName || bskyData?.displayName, avatar: marginData?.avatar || bskyData?.avatar, description: marginData?.description || bskyData?.description, }; setProfile(merged); } catch (e) { console.error("Failed to load profile", e); } finally { setLoading(false); } } }, 400); }; const handleMouseLeave = () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } closeTimeoutRef.current = setTimeout(() => { setIsOpen(false); }, 300); }; const handleCardMouseEnter = () => { if (closeTimeoutRef.current) { clearTimeout(closeTimeoutRef.current); closeTimeoutRef.current = null; } }; const handleCardMouseLeave = () => { setIsOpen(false); }; useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } if (closeTimeoutRef.current) { clearTimeout(closeTimeoutRef.current); } }; }, []); return (
{children} {isOpen && (
{loading ? (
) : profile ? (

{profile.displayName || profile.handle}

@{profile.handle}

{profile.description && (

)} View Profile
) : (

Profile not found

)}
)}
); }