import { Clock, Loader2 } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { type GetFeedParams, getFeed } from "../../api/client"; import Card from "../../components/common/Card"; import { EmptyState } from "../../components/ui"; import type { AnnotationItem } from "../../types"; const LIMIT = 50; const feedCache = new Map< string, { items: AnnotationItem[]; hasMore: boolean; offset: number; timestamp: number; } >(); export interface FeedItemsProps extends Omit< GetFeedParams, "limit" | "offset" > { layout: "list" | "mosaic"; emptyMessage: string; initialItems?: AnnotationItem[]; initialHasMore?: boolean; } export default function FeedItems({ creator, source, tag, type, motivation, emptyMessage, layout, initialItems, initialHasMore, }: FeedItemsProps) { const [items, setItems] = useState(initialItems || []); const [loading, setLoading] = useState(!initialItems); const [loadingMore, setLoadingMore] = useState(false); const [hasMore, setHasMore] = useState(initialHasMore ?? false); const [offset, setOffset] = useState(initialItems?.length ?? 0); const skipInitialFetch = useRef(!!initialItems); useEffect(() => { if (skipInitialFetch.current) { skipInitialFetch.current = false; return; } let cancelled = false; const cacheKey = JSON.stringify({ type, motivation, tag, creator, source }); const cached = feedCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) { setItems(cached.items); setHasMore(cached.hasMore); setOffset(cached.offset); setLoading(false); getFeed({ type, motivation, tag, creator, source, limit: LIMIT, offset: 0, }) .then((data) => { if (cancelled) return; const fetched = data.items; setItems(fetched); setHasMore(data.hasMore); setOffset(data.fetchedCount); feedCache.set(cacheKey, { items: fetched, hasMore: data.hasMore, offset: data.fetchedCount, timestamp: Date.now(), }); }) .catch(console.error); return () => { cancelled = true; }; } setLoading(true); getFeed({ type, motivation, tag, creator, source, limit: LIMIT, offset: 0 }) .then((data) => { if (cancelled) return; const fetched = data.items; setItems(fetched); setHasMore(data.hasMore); setOffset(data.fetchedCount); setLoading(false); feedCache.set(cacheKey, { items: fetched, hasMore: data.hasMore, offset: data.fetchedCount, timestamp: Date.now(), }); }) .catch((e) => { if (cancelled) return; console.error(e); setItems([]); setHasMore(false); setLoading(false); }); return () => { cancelled = true; }; }, [type, motivation, tag, creator, source]); const loadMore = useCallback(async () => { setLoadingMore(true); try { const cacheKey = JSON.stringify({ type, motivation, tag, creator, source, }); const data = await getFeed({ type, motivation, tag, creator, source, limit: LIMIT, offset, }); const fetched = data?.items || []; const newItems = [...items, ...fetched]; setItems(newItems); setHasMore(data.hasMore); const newOffset = offset + data.fetchedCount; setOffset(newOffset); feedCache.set(cacheKey, { items: newItems, hasMore: data.hasMore, offset: newOffset, timestamp: Date.now(), }); } catch (e) { console.error(e); } finally { setLoadingMore(false); } }, [type, motivation, tag, creator, source, offset, items]); const handleDelete = (uri: string) => { setItems((prev) => prev.filter((i) => i.uri !== uri)); }; if (loading) { return (

Loading...

); } if (items.length === 0) { return ( } title="Nothing here yet" message={emptyMessage} /> ); } const loadMoreButton = hasMore && (
); if (layout === "mosaic") { return ( <>
{items.map((item) => (
))}
{loadMoreButton} ); } return ( <>
{items.map((item) => ( ))}
{loadMoreButton} ); }