JFIFxxC      C  " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr{ gilour
import React, {ReactNode, useEffect, useRef, useState} from 'react'; import clsx from 'clsx'; import {UseInfiniteQueryResult} from '@tanstack/react-query/src/types'; import {Trans} from '@common/i18n/trans'; import {Button} from '@common/ui/buttons/button'; import {AnimatePresence, m} from 'framer-motion'; import {opacityAnimation} from '@common/ui/animation/opacity-animation'; import {ProgressCircle} from '@common/ui/progress/progress-circle'; export interface InfiniteScrollSentinelProps { loaderMarginTop?: string; children?: ReactNode; loadMoreExtraContent?: ReactNode; query: UseInfiniteQueryResult; style?: React.CSSProperties; className?: string; variant?: 'infiniteScroll' | 'loadMore'; size?: 'sm' | 'md'; } export function InfiniteScrollSentinel({ query: {isInitialLoading, fetchNextPage, isFetchingNextPage, hasNextPage}, children, loaderMarginTop = 'mt-24', style, className, variant: _variant = 'infiniteScroll', loadMoreExtraContent, size = 'md', }: InfiniteScrollSentinelProps) { const sentinelRef = useRef<HTMLDivElement>(null); const isLoading = isFetchingNextPage || isInitialLoading; const [loadMoreClickCount, setLoadMoreClickCount] = useState(0); const innerVariant = _variant === 'loadMore' && loadMoreClickCount < 3 ? 'loadMore' : 'infiniteScroll'; useEffect(() => { const sentinelEl = sentinelRef.current; if (!sentinelEl || innerVariant === 'loadMore') return; const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting && hasNextPage && !isLoading) { fetchNextPage(); } }); observer.observe(sentinelEl); return () => { observer.unobserve(sentinelEl); }; }, [fetchNextPage, hasNextPage, isLoading, innerVariant]); let content: ReactNode; if (children) { // children might already be wrapped in AnimatePresence, so only wrap default loader with it content = isFetchingNextPage ? children : null; } else if (innerVariant === 'loadMore') { content = !isInitialLoading && hasNextPage && ( <div className={clsx('flex items-center gap-8', loaderMarginTop)}> {loadMoreExtraContent} <Button size={size === 'md' ? 'sm' : 'xs'} className={clsx( size === 'sm' ? 'min-h-24 min-w-96' : 'min-h-36 min-w-112' )} variant="outline" color="primary" onClick={() => { fetchNextPage(); setLoadMoreClickCount(loadMoreClickCount + 1); }} disabled={isLoading} > {loadMoreClickCount >= 2 && !isFetchingNextPage ? ( <Trans message="Load all" /> ) : ( <Trans message="Show more" /> )} </Button> </div> ); } else { content = ( <AnimatePresence> {isFetchingNextPage && ( <m.div className={clsx('flex justify-center w-full', loaderMarginTop)} {...opacityAnimation} > <ProgressCircle size={size} isIndeterminate aria-label="loading" /> </m.div> )} </AnimatePresence> ); } return ( <div style={style} className={clsx('w-full', className, hasNextPage && 'min-h-36')} role="presentation" > <div ref={sentinelRef} aria-hidden /> {content} </div> ); }