"use client"; import * as React from "react"; import { useInfiniteScroll } from "@/app/(dashboard)/_hooks/use-infinite-scroll"; import { ContentCard } from "@/components/content-card"; import { Spinner } from "@/components/ui/spinner"; import { useAudio } from "@/providers/audio-provider"; import type { Content, PaginatedResponse } from "@/types/content"; interface ContentListProps { fetchFn: (params: { limit: number; offset: number; }) => Promise>; title?: string; } export function ContentList({ fetchFn, title }: ContentListProps) { const { setActiveVideo } = useAudio(); const [contents, setContents] = React.useState([]); const [loading, setLoading] = React.useState(true); const [offset, setOffset] = React.useState(0); const [hasMore, setHasMore] = React.useState(true); const containerRef = React.useRef(null); // biome-ignore lint/correctness/useExhaustiveDependencies: On a besoin de contents pour ré-attacher l'observer quand la liste change React.useEffect(() => { const observer = new IntersectionObserver( (entries) => { // On cherche l'entrée avec le plus grand ratio d'intersection parmi celles qui dépassent le seuil let bestEntry: IntersectionObserverEntry | null = null; let maxRatio = 0; for (const entry of entries) { if (entry.isIntersecting && entry.intersectionRatio > maxRatio) { maxRatio = entry.intersectionRatio; bestEntry = entry; } } if (bestEntry && maxRatio >= 0.6) { const contentId = bestEntry.target.getAttribute("data-content-id"); if (contentId) { setActiveVideo(contentId); } } }, { threshold: [0, 0.2, 0.4, 0.6, 0.8, 1.0], // Plusieurs seuils pour une détection plus fine du "meilleur" root: containerRef.current, }, ); const elements = containerRef.current?.querySelectorAll("[data-content-id]"); elements?.forEach((el) => { observer.observe(el); }); return () => observer.disconnect(); }, [setActiveVideo, contents]); const fetchInitial = React.useCallback(async () => { setLoading(true); try { const response = await fetchFn({ limit: 10, offset: 0, }); setContents(response.data); setOffset(0); setHasMore(response.data.length === 10); } catch (error) { console.error("Failed to fetch contents:", error); } finally { setLoading(false); } }, [fetchFn]); React.useEffect(() => { fetchInitial(); }, [fetchInitial]); const loadMore = React.useCallback(async () => { if (!hasMore || loading) return; setLoading(true); try { const response = await fetchFn({ limit: 10, offset: offset + 10, }); setContents((prev) => { const newIds = new Set(response.data.map((item) => item.id)); const filteredPrev = prev.filter((item) => !newIds.has(item.id)); return [...filteredPrev, ...response.data]; }); setOffset((prev) => prev + 10); setHasMore(response.data.length === 10); } catch (error) { console.error("Failed to load more contents:", error); } finally { setLoading(false); } }, [offset, hasMore, loading, fetchFn]); const { loaderRef } = useInfiniteScroll({ hasMore, loading, onLoadMore: loadMore, }); return (
{title &&

{title}

}
{contents.map((content) => (
))}
{loading && } {!hasMore && contents.length > 0 && (

Vous avez atteint la fin ! 🐐

)} {!loading && contents.length === 0 && (

Aucun mème trouvé ici... pour l'instant !

)}
); }