11import { createFileRoute , Link } from '@tanstack/react-router'
22import { useAction , useQuery } from 'convex/react'
3- import { useEffect , useMemo , useRef , useState } from 'react'
3+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
44import { api } from '../../../convex/_generated/api'
55import type { Doc } from '../../../convex/_generated/dataModel'
66import { SkillCard } from '../../components/SkillCard'
@@ -54,6 +54,7 @@ export function SkillsIndex() {
5454 const [ searchLimit , setSearchLimit ] = useState ( pageSize )
5555 const [ isSearching , setIsSearching ] = useState ( false )
5656 const searchRequest = useRef ( 0 )
57+ const loadMoreRef = useRef < HTMLDivElement | null > ( null )
5758
5859 const trimmedQuery = useMemo ( ( ) => query . trim ( ) , [ query ] )
5960 const hasQuery = trimmedQuery . length > 0
@@ -172,7 +173,6 @@ export function SkillsIndex() {
172173 return results
173174 } , [ dir , filtered , sort ] )
174175
175- const showing = sorted . length
176176 const isLoadingSkills = hasQuery
177177 ? isSearching && searchResults . length === 0
178178 : isLoadingList
@@ -182,6 +182,32 @@ export function SkillsIndex() {
182182 const isLoadingMore = hasQuery
183183 ? isSearching && searchResults . length > 0
184184 : listPage === undefined && pages . length > 0
185+ const canAutoLoad = typeof IntersectionObserver !== 'undefined'
186+
187+ const loadMore = useCallback ( ( ) => {
188+ if ( isLoadingMore || ! canLoadMore ) return
189+ if ( hasQuery ) {
190+ setSearchLimit ( ( value ) => value + pageSize )
191+ } else if ( nextCursor ) {
192+ setCursor ( nextCursor )
193+ }
194+ } , [ canLoadMore , hasQuery , isLoadingMore , nextCursor ] )
195+
196+ useEffect ( ( ) => {
197+ if ( ! canLoadMore || typeof IntersectionObserver === 'undefined' ) return
198+ const target = loadMoreRef . current
199+ if ( ! target ) return
200+ const observer = new IntersectionObserver (
201+ ( entries ) => {
202+ if ( entries . some ( ( entry ) => entry . isIntersecting ) ) {
203+ loadMore ( )
204+ }
205+ } ,
206+ { rootMargin : '200px' } ,
207+ )
208+ observer . observe ( target )
209+ return ( ) => observer . disconnect ( )
210+ } , [ canLoadMore , loadMore ] )
185211
186212 return (
187213 < main className = "section" >
@@ -193,9 +219,7 @@ export function SkillsIndex() {
193219 < p className = "section-subtitle" style = { { marginBottom : 0 } } >
194220 { isLoadingSkills
195221 ? 'Loading skills…'
196- : `${ showing } skill${ showing === 1 ? '' : 's' } ${
197- highlightedOnly ? ' (highlighted)' : ''
198- } .`}
222+ : `Browse the skill library${ highlightedOnly ? ' (highlighted)' : '' } .` }
199223 </ p >
200224 </ div >
201225 < div className = "skills-toolbar" >
@@ -364,23 +388,17 @@ export function SkillsIndex() {
364388
365389 { canLoadMore ? (
366390 < div
391+ ref = { canAutoLoad ? loadMoreRef : null }
367392 className = "card"
368393 style = { { marginTop : 16 , display : 'flex' , justifyContent : 'center' } }
369394 >
370- < button
371- className = "btn"
372- type = "button"
373- disabled = { isLoadingMore }
374- onClick = { ( ) => {
375- if ( hasQuery ) {
376- setSearchLimit ( ( value ) => value + pageSize )
377- } else if ( nextCursor ) {
378- setCursor ( nextCursor )
379- }
380- } }
381- >
382- { isLoadingMore ? 'Loading…' : 'Load more' }
383- </ button >
395+ { canAutoLoad ? (
396+ isLoadingMore ? 'Loading more…' : 'Scroll to load more'
397+ ) : (
398+ < button className = "btn" type = "button" onClick = { loadMore } disabled = { isLoadingMore } >
399+ { isLoadingMore ? 'Loading…' : 'Load more' }
400+ </ button >
401+ ) }
384402 </ div >
385403 ) : null }
386404 </ main >
0 commit comments