add changes

This commit is contained in:
AD2025
2025-11-14 21:48:47 +02:00
parent 6f23890407
commit 37b4d565b1
72 changed files with 17104 additions and 246 deletions

View File

@@ -0,0 +1,334 @@
import { Component, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { PaginationState } from '../../../core/services/pagination.service';
/**
* Pagination Component
* Reusable pagination controls with customizable options
*
* Features:
* - Previous/Next buttons
* - First/Last page buttons
* - Page number buttons with active state
* - "Showing X-Y of Z results" display
* - Page size selector
* - Responsive design (fewer buttons on mobile)
* - Accessible with keyboard navigation and ARIA labels
*
* @example
* <app-pagination
* [state]="paginationState()"
* [pageSizeOptions]="[10, 25, 50, 100]"
* [showPageSizeSelector]="true"
* [showFirstLast]="true"
* [maxVisiblePages]="5"
* (pageChange)="onPageChange($event)"
* (pageSizeChange)="onPageSizeChange($event)">
* </app-pagination>
*/
@Component({
selector: 'app-pagination',
imports: [
CommonModule,
MatButtonModule,
MatIconModule,
MatTooltipModule,
MatSelectModule,
MatFormFieldModule
],
template: `
<div class="pagination-container" *ngIf="state()">
<!-- Results info -->
<div class="pagination-info">
<span class="info-text">
Showing <strong>{{ state()!.startIndex }}</strong>
to <strong>{{ state()!.endIndex }}</strong>
of <strong>{{ state()!.totalItems }}</strong> {{ itemLabel() }}
</span>
</div>
<div class="pagination-actions">
<!-- Page size selector -->
@if (showPageSizeSelector()) {
<mat-form-field class="page-size-selector" appearance="outline">
<mat-select
[value]="state()!.pageSize"
(selectionChange)="onPageSizeChange($event.value)"
aria-label="Items per page">
@for (option of pageSizeOptions(); track option) {
<mat-option [value]="option">{{ option }} per page</mat-option>
}
</mat-select>
</mat-form-field>
}
<!-- Pagination controls -->
<div class="pagination-controls">
<!-- First page -->
@if (showFirstLast()) {
<button
mat-icon-button
(click)="onPageChange(1)"
[disabled]="!state()!.hasPrev"
matTooltip="First page"
aria-label="Go to first page">
<mat-icon>first_page</mat-icon>
</button>
}
<!-- Previous page -->
<button
mat-icon-button
(click)="onPageChange(state()!.currentPage - 1)"
[disabled]="!state()!.hasPrev"
matTooltip="Previous page"
aria-label="Go to previous page">
<mat-icon>chevron_left</mat-icon>
</button>
<!-- Page numbers -->
<div class="page-numbers">
@for (page of pageNumbers(); track page) {
@if (page === '...') {
<span class="ellipsis">{{ page }}</span>
} @else {
<button
mat-button
class="page-button"
[class.active]="page === state()!.currentPage"
(click)="handlePageClick(page)"
[attr.aria-label]="'Go to page ' + page"
[attr.aria-current]="page === state()!.currentPage ? 'page' : null">
{{ page }}
</button>
}
}
</div>
<!-- Next page -->
<button
mat-icon-button
(click)="onPageChange(state()!.currentPage + 1)"
[disabled]="!state()!.hasNext"
matTooltip="Next page"
aria-label="Go to next page">
<mat-icon>chevron_right</mat-icon>
</button>
<!-- Last page -->
@if (showFirstLast()) {
<button
mat-icon-button
(click)="onPageChange(state()!.totalPages)"
[disabled]="!state()!.hasNext"
matTooltip="Last page"
aria-label="Go to last page">
<mat-icon>last_page</mat-icon>
</button>
}
</div>
</div>
</div>
`,
styles: [`
.pagination-container {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
background: var(--surface-color, #fff);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.pagination-info {
display: flex;
align-items: center;
justify-content: center;
}
.info-text {
font-size: 0.875rem;
color: var(--text-secondary, #666);
}
.info-text strong {
color: var(--text-primary, #333);
font-weight: 600;
}
.pagination-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.page-size-selector {
min-width: 140px;
}
.page-size-selector ::ng-deep .mat-mdc-form-field-subscript-wrapper {
display: none;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.page-numbers {
display: flex;
align-items: center;
gap: 0.25rem;
}
.page-button {
min-width: 40px;
height: 40px;
padding: 0 8px;
border-radius: 4px;
transition: all 0.2s ease;
}
.page-button:hover {
background-color: var(--hover-color, rgba(0, 0, 0, 0.04));
}
.page-button.active {
background-color: var(--primary-color, #1976d2);
color: white;
font-weight: 600;
}
.page-button.active:hover {
background-color: var(--primary-dark, #1565c0);
}
.ellipsis {
padding: 0 8px;
color: var(--text-secondary, #666);
user-select: none;
}
/* Responsive design */
@media (max-width: 768px) {
.pagination-container {
padding: 0.75rem;
}
.pagination-actions {
flex-direction: column;
align-items: stretch;
}
.page-size-selector {
width: 100%;
}
.pagination-controls {
justify-content: center;
}
.page-numbers {
gap: 0.125rem;
}
.page-button {
min-width: 36px;
height: 36px;
font-size: 0.875rem;
}
/* Hide ellipsis on very small screens */
@media (max-width: 480px) {
.ellipsis {
display: none;
}
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.pagination-container {
background: var(--surface-dark, #1e1e1e);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.info-text {
color: var(--text-secondary-dark, #b0b0b0);
}
.info-text strong {
color: var(--text-primary-dark, #e0e0e0);
}
.page-button:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.ellipsis {
color: var(--text-secondary-dark, #b0b0b0);
}
}
/* Focus styles for accessibility */
.page-button:focus-visible,
button:focus-visible {
outline: 2px solid var(--primary-color, #1976d2);
outline-offset: 2px;
}
/* Disabled state */
button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
`]
})
export class PaginationComponent {
// Input signals
state = input.required<PaginationState | null>();
pageNumbers = input<(number | string)[]>([]);
pageSizeOptions = input<number[]>([10, 25, 50, 100]);
showPageSizeSelector = input<boolean>(true);
showFirstLast = input<boolean>(true);
maxVisiblePages = input<number>(5);
itemLabel = input<string>('results');
// Output events
pageChange = output<number>();
pageSizeChange = output<number>();
/**
* Handle page change
*/
onPageChange(page: number): void {
if (page >= 1 && page <= (this.state()?.totalPages ?? 1)) {
this.pageChange.emit(page);
}
}
/**
* Handle page button click (for page numbers)
*/
handlePageClick(page: number | string): void {
if (typeof page === 'number') {
this.onPageChange(page);
}
}
/**
* Handle page size change
*/
onPageSizeChange(pageSize: number): void {
this.pageSizeChange.emit(pageSize);
}
}