port sidebar and modal components to useInfiniteLoader
This commit is contained in:
parent
133cd9ec5b
commit
fbc9f339be
4 changed files with 85 additions and 78 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue