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
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>
|
|
|