diff --git a/src/entities/Font/model/store/unifiedFontStore.svelte.ts b/src/entities/Font/model/store/unifiedFontStore.svelte.ts index 51d5a52..b229709 100644 --- a/src/entities/Font/model/store/unifiedFontStore.svelte.ts +++ b/src/entities/Font/model/store/unifiedFontStore.svelte.ts @@ -59,6 +59,11 @@ export class UnifiedFontStore extends BaseFontStore { } | null >(null); + /** + * Accumulated fonts from all pages (for infinite scroll) + */ + #accumulatedFonts = $state([]); + /** * Pagination metadata (derived from proxy API response) */ @@ -84,8 +89,53 @@ export class UnifiedFontStore extends BaseFontStore { }; }); + /** + * Track previous filter params to detect changes and reset pagination + */ + #previousFilterParams = $state(''); + + /** + * Cleanup function for the filter tracking effect + */ + #filterCleanup: (() => void) | null = null; + constructor(initialParams: ProxyFontsParams = {}) { super(initialParams); + + // Track filter params (excluding pagination params) + // Wrapped in $effect.root() to prevent effect_orphan error + this.#filterCleanup = $effect.root(() => { + $effect(() => { + const filterParams = JSON.stringify({ + provider: this.params.provider, + category: this.params.category, + subset: this.params.subset, + q: this.params.q, + }); + + // If filters changed, reset offset to 0 + if (filterParams !== this.#previousFilterParams) { + if (this.#previousFilterParams && this.params.offset !== 0) { + this.setParams({ offset: 0 }); + } + this.#previousFilterParams = filterParams; + } + }); + }); + } + + /** + * Clean up both parent and child effects + */ + destroy() { + // Call parent cleanup (TanStack observer effect) + super.destroy(); + + // Call filter tracking effect cleanup + if (this.#filterCleanup) { + this.#filterCleanup(); + this.#filterCleanup = null; + } } /** @@ -136,17 +186,25 @@ export class UnifiedFontStore extends BaseFontStore { offset: response.offset ?? this.params.offset ?? 0, }; + // Accumulate fonts for infinite scroll + if (params.offset === 0) { + // Reset when starting from beginning (new search/filter) + this.#accumulatedFonts = response.fonts; + } else { + // Append new fonts to existing ones + this.#accumulatedFonts = [...this.#accumulatedFonts, ...response.fonts]; + } + return response.fonts; } // --- Getters (proxied from BaseFontStore) --- /** - * Get all fonts from current query result + * Get all accumulated fonts (for infinite scroll) */ get fonts(): UnifiedFont[] { - // The result.data is UnifiedFont[] (from TanStack Query) - return (this.result.data as UnifiedFont[] | undefined) ?? []; + return this.#accumulatedFonts; } /** @@ -288,5 +346,9 @@ export function createUnifiedFontStore(params: ProxyFontsParams = {}) { /** * Singleton instance for global use + * Initialized with a default limit to prevent fetching all fonts at once */ -export const unifiedFontStore = new UnifiedFontStore(); +export const unifiedFontStore = new UnifiedFontStore({ + limit: 50, + offset: 0, +}); diff --git a/src/features/GetFonts/ui/SuggestedFonts/SuggestedFonts.svelte b/src/features/GetFonts/ui/SuggestedFonts/SuggestedFonts.svelte index f477731..b4779f2 100644 --- a/src/features/GetFonts/ui/SuggestedFonts/SuggestedFonts.svelte +++ b/src/features/GetFonts/ui/SuggestedFonts/SuggestedFonts.svelte @@ -15,7 +15,10 @@ import { cn } from '$shared/shadcn/utils/shadcn-utils'; * Load more fonts by moving to the next page */ function loadMore() { - if (!unifiedFontStore.pagination.hasMore || unifiedFontStore.isFetching) { + if ( + !unifiedFontStore.pagination.hasMore + || unifiedFontStore.isFetching + ) { return; } unifiedFontStore.nextPage(); @@ -24,15 +27,14 @@ function loadMore() { /** * Handle scroll near bottom - auto-load next page * - * Triggered when the user scrolls within 5 items of the end of the list. - * Only fetches if there are more pages available and not already fetching. + * Triggered by VirtualList when the user scrolls within 5 items of the end + * of the loaded items. Only fetches if there are more pages available. */ -function handleNearBottom(lastVisibleIndex: number) { - const { hasMore, total } = unifiedFontStore.pagination; - const itemsRemaining = total - lastVisibleIndex; +function handleNearBottom(_lastVisibleIndex: number) { + const { hasMore } = unifiedFontStore.pagination; - // Only trigger if within 5 items of the end, more data exists, and not already fetching - if (itemsRemaining <= 5 && hasMore && !unifiedFontStore.isFetching) { + // VirtualList already checks if we're near the bottom of loaded items + if (hasMore && !unifiedFontStore.isFetching) { loadMore(); } } @@ -47,13 +49,8 @@ const displayRange = $derived.by(() => { }); -{#if unifiedFontStore.pagination.total > 0 && !unifiedFontStore.isLoading} -
- {displayRange} - {#if unifiedFontStore.isFetching} - (Loading...) - {/if} -
+{#if unifiedFontStore.isFetching || unifiedFontStore.isLoading} + (Loading...) {/if} ; + children: Snippet< + [{ item: T; index: number; isVisible: boolean; proximity: number }] + >; } let { @@ -99,7 +101,7 @@ let { }: Props = $props(); const virtualizer = createVirtualizer(() => ({ - count: total, + count: items.length, data: items, estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight, overscan, @@ -109,10 +111,11 @@ $effect(() => { const visibleItems = virtualizer.items.map(item => items[item.index]); onVisibleItemsChange?.(visibleItems); - // Trigger onNearBottom when user scrolls near the end (within 5 items) + // Trigger onNearBottom when user scrolls near the end of loaded items (within 5 items) if (virtualizer.items.length > 0 && onNearBottom) { const lastVisibleItem = virtualizer.items[virtualizer.items.length - 1]; - const itemsRemaining = total - lastVisibleItem.index; + // Compare against loaded items length, not total + const itemsRemaining = items.length - lastVisibleItem.index; if (itemsRemaining <= 5) { onNearBottom(lastVisibleItem.index);