diff --git a/src/app/shared/components/recipe-comments-list/recipe-comments-list.html b/src/app/shared/components/recipe-comments-list/recipe-comments-list.html
index c6d3085..8e8b000 100644
--- a/src/app/shared/components/recipe-comments-list/recipe-comments-list.html
+++ b/src/app/shared/components/recipe-comments-list/recipe-comments-list.html
@@ -11,41 +11,40 @@
} @else {
You must be logged in to comment.
}
- Comments
- @if (commentsQuery.isPending()) {
- Loading comments...
- } @else if (commentsQuery.isError()) {
- There was an error loading the comments.
- } @else {
-
+
+ @if (hasNextSlice()) {
+
+ } @else {
+
No more comments.
+ }
+
diff --git a/src/app/shared/components/recipe-comments-list/recipe-comments-list.ts b/src/app/shared/components/recipe-comments-list/recipe-comments-list.ts
index 08cc76c..d9aa5b5 100644
--- a/src/app/shared/components/recipe-comments-list/recipe-comments-list.ts
+++ b/src/app/shared/components/recipe-comments-list/recipe-comments-list.ts
@@ -1,19 +1,20 @@
-import { Component, computed, inject, input } from '@angular/core';
+import { Component, computed, inject, input, OnInit, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { RecipeService } from '../../services/RecipeService';
-import { injectInfiniteQuery, injectMutation } from '@tanstack/angular-query-experimental';
import { AuthService } from '../../services/AuthService';
import { DateTimeFormatPipe } from '../../pipes/dateTimeFormat.pipe';
import { SliceView } from '../../models/SliceView.model';
import { RecipeComment } from '../../models/RecipeComment.model';
+import { Spinner } from '../spinner/spinner';
+import { range, switchMap, toArray } from 'rxjs';
@Component({
selector: 'app-recipe-comments-list',
- imports: [ReactiveFormsModule, DateTimeFormatPipe],
+ imports: [ReactiveFormsModule, DateTimeFormatPipe, Spinner],
templateUrl: './recipe-comments-list.html',
styleUrl: './recipe-comments-list.css',
})
-export class RecipeCommentsList {
+export class RecipeCommentsList implements OnInit {
public readonly recipeUsername = input.required();
public readonly recipeSlug = input.required();
@@ -23,14 +24,32 @@ export class RecipeCommentsList {
protected readonly username = this.authService.username;
protected readonly isLoggedIn = computed(() => !!this.authService.accessToken());
- protected commentsQuery = injectInfiniteQuery(() => ({
- initialPageParam: 0,
- getNextPageParam: (previousPage: SliceView) =>
- previousPage.slice.hasNext ? previousPage.slice.number + 1 : undefined,
- queryKey: ['recipeComments', this.recipeUsername(), this.recipeSlug()],
- queryFn: ({ pageParam }) => {
- return this.recipeService.getComments(this.recipeUsername(), this.recipeSlug(), {
- page: pageParam,
+ protected readonly totalComments = signal(0);
+
+ protected readonly loadingComments = signal(false);
+ protected readonly loadCommentsError = signal(null);
+ protected readonly commentsSlices = signal[]>([]);
+ protected readonly hasNextSlice = computed(() =>
+ this.commentsSlices().length ? this.commentsSlices()[this.commentsSlices().length - 1].slice.hasNext : null,
+ );
+ protected readonly loadedCommentsCount = computed(() =>
+ this.commentsSlices().reduce((acc, slice) => acc + slice.content.length, 0),
+ );
+
+ protected readonly addCommentForm = new FormGroup({
+ comment: new FormControl('', Validators.required),
+ });
+
+ protected readonly submittingComment = signal(false);
+ protected readonly submitCommentError = signal(null);
+ protected readonly submittedComment = signal('');
+
+ public ngOnInit(): void {
+ this.loadingComments.set(true);
+ this.loadCommentCount();
+ this.recipeService
+ .getComments(this.recipeUsername(), this.recipeSlug(), {
+ page: 0,
size: 10,
sort: [
{
@@ -38,20 +57,100 @@ export class RecipeCommentsList {
order: 'DESC',
},
],
+ })
+ .subscribe({
+ next: (commentsSlice) => {
+ this.loadingComments.set(false);
+ this.commentsSlices.update((old) => [...old, commentsSlice]);
+ },
+ error: (e) => {
+ this.loadingComments.set(false);
+ this.loadCommentsError.set(e);
+ console.error(e);
+ },
});
- },
- }));
+ }
- protected readonly addCommentForm = new FormGroup({
- comment: new FormControl('', Validators.required),
- });
+ private loadCommentCount(): void {
+ this.recipeService.getCommentsCount(this.recipeUsername(), this.recipeSlug()).subscribe({
+ next: (count) => {
+ this.totalComments.set(count);
+ },
+ error: (e) => {
+ console.error(e);
+ },
+ });
+ }
- protected readonly addCommentMutation = injectMutation(() => ({
- mutationFn: (commentText: string) =>
- this.recipeService.addComment(this.recipeUsername(), this.recipeSlug(), commentText),
- }));
+ protected loadMoreComments(): void {
+ if (this.hasNextSlice()) {
+ this.loadingComments.set(true);
+ this.recipeService
+ .getComments(this.recipeUsername(), this.recipeSlug(), {
+ page: this.commentsSlices().length,
+ size: 10,
+ sort: [
+ {
+ property: 'created',
+ order: 'DESC',
+ },
+ ],
+ })
+ .subscribe({
+ next: (nextSlice) => {
+ this.loadingComments.set(false);
+ this.commentsSlices.update((prev) => [...prev, nextSlice]);
+ },
+ error: (e) => {
+ this.loadingComments.set(false);
+ this.loadCommentsError.set(e);
+ console.error(e);
+ },
+ });
+ }
+ }
+
+ private reloadComments(): void {
+ this.loadCommentCount();
+ range(0, this.commentsSlices().length)
+ .pipe(
+ switchMap((page) => {
+ return this.recipeService.getComments(this.recipeUsername(), this.recipeSlug(), {
+ page,
+ size: 10,
+ sort: [
+ {
+ property: 'created',
+ order: 'DESC',
+ },
+ ],
+ });
+ }),
+ toArray(),
+ )
+ .subscribe({
+ next: (slices) => {
+ this.commentsSlices.set(slices);
+ },
+ error: (e) => {
+ console.error(e);
+ },
+ });
+ }
protected onCommentSubmit() {
- this.addCommentMutation.mutate(this.addCommentForm.value.comment!);
+ const comment = this.addCommentForm.value.comment!;
+ this.submittingComment.set(true);
+ this.submittedComment.set(comment);
+ this.recipeService.addComment(this.recipeUsername(), this.recipeSlug(), comment).subscribe({
+ next: () => {
+ this.submittingComment.set(false);
+ this.reloadComments();
+ },
+ error: (e) => {
+ this.submitCommentError.set(e);
+ console.error(e);
+ },
+ });
}
}
diff --git a/src/app/shared/services/RecipeService.ts b/src/app/shared/services/RecipeService.ts
index 30cf7be..5e1b633 100644
--- a/src/app/shared/services/RecipeService.ts
+++ b/src/app/shared/services/RecipeService.ts
@@ -1,6 +1,6 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
-import { firstValueFrom, lastValueFrom, map, Observable } from 'rxjs';
+import { lastValueFrom, map, Observable } from 'rxjs';
import { FullRecipeView, FullRecipeViewWrapper, RecipeInfoView } from '../models/Recipe.model';
import { AuthService } from './AuthService';
import { QueryClient } from '@tanstack/angular-query-experimental';
@@ -27,6 +27,8 @@ export class RecipeService {
'totalTime',
] as const;
+ public static readonly RecipeCommentProperties = ['id', 'created', 'modified'] as const;
+
private readonly http = inject(HttpClient);
private readonly authService = inject(AuthService);
private readonly queryClient = inject(QueryClient);
@@ -123,24 +125,26 @@ export class RecipeService {
}
}
- public getComments(username: string, slug: string, queryParams?: QueryParams): Promise> {
- return firstValueFrom(
- this.http.get>(
- this.endpointService.getUrl('recipes', [username, slug, 'comments'], queryParams),
- ),
+ public getComments(
+ username: string,
+ slug: string,
+ queryParams?: QueryParams,
+ ): Observable> {
+ return this.http.get>(
+ this.endpointService.getUrl('recipes', [username, slug, 'comments'], queryParams),
);
}
- public async addComment(username: string, slug: string, commentText: string): Promise {
- const comment = await firstValueFrom(
- this.http.post(this.endpointService.getUrl('recipes', [username, slug, 'comments']), {
- text: commentText,
- }),
- );
- await this.queryClient.invalidateQueries({
- queryKey: ['recipeComments', username, slug],
+ public getCommentsCount(username: string, slug: string): Observable {
+ return this.http
+ .get<{ count: number }>(this.endpointService.getUrl('recipes', [username, slug, 'comments', 'count']))
+ .pipe(map((res) => res.count));
+ }
+
+ public addComment(username: string, slug: string, commentText: string): Observable {
+ return this.http.post(this.endpointService.getUrl('recipes', [username, slug, 'comments']), {
+ text: commentText,
});
- return comment;
}
public aiSearch(prompt: string): Observable {
Showing {{ loadedCommentsCount() }} of {{ totalComments() }} comments.
++ @if (submittingComment()) { +-
+
+ {{ username() }}
+
+ {{ submittedComment() }}
+
+
+ }
+ @for (commentsSlice of commentsSlices(); track $index) {
+ @for (comment of commentsSlice.content; track $index) {
+ -
- {{ username() }}
+ {{ comment.owner.username }}
+ {{ comment.created | dateTimeFormat }}
- {{ addCommentMutation.variables() }}
+
}
- @for (recipeComments of commentsQuery.data()!.pages; track $index) {
- @for (recipeComment of recipeComments.content; track recipeComment.id) {
- -
-
- {{ recipeComment.owner.username }}
- {{ recipeComment.created | dateTimeFormat }}
-
-
-
- }
- }
-
-Loading comments...
- } @else if (!commentsQuery.hasNextPage()) { -No additional comments to load.
- } -There was an error loading comments.
+ } +