+
+
- @for (suggestion of filteredSuggestions(); track suggestion.id) {
+ @for (suggestion of paginatedSuggestions(); track suggestion.id) {
}
+
+
}
`,
@@ -314,19 +331,32 @@ export class MySuggestionsComponent implements OnInit {
loading = signal(true);
statusFilter = signal<'all' | SuggestionStatus>('all');
+ // Pagination state
+ currentPage = signal(1);
+ pageSize = signal(25);
+
SuggestionStatus = SuggestionStatus;
unreviewedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.UNREVIEWED).length;
acceptedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.ACCEPTED).length;
declinedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.DECLINED).length;
- filteredSuggestions = () => {
+ filteredSuggestions = computed(() => {
const filter = this.statusFilter();
if (filter === 'all') {
return this.suggestions();
}
return this.suggestions().filter(s => s.status === filter);
- };
+ });
+
+ paginatedSuggestions = computed(() => {
+ const suggestions = this.filteredSuggestions();
+ const start = (this.currentPage() - 1) * this.pageSize();
+ const end = start + this.pageSize();
+ return suggestions.slice(start, end);
+ });
+
+ totalFilteredSuggestions = computed(() => this.filteredSuggestions().length);
ngOnInit() {
if (this.authService.isAuthenticated()) {
@@ -350,6 +380,19 @@ export class MySuggestionsComponent implements OnInit {
setFilter(filter: 'all' | SuggestionStatus) {
this.statusFilter.set(filter);
+ this.currentPage.set(1); // Reset to first page when filter changes
+ }
+
+ onPageChange(page: number) {
+ this.currentPage.set(page);
+ }
+
+ onPageSizeChange(pageSize: number) {
+ this.pageSize.set(pageSize);
+ // Calculate new current page to stay on approximately the same content
+ const firstItemIndex = (this.currentPage() - 1) * this.pageSize();
+ const newPage = Math.floor(firstItemIndex / pageSize) + 1;
+ this.currentPage.set(newPage);
}
getStatusLabel(status: SuggestionStatus): string {
diff --git a/apps/frontend/src/app/components/shared/pagination.component.ts b/apps/frontend/src/app/components/shared/pagination.component.ts
new file mode 100644
index 0000000..f7b1156
--- /dev/null
+++ b/apps/frontend/src/app/components/shared/pagination.component.ts
@@ -0,0 +1,280 @@
+/**
+ * @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: `
+
+ `,
+ 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
();
+ @Output() pageSizeChange = new EventEmitter();
+
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/apps/frontend/src/app/components/shows/shows-list.component.ts b/apps/frontend/src/app/components/shows/shows-list.component.ts
index fa7b71c..c01f1a4 100644
--- a/apps/frontend/src/app/components/shows/shows-list.component.ts
+++ b/apps/frontend/src/app/components/shows/shows-list.component.ts
@@ -12,12 +12,13 @@ import { AuthService } from '../../services/auth.service';
import { CommentsService } from '../../services/comments.service';
import { SanitizeService } from '../../services/sanitize.service';
import { SuggestionService } from '../../services/suggestion.service';
+import { PaginationComponent } from '../shared/pagination.component';
import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
@Component({
selector: 'app-shows-list',
standalone: true,
- imports: [CommonModule, FormsModule],
+ imports: [CommonModule, FormsModule, PaginationComponent],
template: `
} @else {
+
+
- @for (show of filteredShows(); track show.id) {
+ @for (show of paginatedShows(); track show.id) {
@if (show.coverImage) {
![]()
@@ -532,6 +541,14 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
}
+
+
}
`,
@@ -1029,6 +1046,10 @@ export class ShowsListComponent implements OnInit {
editingShow = signal(null);
statusFilter = signal<'all' | ShowStatus>('all');
+ // Pagination state
+ currentPage = signal(1);
+ pageSize = signal(25);
+
comments = signal>({});
commentsLoading = signal>({});
expandedComments = signal>({});
@@ -1066,6 +1087,15 @@ export class ShowsListComponent implements OnInit {
return this.shows().filter(show => show.status === filter);
});
+ paginatedShows = computed(() => {
+ const shows = this.filteredShows();
+ const start = (this.currentPage() - 1) * this.pageSize();
+ const end = start + this.pageSize();
+ return shows.slice(start, end);
+ });
+
+ totalFilteredShows = computed(() => this.filteredShows().length);
+
newShow: Partial = {
title: '',
type: ShowType.tvSeries,
@@ -1105,6 +1135,19 @@ export class ShowsListComponent implements OnInit {
setFilter(filter: 'all' | ShowStatus) {
this.statusFilter.set(filter);
+ this.currentPage.set(1); // Reset to first page when filter changes
+ }
+
+ onPageChange(page: number) {
+ this.currentPage.set(page);
+ }
+
+ onPageSizeChange(pageSize: number) {
+ this.pageSize.set(pageSize);
+ // Calculate new current page to stay on approximately the same content
+ const firstItemIndex = (this.currentPage() - 1) * this.pageSize();
+ const newPage = Math.floor(firstItemIndex / pageSize) + 1;
+ this.currentPage.set(newPage);
}
getStatusLabel(status: ShowStatus): string {