Add infinite query to comments, some endpoint api stuff.

This commit is contained in:
Jesse Brault 2025-12-22 14:15:15 -06:00
parent 714fe60d9f
commit 0bc434251e
8 changed files with 123 additions and 29 deletions

3
src/app/endpoints.ts Normal file
View File

@ -0,0 +1,3 @@
export const Endpoints = {
recipes: 'recipes',
};

View File

@ -0,0 +1,11 @@
export interface QueryParams {
page?: number;
size?: number;
sort?: Array<string | Sort>;
}
export interface Sort {
property: string;
order?: 'ASC' | 'DESC';
ignoreCase?: boolean;
}

View File

@ -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[];
}

View File

@ -0,0 +1,5 @@
export interface SliceView {
hasNext: boolean;
number: number;
size: number;
}

View File

@ -14,13 +14,11 @@
<p>You must be logged in to comment.</p>
}
<h3>Comments</h3>
@if (commentsQuery.isLoading()) {
@if (commentsQuery.isPending()) {
<p>Loading comments...</p>
} @else if (commentsQuery.isError()) {
<p>There was an error loading the comments.</p>
} @else if (commentsQuery.isSuccess()) {
@let comments = commentsQuery.data();
@if (comments.length) {
} @else {
<ul>
@if (addCommentMutation.isPending()) {
<li style="opacity: 0.5">
@ -28,15 +26,23 @@
<div>{{ addCommentMutation.variables() }}</div>
</li>
}
@for (comment of comments; track $index) {
@for (recipeComments of commentsQuery.data()?.pages; track $index) {
@for (recipeComment of recipeComments.content; track recipeComment.id) {
<li>
<p>{{ comment.owner.username }} at {{ comment.created }}</p>
<div [innerHTML]="comment.text"></div>
<p>{{ recipeComment.owner.username }} at {{ recipeComment.created }}</p>
<div [innerHTML]="recipeComment.text"></div>
</li>
}
</ul>
} @else {
<p>There are no comments yet.</p>
}
</ul>
<div>
@if (commentsQuery.hasNextPage() && !commentsQuery.isFetchingNextPage()) {
<button (click)="commentsQuery.fetchNextPage()">Load more comments</button>
} @else if (commentsQuery.isFetchingNextPage()) {
<p>Loading comments...</p>
} @else if (!commentsQuery.hasNextPage()) {
<p>No additional comments to load.</p>
}
</div>
}
</div>

View File

@ -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() {

View File

@ -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}`;
}
}

View File

@ -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<Recipe[]> {
return firstValueFrom(
@ -47,11 +50,15 @@ export class RecipeService {
}
}
public getComments(username: string, slug: string): Promise<RecipeComment[]> {
public getComments(
username: string,
slug: string,
queryParams?: QueryParams,
): Promise<RecipeComments> {
return firstValueFrom(
this.http
.get<RecipeComments>(`http://localhost:8080/recipes/${username}/${slug}/comments`)
.pipe(map((res) => res.content)),
this.http.get<RecipeComments>(
this.endpointService.getUrl('recipes', [username, slug, 'comments'], queryParams),
),
);
}