Files
library/apps/frontend/src/app/components/shared/pagination.component.ts
T
2026-02-04 20:17:04 -08:00

280 lines
6.5 KiB
TypeScript

/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Component, Input, Output, EventEmitter, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-pagination',
standalone: true,
imports: [CommonModule],
template: `
<div class="pagination-container">
<div class="pagination-info">
Showing {{ startItem() }} - {{ endItem() }} of {{ totalItems }} items
</div>
<div class="pagination-controls">
<button
(click)="onPageChange(1)"
[disabled]="currentPage === 1"
class="btn btn-sm pagination-btn"
title="First page"
>
</button>
<button
(click)="onPageChange(currentPage - 1)"
[disabled]="currentPage === 1"
class="btn btn-sm pagination-btn"
title="Previous page"
>
</button>
<div class="page-numbers">
@for (page of pageNumbers(); track page) {
@if (page === '...') {
<span class="page-ellipsis">{{ page }}</span>
} @else {
<button
(click)="onPageChange(+page)"
[class.active]="currentPage === +page"
class="btn btn-sm pagination-btn page-number"
>
{{ page }}
</button>
}
}
</div>
<button
(click)="onPageChange(currentPage + 1)"
[disabled]="currentPage === totalPages()"
class="btn btn-sm pagination-btn"
title="Next page"
>
</button>
<button
(click)="onPageChange(totalPages())"
[disabled]="currentPage === totalPages()"
class="btn btn-sm pagination-btn"
title="Last page"
>
</button>
</div>
<div class="page-size-selector">
<label for="page-size">Items per page:</label>
<select
id="page-size"
[value]="pageSize"
(change)="onPageSizeChange($event)"
class="page-size-select"
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
`,
styles: [`
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
flex-wrap: wrap;
gap: 1rem;
}
.pagination-info {
color: var(--witch-plum);
font-size: 0.9rem;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.pagination-btn {
padding: 0.25rem 0.75rem;
border: 1px solid var(--witch-lavender);
background: var(--witch-moon);
color: var(--witch-purple);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
min-width: 2.5rem;
}
.pagination-btn:hover:not(:disabled) {
background: var(--witch-lavender);
border-color: var(--witch-mauve);
transform: translateY(-1px);
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-btn.active {
background: #8b6f47;
color: white;
border-color: #8b6f47;
}
.page-numbers {
display: flex;
align-items: center;
gap: 0.25rem;
}
.page-number {
min-width: 2.5rem;
}
.page-ellipsis {
padding: 0 0.5rem;
color: var(--witch-mauve);
}
.page-size-selector {
display: flex;
align-items: center;
gap: 0.5rem;
}
.page-size-selector label {
font-size: 0.9rem;
color: var(--witch-plum);
}
.page-size-select {
padding: 0.25rem 0.5rem;
border: 2px solid var(--witch-lavender);
border-radius: 4px;
background: var(--witch-moon);
color: var(--witch-purple);
cursor: pointer;
transition: border-color 0.2s;
}
.page-size-select:hover {
border-color: var(--witch-mauve);
}
.page-size-select:focus {
outline: none;
border-color: var(--witch-rose);
box-shadow: 0 0 0 3px rgba(168, 87, 126, 0.2);
}
@media (max-width: 768px) {
.pagination-container {
justify-content: center;
}
.pagination-controls {
order: 2;
width: 100%;
justify-content: center;
}
.pagination-info {
order: 1;
width: 100%;
text-align: center;
}
.page-size-selector {
order: 3;
width: 100%;
justify-content: center;
}
}
`]
})
export class PaginationComponent {
@Input() currentPage = 1;
@Input() pageSize = 25;
@Input() totalItems = 0;
@Output() pageChange = new EventEmitter<number>();
@Output() pageSizeChange = new EventEmitter<number>();
totalPages = computed(() => Math.ceil(this.totalItems / this.pageSize));
startItem = computed(() => {
if (this.totalItems === 0) return 0;
return (this.currentPage - 1) * this.pageSize + 1;
});
endItem = computed(() => {
return Math.min(this.currentPage * this.pageSize, this.totalItems);
});
pageNumbers = computed(() => {
const total = this.totalPages();
const current = this.currentPage;
const pages: (number | string)[] = [];
if (total <= 7) {
// Show all pages if 7 or fewer
for (let i = 1; i <= total; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);
if (current <= 3) {
// Near the beginning
for (let i = 2; i <= 5; i++) {
pages.push(i);
}
pages.push('...');
pages.push(total);
} else if (current >= total - 2) {
// Near the end
pages.push('...');
for (let i = total - 4; i <= total; i++) {
pages.push(i);
}
} else {
// In the middle
pages.push('...');
pages.push(current - 1);
pages.push(current);
pages.push(current + 1);
pages.push('...');
pages.push(total);
}
}
return pages;
});
onPageChange(page: number) {
if (page >= 1 && page <= this.totalPages() && page !== this.currentPage) {
this.pageChange.emit(page);
}
}
onPageSizeChange(event: Event) {
const target = event.target as HTMLSelectElement;
const newSize = parseInt(target.value, 10);
this.pageSizeChange.emit(newSize);
}
}