feat: extract Pagination component and use it on all paginated pages
All checks were successful
Build and Push Frontend Image / build (push) Successful in 1m13s

- New lib/components/pagination/pagination.svelte with numbered pages,
  ellipsis for large ranges, and prev/next buttons
- All 6 admin pages (users, articles, videos, recordings, comments,
  queues) now show enumerated page numbers next to the "Showing X–Y of Z"
  label; offset is derived from page number * limit
- Public pages (videos, models, magazine) replace their inline
  totalPages/pageNumbers derived state with the shared component
- Removes ~80 lines of duplicated pagination logic across 9 files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 12:01:13 +01:00
parent fbd2efa994
commit ab0af9a773
10 changed files with 149 additions and 287 deletions

View File

@@ -0,0 +1,66 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { _ } from "svelte-i18n";
interface Props {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
let { currentPage, totalPages, onPageChange }: Props = $props();
const pageNumbers = $derived(() => {
const pages: (number | -1)[] = [];
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
} else {
pages.push(1);
if (currentPage > 3) pages.push(-1);
for (
let i = Math.max(2, currentPage - 1);
i <= Math.min(totalPages - 1, currentPage + 1);
i++
)
pages.push(i);
if (currentPage < totalPages - 2) pages.push(-1);
pages.push(totalPages);
}
return pages;
});
</script>
{#if totalPages > 1}
<div class="flex items-center gap-1">
<Button
variant="outline"
size="sm"
disabled={currentPage <= 1}
onclick={() => onPageChange(currentPage - 1)}
>
{$_("common.previous")}
</Button>
{#each pageNumbers() as p, i (i)}
{#if p === -1}
<span class="px-2 text-muted-foreground select-none"></span>
{:else}
<Button
variant={p === currentPage ? "default" : "outline"}
size="sm"
class="min-w-9"
onclick={() => onPageChange(p)}
>
{p}
</Button>
{/if}
{/each}
<Button
variant="outline"
size="sm"
disabled={currentPage >= totalPages}
onclick={() => onPageChange(currentPage + 1)}
>
{$_("common.next")}
</Button>
</div>
{/if}

View File

@@ -13,6 +13,7 @@
import type { Article } from "$lib/types";
import TimeAgo from "javascript-time-ago";
import Meta from "$lib/components/meta/meta.svelte";
import Pagination from "$lib/components/pagination/pagination.svelte";
const { data } = $props();
@@ -204,7 +205,7 @@
<!-- Pagination -->
{#if data.total > data.limit}
<div class="flex items-center justify-between mt-4">
<div class="flex items-center justify-between mt-4 flex-wrap gap-3">
<span class="text-sm text-muted-foreground">
{$_("admin.users.showing", {
values: {
@@ -214,32 +215,15 @@
},
})}
</span>
<div class="flex gap-2">
<Button
size="sm"
variant="outline"
disabled={data.offset === 0}
onclick={() => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(Math.max(0, data.offset - data.limit)));
goto(`?${params.toString()}`);
}}
>
{$_("common.previous")}
</Button>
<Button
size="sm"
variant="outline"
disabled={data.offset + data.limit >= data.total}
onclick={() => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(data.offset + data.limit));
goto(`?${params.toString()}`);
}}
>
{$_("common.next")}
</Button>
</div>
<Pagination
currentPage={Math.floor(data.offset / data.limit) + 1}
totalPages={Math.ceil(data.total / data.limit)}
onPageChange={(p) => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String((p - 1) * data.limit));
goto(`?${params.toString()}`);
}}
/>
</div>
{/if}
</div>

View File

@@ -11,6 +11,7 @@
import * as Dialog from "$lib/components/ui/dialog";
import TimeAgo from "javascript-time-ago";
import Meta from "$lib/components/meta/meta.svelte";
import Pagination from "$lib/components/pagination/pagination.svelte";
const { data } = $props();
const timeAgo = new TimeAgo("en");
@@ -153,7 +154,7 @@
</div>
{#if data.total > data.limit}
<div class="flex items-center justify-between mt-4">
<div class="flex items-center justify-between mt-4 flex-wrap gap-3">
<span class="text-sm text-muted-foreground">
{$_("admin.users.showing", {
values: {
@@ -163,28 +164,15 @@
},
})}
</span>
<div class="flex gap-2">
<Button
size="sm"
variant="outline"
disabled={data.offset === 0}
onclick={() => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(Math.max(0, data.offset - data.limit)));
goto(`?${params.toString()}`);
}}>{$_("common.previous")}</Button
>
<Button
size="sm"
variant="outline"
disabled={data.offset + data.limit >= data.total}
onclick={() => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(data.offset + data.limit));
goto(`?${params.toString()}`);
}}>{$_("common.next")}</Button
>
</div>
<Pagination
currentPage={Math.floor(data.offset / data.limit) + 1}
totalPages={Math.ceil(data.total / data.limit)}
onPageChange={(p) => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String((p - 1) * data.limit));
goto(`?${params.toString()}`);
}}
/>
</div>
{/if}
</div>

