MME-34 Remove tanstack from recipe comments list.
This commit is contained in:
parent
cd532ef092
commit
b7c9e06d05
@ -11,41 +11,40 @@
|
||||
} @else {
|
||||
<p>You must be logged in to comment.</p>
|
||||
}
|
||||
<h3>Comments</h3>
|
||||
@if (commentsQuery.isPending()) {
|
||||
<p>Loading comments...</p>
|
||||
} @else if (commentsQuery.isError()) {
|
||||
<p>There was an error loading the comments.</p>
|
||||
} @else {
|
||||
<p>Showing {{ loadedCommentsCount() }} of {{ totalComments() }} comments.</p>
|
||||
<ul id="comments">
|
||||
@if (addCommentMutation.isPending()) {
|
||||
@if (submittingComment()) {
|
||||
<li class="comment" style="opacity: 0.5">
|
||||
<div class="comment-username-time">
|
||||
<span class="comment-username">{{ username() }}</span>
|
||||
</div>
|
||||
<div>{{ addCommentMutation.variables() }}</div>
|
||||
<div>{{ submittedComment() }}</div>
|
||||
<app-spinner size="12px"></app-spinner>
|
||||
</li>
|
||||
}
|
||||
@for (recipeComments of commentsQuery.data()!.pages; track $index) {
|
||||
@for (recipeComment of recipeComments.content; track recipeComment.id) {
|
||||
@for (commentsSlice of commentsSlices(); track $index) {
|
||||
@for (comment of commentsSlice.content; track $index) {
|
||||
<li class="comment">
|
||||
<div class="comment-username-time">
|
||||
<span class="comment-username">{{ recipeComment.owner.username }}</span>
|
||||
<span class="comment-time">{{ recipeComment.created | dateTimeFormat }}</span>
|
||||
<span class="comment-username">{{ comment.owner.username }}</span>
|
||||
<span class="comment-time">{{ comment.created | dateTimeFormat }}</span>
|
||||
</div>
|
||||
<div class="comment-text" [innerHTML]="recipeComment.text"></div>
|
||||
<div class="comment-text" [innerHTML]="comment.text"></div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@if (loadingComments()) {
|
||||
<app-spinner></app-spinner>
|
||||
}
|
||||
@if (loadCommentsError()) {
|
||||
<p>There was an error loading comments.</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>
|
||||
@if (hasNextSlice()) {
|
||||
<button (click)="loadMoreComments()">Load more comments</button>
|
||||
} @else {
|
||||
<p>No more comments.</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -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<string>();
|
||||
public readonly recipeSlug = input.required<string>();
|
||||
|
||||
@ -23,14 +24,99 @@ export class RecipeCommentsList {
|
||||
protected readonly username = this.authService.username;
|
||||
protected readonly isLoggedIn = computed(() => !!this.authService.accessToken());
|
||||
|
||||
protected commentsQuery = injectInfiniteQuery(() => ({
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (previousPage: SliceView<RecipeComment>) =>
|
||||
previousPage.slice.hasNext ? previousPage.slice.number + 1 : undefined,
|
||||
queryKey: ['recipeComments', this.recipeUsername(), this.recipeSlug()],
|
||||
queryFn: ({ pageParam }) => {
|
||||
protected readonly totalComments = signal<number>(0);
|
||||
|
||||
protected readonly loadingComments = signal(false);
|
||||
protected readonly loadCommentsError = signal<Error | null>(null);
|
||||
protected readonly commentsSlices = signal<SliceView<RecipeComment>[]>([]);
|
||||
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<Error | null>(null);
|
||||
protected readonly submittedComment = signal<string | null>('');
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.loadingComments.set(true);
|
||||
this.loadCommentCount();
|
||||
this.recipeService
|
||||
.getComments(this.recipeUsername(), this.recipeSlug(), {
|
||||
page: 0,
|
||||
size: 10,
|
||||
sort: [
|
||||
{
|
||||
property: 'created',
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private loadCommentCount(): void {
|
||||
this.recipeService.getCommentsCount(this.recipeUsername(), this.recipeSlug()).subscribe({
|
||||
next: (count) => {
|
||||
this.totalComments.set(count);
|
||||
},
|
||||
error: (e) => {
|
||||
console.error(e);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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: pageParam,
|
||||
page,
|
||||
size: 10,
|
||||
sort: [
|
||||
{
|
||||
@ -39,19 +125,32 @@ export class RecipeCommentsList {
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
toArray(),
|
||||
)
|
||||
.subscribe({
|
||||
next: (slices) => {
|
||||
this.commentsSlices.set(slices);
|
||||
},
|
||||
error: (e) => {
|
||||
console.error(e);
|
||||
},
|
||||
}));
|
||||
|
||||
protected readonly addCommentForm = new FormGroup({
|
||||
comment: new FormControl('', Validators.required),
|
||||
});
|
||||
|
||||
protected readonly addCommentMutation = injectMutation(() => ({
|
||||
mutationFn: (commentText: string) =>
|
||||
this.recipeService.addComment(this.recipeUsername(), this.recipeSlug(), commentText),
|
||||
}));
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<SliceView<RecipeComment>> {
|
||||
return firstValueFrom(
|
||||
this.http.get<SliceView<RecipeComment>>(
|
||||
public getComments(
|
||||
username: string,
|
||||
slug: string,
|
||||
queryParams?: QueryParams<typeof RecipeService.RecipeCommentProperties>,
|
||||
): Observable<SliceView<RecipeComment>> {
|
||||
return this.http.get<SliceView<RecipeComment>>(
|
||||
this.endpointService.getUrl('recipes', [username, slug, 'comments'], queryParams),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public async addComment(username: string, slug: string, commentText: string): Promise<RecipeComment> {
|
||||
const comment = await firstValueFrom(
|
||||
this.http.post<RecipeComment>(this.endpointService.getUrl('recipes', [username, slug, 'comments']), {
|
||||
public getCommentsCount(username: string, slug: string): Observable<number> {
|
||||
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<RecipeComment> {
|
||||
return this.http.post<RecipeComment>(this.endpointService.getUrl('recipes', [username, slug, 'comments']), {
|
||||
text: commentText,
|
||||
}),
|
||||
);
|
||||
await this.queryClient.invalidateQueries({
|
||||
queryKey: ['recipeComments', username, slug],
|
||||
});
|
||||
return comment;
|
||||
}
|
||||
|
||||
public aiSearch(prompt: string): Observable<FullRecipeView[]> {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user