import { AlertTriangle, ExternalLink, Highlighter, Loader2, PenTool, Search, } from "lucide-react"; import React, { useCallback, useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import { getUserTargetItems } from "../../api/client"; import Card from "../../components/common/Card"; import Avatar from "../../components/ui/Avatar"; import { EmptyState, Tabs } from "../../components/ui"; import type { AnnotationItem, UserProfile } from "../../types"; export default function UserUrlPage() { const params = useParams(); const handle = params.handle; const urlPath = params["*"]; const targetUrl = urlPath || ""; const [profile, setProfile] = useState(null); const [annotations, setAnnotations] = useState([]); const [highlights, setHighlights] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [loadMoreError, setLoadMoreError] = useState(null); const [hasMore, setHasMore] = useState(false); const [offset, setOffset] = useState(0); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState< "all" | "annotations" | "highlights" >("all"); const LIMIT = 50; const [resolvedDid, setResolvedDid] = useState(null); useEffect(() => { async function fetchData() { if (!targetUrl || !handle) { setLoading(false); return; } try { setLoading(true); setError(null); const profileRes = await fetch( `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`, ); let did = handle; if (profileRes.ok) { const profileData = await profileRes.json(); setProfile(profileData); did = profileData.did; } const decodedUrl = decodeURIComponent(targetUrl); setResolvedDid(did); const data = await getUserTargetItems(did, decodedUrl, LIMIT, 0); const fetchedAnnotations = data.annotations || []; const fetchedHighlights = data.highlights || []; setAnnotations(fetchedAnnotations); setHighlights(fetchedHighlights); const totalFetched = fetchedAnnotations.length + fetchedHighlights.length; setHasMore(totalFetched >= LIMIT); setOffset(totalFetched); } catch (err) { setError(err instanceof Error ? err.message : "Unknown error"); } finally { setLoading(false); } } fetchData(); }, [handle, targetUrl]); const loadMore = useCallback(async () => { if (!resolvedDid) return; setLoadingMore(true); setLoadMoreError(null); try { const decodedUrl = decodeURIComponent(targetUrl); const data = await getUserTargetItems( resolvedDid, decodedUrl, LIMIT, offset, ); const fetchedAnnotations = data.annotations || []; const fetchedHighlights = data.highlights || []; setAnnotations((prev) => [...prev, ...fetchedAnnotations]); setHighlights((prev) => [...prev, ...fetchedHighlights]); const totalFetched = fetchedAnnotations.length + fetchedHighlights.length; setHasMore(totalFetched >= LIMIT); setOffset((prev) => prev + totalFetched); } catch (err) { console.error("Failed to load more:", err); const msg = err instanceof Error ? err.message : "Something went wrong"; setLoadMoreError(msg); setTimeout(() => setLoadMoreError(null), 5000); } finally { setLoadingMore(false); } }, [resolvedDid, targetUrl, offset]); const displayName = profile?.displayName || profile?.handle || handle; const displayHandle = profile?.handle || (handle?.startsWith("did:") ? null : handle); const totalItems = annotations.length + highlights.length; const decodedTargetUrl = decodeURIComponent(targetUrl); const items = [ ...(activeTab === "all" || activeTab === "annotations" ? annotations : []), ...(activeTab === "all" || activeTab === "highlights" ? highlights : []), ]; if (activeTab === "all") { items.sort((a, b) => { const dateA = new Date(a.createdAt).getTime(); const dateB = new Date(b.createdAt).getTime(); return dateB - dateA; }); } if (!targetUrl) { return ( } title="No URL specified" message="Please provide a URL to view annotations." /> ); } return (

{displayName}

{displayHandle && (

@{displayHandle}

)}
{loading && (

Loading annotations...

)} {error && (

{error}

)} {!loading && !error && totalItems === 0 && ( } title="No items found" message={`${displayName} hasn't annotated this page yet.`} /> )} {!loading && !error && totalItems > 0 && (
setActiveTab(id as "all" | "annotations" | "highlights") } />
{activeTab === "annotations" && annotations.length === 0 && ( } title="No annotations" message={`${displayName} hasn't annotated this page yet.`} /> )} {activeTab === "highlights" && highlights.length === 0 && ( } title="No highlights" message={`${displayName} hasn't highlighted this page yet.`} /> )} {items.map((item) => ( ))}
{hasMore && (
{loadMoreError && (

Failed to load more: {loadMoreError}

)}
)}
)}
); }