MME-34 Remove tanstack from recipe comments list.
This commit is contained in:
parent
cd532ef092
commit
b7c9e06d05
@ -11,41 +11,40 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<p>You must be logged in to comment.</p>
|
<p>You must be logged in to comment.</p>
|
||||||
}
|
}
|
||||||
<h3>Comments</h3>
|
<p>Showing {{ loadedCommentsCount() }} of {{ totalComments() }} comments.</p>
|
||||||
@if (commentsQuery.isPending()) {
|
<ul id="comments">
|
||||||
<p>Loading comments...</p>
|
@if (submittingComment()) {
|
||||||
} @else if (commentsQuery.isError()) {
|
<li class="comment" style="opacity: 0.5">
|
||||||
<p>There was an error loading the comments.</p>
|
<div class="comment-username-time">
|
||||||
} @else {
|
<span class="comment-username">{{ username() }}</span>
|
||||||
<ul id="comments">
|
</div>
|
||||||
@if (addCommentMutation.isPending()) {
|
<div>{{ submittedComment() }}</div>
|
||||||
<li class="comment" style="opacity: 0.5">
|
<app-spinner size="12px"></app-spinner>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@for (commentsSlice of commentsSlices(); track $index) {
|
||||||
|
@for (comment of commentsSlice.content; track $index) {
|
||||||
|
<li class="comment">
|
||||||
<div class="comment-username-time">
|
<div class="comment-username-time">
|
||||||
<span class="comment-username">{{ username() }}</span>
|
<span class="comment-username">{{ comment.owner.username }}</span>
|
||||||
|
<span class="comment-time">{{ comment.created | dateTimeFormat }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>{{ addCommentMutation.variables() }}</div>
|
<div class="comment-text" [innerHTML]="comment.text"></div>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@for (recipeComments of commentsQuery.data()!.pages; track $index) {
|
}
|
||||||
@for (recipeComment of recipeComments.content; track recipeComment.id) {
|
@if (loadingComments()) {
|
||||||
<li class="comment">
|
<app-spinner></app-spinner>
|
||||||
<div class="comment-username-time">
|
}
|
||||||
<span class="comment-username">{{ recipeComment.owner.username }}</span>
|
@if (loadCommentsError()) {
|
||||||
<span class="comment-time">{{ recipeComment.created | dateTimeFormat }}</span>
|
<p>There was an error loading comments.</p>
|
||||||
</div>
|
}
|
||||||
<div class="comment-text" [innerHTML]="recipeComment.text"></div>
|
</ul>
|
||||||
</li>
|
<div>
|
||||||
}
|
@if (hasNextSlice()) {
|
||||||
}
|
<button (click)="loadMoreComments()">Load more comments</button>
|
||||||
</ul>
|
} @else {
|
||||||
<div>
|
<p>No more comments.</p>
|
||||||
@if (commentsQuery.hasNextPage() && !commentsQuery.isFetchingNextPage()) {
|
}
|
||||||
<button (click)="commentsQuery.fetchNextPage()">Load more comments</button>
|
</div>
|
||||||
} @else if (commentsQuery.isFetchingNextPage()) {
|
|
||||||
<p>Loading comments...</p>
|
|
||||||
} @else if (!commentsQuery.hasNextPage()) {
|
|
||||||
<p>No additional comments to load.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</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 { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { RecipeService } from '../../services/RecipeService';
|
import { RecipeService } from '../../services/RecipeService';
|
||||||
import { injectInfiniteQuery, injectMutation } from '@tanstack/angular-query-experimental';
|
|
||||||
import { AuthService } from '../../services/AuthService';
|
import { AuthService } from '../../services/AuthService';
|
||||||
import { DateTimeFormatPipe } from '../../pipes/dateTimeFormat.pipe';
|
import { DateTimeFormatPipe } from '../../pipes/dateTimeFormat.pipe';
|
||||||
import { SliceView } from '../../models/SliceView.model';
|
import { SliceView } from '../../models/SliceView.model';
|
||||||
import { RecipeComment } from '../../models/RecipeComment.model';
|
import { RecipeComment } from '../../models/RecipeComment.model';
|
||||||
|
import { Spinner } from '../spinner/spinner';
|
||||||
|
import { range, switchMap, toArray } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-recipe-comments-list',
|
selector: 'app-recipe-comments-list',
|
||||||
imports: [ReactiveFormsModule, DateTimeFormatPipe],
|
imports: [ReactiveFormsModule, DateTimeFormatPipe, Spinner],
|
||||||
templateUrl: './recipe-comments-list.html',
|
templateUrl: './recipe-comments-list.html',
|
||||||
styleUrl: './recipe-comments-list.css',
|
styleUrl: './recipe-comments-list.css',
|
||||||
})
|
})
|
||||||
export class RecipeCommentsList {
|
export class RecipeCommentsList implements OnInit {
|
||||||
public readonly recipeUsername = input.required<string>();
|
public readonly recipeUsername = input.required<string>();
|
||||||
public readonly recipeSlug = input.required<string>();
|
public readonly recipeSlug = input.required<string>();
|
||||||
|
|
||||||
@ -23,14 +24,32 @@ export class RecipeCommentsList {
|
|||||||
protected readonly username = this.authService.username;
|
protected readonly username = this.authService.username;
|
||||||
protected readonly isLoggedIn = computed(() => !!this.authService.accessToken());
|
protected readonly isLoggedIn = computed(() => !!this.authService.accessToken());
|
||||||
|
|
||||||
protected commentsQuery = injectInfiniteQuery(() => ({
|
protected readonly totalComments = signal<number>(0);
|
||||||
initialPageParam: 0,
|
|
||||||
getNextPageParam: (previousPage: SliceView<RecipeComment>) =>
|
protected readonly loadingComments = signal(false);
|
||||||
previousPage.slice.hasNext ? previousPage.slice.number + 1 : undefined,
|
protected readonly loadCommentsError = signal<Error | null>(null);
|
||||||
queryKey: ['recipeComments', this.recipeUsername(), this.recipeSlug()],
|
protected readonly commentsSlices = signal<SliceView<RecipeComment>[]>([]);
|
||||||
queryFn: ({ pageParam }) => {
|
protected readonly hasNextSlice = computed(() =>
|
||||||
return this.recipeService.getComments(this.recipeUsername(), this.recipeSlug(), {
|
this.commentsSlices().length ? this.commentsSlices()[this.commentsSlices().length - 1].slice.hasNext : null,
|
||||||
page: pageParam,
|
);
|
||||||
|
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,
|
size: 10,
|
||||||
sort: [
|
sort: [
|
||||||
{
|
{
|
||||||
@ -38,20 +57,100 @@ export class RecipeCommentsList {
|
|||||||
order: 'DESC',
|
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({
|
private loadCommentCount(): void {
|
||||||
comment: new FormControl('', Validators.required),
|
this.recipeService.getCommentsCount(this.recipeUsername(), this.recipeSlug()).subscribe({
|
||||||
});
|
next: (count) => {
|
||||||
|
this.totalComments.set(count);
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
console.error(e);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly addCommentMutation = injectMutation(() => ({
|
protected loadMoreComments(): void {
|
||||||
mutationFn: (commentText: string) =>
|
if (this.hasNextSlice()) {
|
||||||
this.recipeService.addComment(this.recipeUsername(), this.recipeSlug(), commentText),
|
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() {
|
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 { inject, Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
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 { FullRecipeView, FullRecipeViewWrapper, RecipeInfoView } from '../models/Recipe.model';
|
||||||
import { AuthService } from './AuthService';
|
import { AuthService } from './AuthService';
|
||||||
import { QueryClient } from '@tanstack/angular-query-experimental';
|
import { QueryClient } from '@tanstack/angular-query-experimental';
|
||||||
@ -27,6 +27,8 @@ export class RecipeService {
|
|||||||
'totalTime',
|
'totalTime',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
public static readonly RecipeCommentProperties = ['id', 'created', 'modified'] as const;
|
||||||
|
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly authService = inject(AuthService);
|
private readonly authService = inject(AuthService);
|
||||||
private readonly queryClient = inject(QueryClient);
|
private readonly queryClient = inject(QueryClient);
|
||||||
@ -123,24 +125,26 @@ export class RecipeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getComments(username: string, slug: string, queryParams?: QueryParams): Promise<SliceView<RecipeComment>> {
|
public getComments(
|
||||||
return firstValueFrom(
|
username: string,
|
||||||
this.http.get<SliceView<RecipeComment>>(
|
slug: string,
|
||||||
this.endpointService.getUrl('recipes', [username, slug, 'comments'], queryParams),
|
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> {
|
public getCommentsCount(username: string, slug: string): Observable<number> {
|
||||||
const comment = await firstValueFrom(
|
return this.http
|
||||||
this.http.post<RecipeComment>(this.endpointService.getUrl('recipes', [username, slug, 'comments']), {
|
.get<{ count: number }>(this.endpointService.getUrl('recipes', [username, slug, 'comments', 'count']))
|
||||||
text: commentText,
|
.pipe(map((res) => res.count));
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
await this.queryClient.invalidateQueries({
|
public addComment(username: string, slug: string, commentText: string): Observable<RecipeComment> {
|
||||||
queryKey: ['recipeComments', username, slug],
|
return this.http.post<RecipeComment>(this.endpointService.getUrl('recipes', [username, slug, 'comments']), {
|
||||||
|
text: commentText,
|
||||||
});
|
});
|
||||||
return comment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public aiSearch(prompt: string): Observable<FullRecipeView[]> {
|
public aiSearch(prompt: string): Observable<FullRecipeView[]> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user