Scrapboard.org client
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}