From b97803d31d1d0610fe998bb84394de58737fd785 Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Mon, 16 Feb 2026 18:03:45 -0600 Subject: [PATCH] MME-36 Add paginator to recipes-page. --- src/app/pages/recipes-page/recipes-page.html | 7 +++ src/app/pages/recipes-page/recipes-page.ts | 43 +++++++++++++++++-- .../recipe-edit-form/recipe-edit-form.ts | 6 +-- src/app/shared/services/RecipeService.ts | 35 ++++++++++++--- 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/app/pages/recipes-page/recipes-page.html b/src/app/pages/recipes-page/recipes-page.html index cf69ce0..bd99848 100644 --- a/src/app/pages/recipes-page/recipes-page.html +++ b/src/app/pages/recipes-page/recipes-page.html @@ -5,4 +5,11 @@

There was an error loading recipes: {{ loadRecipesError() }}

} @else { + } diff --git a/src/app/pages/recipes-page/recipes-page.ts b/src/app/pages/recipes-page/recipes-page.ts index 0c9f0cf..738acd6 100644 --- a/src/app/pages/recipes-page/recipes-page.ts +++ b/src/app/pages/recipes-page/recipes-page.ts @@ -3,10 +3,12 @@ import { RecipeService } from '../../shared/services/RecipeService'; import { RecipeCardGrid } from '../../shared/components/recipe-card-grid/recipe-card-grid'; import { RecipeInfoView } from '../../shared/models/Recipe.model'; import { Spinner } from '../../shared/components/spinner/spinner'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { combineLatest } from 'rxjs'; @Component({ selector: 'app-recipes-page', - imports: [RecipeCardGrid, Spinner], + imports: [RecipeCardGrid, Spinner, MatPaginator], templateUrl: './recipes-page.html', styleUrl: './recipes-page.css', }) @@ -17,12 +19,27 @@ export class RecipesPage implements OnInit { protected readonly loadRecipesError = signal(null); protected readonly recipes = signal([]); + protected readonly recipeCount = signal(50); + protected readonly currentPage = signal(0); + protected readonly pageSize = signal(10); + public ngOnInit(): void { + this.loadRecipes(); + } + + private loadRecipes() { this.loadingRecipes.set(true); - this.recipeService.getRecipes().subscribe({ - next: (sliceView) => { + combineLatest([ + this.recipeService.getRecipes({ + page: this.currentPage(), + size: this.pageSize(), + }), + this.recipeService.getRecipeCount(), + ]).subscribe({ + next: ([sliceView, count]) => { this.loadingRecipes.set(false); this.recipes.set(sliceView.content); + this.recipeCount.set(count); }, error: (e) => { this.loadingRecipes.set(false); @@ -30,4 +47,24 @@ export class RecipesPage implements OnInit { }, }); } + + protected onPage(pageEvent: PageEvent): void { + // chart + // | size-change | size-same + // page-change | reload | reload + // page-same | reload | nothing + if (pageEvent.pageSize !== this.pageSize() || pageEvent.pageIndex !== this.currentPage()) { + this.pageSize.set(pageEvent.pageSize); + this.currentPage.update((old) => { + if (pageEvent.pageIndex < old) { + return Math.max(old - 1, 0); + } else if (pageEvent.pageIndex > old) { + return (this.currentPage() + 1) * this.pageSize() <= this.recipeCount() ? old + 1 : old; + } else { + return old; + } + }); + this.loadRecipes(); + } + } } diff --git a/src/app/shared/components/recipe-edit-form/recipe-edit-form.ts b/src/app/shared/components/recipe-edit-form/recipe-edit-form.ts index 4a3ff86..1703e98 100644 --- a/src/app/shared/components/recipe-edit-form/recipe-edit-form.ts +++ b/src/app/shared/components/recipe-edit-form/recipe-edit-form.ts @@ -14,7 +14,7 @@ import { } from '@angular/core'; import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { ImageSelect } from '../../../pages/recipe-upload-page/steps/enter-recipe-data/image-select/image-select'; +import { ImageSelect } from './image-select/image-select'; import { MatButton } from '@angular/material/button'; import { MatCell, @@ -34,11 +34,11 @@ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angula import { faBars, faEllipsis } from '@fortawesome/free-solid-svg-icons'; import { IngredientDraftClientModel } from '../../client-models/IngredientDraftClientModel'; import { ImageView } from '../../models/ImageView.model'; -import { IngredientDialog } from '../../../pages/recipe-upload-page/steps/enter-recipe-data/ingredient-dialog/ingredient-dialog'; +import { IngredientDialog } from './ingredient-dialog/ingredient-dialog'; import { RecipeDraftViewModel } from '../../models/RecipeDraftView.model'; import { RecipeEditFormSubmitEvent } from './RecipeEditFormSubmitEvent'; import { MatDialog } from '@angular/material/dialog'; -import { ImageUploadDialog } from '../../../pages/recipe-upload-page/steps/enter-recipe-data/image-upload-dialog/image-upload-dialog'; +import { ImageUploadDialog } from './image-upload-dialog/image-upload-dialog'; import { FullRecipeView } from '../../models/Recipe.model'; @Component({ diff --git a/src/app/shared/services/RecipeService.ts b/src/app/shared/services/RecipeService.ts index ec0124c..21af289 100644 --- a/src/app/shared/services/RecipeService.ts +++ b/src/app/shared/services/RecipeService.ts @@ -16,6 +16,17 @@ import { RecipeUpdateBody } from '../bodies/RecipeUpdateBody'; providedIn: 'root', }) export class RecipeService { + public static readonly RecipeProperties = [ + 'id', + 'created', + 'modified', + 'title', + 'slug', + 'preparationTime', + 'cookingTime', + 'totalTime', + ] as const; + private readonly http = inject(HttpClient); private readonly authService = inject(AuthService); private readonly queryClient = inject(QueryClient); @@ -46,13 +57,23 @@ export class RecipeService { }; } - public getRecipes(): Observable> { - return this.http.get>>(this.endpointService.getUrl('recipes')).pipe( - map((sliceView) => ({ - ...sliceView, - content: sliceView.content.map((withStringDates) => this.hydrateRecipeInfoView(withStringDates)), - })), - ); + public getRecipes( + queryParams?: QueryParams, + ): Observable> { + return this.http + .get>>(this.endpointService.getUrl('recipes', [], queryParams)) + .pipe( + map((sliceView) => ({ + ...sliceView, + content: sliceView.content.map((withStringDates) => this.hydrateRecipeInfoView(withStringDates)), + })), + ); + } + + public getRecipeCount(): Observable { + return this.http + .get<{ count: number }>(this.endpointService.getUrl('recipes', ['meta', 'count'])) + .pipe(map((res) => res.count)); } public getRecipeView(username: string, slug: string): Promise {