View File

@@ -9,6 +9,7 @@
import { Badge } from "$lib/components/ui/badge";
import type { Job } from "$lib/services";
import Meta from "$lib/components/meta/meta.svelte";
import Pagination from "$lib/components/pagination/pagination.svelte";
const { data } = $props();
@@ -271,7 +272,7 @@
</div>
{#if data.total > data.limit}
<div class="flex items-center justify-between mt-4">
<div class="flex items-center justify-between mt-4 flex-wrap gap-3">
<span class="text-sm text-muted-foreground">
{$_("admin.users.showing", {
values: {
@@ -281,24 +282,11 @@
},
})}
</span>
<div class="flex gap-2">
<Button
size="sm"
variant="outline"
disabled={data.offset === 0}
onclick={() => navigate({ offset: String(Math.max(0, data.offset - data.limit)) })}
>
{$_("common.previous")}
</Button>
<Button
size="sm"
variant="outline"
disabled={data.offset + data.limit >= data.total}
onclick={() => navigate({ offset: String(data.offset + data.limit) })}
>
{$_("common.next")}
</Button>
</div>
<Pagination
currentPage={Math.floor(data.offset / data.limit) + 1}
totalPages={Math.ceil(data.total / data.limit)}
onPageChange={(p) => navigate({ offset: String((p - 1) * data.limit) })}
/>
</div>
{/if}
{/if}

View File

@@ -12,6 +12,7 @@
import type { Recording } from "$lib/types";
import TimeAgo from "javascript-time-ago";
import Meta from "$lib/components/meta/meta.svelte";
import Pagination from "$lib/components/pagination/pagination.svelte";
const { data } = $props();
const timeAgo = new TimeAgo("en");
@@ -179,7 +180,7 @@
</div>
{#if data.total > data.limit}
<div class="flex items-center justify-between mt-4">
<div class="flex items-center justify-between mt-4 flex-wrap gap-3">
<span class="text-sm text-muted-foreground">
{$_("admin.users.showing", {
values: {
@@ -189,28 +190,15 @@
},
})}
</span>
<div class="flex gap-2">
<Button
size="sm"
variant="outline"
disabled={data.offset === 0}
onclick={() => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(Math.max(0, data.offset - data.limit)));
goto(`?${params.toString()}`);
}}>{$_("common.previous")}</Button
>
<Button
size="sm"
variant="outline"
disabled={data.offset + data.limit >= data.total}
onclick={() => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(data.offset + data.limit));
goto(`?${params.toString()}`);
}}>{$_("common.next")}</Button
>
</div>
<Pagination
currentPage={Math.floor(data.offset / data.limit) + 1}
totalPages={Math.ceil(data.total / data.limit)}
onPageChange={(p) => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String((p - 1) * data.limit));
goto(`?${params.toString()}`);
}}
/>
</div>
{/if}
</div>

View File

@@ -13,6 +13,7 @@
import * as Dialog from "$lib/components/ui/dialog";
import type { User } from "$lib/types";
import Meta from "$lib/components/meta/meta.svelte";
import Pagination from "$lib/components/pagination/pagination.svelte";
const { data } = $props();
@@ -228,7 +229,7 @@
<!-- Pagination -->
{#if data.total > data.limit}
<div class="flex items-center justify-between mt-4">
<div class="flex items-center justify-between mt-4 flex-wrap gap-3">
<span class="text-sm text-muted-foreground">
{$_("admin.users.showing", {
values: {
@@ -238,32 +239,15 @@
},
})}
</span>
<div class="flex gap-2">
<Button
size="sm"
variant="outline"
disabled={data.offset === 0}
onclick={() => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(Math.max(0, data.offset - data.limit)));
goto(`?${params.toString()}`);
}}
>
{$_("common.previous")}
</Button>
<Button
size="sm"
variant="outline"
disabled={data.offset + data.limit >= data.total}
onclick={() => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(data.offset + data.limit));
goto(`?${params.toString()}`);
}}
>
{$_("common.next")}
</Button>
</div>
<Pagination
currentPage={Math.floor(data.offset / data.limit) + 1}
totalPages={Math.ceil(data.total / data.limit)}
onPageChange={(p) => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String((p - 1) * data.limit));
goto(`?${params.toString()}`);
}}
/>
</div>
{/if}
</div>

