11import './CLNOffersList.scss' ;
2- import { useState } from 'react' ;
2+ import { useCallback , useEffect , useRef , useState } from 'react' ;
33import { motion , AnimatePresence } from 'framer-motion' ;
44import { Row , Col , Spinner , Alert } from 'react-bootstrap' ;
5+ import PerfectScrollbar from 'react-perfect-scrollbar' ;
56
67import { IncomingArrowSVG } from '../../../svgs/IncomingArrow' ;
78import Offer from '../CLNOffer/CLNOffer' ;
8- import { TRANSITION_DURATION } from '../../../utilities/constants' ;
9+ import { SCROLL_BATCH_SIZE , SCROLL_THRESHOLD , TRANSITION_DURATION } from '../../../utilities/constants' ;
910import { NoCLNTransactionLightSVG } from '../../../svgs/NoCLNTransactionLight' ;
1011import { NoCLNTransactionDarkSVG } from '../../../svgs/NoCLNTransactionDark' ;
1112import { useSelector } from 'react-redux' ;
@@ -14,7 +15,7 @@ import { selectIsAuthenticated, selectIsDarkMode } from '../../../store/rootSele
1415
1516const OfferHeader = ( { offer } ) => {
1617 return (
17- < Row data-testid = 'cln-offer-header' className = "offer-list-item d-flex justify-content-between align-items-center" >
18+ < Row className = "offer-list-item d-flex justify-content-between align-items-center" >
1819 < Col xs = { 2 } >
1920 < IncomingArrowSVG className = "me-1" txStatus = { offer . used ? 'used' : 'unused' } />
2021 </ Col >
@@ -57,6 +58,7 @@ const CLNOffersAccordion = ({
5758 return (
5859 < >
5960 < motion . div
61+ data-testid = 'cln-offer-header'
6062 className = { 'cln-offer-header ' + ( expanded [ i ] ? 'expanded' : '' ) }
6163 initial = { false }
6264 animate = { { backgroundColor : ( isDarkMode ? ( expanded [ i ] ? '#0C0C0F' : '#2A2A2C' ) : ( expanded [ i ] ? '#EBEFF9' : '#FFFFFF' ) ) } }
@@ -72,11 +74,11 @@ const CLNOffersAccordion = ({
7274 { expanded [ i ] && (
7375 < motion . div
7476 data-testid = 'cln-offer-details'
75- className = " cln-offer-details"
76- key = " content"
77- initial = " collapsed"
78- animate = " open"
79- exit = " collapsed"
77+ className = ' cln-offer-details'
78+ key = ' content'
79+ initial = ' collapsed'
80+ animate = ' open'
81+ exit = ' collapsed'
8082 variants = { {
8183 open : { opacity : 1 , height : 'auto' } ,
8284 collapsed : { opacity : 0 , height : 0 } ,
@@ -98,6 +100,68 @@ export const CLNOffersList = () => {
98100 const initExpansions = ( listOffers . offers ?. reduce ( ( acc : boolean [ ] ) => [ ...acc , false ] , [ ] ) || [ ] ) ;
99101 const [ expanded , setExpanded ] = useState < boolean [ ] > ( initExpansions ) ;
100102
103+ const [ displayedOffers , setDisplayedOffers ] = useState < any [ ] > ( [ ] ) ;
104+ const [ currentIndex , setCurrentIndex ] = useState ( 0 ) ;
105+ const [ isLoading , setIsLoading ] = useState ( false ) ;
106+ const [ allOffersLoaded , setAllOffersLoaded ] = useState ( false ) ;
107+ const containerRef = useRef < HTMLDivElement > ( null ) ;
108+
109+ const setContainerRef = useCallback ( ( ref : HTMLElement | null ) => {
110+ if ( ref ) {
111+ ( containerRef as React . MutableRefObject < HTMLElement | null > ) . current = ref ;
112+ }
113+ } , [ ] ) ;
114+
115+ useEffect ( ( ) => {
116+ if ( listOffers && listOffers . offers && listOffers . offers . length > 0 ) {
117+ const initialBatch = listOffers . offers . slice ( 0 , SCROLL_BATCH_SIZE ) ;
118+ setDisplayedOffers ( initialBatch ) ;
119+ setCurrentIndex ( SCROLL_BATCH_SIZE ) ;
120+ if ( SCROLL_BATCH_SIZE >= listOffers ?. offers ?. length ) {
121+ setAllOffersLoaded ( true ) ;
122+ }
123+ }
124+ } , [ listOffers ] ) ;
125+
126+ const loadMoreTransactions = useCallback ( ( ) => {
127+ if ( isLoading || allOffersLoaded ) return ;
128+ setIsLoading ( true ) ;
129+ setTimeout ( ( ) => {
130+ const nextIndex = currentIndex + SCROLL_BATCH_SIZE ;
131+ const newOffers = listOffers ?. offers ?. slice (
132+ currentIndex ,
133+ nextIndex
134+ ) || [ ] ;
135+ setDisplayedOffers ( prev => [ ...prev , ...newOffers ] ) ;
136+ setCurrentIndex ( nextIndex ) ;
137+
138+ if ( listOffers && listOffers . offers && nextIndex >= listOffers ?. offers ?. length ) {
139+ setAllOffersLoaded ( true ) ;
140+ }
141+
142+ setIsLoading ( false ) ;
143+ } , 300 ) ;
144+ } , [ currentIndex , isLoading , allOffersLoaded , listOffers ] ) ;
145+
146+ const handleScroll = useCallback ( ( container ) => {
147+ if ( ! container || isLoading || allOffersLoaded ) return ;
148+
149+ const { scrollTop, scrollHeight, clientHeight } = container ;
150+ const bottomOffset = scrollHeight - scrollTop - clientHeight ;
151+
152+ if ( bottomOffset < SCROLL_THRESHOLD ) {
153+ loadMoreTransactions ( ) ;
154+ }
155+ } , [ isLoading , allOffersLoaded , loadMoreTransactions ] ) ;
156+
157+ useEffect ( ( ) => {
158+ const container = containerRef . current ;
159+ if ( container ) {
160+ container ?. addEventListener ( 'scroll' , handleScroll ) ;
161+ return ( ) => container ?. removeEventListener ( 'scroll' , handleScroll ) ;
162+ }
163+ } , [ handleScroll ] ) ;
164+
101165 return (
102166 isAuthenticated && listOffers . isLoading ?
103167 < span className = 'h-100 d-flex justify-content-center align-items-center' >
@@ -107,13 +171,28 @@ export const CLNOffersList = () => {
107171 listOffers . error ?
108172 < Alert className = 'py-0 px-1 fs-7' variant = 'danger' data-testid = 'cln-offers-list-error' > { listOffers . error } </ Alert > :
109173 listOffers ?. offers && listOffers ?. offers . length && listOffers ?. offers . length > 0 ?
110- < div className = 'cln-offers-list' data-testid = 'cln-offers-list' >
111- {
112- listOffers ?. offers ?. map ( ( offer , i ) => (
113- < CLNOffersAccordion key = { i } i = { i } expanded = { expanded } setExpanded = { setExpanded } initExpansions = { initExpansions } offer = { offer } />
114- ) )
174+ < PerfectScrollbar
175+ containerRef = { setContainerRef }
176+ onScrollY = { handleScroll }
177+ className = 'cln-offers-list'
178+ data-testid = 'cln-offers-list'
179+ options = { {
180+ suppressScrollX : true ,
181+ wheelPropagation : false
182+ } }
183+ >
184+ { displayedOffers . map ( ( offer , i ) => (
185+ < CLNOffersAccordion key = { i } i = { i } expanded = { expanded } setExpanded = { setExpanded } initExpansions = { initExpansions } offer = { offer } />
186+ ) ) }
187+ { isLoading && (
188+ < Col xs = { 12 } className = 'd-flex align-items-center justify-content-center mb-5' >
189+ < Spinner animation = 'grow' variant = 'primary' />
190+ </ Col >
191+ ) }
192+ { allOffersLoaded && listOffers ?. offers . length > 100 &&
193+ < h6 className = 'd-flex align-self-center py-4 text-muted' > No more offers to load!</ h6 >
115194 }
116- </ div >
195+ </ PerfectScrollbar >
117196 :
118197 < Row className = 'text-light fs-6 h-75 mt-5 align-items-center justify-content-center' >
119198 < Row className = 'd-flex align-items-center justify-content-center mt-2' >
0 commit comments