diff --git a/frontend/src/routes/categories/+page.svelte b/frontend/src/routes/categories/+page.svelte index 2be4cec..1d41806 100644 --- a/frontend/src/routes/categories/+page.svelte +++ b/frontend/src/routes/categories/+page.svelte @@ -2,6 +2,8 @@ import { goto } from '$app/navigation'; import { api, ApiError } from '$lib/api/client'; import { categorySorting, type CategorySortField } from '$lib/stores/sorting'; + import InfiniteScroll from '$lib/components/common/InfiniteScroll.svelte'; + import { tick } from 'svelte'; import type { Category, CategoryOffsetPage } from '$lib/api/types'; const LIMIT = 100; @@ -13,6 +15,7 @@ ]; let categories = $state([]); + let scrollContainer = $state(); let total = $state(0); let offset = $state(0); let loading = $state(false); @@ -61,6 +64,12 @@ loading = false; initialLoaded = true; } + // Keep loading until the content fills the viewport so the infinite-scroll + // sentinel ends up below the fold; then stop. + await tick(); + if (hasMore && scrollContainer && scrollContainer.scrollHeight <= scrollContainer.clientHeight) { + void load(); + } } let hasMore = $derived(categories.length < total); @@ -125,7 +134,7 @@ -
+
{#if error} {/if} @@ -142,15 +151,7 @@ {/each} - {#if loading} -
- -
- {/if} - - {#if hasMore && !loading} - - {/if} + {#if !loading && categories.length === 0}
@@ -330,40 +331,6 @@ filter: brightness(1.15); } - .loading-row { - display: flex; - justify-content: center; - padding: 20px; - } - - .spinner { - display: block; - width: 28px; - height: 28px; - border: 3px solid color-mix(in srgb, var(--color-accent) 25%, transparent); - border-top-color: var(--color-accent); - border-radius: 50%; - animation: spin 0.7s linear infinite; - } - - @keyframes spin { to { transform: rotate(360deg); } } - - .load-more { - display: block; - margin: 16px auto 0; - padding: 8px 24px; - border-radius: 6px; - border: 1px solid color-mix(in srgb, var(--color-accent) 40%, transparent); - background: none; - color: var(--color-accent); - font-family: inherit; - font-size: 0.85rem; - cursor: pointer; - } - - .load-more:hover { - background-color: color-mix(in srgb, var(--color-accent) 10%, transparent); - } .error { color: var(--color-danger); diff --git a/frontend/src/routes/pools/+page.svelte b/frontend/src/routes/pools/+page.svelte index ac33fa6..05c63a6 100644 --- a/frontend/src/routes/pools/+page.svelte +++ b/frontend/src/routes/pools/+page.svelte @@ -2,6 +2,8 @@ import { goto } from '$app/navigation'; import { api, ApiError } from '$lib/api/client'; import { poolSorting, type PoolSortField } from '$lib/stores/sorting'; + import InfiniteScroll from '$lib/components/common/InfiniteScroll.svelte'; + import { tick } from 'svelte'; import type { Pool, PoolOffsetPage } from '$lib/api/types'; const LIMIT = 50; @@ -12,6 +14,7 @@ ]; let pools = $state([]); + let scrollContainer = $state(); let total = $state(0); let offset = $state(0); let loading = $state(false); @@ -60,6 +63,12 @@ loading = false; initialLoaded = true; } + // Keep loading until the content fills the viewport so the infinite-scroll + // sentinel ends up below the fold; then stop. + await tick(); + if (hasMore && scrollContainer && scrollContainer.scrollHeight <= scrollContainer.clientHeight) { + void load(); + } } let hasMore = $derived(pools.length < total); @@ -128,7 +137,7 @@
-
+
{#if error} {/if} @@ -159,15 +168,7 @@ {/each} - {#if loading} -
- -
- {/if} - - {#if hasMore && !loading} - - {/if} + {#if !loading && pools.length === 0}
@@ -393,40 +394,6 @@ opacity: 0.5; } - .loading-row { - display: flex; - justify-content: center; - padding: 20px; - } - - .spinner { - display: block; - width: 28px; - height: 28px; - border: 3px solid color-mix(in srgb, var(--color-accent) 25%, transparent); - border-top-color: var(--color-accent); - border-radius: 50%; - animation: spin 0.7s linear infinite; - } - - @keyframes spin { to { transform: rotate(360deg); } } - - .load-more { - display: block; - margin: 16px auto 0; - padding: 8px 24px; - border-radius: 6px; - border: 1px solid color-mix(in srgb, var(--color-accent) 40%, transparent); - background: none; - color: var(--color-accent); - font-family: inherit; - font-size: 0.85rem; - cursor: pointer; - } - - .load-more:hover { - background-color: color-mix(in srgb, var(--color-accent) 10%, transparent); - } .error { color: var(--color-danger); diff --git a/frontend/src/routes/tags/+page.svelte b/frontend/src/routes/tags/+page.svelte index e881354..bf822c9 100644 --- a/frontend/src/routes/tags/+page.svelte +++ b/frontend/src/routes/tags/+page.svelte @@ -3,6 +3,8 @@ import { api, ApiError } from '$lib/api/client'; import { tagSorting, type TagSortField } from '$lib/stores/sorting'; import TagBadge from '$lib/components/tag/TagBadge.svelte'; + import InfiniteScroll from '$lib/components/common/InfiniteScroll.svelte'; + import { tick } from 'svelte'; import type { Tag, TagOffsetPage } from '$lib/api/types'; const LIMIT = 100; @@ -15,6 +17,7 @@ ]; let tags = $state([]); + let scrollContainer = $state(); let total = $state(0); let offset = $state(0); let loading = $state(false); @@ -66,6 +69,12 @@ loading = false; initialLoaded = true; } + // Keep loading until the content fills the viewport so the infinite-scroll + // sentinel ends up below the fold; then stop. + await tick(); + if (hasMore && scrollContainer && scrollContainer.scrollHeight <= scrollContainer.clientHeight) { + void load(); + } } function onSearch(e: Event) { @@ -136,7 +145,7 @@
-
+
{#if error} {/if} @@ -147,15 +156,7 @@ {/each} - {#if loading} -
- -
- {/if} - - {#if hasMore && !loading} - - {/if} + {#if !loading && tags.length === 0}
@@ -315,41 +316,6 @@ align-content: flex-start; } - .loading-row { - display: flex; - justify-content: center; - padding: 20px; - } - - .spinner { - display: block; - width: 28px; - height: 28px; - border: 3px solid color-mix(in srgb, var(--color-accent) 25%, transparent); - border-top-color: var(--color-accent); - border-radius: 50%; - animation: spin 0.7s linear infinite; - } - - @keyframes spin { to { transform: rotate(360deg); } } - - .load-more { - display: block; - margin: 16px auto 0; - padding: 8px 24px; - border-radius: 6px; - border: 1px solid color-mix(in srgb, var(--color-accent) 40%, transparent); - background: none; - color: var(--color-accent); - font-family: inherit; - font-size: 0.85rem; - cursor: pointer; - } - - .load-more:hover { - background-color: color-mix(in srgb, var(--color-accent) 10%, transparent); - } - .error { color: var(--color-danger); font-size: 0.875rem;