import { useStore } from "@nanostores/react"; import { AlertTriangle, Check, Copy, ExternalLink, Globe, Highlighter, Loader2, PenTool, Search, User, Users, } from "lucide-react"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { getByTarget } from "../../api/client"; import Card from "../../components/common/Card"; import { Button, EmptyState, Input, Tabs } from "../../components/ui"; import { $user } from "../../store/auth"; import type { AnnotationItem } from "../../types"; export default function UrlPage() { const params = useParams(); const navigate = useNavigate(); const urlPath = params["*"]; const targetUrl = urlPath ? decodeURIComponent(urlPath) : ""; 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 [copied, setCopied] = useState(false); const user = useStore($user); const LIMIT = 50; const loadMoreTimerRef = useRef | null>(null); useEffect(() => { return () => { if (loadMoreTimerRef.current) clearTimeout(loadMoreTimerRef.current); }; }, []); useEffect(() => { async function fetchData() { if (!targetUrl) { setLoading(false); return; } try { setLoading(true); setError(null); const data = await getByTarget(targetUrl, 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 : "Failed to load data"); } finally { setLoading(false); } } fetchData(); }, [targetUrl]); const loadMore = useCallback(async () => { setLoadingMore(true); setLoadMoreError(null); try { const data = await getByTarget(targetUrl, 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); if (loadMoreTimerRef.current) clearTimeout(loadMoreTimerRef.current); loadMoreTimerRef.current = setTimeout(() => setLoadMoreError(null), 5000); } finally { setLoadingMore(false); } }, [targetUrl, offset]); const handleCopyLink = useCallback(async () => { try { await navigator.clipboard.writeText(window.location.href); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error("Failed to copy link:", err); } }, []); const handleNavigateMyAnnotations = useCallback(async () => { if (!user?.handle || !targetUrl) return; navigate(`/${user.handle}/url/${encodeURIComponent(targetUrl)}`); }, [user?.handle, targetUrl, navigate]); const totalItems = annotations.length + highlights.length; const uniqueAuthors = new Map< string, { did: string; handle?: string; displayName?: string; avatar?: string } >(); [...annotations, ...highlights].forEach((item) => { const author = item.author || item.creator; if (author?.did && !uniqueAuthors.has(author.did)) { uniqueAuthors.set(author.did, author); } }); const authorCount = uniqueAuthors.size; const hostname = (() => { try { return new URL(targetUrl).hostname; } catch { return targetUrl; } })(); const favicon = targetUrl ? `https://www.google.com/s2/favicons?domain=${hostname}&sz=32` : null; if (!targetUrl) { return (

URL Annotations

Enter a URL to see all public annotations and highlights from the Margin community.

{ e.preventDefault(); const formData = new FormData(e.currentTarget); const q = (formData.get("q") as string)?.trim(); if (q) { const encoded = encodeURIComponent(q); navigate(`/url/${encoded}`); } }} className="max-w-md mx-auto flex gap-2" >
); } 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; }); } return (
{favicon && ( { (e.target as HTMLImageElement).style.display = "none"; }} /> )}

{hostname}

{targetUrl}
{user && ( )}
{!loading && totalItems > 0 && (
{authorCount} contributor{authorCount !== 1 ? "s" : ""}
)}
{loading && (

Loading annotations...

)} {error && (

{error}

)} {!loading && !error && totalItems === 0 && ( } title="No annotations yet" message="Nobody has annotated this page yet. Be the first — install the Margin extension and start annotating!" /> )} {!loading && !error && totalItems > 0 && (
setActiveTab(id as "all" | "annotations" | "highlights") } />
{activeTab === "annotations" && annotations.length === 0 && ( } title="No annotations" message="There are no annotations for this page yet." /> )} {activeTab === "highlights" && highlights.length === 0 && ( } title="No highlights" message="There are no highlights for this page yet." /> )} {items.map((item) => ( ))}
{hasMore && (
{loadMoreError && (

Failed to load more: {loadMoreError}

)}
)}
)}
); }