"use client" import * as React from "react" import { Button } from "@/components/ui/button" import type { PageData } from "@/lib/api/types" type LoadMoreLabels = { loadMore: string noMore: string loading?: string error?: string } type LoadMoreRequest = { cursor: string force: boolean } export function LoadMore({ initialItems = [], initialCursor, initialHasMore, initialLoad = false, resetKey, labels, loadPage, renderItems, renderEmpty, alwaysShowButton = false, autoLoadOnScroll = false, }: { initialItems?: T[] | null initialCursor?: string initialHasMore: boolean initialLoad?: boolean resetKey?: string labels: LoadMoreLabels loadPage: (request: LoadMoreRequest) => Promise> renderItems: (items: T[]) => React.ReactNode renderEmpty?: () => React.ReactNode alwaysShowButton?: boolean autoLoadOnScroll?: boolean }) { const safeInitialItems = Array.isArray(initialItems) ? initialItems : [] const contentKey = React.useMemo( () => resetKey || JSON.stringify({ initialCursor: initialCursor || "", initialHasMore, initialLoad, initialItemsLength: safeInitialItems.length, }), [initialCursor, initialHasMore, initialLoad, resetKey, safeInitialItems.length] ) return ( ) } function LoadMoreContent({ initialItems, initialCursor, initialHasMore, initialLoad, labels, loadPage, renderItems, renderEmpty, alwaysShowButton, autoLoadOnScroll, }: { initialItems: T[] initialCursor?: string initialHasMore: boolean initialLoad: boolean labels: LoadMoreLabels loadPage: (request: LoadMoreRequest) => Promise> renderItems: (items: T[]) => React.ReactNode renderEmpty?: () => React.ReactNode alwaysShowButton: boolean autoLoadOnScroll: boolean }) { const [cursor, setCursor] = React.useState(initialCursor || "") const [hasMore, setHasMore] = React.useState(initialHasMore || (initialLoad && initialItems.length === 0)) const [items, setItems] = React.useState(initialItems) const [loaded, setLoaded] = React.useState(!initialLoad || initialItems.length > 0) const [loading, setLoading] = React.useState(false) const [error, setError] = React.useState(null) const inFlightRef = React.useRef(false) const mountedRef = React.useRef(true) const loadPageRef = React.useRef(loadPage) const sentinelRef = React.useRef(null) React.useEffect(() => { loadPageRef.current = loadPage }, [loadPage]) React.useEffect(() => { mountedRef.current = true return () => { mountedRef.current = false } }, []) const loadMore = React.useCallback( async (force = false) => { if (inFlightRef.current || (!force && !hasMore)) { return } inFlightRef.current = true setLoading(true) setError(null) try { const data = await loadPageRef.current({ cursor: force ? "" : cursor, force, }) if (!mountedRef.current) { return } setItems((current) => (force ? data.results || [] : [...current, ...(data.results || [])])) setCursor(data.cursor || "") setHasMore(Boolean(data.hasMore)) setLoaded(true) } catch (err) { if (mountedRef.current) { setError(err instanceof Error ? err.message : labels.error || "Could not load more items.") } } finally { inFlightRef.current = false if (mountedRef.current) { setLoading(false) } } }, [cursor, hasMore, labels.error] ) React.useEffect(() => { if (!initialLoad || initialItems.length > 0 || loaded || loading || inFlightRef.current) { return } const timer = window.setTimeout(() => { void loadMore(true) }, 0) return () => window.clearTimeout(timer) }, [initialItems.length, initialLoad, loadMore, loaded, loading]) React.useEffect(() => { if (!autoLoadOnScroll || !hasMore || loading) { return } const sentinel = sentinelRef.current if (!sentinel || typeof IntersectionObserver === "undefined") { return } const observer = new IntersectionObserver( (entries) => { if (entries[0]?.isIntersecting) { void loadMore() } }, { rootMargin: "160px 0px" } ) observer.observe(sentinel) return () => observer.disconnect() }, [autoLoadOnScroll, hasMore, loadMore, loading]) async function onLoadMore() { if (inFlightRef.current || !hasMore) { return } await loadMore() } const showButton = alwaysShowButton || items.length > 0 || hasMore || loading return ( <> {items.length ? renderItems(items) : loaded && !loading && !error ? renderEmpty?.() : null} {showButton ? ( ) : null} {autoLoadOnScroll ?