generated from nhcarrigan/template
feat: pagination
This commit is contained in:
@@ -4,17 +4,18 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
import { Component, OnInit, inject, signal, computed } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { SuggestionService } from '../../services/suggestion.service';
|
import { SuggestionService } from '../../services/suggestion.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
|
import { PaginationComponent } from '../shared/pagination.component';
|
||||||
import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-types';
|
import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-admin-suggestions',
|
selector: 'app-admin-suggestions',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule, PaginationComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
@@ -65,8 +66,16 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
<p>No suggestions found.</p>
|
<p>No suggestions found.</p>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredSuggestions()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
|
|
||||||
<div class="suggestions-list">
|
<div class="suggestions-list">
|
||||||
@for (suggestion of filteredSuggestions(); track suggestion.id) {
|
@for (suggestion of paginatedSuggestions(); track suggestion.id) {
|
||||||
<div class="suggestion-card" [class]="'status-' + suggestion.status.toLowerCase()">
|
<div class="suggestion-card" [class]="'status-' + suggestion.status.toLowerCase()">
|
||||||
<div class="suggestion-header">
|
<div class="suggestion-header">
|
||||||
<div class="badges">
|
<div class="badges">
|
||||||
@@ -185,6 +194,14 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredSuggestions()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,19 +549,32 @@ export class AdminSuggestionsComponent implements OnInit {
|
|||||||
decliningsuggestion = signal<Suggestion | null>(null);
|
decliningsuggestion = signal<Suggestion | null>(null);
|
||||||
declineReason = '';
|
declineReason = '';
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
currentPage = signal(1);
|
||||||
|
pageSize = signal(25);
|
||||||
|
|
||||||
SuggestionStatus = SuggestionStatus;
|
SuggestionStatus = SuggestionStatus;
|
||||||
|
|
||||||
unreviewedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.UNREVIEWED).length;
|
unreviewedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.UNREVIEWED).length;
|
||||||
acceptedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.ACCEPTED).length;
|
acceptedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.ACCEPTED).length;
|
||||||
declinedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.DECLINED).length;
|
declinedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.DECLINED).length;
|
||||||
|
|
||||||
filteredSuggestions = () => {
|
filteredSuggestions = computed(() => {
|
||||||
const filter = this.statusFilter();
|
const filter = this.statusFilter();
|
||||||
if (filter === 'all') {
|
if (filter === 'all') {
|
||||||
return this.suggestions();
|
return this.suggestions();
|
||||||
}
|
}
|
||||||
return this.suggestions().filter(s => s.status === filter);
|
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() {
|
ngOnInit() {
|
||||||
if (this.authService.isAdmin()) {
|
if (this.authService.isAdmin()) {
|
||||||
@@ -568,6 +598,19 @@ export class AdminSuggestionsComponent implements OnInit {
|
|||||||
|
|
||||||
setFilter(filter: 'all' | SuggestionStatus) {
|
setFilter(filter: 'all' | SuggestionStatus) {
|
||||||
this.statusFilter.set(filter);
|
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 {
|
getStatusLabel(status: SuggestionStatus): string {
|
||||||
|
|||||||
@@ -4,17 +4,18 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
import { Component, OnInit, inject, signal, computed } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { UserService } from '../../services/user.service';
|
import { UserService } from '../../services/user.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
|
import { PaginationComponent } from '../shared/pagination.component';
|
||||||
import { User } from '@library/shared-types';
|
import { User } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-admin-users',
|
selector: 'app-admin-users',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, PaginationComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="admin-container">
|
<div class="admin-container">
|
||||||
<h2>User Management</h2>
|
<h2>User Management</h2>
|
||||||
@@ -24,8 +25,16 @@ import { User } from '@library/shared-types';
|
|||||||
} @else if (error()) {
|
} @else if (error()) {
|
||||||
<p class="error">{{ error() }}</p>
|
<p class="error">{{ error() }}</p>
|
||||||
} @else {
|
} @else {
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalUsers()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
|
|
||||||
<div class="users-list">
|
<div class="users-list">
|
||||||
@for (user of users(); track user.id) {
|
@for (user of paginatedUsers(); track user.id) {
|
||||||
<div class="user-card" [class.banned]="user.isBanned">
|
<div class="user-card" [class.banned]="user.isBanned">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
@if (user.avatar) {
|
@if (user.avatar) {
|
||||||
@@ -70,6 +79,14 @@ import { User } from '@library/shared-types';
|
|||||||
<p class="no-users">No users found.</p>
|
<p class="no-users">No users found.</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalUsers()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@@ -270,6 +287,18 @@ export class AdminUsersComponent implements OnInit {
|
|||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
error = signal<string | null>(null);
|
error = signal<string | null>(null);
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
currentPage = signal(1);
|
||||||
|
pageSize = signal(25);
|
||||||
|
|
||||||
|
paginatedUsers = computed(() => {
|
||||||
|
const start = (this.currentPage() - 1) * this.pageSize();
|
||||||
|
const end = start + this.pageSize();
|
||||||
|
return this.users().slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
totalUsers = computed(() => this.users().length);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!this.authService.isAdmin()) {
|
if (!this.authService.isAdmin()) {
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
@@ -320,4 +349,16 @@ export class AdminUsersComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPageChange(page: number): void {
|
||||||
|
this.currentPage.set(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageSizeChange(pageSize: number): void {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
import { Component, OnInit, inject, signal, computed } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ArtService } from '../../services/art.service';
|
import { ArtService } from '../../services/art.service';
|
||||||
@@ -12,12 +12,13 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.service';
|
import { SuggestionService } from '../../services/suggestion.service';
|
||||||
|
import { PaginationComponent } from '../shared/pagination.component';
|
||||||
import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-art-gallery',
|
selector: 'app-art-gallery',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule, PaginationComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
@@ -337,8 +338,16 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
|
|||||||
<p>No artwork in the gallery yet.</p>
|
<p>No artwork in the gallery yet.</p>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalArtPieces()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
|
|
||||||
<div class="gallery-grid">
|
<div class="gallery-grid">
|
||||||
@for (art of artPieces(); track art.id) {
|
@for (art of paginatedArtPieces(); track art.id) {
|
||||||
<div class="art-card">
|
<div class="art-card">
|
||||||
<div class="art-image-container">
|
<div class="art-image-container">
|
||||||
<img
|
<img
|
||||||
@@ -465,6 +474,14 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalArtPieces()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (lightboxArt()) {
|
@if (lightboxArt()) {
|
||||||
@@ -1047,6 +1064,10 @@ export class ArtGalleryComponent implements OnInit {
|
|||||||
editingArt = signal<Art | null>(null);
|
editingArt = signal<Art | null>(null);
|
||||||
lightboxArt = signal<Art | null>(null);
|
lightboxArt = signal<Art | null>(null);
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
currentPage = signal(1);
|
||||||
|
pageSize = signal(25);
|
||||||
|
|
||||||
// Comments state
|
// Comments state
|
||||||
comments = signal<Record<string, Comment[]>>({});
|
comments = signal<Record<string, Comment[]>>({});
|
||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
@@ -1083,6 +1104,16 @@ export class ArtGalleryComponent implements OnInit {
|
|||||||
editLinkTitle = '';
|
editLinkTitle = '';
|
||||||
editLinkUrl = '';
|
editLinkUrl = '';
|
||||||
|
|
||||||
|
// Computed properties for pagination
|
||||||
|
paginatedArtPieces = computed(() => {
|
||||||
|
const artPieces = this.artPieces();
|
||||||
|
const start = (this.currentPage() - 1) * this.pageSize();
|
||||||
|
const end = start + this.pageSize();
|
||||||
|
return artPieces.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
totalArtPieces = computed(() => this.artPieces().length);
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadArt();
|
this.loadArt();
|
||||||
}
|
}
|
||||||
@@ -1388,4 +1419,16 @@ export class ArtGalleryComponent implements OnInit {
|
|||||||
alert('Failed to submit suggestion. Please try again.');
|
alert('Failed to submit suggestion. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.service';
|
import { SuggestionService } from '../../services/suggestion.service';
|
||||||
|
import { PaginationComponent } from '../shared/pagination.component';
|
||||||
import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-books-list',
|
selector: 'app-books-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule, PaginationComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
@@ -434,8 +435,16 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
<p>No books found in this category.</p>
|
<p>No books found in this category.</p>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredBooks()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
|
|
||||||
<div class="books-grid">
|
<div class="books-grid">
|
||||||
@for (book of filteredBooks(); track book.id) {
|
@for (book of paginatedBooks(); track book.id) {
|
||||||
<div class="book-card" [class.finished]="book.status === BookStatus.finished">
|
<div class="book-card" [class.finished]="book.status === BookStatus.finished">
|
||||||
@if (book.coverImage) {
|
@if (book.coverImage) {
|
||||||
<img [src]="book.coverImage" [alt]="book.title" class="book-cover">
|
<img [src]="book.coverImage" [alt]="book.title" class="book-cover">
|
||||||
@@ -584,6 +593,14 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredBooks()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@@ -1173,6 +1190,10 @@ export class BooksListComponent implements OnInit {
|
|||||||
editingBook = signal<Book | null>(null);
|
editingBook = signal<Book | null>(null);
|
||||||
statusFilter = signal<'all' | BookStatus>('all');
|
statusFilter = signal<'all' | BookStatus>('all');
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
currentPage = signal(1);
|
||||||
|
pageSize = signal(25);
|
||||||
|
|
||||||
// Comments state
|
// Comments state
|
||||||
comments = signal<Record<string, Comment[]>>({});
|
comments = signal<Record<string, Comment[]>>({});
|
||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
@@ -1208,12 +1229,21 @@ export class BooksListComponent implements OnInit {
|
|||||||
|
|
||||||
filteredBooks = computed(() => {
|
filteredBooks = computed(() => {
|
||||||
const filter = this.statusFilter();
|
const filter = this.statusFilter();
|
||||||
if (filter === 'all') {
|
const books = filter === 'all'
|
||||||
return this.books();
|
? this.books()
|
||||||
}
|
: this.books().filter(book => book.status === filter);
|
||||||
return this.books().filter(book => book.status === filter);
|
return books;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
paginatedBooks = computed(() => {
|
||||||
|
const books = this.filteredBooks();
|
||||||
|
const start = (this.currentPage() - 1) * this.pageSize();
|
||||||
|
const end = start + this.pageSize();
|
||||||
|
return books.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
totalFilteredBooks = computed(() => this.filteredBooks().length);
|
||||||
|
|
||||||
newBook: Partial<CreateBookDto> = {
|
newBook: Partial<CreateBookDto> = {
|
||||||
title: '',
|
title: '',
|
||||||
author: '',
|
author: '',
|
||||||
@@ -1254,6 +1284,19 @@ export class BooksListComponent implements OnInit {
|
|||||||
|
|
||||||
setFilter(filter: 'all' | BookStatus) {
|
setFilter(filter: 'all' | BookStatus) {
|
||||||
this.statusFilter.set(filter);
|
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: BookStatus): string {
|
getStatusLabel(status: BookStatus): string {
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.service';
|
import { SuggestionService } from '../../services/suggestion.service';
|
||||||
|
import { PaginationComponent } from '../shared/pagination.component';
|
||||||
import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-games-list',
|
selector: 'app-games-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule, PaginationComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
@@ -398,8 +399,16 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
<p>No games found in this category.</p>
|
<p>No games found in this category.</p>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredGames()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
|
|
||||||
<div class="games-grid">
|
<div class="games-grid">
|
||||||
@for (game of filteredGames(); track game.id) {
|
@for (game of paginatedGames(); track game.id) {
|
||||||
<div class="game-card" [class.completed]="game.status === GameStatus.completed">
|
<div class="game-card" [class.completed]="game.status === GameStatus.completed">
|
||||||
@if (game.coverImage) {
|
@if (game.coverImage) {
|
||||||
<img [src]="game.coverImage" [alt]="game.title" class="game-cover">
|
<img [src]="game.coverImage" [alt]="game.title" class="game-cover">
|
||||||
@@ -537,6 +546,14 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredGames()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@@ -1029,6 +1046,10 @@ export class GamesListComponent implements OnInit {
|
|||||||
editingGame = signal<Game | null>(null);
|
editingGame = signal<Game | null>(null);
|
||||||
statusFilter = signal<'all' | GameStatus>('all');
|
statusFilter = signal<'all' | GameStatus>('all');
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
currentPage = signal(1);
|
||||||
|
pageSize = signal(25);
|
||||||
|
|
||||||
// Comments state
|
// Comments state
|
||||||
comments = signal<Record<string, Comment[]>>({});
|
comments = signal<Record<string, Comment[]>>({});
|
||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
@@ -1069,6 +1090,15 @@ export class GamesListComponent implements OnInit {
|
|||||||
return this.games().filter(game => game.status === filter);
|
return this.games().filter(game => game.status === filter);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
paginatedGames = computed(() => {
|
||||||
|
const games = this.filteredGames();
|
||||||
|
const start = (this.currentPage() - 1) * this.pageSize();
|
||||||
|
const end = start + this.pageSize();
|
||||||
|
return games.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
totalFilteredGames = computed(() => this.filteredGames().length);
|
||||||
|
|
||||||
newGame: Partial<CreateGameDto> = {
|
newGame: Partial<CreateGameDto> = {
|
||||||
title: '',
|
title: '',
|
||||||
platform: '',
|
platform: '',
|
||||||
@@ -1108,6 +1138,19 @@ export class GamesListComponent implements OnInit {
|
|||||||
|
|
||||||
setFilter(filter: 'all' | GameStatus) {
|
setFilter(filter: 'all' | GameStatus) {
|
||||||
this.statusFilter.set(filter);
|
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: GameStatus): string {
|
getStatusLabel(status: GameStatus): string {
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.service';
|
import { SuggestionService } from '../../services/suggestion.service';
|
||||||
|
import { PaginationComponent } from '../shared/pagination.component';
|
||||||
import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-manga-list',
|
selector: 'app-manga-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule, PaginationComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
@@ -401,8 +402,16 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
|
|||||||
<p>No manga found in this category.</p>
|
<p>No manga found in this category.</p>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredManga()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
|
|
||||||
<div class="manga-grid">
|
<div class="manga-grid">
|
||||||
@for (manga of filteredManga(); track manga.id) {
|
@for (manga of paginatedManga(); track manga.id) {
|
||||||
<div class="manga-card" [class.completed]="manga.status === MangaStatus.completed">
|
<div class="manga-card" [class.completed]="manga.status === MangaStatus.completed">
|
||||||
@if (manga.coverImage) {
|
@if (manga.coverImage) {
|
||||||
<img [src]="manga.coverImage" [alt]="manga.title" class="manga-cover">
|
<img [src]="manga.coverImage" [alt]="manga.title" class="manga-cover">
|
||||||
@@ -538,6 +547,14 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredManga()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@@ -1036,6 +1053,10 @@ export class MangaListComponent implements OnInit {
|
|||||||
editingManga = signal<Manga | null>(null);
|
editingManga = signal<Manga | null>(null);
|
||||||
statusFilter = signal<'all' | MangaStatus>('all');
|
statusFilter = signal<'all' | MangaStatus>('all');
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
currentPage = signal(1);
|
||||||
|
pageSize = signal(25);
|
||||||
|
|
||||||
comments = signal<Record<string, Comment[]>>({});
|
comments = signal<Record<string, Comment[]>>({});
|
||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
expandedComments = signal<Record<string, boolean>>({});
|
expandedComments = signal<Record<string, boolean>>({});
|
||||||
@@ -1072,6 +1093,15 @@ export class MangaListComponent implements OnInit {
|
|||||||
return this.mangaList().filter(m => m.status === filter);
|
return this.mangaList().filter(m => m.status === filter);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
paginatedManga = computed(() => {
|
||||||
|
const manga = this.filteredManga();
|
||||||
|
const start = (this.currentPage() - 1) * this.pageSize();
|
||||||
|
const end = start + this.pageSize();
|
||||||
|
return manga.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
totalFilteredManga = computed(() => this.filteredManga().length);
|
||||||
|
|
||||||
newManga: Partial<CreateMangaDto> = {
|
newManga: Partial<CreateMangaDto> = {
|
||||||
title: '',
|
title: '',
|
||||||
author: '',
|
author: '',
|
||||||
@@ -1111,6 +1141,19 @@ export class MangaListComponent implements OnInit {
|
|||||||
|
|
||||||
setFilter(filter: 'all' | MangaStatus) {
|
setFilter(filter: 'all' | MangaStatus) {
|
||||||
this.statusFilter.set(filter);
|
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: MangaStatus): string {
|
getStatusLabel(status: MangaStatus): string {
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.service';
|
import { SuggestionService } from '../../services/suggestion.service';
|
||||||
|
import { PaginationComponent } from '../shared/pagination.component';
|
||||||
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-music-list',
|
selector: 'app-music-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule, PaginationComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
@@ -463,8 +464,16 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
|||||||
<p>No music found with these filters.</p>
|
<p>No music found with these filters.</p>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredMusic()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
|
|
||||||
<div class="music-grid">
|
<div class="music-grid">
|
||||||
@for (music of filteredMusic(); track music.id) {
|
@for (music of paginatedMusic(); track music.id) {
|
||||||
<div class="music-card" [class.completed]="music.status === MusicStatus.completed">
|
<div class="music-card" [class.completed]="music.status === MusicStatus.completed">
|
||||||
@if (music.coverArt) {
|
@if (music.coverArt) {
|
||||||
<img [src]="music.coverArt" [alt]="music.title" class="music-cover">
|
<img [src]="music.coverArt" [alt]="music.title" class="music-cover">
|
||||||
@@ -620,6 +629,14 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredMusic()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@@ -1244,6 +1261,10 @@ export class MusicListComponent implements OnInit {
|
|||||||
typeFilter = signal<'all' | MusicType>('all');
|
typeFilter = signal<'all' | MusicType>('all');
|
||||||
statusFilter = signal<'all' | MusicStatus>('all');
|
statusFilter = signal<'all' | MusicStatus>('all');
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
currentPage = signal(1);
|
||||||
|
pageSize = signal(25);
|
||||||
|
|
||||||
// Comments state
|
// Comments state
|
||||||
comments = signal<Record<string, Comment[]>>({});
|
comments = signal<Record<string, Comment[]>>({});
|
||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
@@ -1299,6 +1320,15 @@ export class MusicListComponent implements OnInit {
|
|||||||
return filtered;
|
return filtered;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
paginatedMusic = computed(() => {
|
||||||
|
const music = this.filteredMusic();
|
||||||
|
const start = (this.currentPage() - 1) * this.pageSize();
|
||||||
|
const end = start + this.pageSize();
|
||||||
|
return music.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
totalFilteredMusic = computed(() => this.filteredMusic().length);
|
||||||
|
|
||||||
newMusic: Partial<CreateMusicDto> = {
|
newMusic: Partial<CreateMusicDto> = {
|
||||||
title: '',
|
title: '',
|
||||||
artist: '',
|
artist: '',
|
||||||
@@ -1339,10 +1369,24 @@ export class MusicListComponent implements OnInit {
|
|||||||
|
|
||||||
setTypeFilter(filter: 'all' | MusicType) {
|
setTypeFilter(filter: 'all' | MusicType) {
|
||||||
this.typeFilter.set(filter);
|
this.typeFilter.set(filter);
|
||||||
|
this.currentPage.set(1); // Reset to first page when filter changes
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatusFilter(filter: 'all' | MusicStatus) {
|
setStatusFilter(filter: 'all' | MusicStatus) {
|
||||||
this.statusFilter.set(filter);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTypeLabel(type: MusicType): string {
|
getTypeLabel(type: MusicType): string {
|
||||||
|
|||||||
@@ -4,16 +4,17 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
import { Component, OnInit, inject, signal, computed } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { SuggestionService } from '../../services/suggestion.service';
|
import { SuggestionService } from '../../services/suggestion.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
|
import { PaginationComponent } from '../shared/pagination.component';
|
||||||
import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-types';
|
import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-my-suggestions',
|
selector: 'app-my-suggestions',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, PaginationComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
@@ -64,8 +65,16 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredSuggestions()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
|
|
||||||
<div class="suggestions-list">
|
<div class="suggestions-list">
|
||||||
@for (suggestion of filteredSuggestions(); track suggestion.id) {
|
@for (suggestion of paginatedSuggestions(); track suggestion.id) {
|
||||||
<div class="suggestion-card" [class]="'status-' + suggestion.status.toLowerCase()">
|
<div class="suggestion-card" [class]="'status-' + suggestion.status.toLowerCase()">
|
||||||
<div class="suggestion-header">
|
<div class="suggestion-header">
|
||||||
<span class="entity-badge" [class]="'entity-' + suggestion.entityType.toLowerCase()">
|
<span class="entity-badge" [class]="'entity-' + suggestion.entityType.toLowerCase()">
|
||||||
@@ -123,6 +132,14 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredSuggestions()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@@ -314,19 +331,32 @@ export class MySuggestionsComponent implements OnInit {
|
|||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
statusFilter = signal<'all' | SuggestionStatus>('all');
|
statusFilter = signal<'all' | SuggestionStatus>('all');
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
currentPage = signal(1);
|
||||||
|
pageSize = signal(25);
|
||||||
|
|
||||||
SuggestionStatus = SuggestionStatus;
|
SuggestionStatus = SuggestionStatus;
|
||||||
|
|
||||||
unreviewedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.UNREVIEWED).length;
|
unreviewedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.UNREVIEWED).length;
|
||||||
acceptedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.ACCEPTED).length;
|
acceptedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.ACCEPTED).length;
|
||||||
declinedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.DECLINED).length;
|
declinedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.DECLINED).length;
|
||||||
|
|
||||||
filteredSuggestions = () => {
|
filteredSuggestions = computed(() => {
|
||||||
const filter = this.statusFilter();
|
const filter = this.statusFilter();
|
||||||
if (filter === 'all') {
|
if (filter === 'all') {
|
||||||
return this.suggestions();
|
return this.suggestions();
|
||||||
}
|
}
|
||||||
return this.suggestions().filter(s => s.status === filter);
|
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() {
|
ngOnInit() {
|
||||||
if (this.authService.isAuthenticated()) {
|
if (this.authService.isAuthenticated()) {
|
||||||
@@ -350,6 +380,19 @@ export class MySuggestionsComponent implements OnInit {
|
|||||||
|
|
||||||
setFilter(filter: 'all' | SuggestionStatus) {
|
setFilter(filter: 'all' | SuggestionStatus) {
|
||||||
this.statusFilter.set(filter);
|
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 {
|
getStatusLabel(status: SuggestionStatus): string {
|
||||||
|
|||||||
@@ -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: `
|
||||||
|
<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,12 +12,13 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.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';
|
import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-shows-list',
|
selector: 'app-shows-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule, PaginationComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
@@ -395,8 +396,16 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
|
|||||||
<p>No shows found in this category.</p>
|
<p>No shows found in this category.</p>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredShows()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
|
|
||||||
<div class="shows-grid">
|
<div class="shows-grid">
|
||||||
@for (show of filteredShows(); track show.id) {
|
@for (show of paginatedShows(); track show.id) {
|
||||||
<div class="show-card" [class.completed]="show.status === ShowStatus.completed">
|
<div class="show-card" [class.completed]="show.status === ShowStatus.completed">
|
||||||
@if (show.coverImage) {
|
@if (show.coverImage) {
|
||||||
<img [src]="show.coverImage" [alt]="show.title" class="show-cover">
|
<img [src]="show.coverImage" [alt]="show.title" class="show-cover">
|
||||||
@@ -532,6 +541,14 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-pagination
|
||||||
|
[currentPage]="currentPage()"
|
||||||
|
[pageSize]="pageSize()"
|
||||||
|
[totalItems]="totalFilteredShows()"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
></app-pagination>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@@ -1029,6 +1046,10 @@ export class ShowsListComponent implements OnInit {
|
|||||||
editingShow = signal<Show | null>(null);
|
editingShow = signal<Show | null>(null);
|
||||||
statusFilter = signal<'all' | ShowStatus>('all');
|
statusFilter = signal<'all' | ShowStatus>('all');
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
currentPage = signal(1);
|
||||||
|
pageSize = signal(25);
|
||||||
|
|
||||||
comments = signal<Record<string, Comment[]>>({});
|
comments = signal<Record<string, Comment[]>>({});
|
||||||
commentsLoading = signal<Record<string, boolean>>({});
|
commentsLoading = signal<Record<string, boolean>>({});
|
||||||
expandedComments = signal<Record<string, boolean>>({});
|
expandedComments = signal<Record<string, boolean>>({});
|
||||||
@@ -1066,6 +1087,15 @@ export class ShowsListComponent implements OnInit {
|
|||||||
return this.shows().filter(show => show.status === filter);
|
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<CreateShowDto> = {
|
newShow: Partial<CreateShowDto> = {
|
||||||
title: '',
|
title: '',
|
||||||
type: ShowType.tvSeries,
|
type: ShowType.tvSeries,
|
||||||
@@ -1105,6 +1135,19 @@ export class ShowsListComponent implements OnInit {
|
|||||||
|
|
||||||
setFilter(filter: 'all' | ShowStatus) {
|
setFilter(filter: 'all' | ShowStatus) {
|
||||||
this.statusFilter.set(filter);
|
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 {
|
getStatusLabel(status: ShowStatus): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user