You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

232 lines
5.7 KiB

<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
interface Props {
totalItems: number;
itemsPerPage: number;
currentPage?: number;
onPageChange?: (page: number) => void;
}
let { totalItems, itemsPerPage, currentPage: providedCurrentPage, onPageChange }: Props = $props();
// Get current page from URL query param or use provided
const currentPage = $derived(providedCurrentPage ?? parseInt($page.url.searchParams.get('page') || '1', 10));
const totalPages = $derived(Math.ceil(totalItems / itemsPerPage));
const startItem = $derived((currentPage - 1) * itemsPerPage + 1);
const endItem = $derived(Math.min(currentPage * itemsPerPage, totalItems));
function goToPage(pageNum: number) {
if (pageNum < 1 || pageNum > totalPages) return;
if (onPageChange) {
onPageChange(pageNum);
} else {
// Update URL query param
const url = new URL($page.url);
if (pageNum === 1) {
url.searchParams.delete('page');
} else {
url.searchParams.set('page', pageNum.toString());
}
goto(url.pathname + url.search, { replaceState: true, noScroll: false });
}
// Scroll to top of page
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function getPageNumbers(): number[] {
const pages: number[] = [];
const maxVisible = 7;
if (totalPages <= maxVisible) {
// Show all pages
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Show first page, last page, current page, and pages around current
pages.push(1);
if (currentPage > 3) {
pages.push(-1); // Ellipsis marker
}
const start = Math.max(2, currentPage - 1);
const end = Math.min(totalPages - 1, currentPage + 1);
for (let i = start; i <= end; i++) {
pages.push(i);
}
if (currentPage < totalPages - 2) {
pages.push(-1); // Ellipsis marker
}
pages.push(totalPages);
}
return pages;
}
</script>
{#if totalPages > 1}
<div class="pagination">
<div class="pagination-info">
Showing {startItem}-{endItem} of {totalItems}
</div>
<div class="pagination-controls">
<button
onclick={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
class="pagination-btn"
aria-label="Previous page"
>
Previous
</button>
<div class="pagination-numbers">
{#each getPageNumbers() as pageNum}
{#if pageNum === -1}
<span class="pagination-ellipsis">...</span>
{:else}
<button
onclick={() => goToPage(pageNum)}
class="pagination-btn"
class:active={pageNum === currentPage}
aria-label="Go to page {pageNum}"
aria-current={pageNum === currentPage ? 'page' : undefined}
>
{pageNum}
</button>
{/if}
{/each}
</div>
<button
onclick={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
class="pagination-btn"
aria-label="Next page"
>
Next
</button>
</div>
</div>
{/if}
<style>
.pagination {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 1.5rem 0;
margin-top: 2rem;
border-top: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .pagination {
border-top-color: var(--fog-dark-border, #374151);
}
.pagination-info {
font-size: 0.875rem;
color: var(--fog-text-light, #52667a);
}
:global(.dark) .pagination-info {
color: var(--fog-dark-text-light, #a8b8d0);
}
.pagination-controls {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
}
.pagination-numbers {
display: flex;
align-items: center;
gap: 0.25rem;
}
.pagination-btn {
min-width: 2.5rem;
height: 2.5rem;
padding: 0.5rem 0.75rem;
background: var(--fog-post, #ffffff);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
color: var(--fog-text, #1f2937);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.pagination-btn:hover:not(:disabled) {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-accent, #64748b);
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-btn.active {
background: var(--fog-accent, #64748b);
color: white;
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .pagination-btn {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f9fafb);
}
:global(.dark) .pagination-btn:hover:not(:disabled) {
background: var(--fog-dark-highlight, #374151);
border-color: var(--fog-dark-accent, #94a3b8);
}
:global(.dark) .pagination-btn.active {
background: var(--fog-dark-accent, #94a3b8);
color: var(--fog-dark-text, #f9fafb);
border-color: var(--fog-dark-accent, #94a3b8);
}
.pagination-ellipsis {
padding: 0 0.5rem;
color: var(--fog-text-light, #52667a);
user-select: none;
}
:global(.dark) .pagination-ellipsis {
color: var(--fog-dark-text-light, #a8b8d0);
}
@media (max-width: 640px) {
.pagination-controls {
gap: 0.25rem;
}
.pagination-btn {
min-width: 2rem;
height: 2rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
}
</style>