import React, { useState, useEffect, useCallback, useRef } from "react"; import { Loader2, ExternalLink, Compass, Tag } from "lucide-react"; import { useStore } from "@nanostores/react"; import { clsx } from "clsx"; import { getDocuments, getRecommendations } from "../../api/client"; import type { DocumentItem } from "../../api/client"; import { Tabs, EmptyState } from "../../components/ui"; import LayoutToggle from "../../components/ui/LayoutToggle"; import { $user } from "../../store/auth"; import { $feedLayout } from "../../store/feedLayout"; import { formatDistanceToNow } from "date-fns"; export default function Discover() { const user = useStore($user); const layout = useStore($feedLayout); const [activeTab, setActiveTab] = useState("new"); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [hasMore, setHasMore] = useState(false); const [offset, setOffset] = useState(0); const [recommendationsUnavailable, setRecommendationsUnavailable] = useState(false); const fetchIdRef = useRef(0); const limit = 30; const tabs = [ { id: "new", label: "New" }, { id: "popular", label: "Popular" }, ...(user ? [{ id: "recommended", label: "For You" }] : []), ]; const fetchItems = useCallback( async (tab: string, newOffset = 0, append = false) => { const id = ++fetchIdRef.current; setLoading(true); let data: { items: DocumentItem[]; totalItems: number }; if (tab === "recommended") { const res = await getRecommendations(limit); if ("unavailable" in res && res.unavailable) { setRecommendationsUnavailable(true); setLoading(false); return; } setRecommendationsUnavailable(false); data = res; } else { data = await getDocuments({ sort: tab, limit, offset: newOffset }); } if (id !== fetchIdRef.current) return; setItems((prev) => (append ? [...prev, ...data.items] : data.items)); setHasMore( tab !== "recommended" && newOffset + data.items.length < data.totalItems, ); setOffset(newOffset + data.items.length); setLoading(false); }, [limit], ); useEffect(() => { queueMicrotask(() => fetchItems(activeTab, 0)); }, [activeTab, fetchItems]); const handleTabChange = (id: string) => { if (id === activeTab) return; setActiveTab(id); window.scrollTo({ top: 0, behavior: "smooth" }); }; const loadMore = () => { fetchItems(activeTab, offset, true); }; return (
{loading && items.length === 0 ? (
) : activeTab === "recommended" && recommendationsUnavailable ? ( } title="Coming soon" message="Personalized recommendations aren't available on this server yet." /> ) : items.length === 0 ? ( } title="Nothing here yet" message={ activeTab === "recommended" ? "Start annotating and highlighting to get personalized recommendations." : "No documents have been discovered yet. Check back soon!" } /> ) : (
{items.map((doc) => (
))} {loading && (
)} {hasMore && !loading && ( )}
)}
); } function DocumentCard({ doc }: { doc: DocumentItem }) { const [ogData, setOgData] = useState<{ title?: string; description?: string; image?: string; icon?: string; } | null>(() => { try { const cached = sessionStorage.getItem(`og:${doc.canonicalUrl}`); return cached ? JSON.parse(cached) : null; } catch { return null; } }); useEffect(() => { if (!doc.canonicalUrl || ogData) return; fetch(`/api/url-metadata?url=${encodeURIComponent(doc.canonicalUrl)}`) .then((res) => (res.ok ? res.json() : null)) .then((data) => { if (data) { setOgData(data); try { sessionStorage.setItem( `og:${doc.canonicalUrl}`, JSON.stringify(data), ); } catch { /* quota exceeded */ } } }) .catch(() => {}); }, [doc.canonicalUrl, ogData]); const displayUrl = doc.canonicalUrl .replace(/^https?:\/\//, "") .replace(/\/$/, ""); const hostname = (() => { try { return new URL(doc.canonicalUrl).hostname; } catch { return null; } })(); return ( {ogData?.image && (
(e.currentTarget.style.display = "none")} />
)}

{doc.title || displayUrl}

{doc.description && (

{doc.description}

)}
{ogData?.icon ? ( (e.currentTarget.style.display = "none")} /> ) : ( )} {hostname || displayUrl} {doc.publishedAt && ( {formatDistanceToNow(new Date(doc.publishedAt), { addSuffix: true, })} )}
{doc.tags && doc.tags.length > 0 && (
{doc.tags.slice(0, 5).map((tag) => ( {tag} ))}
)}
); }