port sidebar and modal components to useInfiniteLoader

This commit is contained in:
Justin Edmund 2025-12-20 15:20:20 -08:00
parent 133cd9ec5b
commit fbc9f339be
4 changed files with 85 additions and 78 deletions

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
import { onDestroy } from 'svelte'
import { collectionQueries } from '$lib/api/queries/collection.queries'
import {
searchQueries,
@ -26,7 +27,7 @@
import SelectableWeaponRow from './SelectableWeaponRow.svelte'
import SelectableSummonCard from './SelectableSummonCard.svelte'
import SelectableSummonRow from './SelectableSummonRow.svelte'
import { IsInViewport } from 'runed'
import { useInfiniteLoader } from '$lib/stores/loaderState.svelte'
import { viewMode, type ViewMode } from '$lib/stores/viewMode.svelte'
type SearchResultItem = SearchPageResult['results'][number]
@ -153,22 +154,18 @@
: addSummonMutation
)
// Infinite scroll
const inViewport = new IsInViewport(() => sentinelEl, {
rootMargin: '200px'
// State-gated infinite scroll
const loader = useInfiniteLoader(() => searchResults, () => sentinelEl, { rootMargin: '200px' })
// Reset loader when filters or showOnlySelected changes
$effect(() => {
void searchFilters
void showOnlySelected
loader.reset()
})
$effect(() => {
if (
inViewport.current &&
searchResults.hasNextPage &&
!searchResults.isFetchingNextPage &&
!searchResults.isLoading &&
!showOnlySelected
) {
searchResults.fetchNextPage()
}
})
// Cleanup on destroy
onDestroy(() => loader.destroy())
// Reset state when modal closes or entity type changes
$effect(() => {
@ -438,9 +435,11 @@
{/if}
{#if displayedResults.length > 0}
{#if !showOnlySelected && searchResults.hasNextPage}
<div class="load-more-sentinel" bind:this={sentinelEl}></div>
{/if}
<div
class="load-more-sentinel"
bind:this={sentinelEl}
class:hidden={showOnlySelected || !searchResults.hasNextPage}
></div>
{#if searchResults.isFetchingNextPage}
<div class="loading-more">
@ -569,6 +568,10 @@
.load-more-sentinel {
height: 1px;
margin-top: $unit;
&.hidden {
display: none;
}
}
.loading-more {

View file

@ -11,8 +11,9 @@
import type { CollectionArtifact } from '$lib/types/api/artifact'
import type { Character } from '$lib/types/api/entities'
import { createInfiniteQuery } from '@tanstack/svelte-query'
import { onDestroy } from 'svelte'
import { artifactQueries } from '$lib/api/queries/artifact.queries'
import { IsInViewport } from 'runed'
import { useInfiniteLoader } from '$lib/stores/loaderState.svelte'
import { getArtifactImage } from '$lib/utils/images'
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
@ -56,25 +57,20 @@
return collectionQuery.data.pages.flatMap((page) => page.results ?? [])
})
// Infinite scroll
const inViewport = new IsInViewport(() => sentinelEl, {
rootMargin: '200px'
// State-gated infinite scroll
const loader = useInfiniteLoader(() => collectionQuery, () => sentinelEl, { rootMargin: '200px' })
// Reset loader when filters change
$effect(() => {
void queryFilters
loader.reset()
})
$effect(() => {
if (
inViewport.current &&
collectionQuery.hasNextPage &&
!collectionQuery.isFetchingNextPage &&
!collectionQuery.isLoading
) {
collectionQuery.fetchNextPage()
}
})
// Cleanup on destroy
onDestroy(() => loader.destroy())
const isLoading = $derived(collectionQuery.isLoading)
const isEmpty = $derived(!isLoading && allArtifacts.length === 0)
const showSentinel = $derived(collectionQuery.hasNextPage && !collectionQuery.isFetchingNextPage)
// Get display name for artifact
function getDisplayName(artifact: CollectionArtifact): string {
@ -168,9 +164,11 @@
</button>
{/each}
{#if showSentinel}
<div class="load-more-sentinel" bind:this={sentinelEl}></div>
{/if}
<div
class="load-more-sentinel"
bind:this={sentinelEl}
class:hidden={!collectionQuery.hasNextPage}
></div>
{#if collectionQuery.isFetchingNextPage}
<div class="loading-more">
@ -347,6 +345,10 @@
.load-more-sentinel {
height: 1px;
&.hidden {
display: none;
}
}
.loading-more {

View file

@ -2,6 +2,7 @@
<script lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'
import { onDestroy } from 'svelte'
import type { Job, JobSkill } from '$lib/types/api/entities'
import type { JobSkillList } from '$lib/types/api/party'
import { jobQueries } from '$lib/api/queries/job.queries'
@ -10,7 +11,7 @@
import Input from '../ui/Input.svelte'
import Select from '../ui/Select.svelte'
import Icon from '../Icon.svelte'
import { IsInViewport } from 'runed'
import { useInfiniteLoader } from '$lib/stores/loaderState.svelte'
import * as m from '$lib/paraglide/messages'
interface Props {
@ -101,28 +102,21 @@
// Sentinel element for intersection observation
let sentinelEl = $state<HTMLElement>()
// Use runed's IsInViewport for viewport detection
const inViewport = new IsInViewport(() => sentinelEl, {
rootMargin: '200px'
// State-gated infinite scroll
const loader = useInfiniteLoader(() => skillsQuery, () => sentinelEl, { rootMargin: '200px' })
// Reset loader when filters change
$effect(() => {
void debouncedSearchQuery
void skillCategory
loader.reset()
})
// Auto-fetch next page when sentinel is visible
$effect(() => {
if (
inViewport.current &&
skillsQuery.hasNextPage &&
!skillsQuery.isFetchingNextPage &&
!skillsQuery.isLoading
) {
skillsQuery.fetchNextPage()
}
})
// Cleanup on destroy
onDestroy(() => loader.destroy())
// Computed states
const isEmpty = $derived(skills.length === 0 && !skillsQuery.isLoading && !skillsQuery.isError)
const showSentinel = $derived(
!skillsQuery.isLoading && skillsQuery.hasNextPage && skills.length > 0
)
function handleSelectSkill(skill: JobSkill) {
// Clear any previous errors
@ -258,9 +252,11 @@
</div>
{/if}
{#if showSentinel}
<div class="load-more-sentinel" bind:this={sentinelEl}></div>
{/if}
<div
class="load-more-sentinel"
bind:this={sentinelEl}
class:hidden={!skillsQuery.hasNextPage}
></div>
{#if skillsQuery.isFetchingNextPage}
<div class="loading-more">
@ -421,6 +417,10 @@
.load-more-sentinel {
height: 1px;
margin-top: $unit;
&.hidden {
display: none;
}
}
.loading-more {

View file

@ -2,13 +2,14 @@
<script lang="ts">
import { createInfiniteQuery, createQuery } from '@tanstack/svelte-query'
import { onDestroy } from 'svelte'
import type { SearchResult } from '$lib/api/adapters/search.adapter'
import { searchQueries, type SearchFilters } from '$lib/api/queries/search.queries'
import { collectionQueries } from '$lib/api/queries/collection.queries'
import Button from '../ui/Button.svelte'
import Icon from '../Icon.svelte'
import CharacterTags from '$lib/components/tags/CharacterTags.svelte'
import { IsInViewport } from 'runed'
import { useInfiniteLoader } from '$lib/stores/loaderState.svelte'
import { getCharacterImage, getWeaponImage, getSummonImage } from '$lib/features/database/detail/image'
import type { AddItemResult, SearchMode } from '$lib/types/api/search'
import type { CollectionCharacter, CollectionWeapon, CollectionSummon } from '$lib/types/api/collection'
@ -245,35 +246,30 @@
return deduped as AddItemResult[]
})
// Use runed's IsInViewport for viewport detection
const inViewport = new IsInViewport(() => sentinelEl, {
rootMargin: '200px'
})
// Get the active query based on search mode
const activeQuery = $derived(
searchMode === 'collection' && authUserId ? collectionQueryResult : searchQueryResult
)
// Auto-fetch next page when sentinel is visible
// State-gated infinite scroll
// Type assertion needed because activeQuery is a union of different query types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loader = useInfiniteLoader(() => activeQuery as any, () => sentinelEl, { rootMargin: '200px' })
// Reset loader when search mode or filters change
$effect(() => {
if (
inViewport.current &&
activeQuery.hasNextPage &&
!activeQuery.isFetchingNextPage &&
!activeQuery.isLoading
) {
activeQuery.fetchNextPage()
}
void searchMode
void filters
loader.reset()
})
// Cleanup on destroy
onDestroy(() => loader.destroy())
// Computed states
const isEmpty = $derived(
searchResults.length === 0 && !activeQuery.isLoading && !activeQuery.isError
)
const showSentinel = $derived(
!activeQuery.isLoading && activeQuery.hasNextPage && searchResults.length > 0
)
// Focus search input on mount
$effect(() => {
@ -477,9 +473,11 @@
{/each}
</ul>
{#if showSentinel}
<div class="load-more-sentinel" bind:this={sentinelEl}></div>
{/if}
<div
class="load-more-sentinel"
bind:this={sentinelEl}
class:hidden={!activeQuery.hasNextPage}
></div>
{#if activeQuery.isFetchingNextPage}
<div class="loading-more">
@ -771,6 +769,10 @@
.load-more-sentinel {
height: 1px;
margin-top: $unit;
&.hidden {
display: none;
}
}
.loading-more {