port collection pages to useInfiniteLoader

This commit is contained in:
Justin Edmund 2025-12-20 15:20:08 -08:00
parent 26299d5d10
commit 1933391d38
4 changed files with 68 additions and 70 deletions

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type { PageData } from './$types'
import type { CollectionArtifact } from '$lib/types/api/artifact'
import { getContext } from 'svelte'
import { getContext, onDestroy } from 'svelte'
import { createInfiniteQuery, createQuery } from '@tanstack/svelte-query'
import { artifactQueries } from '$lib/api/queries/artifact.queries'
import CollectionArtifactDetailPane from '$lib/components/collection/CollectionArtifactDetailPane.svelte'
@ -12,12 +12,12 @@
import Icon from '$lib/components/Icon.svelte'
import ViewModeToggle from '$lib/components/ui/ViewModeToggle.svelte'
import MultiSelect from '$lib/components/ui/MultiSelect.svelte'
import { IsInViewport } from 'runed'
import { sidebar } from '$lib/stores/sidebar.svelte'
import { viewMode, type ViewMode } from '$lib/stores/viewMode.svelte'
import Select from '$lib/components/ui/Select.svelte'
import { getArtifactImage } from '$lib/utils/images'
import { LOADED_IDS_KEY, type LoadedIdsContext } from '$lib/stores/selectionMode.svelte'
import { useInfiniteLoader } from '$lib/stores/loaderState.svelte'
const { data }: { data: PageData } = $props()
@ -130,6 +130,10 @@
return artifactQueries.collection(userId, filters)
})
// State-gated infinite scroll (inspired by svelte-infinite)
// Encapsulates intersection observer, state machine, and all reactive effects
const loader = useInfiniteLoader(() => collectionQuery, () => sentinelEl)
// Flatten all artifacts from pages
const allArtifacts = $derived.by((): CollectionArtifact[] => {
if (!collectionQuery.data?.pages) {
@ -144,25 +148,17 @@
loadedIdsContext?.setIds(ids)
})
// Infinite scroll
const inViewport = new IsInViewport(() => sentinelEl, {
rootMargin: '200px'
// Reset loader state 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)
// Current view mode from store
const currentViewMode = $derived(viewMode.collectionView)
@ -291,9 +287,12 @@
{/if}
{#if !isLoading && !isEmpty}
{#if showSentinel}
<div class="load-more-sentinel" bind:this={sentinelEl}></div>
{/if}
<!-- Sentinel always in DOM to avoid Svelte block tracking issues during rapid updates -->
<div
class="load-more-sentinel"
bind:this={sentinelEl}
class:hidden={!collectionQuery.hasNextPage}
></div>
{#if collectionQuery.isFetchingNextPage}
<div class="loading-more">
@ -301,7 +300,6 @@
<span>Loading more...</span>
</div>
{/if}
{/if}
</div>
</div>
@ -397,6 +395,10 @@
.load-more-sentinel {
height: 1px;
margin-top: $unit;
&.hidden {
display: none;
}
}
.loading-more {

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type { PageData } from './$types'
import type { CollectionCharacter, CollectionSortKey } from '$lib/types/api/collection'
import { getContext } from 'svelte'
import { getContext, onDestroy } from 'svelte'
import { createInfiniteQuery } from '@tanstack/svelte-query'
import { collectionQueries } from '$lib/api/queries/collection.queries'
import CollectionFilters, {
@ -13,10 +13,10 @@
import SelectableCollectionCard from '$lib/components/collection/SelectableCollectionCard.svelte'
import SelectableCollectionRow from '$lib/components/collection/SelectableCollectionRow.svelte'
import Icon from '$lib/components/Icon.svelte'
import { IsInViewport } from 'runed'
import { sidebar } from '$lib/stores/sidebar.svelte'
import { viewMode, type ViewMode } from '$lib/stores/viewMode.svelte'
import { LOADED_IDS_KEY, type LoadedIdsContext } from '$lib/stores/selectionMode.svelte'
import { useInfiniteLoader } from '$lib/stores/loaderState.svelte'
const { data }: { data: PageData } = $props()
@ -58,6 +58,10 @@
return collectionQueries.characters(userId, filters)
})
// State-gated infinite scroll (inspired by svelte-infinite)
// Encapsulates intersection observer, state machine, and all reactive effects
const loader = useInfiniteLoader(() => collectionQuery, () => sentinelEl)
// Flatten all characters from pages
const allCharacters = $derived.by((): CollectionCharacter[] => {
if (!collectionQuery.data?.pages) {
@ -72,21 +76,14 @@
loadedIdsContext?.setIds(ids)
})
// Infinite scroll
const inViewport = new IsInViewport(() => sentinelEl, {
rootMargin: '200px'
// Reset loader state 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 && allCharacters.length === 0)

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type { PageData } from './$types'
import type { CollectionSummon, CollectionSortKey } from '$lib/types/api/collection'
import { getContext } from 'svelte'
import { getContext, onDestroy } from 'svelte'
import { createInfiniteQuery } from '@tanstack/svelte-query'
import { collectionQueries } from '$lib/api/queries/collection.queries'
import CollectionFilters, {
@ -13,10 +13,10 @@
import SelectableCollectionCard from '$lib/components/collection/SelectableCollectionCard.svelte'
import SelectableCollectionRow from '$lib/components/collection/SelectableCollectionRow.svelte'
import Icon from '$lib/components/Icon.svelte'
import { IsInViewport } from 'runed'
import { sidebar } from '$lib/stores/sidebar.svelte'
import { viewMode, type ViewMode } from '$lib/stores/viewMode.svelte'
import { LOADED_IDS_KEY, type LoadedIdsContext } from '$lib/stores/selectionMode.svelte'
import { useInfiniteLoader } from '$lib/stores/loaderState.svelte'
const { data }: { data: PageData } = $props()
@ -52,6 +52,10 @@
return collectionQueries.summons(userId, filters)
})
// State-gated infinite scroll (inspired by svelte-infinite)
// Encapsulates intersection observer, state machine, and all reactive effects
const loader = useInfiniteLoader(() => collectionQuery, () => sentinelEl)
// Flatten all summons from pages
const allSummons = $derived.by((): CollectionSummon[] => {
if (!collectionQuery.data?.pages) {
@ -66,25 +70,17 @@
loadedIdsContext?.setIds(ids)
})
// Infinite scroll
const inViewport = new IsInViewport(() => sentinelEl, {
rootMargin: '200px'
// Reset loader state 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 && allSummons.length === 0)
const showSentinel = $derived(collectionQuery.hasNextPage && !collectionQuery.isFetchingNextPage)
// Current view mode from store
const currentViewMode = $derived(viewMode.collectionView)
@ -165,9 +161,12 @@
{/if}
{#if !isLoading && !isEmpty}
{#if showSentinel}
<div class="load-more-sentinel" bind:this={sentinelEl}></div>
{/if}
<!-- Sentinel always in DOM to avoid Svelte block tracking issues during rapid updates -->
<div
class="load-more-sentinel"
bind:this={sentinelEl}
class:hidden={!collectionQuery.hasNextPage}
></div>
{#if collectionQuery.isFetchingNextPage}
<div class="loading-more">
@ -175,7 +174,6 @@
<span>Loading more...</span>
</div>
{/if}
{/if}
</div>
</div>
@ -247,6 +245,10 @@
.load-more-sentinel {
height: 1px;
margin-top: $unit;
&.hidden {
display: none;
}
}
.loading-more {

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type { PageData } from './$types'
import type { CollectionWeapon, CollectionSortKey } from '$lib/types/api/collection'
import { getContext } from 'svelte'
import { getContext, onDestroy } from 'svelte'
import { createInfiniteQuery } from '@tanstack/svelte-query'
import { collectionQueries } from '$lib/api/queries/collection.queries'
import CollectionFilters, {
@ -13,10 +13,10 @@
import SelectableCollectionCard from '$lib/components/collection/SelectableCollectionCard.svelte'
import SelectableCollectionRow from '$lib/components/collection/SelectableCollectionRow.svelte'
import Icon from '$lib/components/Icon.svelte'
import { IsInViewport } from 'runed'
import { sidebar } from '$lib/stores/sidebar.svelte'
import { viewMode, type ViewMode } from '$lib/stores/viewMode.svelte'
import { LOADED_IDS_KEY, type LoadedIdsContext } from '$lib/stores/selectionMode.svelte'
import { useInfiniteLoader } from '$lib/stores/loaderState.svelte'
const { data }: { data: PageData } = $props()
@ -56,6 +56,10 @@
return collectionQueries.weapons(userId, filters)
})
// State-gated infinite scroll (inspired by svelte-infinite)
// Encapsulates intersection observer, state machine, and all reactive effects
const loader = useInfiniteLoader(() => collectionQuery, () => sentinelEl)
// Flatten all weapons from pages
const allWeapons = $derived.by((): CollectionWeapon[] => {
if (!collectionQuery.data?.pages) {
@ -70,21 +74,14 @@
loadedIdsContext?.setIds(ids)
})
// Infinite scroll
const inViewport = new IsInViewport(() => sentinelEl, {
rootMargin: '200px'
// Reset loader state 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 && allWeapons.length === 0)