feature/comparison-slider #19

Merged
ilia merged 129 commits from feature/comparison-slider into main 2026-02-02 09:23:46 +00:00
3 changed files with 85 additions and 23 deletions
Showing only changes of commit b1ce734f19 - Show all commits

View File

@@ -59,6 +59,11 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
} | null } | null
>(null); >(null);
/**
* Accumulated fonts from all pages (for infinite scroll)
*/
#accumulatedFonts = $state<UnifiedFont[]>([]);
/** /**
* Pagination metadata (derived from proxy API response) * Pagination metadata (derived from proxy API response)
*/ */
@@ -84,8 +89,53 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
}; };
}); });
/**
* Track previous filter params to detect changes and reset pagination
*/
#previousFilterParams = $state<string>('');
/**
* Cleanup function for the filter tracking effect
*/
#filterCleanup: (() => void) | null = null;
constructor(initialParams: ProxyFontsParams = {}) { constructor(initialParams: ProxyFontsParams = {}) {
super(initialParams); 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<ProxyFontsParams> {
offset: response.offset ?? this.params.offset ?? 0, 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; return response.fonts;
} }
// --- Getters (proxied from BaseFontStore) --- // --- Getters (proxied from BaseFontStore) ---
/** /**
* Get all fonts from current query result * Get all accumulated fonts (for infinite scroll)
*/ */
get fonts(): UnifiedFont[] { get fonts(): UnifiedFont[] {
// The result.data is UnifiedFont[] (from TanStack Query) return this.#accumulatedFonts;
return (this.result.data as UnifiedFont[] | undefined) ?? [];
} }
/** /**
@@ -288,5 +346,9 @@ export function createUnifiedFontStore(params: ProxyFontsParams = {}) {
/** /**
* Singleton instance for global use * 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,
});

View File

@@ -15,7 +15,10 @@ import { cn } from '$shared/shadcn/utils/shadcn-utils';
* Load more fonts by moving to the next page * Load more fonts by moving to the next page
*/ */
function loadMore() { function loadMore() {
if (!unifiedFontStore.pagination.hasMore || unifiedFontStore.isFetching) { if (
!unifiedFontStore.pagination.hasMore
|| unifiedFontStore.isFetching
) {
return; return;
} }
unifiedFontStore.nextPage(); unifiedFontStore.nextPage();
@@ -24,15 +27,14 @@ function loadMore() {
/** /**
* Handle scroll near bottom - auto-load next page * Handle scroll near bottom - auto-load next page
* *
* Triggered when the user scrolls within 5 items of the end of the list. * Triggered by VirtualList when the user scrolls within 5 items of the end
* Only fetches if there are more pages available and not already fetching. * of the loaded items. Only fetches if there are more pages available.
*/ */
function handleNearBottom(lastVisibleIndex: number) { function handleNearBottom(_lastVisibleIndex: number) {
const { hasMore, total } = unifiedFontStore.pagination; const { hasMore } = unifiedFontStore.pagination;
const itemsRemaining = total - lastVisibleIndex;
// Only trigger if within 5 items of the end, more data exists, and not already fetching // VirtualList already checks if we're near the bottom of loaded items
if (itemsRemaining <= 5 && hasMore && !unifiedFontStore.isFetching) { if (hasMore && !unifiedFontStore.isFetching) {
loadMore(); loadMore();
} }
} }
@@ -47,13 +49,8 @@ const displayRange = $derived.by(() => {
}); });
</script> </script>
{#if unifiedFontStore.pagination.total > 0 && !unifiedFontStore.isLoading} {#if unifiedFontStore.isFetching || unifiedFontStore.isLoading}
<div class="text-sm text-muted-foreground px-2 py-2"> <span class="ml-2 text-xs text-muted-foreground/70">(Loading...)</span>
{displayRange}
{#if unifiedFontStore.isFetching}
<span class="ml-2 text-xs text-muted-foreground/70">(Loading...)</span>
{/if}
</div>
{/if} {/if}
<FontVirtualList <FontVirtualList

View File

@@ -84,7 +84,9 @@ interface Props {
* *
* @template T - The type of items in the list * @template T - The type of items in the list
*/ */
children: Snippet<[{ item: T; index: number; isVisible: boolean; proximity: number }]>; children: Snippet<
[{ item: T; index: number; isVisible: boolean; proximity: number }]
>;
} }
let { let {
@@ -99,7 +101,7 @@ let {
}: Props = $props(); }: Props = $props();
const virtualizer = createVirtualizer(() => ({ const virtualizer = createVirtualizer(() => ({
count: total, count: items.length,
data: items, data: items,
estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight, estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight,
overscan, overscan,
@@ -109,10 +111,11 @@ $effect(() => {
const visibleItems = virtualizer.items.map(item => items[item.index]); const visibleItems = virtualizer.items.map(item => items[item.index]);
onVisibleItemsChange?.(visibleItems); 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) { if (virtualizer.items.length > 0 && onNearBottom) {
const lastVisibleItem = virtualizer.items[virtualizer.items.length - 1]; 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) { if (itemsRemaining <= 5) {
onNearBottom(lastVisibleItem.index); onNearBottom(lastVisibleItem.index);