"use client" import { useCallback, useEffect, useRef } from "react" interface InfiniteScrollProps { hasMore: boolean onLoadMore: () => void rootMargin?: string className?: string loadingLabel?: string endLabel?: string totalLabel?: string } /** * Renders a sentinel that triggers `onLoadMore` whenever it scrolls into view. * The sentinel is also clickable (and acts as the "end" footer) for keyboard * users and as a visible affordance. */ export function InfiniteScrollSentinel({ hasMore, onLoadMore, rootMargin = "600px", className, loadingLabel = "Loading more…", endLabel = "End of list", totalLabel, }: InfiniteScrollProps) { const ref = useRef(null) const onLoadMoreRef = useRef(onLoadMore) useEffect(() => { onLoadMoreRef.current = onLoadMore }, [onLoadMore]) useEffect(() => { const node = ref.current if (!node || !hasMore) return const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { onLoadMoreRef.current() } } }, { rootMargin }, ) observer.observe(node) return () => observer.disconnect() }, [hasMore, rootMargin]) const handleClick = useCallback(() => { if (hasMore) onLoadMoreRef.current() }, [hasMore]) return (
{ if (hasMore && (e.key === "Enter" || e.key === " ")) { e.preventDefault() onLoadMoreRef.current() } }} className={ className ?? "mt-8 flex items-center justify-center gap-3 border-t border-[color:var(--border-soft)] py-6 font-mono text-[10.5px] uppercase tracking-[0.18em] text-[color:var(--fg-subtle)]" } style={{ cursor: hasMore ? "pointer" : "default" }} > {hasMore ? ( <> {loadingLabel} ) : ( <> {endLabel} {totalLabel && ( <> · {totalLabel} )} )}
) }