View File

@@ -12,6 +12,7 @@
import * as Dialog from "$lib/components/ui/dialog";
import type { Video } from "$lib/types";
import Meta from "$lib/components/meta/meta.svelte";
import Pagination from "$lib/components/pagination/pagination.svelte";
const { data } = $props();
@@ -209,7 +210,7 @@
<!-- Pagination -->
{#if data.total > data.limit}
<div class="flex items-center justify-between mt-4">
<div class="flex items-center justify-between mt-4 flex-wrap gap-3">
<span class="text-sm text-muted-foreground">
{$_("admin.users.showing", {
values: {
@@ -219,32 +220,15 @@
},
})}
</span>
<div class="flex gap-2">
<Button
size="sm"
variant="outline"
disabled={data.offset === 0}
onclick={() => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(Math.max(0, data.offset - data.limit)));
goto(`?${params.toString()}`);
}}
>
{$_("common.previous")}
</Button>
<Button
size="sm"
variant="outline"
disabled={data.offset + data.limit >= data.total}
onclick={() => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(data.offset + data.limit));
goto(`?${params.toString()}`);
}}
>
{$_("common.next")}
</Button>
</div>
<Pagination
currentPage={Math.floor(data.offset / data.limit) + 1}
totalPages={Math.ceil(data.total / data.limit)}
onPageChange={(p) => {
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String((p - 1) * data.limit));
goto(`?${params.toString()}`);
}}
/>
</div>
{/if}
</div>

View File

