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">
|
<script lang="ts">
|
||||||
import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
|
import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
|
||||||
|
import { onDestroy } from 'svelte'
|
||||||
import { collectionQueries } from '$lib/api/queries/collection.queries'
|
import { collectionQueries } from '$lib/api/queries/collection.queries'
|
||||||
import {
|
import {
|
||||||
searchQueries,
|
searchQueries,
|
||||||
|
|
@ -26,7 +27,7 @@
|
||||||
import SelectableWeaponRow from './SelectableWeaponRow.svelte'
|
import SelectableWeaponRow from './SelectableWeaponRow.svelte'
|
||||||
import SelectableSummonCard from './SelectableSummonCard.svelte'
|
import SelectableSummonCard from './SelectableSummonCard.svelte'
|
||||||
import SelectableSummonRow from './SelectableSummonRow.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'
|
import { viewMode, type ViewMode } from '$lib/stores/viewMode.svelte'
|
||||||
|
|
||||||
type SearchResultItem = SearchPageResult['results'][number]
|
type SearchResultItem = SearchPageResult['results'][number]
|
||||||
|
|
@ -153,22 +154,18 @@
|
||||||
: addSummonMutation
|
: addSummonMutation
|
||||||
)
|
)
|
||||||
|
|
||||||
// Infinite scroll
|
// State-gated infinite scroll
|
||||||
const inViewport = new IsInViewport(() => sentinelEl, {
|
const loader = useInfiniteLoader(() => searchResults, () => sentinelEl, { rootMargin: '200px' })
|
||||||
rootMargin: '200px'
|
|
||||||
|
// Reset loader when filters or showOnlySelected changes
|
||||||
|
$effect(() => {
|
||||||
|
void searchFilters
|
||||||
|
void showOnlySelected
|
||||||
|
loader.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
// Cleanup on destroy
|
||||||
if (
|
onDestroy(() => loader.destroy())
|
||||||
inViewport.current &&
|
|
||||||
searchResults.hasNextPage &&
|
|
||||||
!searchResults.isFetchingNextPage &&
|
|
||||||
!searchResults.isLoading &&
|
|
||||||
!showOnlySelected
|
|
||||||
) {
|
|
||||||
searchResults.fetchNextPage()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reset state when modal closes or entity type changes
|
// Reset state when modal closes or entity type changes
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
@ -438,9 +435,11 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if displayedResults.length > 0}
|
{#if displayedResults.length > 0}
|
||||||
{#if !showOnlySelected && searchResults.hasNextPage}
|
<div
|
||||||
<div class="load-more-sentinel" bind:this={sentinelEl}></div>
|
class="load-more-sentinel"
|
||||||
{/if}
|
bind:this={sentinelEl}
|
||||||
|
class:hidden={showOnlySelected || !searchResults.hasNextPage}
|
||||||
|
></div>
|
||||||
|
|
||||||
{#if searchResults.isFetchingNextPage}
|
{#if searchResults.isFetchingNextPage}
|
||||||
<div class="loading-more">
|
<div class="loading-more">
|
||||||
|
|
@ -569,6 +568,10 @@
|
||||||
.load-more-sentinel {
|
.load-more-sentinel {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin-top: $unit;
|
margin-top: $unit;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-more {
|
.loading-more {
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@
|
||||||
import type { CollectionArtifact } from '$lib/types/api/artifact'
|
import type { CollectionArtifact } from '$lib/types/api/artifact'
|
||||||
import type { Character } from '$lib/types/api/entities'
|
import type { Character } from '$lib/types/api/entities'
|
||||||
import { createInfiniteQuery } from '@tanstack/svelte-query'
|
import { createInfiniteQuery } from '@tanstack/svelte-query'
|
||||||
|
import { onDestroy } from 'svelte'
|
||||||
import { artifactQueries } from '$lib/api/queries/artifact.queries'
|
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 { getArtifactImage } from '$lib/utils/images'
|
||||||
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
|
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
|
||||||
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
|
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
|
||||||
|
|
@ -56,25 +57,20 @@
|
||||||
return collectionQuery.data.pages.flatMap((page) => page.results ?? [])
|
return collectionQuery.data.pages.flatMap((page) => page.results ?? [])
|
||||||
})
|
})
|
||||||
|
|
||||||
// Infinite scroll
|
// State-gated infinite scroll
|
||||||
const inViewport = new IsInViewport(() => sentinelEl, {
|
const loader = useInfiniteLoader(() => collectionQuery, () => sentinelEl, { rootMargin: '200px' })
|
||||||
rootMargin: '200px'
|
|
||||||
|
// Reset loader when filters change
|
||||||
|
$effect(() => {
|
||||||
|
void queryFilters
|
||||||
|
loader.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
// Cleanup on destroy
|
||||||
if (
|
onDestroy(() => loader.destroy())
|
||||||
inViewport.current &&
|
|
||||||
collectionQuery.hasNextPage &&
|
|
||||||
!collectionQuery.isFetchingNextPage &&
|
|
||||||
!collectionQuery.isLoading
|
|
||||||
) {
|
|
||||||
collectionQuery.fetchNextPage()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isLoading = $derived(collectionQuery.isLoading)
|
const isLoading = $derived(collectionQuery.isLoading)
|
||||||
const isEmpty = $derived(!isLoading && allArtifacts.length === 0)
|
const isEmpty = $derived(!isLoading && allArtifacts.length === 0)
|
||||||
const showSentinel = $derived(collectionQuery.hasNextPage && !collectionQuery.isFetchingNextPage)
|
|
||||||
|
|
||||||
// Get display name for artifact
|
// Get display name for artifact
|
||||||
function getDisplayName(artifact: CollectionArtifact): string {
|
function getDisplayName(artifact: CollectionArtifact): string {
|
||||||
|
|
@ -168,9 +164,11 @@
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if showSentinel}
|
<div
|
||||||
<div class="load-more-sentinel" bind:this={sentinelEl}></div>
|
class="load-more-sentinel"
|
||||||
{/if}
|
bind:this={sentinelEl}
|
||||||
|
class:hidden={!collectionQuery.hasNextPage}
|
||||||
|
></div>
|
||||||
|
|
||||||
{#if collectionQuery.isFetchingNextPage}
|
{#if collectionQuery.isFetchingNextPage}
|
||||||
<div class="loading-more">
|
<div class="loading-more">
|
||||||
|
|
@ -347,6 +345,10 @@
|
||||||
|
|
||||||
.load-more-sentinel {
|
.load-more-sentinel {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-more {
|
.loading-more {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createInfiniteQuery } from '@tanstack/svelte-query'
|
import { createInfiniteQuery } from '@tanstack/svelte-query'
|
||||||
|
import { onDestroy } from 'svelte'
|
||||||
import type { Job, JobSkill } from '$lib/types/api/entities'
|
import type { Job, JobSkill } from '$lib/types/api/entities'
|
||||||
import type { JobSkillList } from '$lib/types/api/party'
|
import type { JobSkillList } from '$lib/types/api/party'
|
||||||
import { jobQueries } from '$lib/api/queries/job.queries'
|
import { jobQueries } from '$lib/api/queries/job.queries'
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
import Input from '../ui/Input.svelte'
|
import Input from '../ui/Input.svelte'
|
||||||
import Select from '../ui/Select.svelte'
|
import Select from '../ui/Select.svelte'
|
||||||
import Icon from '../Icon.svelte'
|
import Icon from '../Icon.svelte'
|
||||||
import { IsInViewport } from 'runed'
|
import { useInfiniteLoader } from '$lib/stores/loaderState.svelte'
|
||||||
import * as m from '$lib/paraglide/messages'
|
import * as m from '$lib/paraglide/messages'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -101,28 +102,21 @@
|
||||||
// Sentinel element for intersection observation
|
// Sentinel element for intersection observation
|
||||||
let sentinelEl = $state<HTMLElement>()
|
let sentinelEl = $state<HTMLElement>()
|
||||||
|
|
||||||
// Use runed's IsInViewport for viewport detection
|
// State-gated infinite scroll
|
||||||
const inViewport = new IsInViewport(() => sentinelEl, {
|
const loader = useInfiniteLoader(() => skillsQuery, () => sentinelEl, { rootMargin: '200px' })
|
||||||
rootMargin: '200px'
|
|
||||||
|
// Reset loader when filters change
|
||||||
|
$effect(() => {
|
||||||
|
void debouncedSearchQuery
|
||||||
|
void skillCategory
|
||||||
|
loader.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Auto-fetch next page when sentinel is visible
|
// Cleanup on destroy
|
||||||
$effect(() => {
|
onDestroy(() => loader.destroy())
|
||||||
if (
|
|
||||||
inViewport.current &&
|
|
||||||
skillsQuery.hasNextPage &&
|
|
||||||
!skillsQuery.isFetchingNextPage &&
|
|
||||||
!skillsQuery.isLoading
|
|
||||||
) {
|
|
||||||
skillsQuery.fetchNextPage()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Computed states
|
// Computed states
|
||||||
const isEmpty = $derived(skills.length === 0 && !skillsQuery.isLoading && !skillsQuery.isError)
|
const isEmpty = $derived(skills.length === 0 && !skillsQuery.isLoading && !skillsQuery.isError)
|
||||||
const showSentinel = $derived(
|
|
||||||
!skillsQuery.isLoading && skillsQuery.hasNextPage && skills.length > 0
|
|
||||||
)
|
|
||||||
|
|
||||||
function handleSelectSkill(skill: JobSkill) {
|
function handleSelectSkill(skill: JobSkill) {
|
||||||
// Clear any previous errors
|
// Clear any previous errors
|
||||||
|
|
@ -258,9 +252,11 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showSentinel}
|
<div
|
||||||
<div class="load-more-sentinel" bind:this={sentinelEl}></div>
|
class="load-more-sentinel"
|
||||||
{/if}
|
bind:this={sentinelEl}
|
||||||
|
class:hidden={!skillsQuery.hasNextPage}
|
||||||
|
></div>
|
||||||
|
|
||||||
{#if skillsQuery.isFetchingNextPage}
|
{#if skillsQuery.isFetchingNextPage}
|
||||||
<div class="loading-more">
|
<div class="loading-more">
|
||||||
|
|
@ -421,6 +417,10 @@
|
||||||
.load-more-sentinel {
|
.load-more-sentinel {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin-top: $unit;
|
margin-top: $unit;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-more {
|
.loading-more {
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createInfiniteQuery, createQuery } from '@tanstack/svelte-query'
|
import { createInfiniteQuery, createQuery } from '@tanstack/svelte-query'
|
||||||
|
import { onDestroy } from 'svelte'
|
||||||
import type { SearchResult } from '$lib/api/adapters/search.adapter'
|
import type { SearchResult } from '$lib/api/adapters/search.adapter'
|
||||||
import { searchQueries, type SearchFilters } from '$lib/api/queries/search.queries'
|
import { searchQueries, type SearchFilters } from '$lib/api/queries/search.queries'
|
||||||
import { collectionQueries } from '$lib/api/queries/collection.queries'
|
import { collectionQueries } from '$lib/api/queries/collection.queries'
|
||||||
import Button from '../ui/Button.svelte'
|
import Button from '../ui/Button.svelte'
|
||||||
import Icon from '../Icon.svelte'
|
import Icon from '../Icon.svelte'
|
||||||
import CharacterTags from '$lib/components/tags/CharacterTags.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 { getCharacterImage, getWeaponImage, getSummonImage } from '$lib/features/database/detail/image'
|
||||||
import type { AddItemResult, SearchMode } from '$lib/types/api/search'
|
import type { AddItemResult, SearchMode } from '$lib/types/api/search'
|
||||||
import type { CollectionCharacter, CollectionWeapon, CollectionSummon } from '$lib/types/api/collection'
|
import type { CollectionCharacter, CollectionWeapon, CollectionSummon } from '$lib/types/api/collection'
|
||||||
|
|
@ -245,35 +246,30 @@
|
||||||
return deduped as AddItemResult[]
|
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
|
// Get the active query based on search mode
|
||||||
const activeQuery = $derived(
|
const activeQuery = $derived(
|
||||||
searchMode === 'collection' && authUserId ? collectionQueryResult : searchQueryResult
|
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(() => {
|
$effect(() => {
|
||||||
if (
|
void searchMode
|
||||||
inViewport.current &&
|
void filters
|
||||||
activeQuery.hasNextPage &&
|
loader.reset()
|
||||||
!activeQuery.isFetchingNextPage &&
|
|
||||||
!activeQuery.isLoading
|
|
||||||
) {
|
|
||||||
activeQuery.fetchNextPage()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Cleanup on destroy
|
||||||
|
onDestroy(() => loader.destroy())
|
||||||
|
|
||||||
// Computed states
|
// Computed states
|
||||||
const isEmpty = $derived(
|
const isEmpty = $derived(
|
||||||
searchResults.length === 0 && !activeQuery.isLoading && !activeQuery.isError
|
searchResults.length === 0 && !activeQuery.isLoading && !activeQuery.isError
|
||||||
)
|
)
|
||||||
const showSentinel = $derived(
|
|
||||||
!activeQuery.isLoading && activeQuery.hasNextPage && searchResults.length > 0
|
|
||||||
)
|
|
||||||
|
|
||||||
// Focus search input on mount
|
// Focus search input on mount
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
@ -477,9 +473,11 @@
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{#if showSentinel}
|
<div
|
||||||
<div class="load-more-sentinel" bind:this={sentinelEl}></div>
|
class="load-more-sentinel"
|
||||||
{/if}
|
bind:this={sentinelEl}
|
||||||
|
class:hidden={!activeQuery.hasNextPage}
|
||||||
|
></div>
|
||||||
|
|
||||||
{#if activeQuery.isFetchingNextPage}
|
{#if activeQuery.isFetchingNextPage}
|
||||||
<div class="loading-more">
|
<div class="loading-more">
|
||||||
|
|
@ -771,6 +769,10 @@
|
||||||
.load-more-sentinel {
|
.load-more-sentinel {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin-top: $unit;
|
margin-top: $unit;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-more {
|
.loading-more {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue