Scrapboard.org client
at main 103 lines 2.7 kB view raw
1import React, { useEffect, useRef, useCallback } from "react"; 2import { LoaderCircle } from "lucide-react"; 3 4interface InfiniteScrollWrapperProps { 5 children: React.ReactNode; 6 hasMore?: boolean; 7 onLoadMore?: () => Promise<void>; 8 isLoadingMore?: boolean; 9 className?: string; 10 loadingMessage?: string; 11 endMessage?: string; 12 threshold?: number; 13 rootMargin?: string; 14 showEndMessage?: boolean; 15} 16 17export function InfiniteScrollWrapper({ 18 children, 19 hasMore = false, 20 onLoadMore, 21 isLoadingMore = false, 22 className = "", 23 loadingMessage = "Loading more posts...", 24 endMessage = "No more posts to load", 25 threshold = 0.1, 26 rootMargin = "100px", 27 showEndMessage = true, 28}: InfiniteScrollWrapperProps) { 29 const loadMoreRef = useRef<HTMLDivElement>(null); 30 const observerRef = useRef<IntersectionObserver | null>(null); 31 32 // Intersection Observer for infinite scroll 33 const handleObserver = useCallback( 34 (entries: IntersectionObserverEntry[]) => { 35 const [target] = entries; 36 if (target.isIntersecting && hasMore && onLoadMore && !isLoadingMore) { 37 onLoadMore(); 38 } 39 }, 40 [hasMore, onLoadMore, isLoadingMore] 41 ); 42 43 useEffect(() => { 44 const element = loadMoreRef.current; 45 if (!element || !onLoadMore) return; 46 47 // Disconnect existing observer 48 if (observerRef.current) { 49 observerRef.current.disconnect(); 50 } 51 52 // Create new observer 53 observerRef.current = new IntersectionObserver(handleObserver, { 54 threshold, 55 rootMargin, 56 }); 57 58 observerRef.current.observe(element); 59 60 return () => { 61 if (observerRef.current) { 62 observerRef.current.disconnect(); 63 } 64 }; 65 }, [ 66 handleObserver, 67 threshold, 68 rootMargin, 69 onLoadMore, 70 isLoadingMore, 71 hasMore, 72 ]); 73 74 return ( 75 <div className={className}> 76 {children} 77 78 {/* Infinite scroll trigger and loading states */} 79 {hasMore && ( 80 <div 81 ref={loadMoreRef} 82 className="flex items-center justify-center py-8" 83 > 84 {isLoadingMore ? ( 85 <div className="flex items-center gap-2 text-black/70 dark:text-white/70"> 86 <LoaderCircle className="animate-spin w-5 h-5" /> 87 <span className="text-sm">{loadingMessage}</span> 88 </div> 89 ) : ( 90 <div className="text-gray-400 text-sm">Scroll to load more</div> 91 )} 92 </div> 93 )} 94 95 {/* End of posts indicator */} 96 {!hasMore && showEndMessage && ( 97 <div className="flex items-center justify-center py-6"> 98 <p className="text-gray-400 text-sm">{endMessage}</p> 99 </div> 100 )} 101 </div> 102 ); 103}