diff --git a/src/app/endpoints.ts b/src/app/endpoints.ts new file mode 100644 index 0000000..4edad30 --- /dev/null +++ b/src/app/endpoints.ts @@ -0,0 +1,3 @@ +export const Endpoints = { + recipes: 'recipes', +}; diff --git a/src/app/model/Query.model.ts b/src/app/model/Query.model.ts new file mode 100644 index 0000000..237a1fe --- /dev/null +++ b/src/app/model/Query.model.ts @@ -0,0 +1,11 @@ +export interface QueryParams { + page?: number; + size?: number; + sort?: Array; +} + +export interface Sort { + property: string; + order?: 'ASC' | 'DESC'; + ignoreCase?: boolean; +} diff --git a/src/app/model/RecipeComment.model.ts b/src/app/model/RecipeComment.model.ts index 90ed00d..efeab1c 100644 --- a/src/app/model/RecipeComment.model.ts +++ b/src/app/model/RecipeComment.model.ts @@ -1,10 +1,8 @@ import { ResourceOwner } from './ResourceOwner.model'; +import { SliceView } from './SliceView.model'; export interface RecipeComments { - slice: { - number: number; - size: number; - }; + slice: SliceView; content: RecipeComment[]; } diff --git a/src/app/model/SliceView.model.ts b/src/app/model/SliceView.model.ts new file mode 100644 index 0000000..7de685c --- /dev/null +++ b/src/app/model/SliceView.model.ts @@ -0,0 +1,5 @@ +export interface SliceView { + hasNext: boolean; + number: number; + size: number; +} diff --git a/src/app/recipe-comments-list/recipe-comments-list.component.html b/src/app/recipe-comments-list/recipe-comments-list.component.html index 601bfc3..0a40a11 100644 --- a/src/app/recipe-comments-list/recipe-comments-list.component.html +++ b/src/app/recipe-comments-list/recipe-comments-list.component.html @@ -14,29 +14,35 @@

You must be logged in to comment.

}

Comments

- @if (commentsQuery.isLoading()) { + @if (commentsQuery.isPending()) {

Loading comments...

} @else if (commentsQuery.isError()) {

There was an error loading the comments.

- } @else if (commentsQuery.isSuccess()) { - @let comments = commentsQuery.data(); - @if (comments.length) { - +
+ @if (commentsQuery.hasNextPage() && !commentsQuery.isFetchingNextPage()) { + + } @else if (commentsQuery.isFetchingNextPage()) { +

Loading comments...

+ } @else if (!commentsQuery.hasNextPage()) { +

No additional comments to load.

+ } +
} diff --git a/src/app/recipe-comments-list/recipe-comments-list.component.ts b/src/app/recipe-comments-list/recipe-comments-list.component.ts index 8d58a8f..c7b111d 100644 --- a/src/app/recipe-comments-list/recipe-comments-list.component.ts +++ b/src/app/recipe-comments-list/recipe-comments-list.component.ts @@ -1,8 +1,9 @@ import { Component, computed, inject, input } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { RecipeService } from '../service/recipe.service'; -import { injectMutation, injectQuery } from '@tanstack/angular-query-experimental'; +import { injectInfiniteQuery, injectMutation } from '@tanstack/angular-query-experimental'; import { AuthService } from '../service/auth.service'; +import { RecipeComments } from '../model/RecipeComment.model'; @Component({ selector: 'app-comments-list', @@ -20,9 +21,23 @@ export class RecipeCommentsList { protected readonly username = this.authService.username; protected readonly isLoggedIn = computed(() => !!this.authService.accessToken()); - protected readonly commentsQuery = injectQuery(() => ({ + protected commentsQuery = injectInfiniteQuery(() => ({ + initialPageParam: 0, + getNextPageParam: (previousPage: RecipeComments) => + previousPage.slice.hasNext ? previousPage.slice.number + 1 : undefined, queryKey: ['recipeComments', this.recipeUsername(), this.recipeSlug()], - queryFn: () => this.recipeService.getComments(this.recipeUsername(), this.recipeSlug()), + queryFn: ({ pageParam }) => { + return this.recipeService.getComments(this.recipeUsername(), this.recipeSlug(), { + page: pageParam, + size: 1, + sort: [ + { + property: 'created', + order: 'DESC', + }, + ], + }); + }, })); protected readonly addCommentForm = new FormGroup({ @@ -32,6 +47,7 @@ export class RecipeCommentsList { protected readonly addCommentMutation = injectMutation(() => ({ mutationFn: (commentText: string) => this.recipeService.addComment(this.recipeUsername(), this.recipeSlug(), commentText), + onSuccess: () => this.commentsQuery.fetchNextPage(), })); protected onCommentSubmit() { diff --git a/src/app/service/endpoint.service.ts b/src/app/service/endpoint.service.ts new file mode 100644 index 0000000..a8be3d5 --- /dev/null +++ b/src/app/service/endpoint.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { Endpoints } from '../endpoints'; +import { QueryParams } from '../model/Query.model'; + +@Injectable({ + providedIn: 'root', +}) +export class EndpointService { + public getUrl( + endpoint: keyof typeof Endpoints, + pathParams?: string[], + queryParams?: QueryParams, + ): string { + const urlSearchParams = new URLSearchParams(); + if (queryParams?.page !== undefined) { + urlSearchParams.set('page', queryParams.page.toString()); + } + if (queryParams?.size !== undefined) { + urlSearchParams.set('size', queryParams.size.toString()); + } + queryParams?.sort?.forEach((sort) => { + if (typeof sort === 'string') { + urlSearchParams.append('sort', sort); + } else { + let sortString = sort.property; + if (sort.order) { + sortString += ',' + sort.order; + } + if (sort.ignoreCase) { + sortString += ',IgnoreCase'; + } + urlSearchParams.append('sort', sortString); + } + }); + + let pathString = pathParams?.join('/'); + if (pathString?.length) { + pathString = '/' + pathString; + } + + let queryString = urlSearchParams.toString(); + if (queryString.length) { + queryString = '?' + queryString; + } + + return `http://localhost:8080/${Endpoints[endpoint]}${pathString}${queryString}`; + } +} diff --git a/src/app/service/recipe.service.ts b/src/app/service/recipe.service.ts index 01d1443..0e1a1e1 100644 --- a/src/app/service/recipe.service.ts +++ b/src/app/service/recipe.service.ts @@ -5,6 +5,8 @@ import { Recipe, RecipeInfoViews, RecipeView } from '../model/Recipe.model'; import { AuthService } from './auth.service'; import { QueryClient } from '@tanstack/angular-query-experimental'; import { RecipeComment, RecipeComments } from '../model/RecipeComment.model'; +import { QueryParams } from '../model/Query.model'; +import { EndpointService } from './endpoint.service'; @Injectable({ providedIn: 'root', @@ -13,6 +15,7 @@ export class RecipeService { private readonly http = inject(HttpClient); private readonly authService = inject(AuthService); private readonly queryClient = inject(QueryClient); + private readonly endpointService = inject(EndpointService); public getRecipes(): Promise { return firstValueFrom( @@ -47,11 +50,15 @@ export class RecipeService { } } - public getComments(username: string, slug: string): Promise { + public getComments( + username: string, + slug: string, + queryParams?: QueryParams, + ): Promise { return firstValueFrom( - this.http - .get(`http://localhost:8080/recipes/${username}/${slug}/comments`) - .pipe(map((res) => res.content)), + this.http.get( + this.endpointService.getUrl('recipes', [username, slug, 'comments'], queryParams), + ), ); }