Skip to content

Commit c2cc61d

Browse files
feat: add skills lazy loading
1 parent 7a8a3f7 commit c2cc61d

File tree

1 file changed

+37
-19
lines changed

1 file changed

+37
-19
lines changed

src/routes/skills/index.tsx

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createFileRoute, Link } from '@tanstack/react-router'
22
import { useAction, useQuery } from 'convex/react'
3-
import { useEffect, useMemo, useRef, useState } from 'react'
3+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { api } from '../../../convex/_generated/api'
55
import type { Doc } from '../../../convex/_generated/dataModel'
66
import { 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

Comments
 (0)