Add basic commenting to recipes.
This commit is contained in:
parent
2f6de0ade3
commit
714fe60d9f
19
src/app/model/RecipeComment.model.ts
Normal file
19
src/app/model/RecipeComment.model.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { ResourceOwner } from './ResourceOwner.model';
|
||||||
|
|
||||||
|
export interface RecipeComments {
|
||||||
|
slice: {
|
||||||
|
number: number;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
content: RecipeComment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecipeComment {
|
||||||
|
id: number;
|
||||||
|
created: string;
|
||||||
|
modified: string | null;
|
||||||
|
text: string;
|
||||||
|
rawText: string | null;
|
||||||
|
owner: ResourceOwner;
|
||||||
|
recipeId: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
<div id="comments-list-container">
|
||||||
|
<h2>Comments</h2>
|
||||||
|
@if (isLoggedIn()) {
|
||||||
|
<div id="add-comment-container">
|
||||||
|
<h3>Add Comment</h3>
|
||||||
|
<form [formGroup]="addCommentForm" (ngSubmit)="onCommentSubmit()">
|
||||||
|
<label for="comment">Comment</label>
|
||||||
|
<input id="comment" formControlName="comment" type="text" />
|
||||||
|
|
||||||
|
<button type="submit" [disabled]="!addCommentForm.valid">Post Comment</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<p>You must be logged in to comment.</p>
|
||||||
|
}
|
||||||
|
<h3>Comments</h3>
|
||||||
|
@if (commentsQuery.isLoading()) {
|
||||||
|
<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) {
|
||||||
|
<ul>
|
||||||
|
@if (addCommentMutation.isPending()) {
|
||||||
|
<li style="opacity: 0.5">
|
||||||
|
<p>{{ username() }}</p>
|
||||||
|
<div>{{ addCommentMutation.variables() }}</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@for (comment of comments; track $index) {
|
||||||
|
<li>
|
||||||
|
<p>{{ comment.owner.username }} at {{ comment.created }}</p>
|
||||||
|
<div [innerHTML]="comment.text"></div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
} @else {
|
||||||
|
<p>There are no comments yet.</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { RecipeCommentsList } from './recipe-comments-list.component';
|
||||||
|
|
||||||
|
describe('CommentsList', () => {
|
||||||
|
let component: RecipeCommentsList;
|
||||||
|
let fixture: ComponentFixture<RecipeCommentsList>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [RecipeCommentsList],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(RecipeCommentsList);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
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 { AuthService } from '../service/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comments-list',
|
||||||
|
imports: [ReactiveFormsModule],
|
||||||
|
templateUrl: './recipe-comments-list.component.html',
|
||||||
|
styleUrl: './recipe-comments-list.component.css',
|
||||||
|
})
|
||||||
|
export class RecipeCommentsList {
|
||||||
|
public readonly recipeUsername = input.required<string>();
|
||||||
|
public readonly recipeSlug = input.required<string>();
|
||||||
|
|
||||||
|
private readonly recipeService = inject(RecipeService);
|
||||||
|
private readonly authService = inject(AuthService);
|
||||||
|
|
||||||
|
protected readonly username = this.authService.username;
|
||||||
|
protected readonly isLoggedIn = computed(() => !!this.authService.accessToken());
|
||||||
|
|
||||||
|
protected readonly commentsQuery = injectQuery(() => ({
|
||||||
|
queryKey: ['recipeComments', this.recipeUsername(), this.recipeSlug()],
|
||||||
|
queryFn: () => this.recipeService.getComments(this.recipeUsername(), this.recipeSlug()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
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!);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -38,4 +38,5 @@
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<div [innerHTML]="recipe.text"></div>
|
<div [innerHTML]="recipe.text"></div>
|
||||||
|
<app-comments-list [recipeUsername]="recipe.owner.username" [recipeSlug]="recipe.slug" />
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@ -6,10 +6,11 @@ import { faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-ico
|
|||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { RecipeService } from '../../service/recipe.service';
|
import { RecipeService } from '../../service/recipe.service';
|
||||||
import { AuthService } from '../../service/auth.service';
|
import { AuthService } from '../../service/auth.service';
|
||||||
|
import { RecipeCommentsList } from '../../recipe-comments-list/recipe-comments-list.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-recipe-page-content',
|
selector: 'app-recipe-page-content',
|
||||||
imports: [FaIconComponent],
|
imports: [FaIconComponent, RecipeCommentsList],
|
||||||
templateUrl: './recipe-page-content.html',
|
templateUrl: './recipe-page-content.html',
|
||||||
styleUrl: './recipe-page-content.css',
|
styleUrl: './recipe-page-content.css',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { firstValueFrom, lastValueFrom, map } from 'rxjs';
|
|||||||
import { Recipe, RecipeInfoViews, RecipeView } from '../model/Recipe.model';
|
import { Recipe, RecipeInfoViews, RecipeView } from '../model/Recipe.model';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { QueryClient } from '@tanstack/angular-query-experimental';
|
import { QueryClient } from '@tanstack/angular-query-experimental';
|
||||||
|
import { RecipeComment, RecipeComments } from '../model/RecipeComment.model';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@ -21,7 +22,7 @@ export class RecipeService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecipeView(username: string, slug: string): Promise<RecipeView> {
|
public getRecipeView(username: string, slug: string): Promise<RecipeView> {
|
||||||
return firstValueFrom(
|
return firstValueFrom(
|
||||||
this.http.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`),
|
this.http.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`),
|
||||||
);
|
);
|
||||||
@ -31,7 +32,7 @@ export class RecipeService {
|
|||||||
return `http://localhost:8080/recipes/${recipeView.recipe.owner.username}/${recipeView.recipe.slug}`;
|
return `http://localhost:8080/recipes/${recipeView.recipe.owner.username}/${recipeView.recipe.slug}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async toggleStar(recipeView: RecipeView) {
|
public async toggleStar(recipeView: RecipeView): Promise<void> {
|
||||||
if (this.authService.accessToken()) {
|
if (this.authService.accessToken()) {
|
||||||
if (recipeView.isStarred) {
|
if (recipeView.isStarred) {
|
||||||
await lastValueFrom(this.http.delete(this.getRecipeUrl(recipeView) + '/star'));
|
await lastValueFrom(this.http.delete(this.getRecipeUrl(recipeView) + '/star'));
|
||||||
@ -45,4 +46,31 @@ export class RecipeService {
|
|||||||
throw new Error('Cannot star a recipe when not logged in.');
|
throw new Error('Cannot star a recipe when not logged in.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getComments(username: string, slug: string): Promise<RecipeComment[]> {
|
||||||
|
return firstValueFrom(
|
||||||
|
this.http
|
||||||
|
.get<RecipeComments>(`http://localhost:8080/recipes/${username}/${slug}/comments`)
|
||||||
|
.pipe(map((res) => res.content)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addComment(
|
||||||
|
username: string,
|
||||||
|
slug: string,
|
||||||
|
commentText: string,
|
||||||
|
): Promise<RecipeComment> {
|
||||||
|
const comment = await firstValueFrom(
|
||||||
|
this.http.post<RecipeComment>(
|
||||||
|
`http://localhost:8080/recipes/${username}/${slug}/comments`,
|
||||||
|
{
|
||||||
|
text: commentText,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await this.queryClient.invalidateQueries({
|
||||||
|
queryKey: ['recipeComments', username, slug],
|
||||||
|
});
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user