@@ -13,6 +13,7 @@
import Meta from "$lib/components/meta/meta.svelte";
import SexyBackground from "$lib/components/background/background.svelte";
import PageHero from "$lib/components/page-hero/page-hero.svelte";
import Pagination from "$lib/components/pagination/pagination.svelte";
const timeAgo = new TimeAgo("en");
const { data } = $props();
@@ -50,22 +51,6 @@
goto(`?${params.toString()}`);
}
const totalPages = $derived(Math.ceil(data.total / data.limit));
const pageNumbers = $derived(() => {
const pages: (number | -1)[] = [];
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
} else {
pages.push(1);
if (data.page > 3) pages.push(-1);
for (let i = Math.max(2, data.page - 1); i <= Math.min(totalPages - 1, data.page + 1); i++)
pages.push(i);
if (data.page < totalPages - 2) pages.push(-1);
pages.push(totalPages);
}
return pages;
});
</script>
<Meta title={$_("magazine.title")} description={$_("magazine.description")} />
@@ -308,38 +293,13 @@
{/if}
<!-- Pagination -->
{#if totalPages > 1}
{#if Math.ceil(data.total / data.limit) > 1}
<div class="flex flex-col items-center gap-3 mt-10">
<div class="flex items-center gap-1">
<Button
variant="outline"
size="sm"
disabled={data.page <= 1}
onclick={() => goToPage(data.page - 1)}
class="border-primary/20 hover:bg-primary/10">{$_("common.previous")}</Button
>
{#each pageNumbers() as p, i (i)}
{#if p === -1}
<span class="px-2 text-muted-foreground select-none"></span>
{:else}
<Button
variant={p === data.page ? "default" : "outline"}
size="sm"
onclick={() => goToPage(p)}
class={p === data.page
? "bg-gradient-to-r from-primary to-accent min-w-9"
: "border-primary/20 hover:bg-primary/10 min-w-9"}>{p}</Button
>
{/if}
{/each}
<Button
variant="outline"
size="sm"
disabled={data.page >= totalPages}
onclick={() => goToPage(data.page + 1)}
class="border-primary/20 hover:bg-primary/10">{$_("common.next")}</Button
>
</div>
<Pagination
currentPage={data.page}
totalPages={Math.ceil(data.total / data.limit)}
onPageChange={goToPage}
/>
<p class="text-sm text-muted-foreground">
{$_("common.total_results", { values: { total: data.total } })}
</p>

View File

@@ -11,6 +11,7 @@
import Meta from "$lib/components/meta/meta.svelte";
import SexyBackground from "$lib/components/background/background.svelte";
import PageHero from "$lib/components/page-hero/page-hero.svelte";
import Pagination from "$lib/components/pagination/pagination.svelte";
const { data } = $props();
@@ -43,22 +44,6 @@
goto(`?${params.toString()}`);
}
const totalPages = $derived(Math.ceil(data.total / data.limit));
const pageNumbers = $derived(() => {
const pages: (number | -1)[] = [];
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
} else {
pages.push(1);
if (data.page > 3) pages.push(-1);
for (let i = Math.max(2, data.page - 1); i <= Math.min(totalPages - 1, data.page + 1); i++)
pages.push(i);
if (data.page < totalPages - 2) pages.push(-1);
pages.push(totalPages);
}
return pages;
});
</script>
<Meta title={$_("models.title")} description={$_("models.description")} />
@@ -196,38 +181,13 @@
{/if}
<!-- Pagination -->
{#if totalPages > 1}
{#if Math.ceil(data.total / data.limit) > 1}
<div class="flex flex-col items-center gap-3 mt-10">
<div class="flex items-center gap-1">
<Button
variant="outline"
size="sm"
disabled={data.page <= 1}
onclick={() => goToPage(data.page - 1)}
class="border-primary/20 hover:bg-primary/10">{$_("common.previous")}</Button
>
{#each pageNumbers() as p, i (i)}
{#if p === -1}
<span class="px-2 text-muted-foreground select-none"></span>
{:else}
<Button
variant={p === data.page ? "default" : "outline"}
size="sm"
onclick={() => goToPage(p)}
class={p === data.page
? "bg-gradient-to-r from-primary to-accent min-w-9"
: "border-primary/20 hover:bg-primary/10 min-w-9"}>{p}</Button
>
{/if}
{/each}
<Button
variant="outline"
size="sm"
disabled={data.page >= totalPages}
onclick={() => goToPage(data.page + 1)}
class="border-primary/20 hover:bg-primary/10">{$_("common.next")}</Button
>
</div>
<Pagination
currentPage={data.page}
totalPages={Math.ceil(data.total / data.limit)}
onPageChange={goToPage}
/>
<p class="text-sm text-muted-foreground">
{$_("common.total_results", { values: { total: data.total } })}
</p>

View File

@@ -11,6 +11,7 @@
import Meta from "$lib/components/meta/meta.svelte";
import SexyBackground from "$lib/components/background/background.svelte";
import PageHero from "$lib/components/page-hero/page-hero.svelte";
import Pagination from "$lib/components/pagination/pagination.svelte";
import TimeAgo from "javascript-time-ago";
import { formatVideoDuration } from "$lib/utils";
@@ -46,22 +47,6 @@
goto(`?${params.toString()}`);
}
const totalPages = $derived(Math.ceil(data.total / data.limit));
const pageNumbers = $derived(() => {
const pages: (number | -1)[] = [];
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
} else {
pages.push(1);
if (data.page > 3) pages.push(-1);
for (let i = Math.max(2, data.page - 1); i <= Math.min(totalPages - 1, data.page + 1); i++)
pages.push(i);
if (data.page < totalPages - 2) pages.push(-1);
pages.push(totalPages);
}
return pages;
});
</script>
<Meta title={$_("videos.title")} description={$_("videos.description")} />
@@ -256,38 +241,13 @@
{/if}
<!-- Pagination -->
{#if totalPages > 1}
{#if Math.ceil(data.total / data.limit) > 1}
<div class="flex flex-col items-center gap-3 mt-10">
<div class="flex items-center gap-1">
<Button
variant="outline"
size="sm"
disabled={data.page <= 1}
onclick={() => goToPage(data.page - 1)}
class="border-primary/20 hover:bg-primary/10">{$_("common.previous")}</Button
>
{#each pageNumbers() as p, i (i)}
{#if p === -1}
<span class="px-2 text-muted-foreground select-none"></span>
{:else}
<Button
variant={p === data.page ? "default" : "outline"}
size="sm"
onclick={() => goToPage(p)}
class={p === data.page
? "bg-gradient-to-r from-primary to-accent min-w-9"
: "border-primary/20 hover:bg-primary/10 min-w-9"}>{p}</Button
>
{/if}
{/each}
<Button
variant="outline"
size="sm"
disabled={data.page >= totalPages}
onclick={() => goToPage(data.page + 1)}
class="border-primary/20 hover:bg-primary/10">{$_("common.next")}</Button
>
</div>
<Pagination
currentPage={data.page}
totalPages={Math.ceil(data.total / data.limit)}
onPageChange={goToPage}
/>
<p class="text-sm text-muted-foreground">
{$_("common.total_results", { values: { total: data.total } })}
</p>