Add starring ability.
This commit is contained in:
parent
1a51a8f751
commit
28222e0655
@ -1,11 +1,27 @@
|
||||
<h1>{{ recipe.title }}</h1>
|
||||
@if (mainImageUrl.isSuccess()) {
|
||||
<!--suppress AngularNgOptimizedImage -->
|
||||
@let recipe = recipeView().recipe;
|
||||
<article>
|
||||
<div id="recipe-header">
|
||||
<h1>{{ recipe.title }}</h1>
|
||||
@if (isLoggedIn()) {
|
||||
<button id="star" (click)="starMutation.mutate()">
|
||||
<fa-icon [icon]="faStar"/>
|
||||
<span>Star</span>
|
||||
<span id="star-count">{{ recipe.starCount }}</span>
|
||||
</button>
|
||||
} @else {
|
||||
<div>
|
||||
<fa-icon [icon]="faStar"/>
|
||||
<span id="star-count">{{ recipe.starCount }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (mainImageUrl.isSuccess()) {
|
||||
<img
|
||||
[src]="mainImageUrl.data()"
|
||||
[alt]="recipe.mainImage.alt"
|
||||
[height]="recipe.mainImage.height"
|
||||
[width]="recipe.mainImage.width"
|
||||
/>
|
||||
}
|
||||
<div [innerHTML]="recipe.text"></div>
|
||||
}
|
||||
<div [innerHTML]="recipe.text"></div>
|
||||
</article>
|
||||
|
||||
@ -1,22 +1,42 @@
|
||||
import { Component, inject, Input } from '@angular/core';
|
||||
import { Recipe } from '../../model/Recipe.model';
|
||||
import { injectQuery } from '@tanstack/angular-query-experimental';
|
||||
import { Component, computed, inject, input, Input } from '@angular/core';
|
||||
import { RecipeView } from '../../model/Recipe.model';
|
||||
import { injectMutation, injectQuery } from '@tanstack/angular-query-experimental';
|
||||
import { ImageService } from '../../service/image.service';
|
||||
import { faStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { RecipeService } from '../../service/recipe.service';
|
||||
import { AuthService } from '../../service/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recipe-page-content',
|
||||
imports: [],
|
||||
imports: [
|
||||
FaIconComponent
|
||||
],
|
||||
templateUrl: './recipe-page-content.html',
|
||||
styleUrl: './recipe-page-content.css',
|
||||
})
|
||||
export class RecipePageContent {
|
||||
@Input({ required: true })
|
||||
public recipe!: Recipe;
|
||||
|
||||
public recipeView = input.required<RecipeView>();
|
||||
|
||||
private readonly imageService = inject(ImageService);
|
||||
private readonly recipeService = inject(RecipeService);
|
||||
private readonly authService = inject(AuthService);
|
||||
|
||||
protected mainImageUrl = injectQuery(() => ({
|
||||
queryKey: ['images', this.recipe.mainImage.owner.username, this.recipe.mainImage.filename],
|
||||
queryFn: () => this.imageService.getImage(this.recipe.mainImage.url),
|
||||
protected readonly isLoggedIn = computed(() => !!this.authService.accessToken());
|
||||
|
||||
protected readonly mainImageUrl = injectQuery(() => {
|
||||
const recipe = this.recipeView().recipe;
|
||||
return {
|
||||
queryKey: ['images', recipe.mainImage.owner.username, recipe.mainImage.filename],
|
||||
queryFn: () => this.imageService.getImage(recipe.mainImage.url),
|
||||
}
|
||||
});
|
||||
|
||||
protected readonly starMutation = injectMutation(() => ({
|
||||
mutationFn: () => this.recipeService.toggleStar(this.recipeView()),
|
||||
}));
|
||||
|
||||
protected readonly faStar = faStar;
|
||||
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
@if (recipe.isLoading()) {
|
||||
@if (recipeView.isLoading()) {
|
||||
<p>Loading...</p>
|
||||
} @else if (recipe.isSuccess()) {
|
||||
<app-recipe-page-content [recipe]="recipe.data()"></app-recipe-page-content>
|
||||
} @else if (recipe.error(); as error) {
|
||||
} @else if (recipeView.isSuccess()) {
|
||||
<app-recipe-page-content [recipeView]="recipeView.data()"></app-recipe-page-content>
|
||||
} @else if (recipeView.error(); as error) {
|
||||
<p>{{ error.message }}</p>
|
||||
} @else {
|
||||
<p>There was an error loading the recipe.</p>
|
||||
|
||||
@ -16,8 +16,8 @@ export class RecipePage {
|
||||
private username = this.route.snapshot.paramMap.get('username') as string;
|
||||
private slug = this.route.snapshot.paramMap.get('slug') as string;
|
||||
|
||||
protected recipe = injectQuery(() => ({
|
||||
protected recipeView = injectQuery(() => ({
|
||||
queryKey: ['recipe', this.username, this.slug],
|
||||
queryFn: () => this.recipeService.getRecipe(this.username, this.slug),
|
||||
queryFn: () => this.recipeService.getRecipeView(this.username, this.slug),
|
||||
}));
|
||||
}
|
||||
|
||||
@ -1,5 +1,22 @@
|
||||
.recipe-card-image {
|
||||
#recipe-card-image {
|
||||
max-height: 200px;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
article {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 5px;
|
||||
}
|
||||
|
||||
#recipe-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#title-and-visibility,
|
||||
#user-and-stars {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
<article>
|
||||
<a [routerLink]="recipePageLink()">
|
||||
@if (mainImage.isSuccess()) {
|
||||
<!--suppress AngularNgOptimizedImage -->
|
||||
<img [src]="mainImage.data()" class="recipe-card-image" [alt]="recipe.mainImage.alt" />
|
||||
<img [src]="mainImage.data()" id="recipe-card-image" [alt]="recipe.mainImage.alt" />
|
||||
}
|
||||
</a>
|
||||
<div>
|
||||
<div id="title-and-visibility">
|
||||
<a [routerLink]="recipePageLink()">
|
||||
<h1>{{ recipe.title }}</h1>
|
||||
<h1 id="recipe-title">{{ recipe.title }}</h1>
|
||||
</a>
|
||||
@if (recipe.isPublic) {
|
||||
<fa-icon [icon]="faGlobe" />
|
||||
@ -15,7 +14,7 @@
|
||||
<fa-icon [icon]="faLock" />
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<div id="user-and-stars">
|
||||
<span><fa-icon [icon]="faUser" /> {{ recipe.owner.username }}</span>
|
||||
<span><fa-icon [icon]="faStar" /> {{ recipe.starCount }}</span>
|
||||
</div>
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom, map } from 'rxjs';
|
||||
import { firstValueFrom, lastValueFrom, map } from 'rxjs';
|
||||
import { Recipe, RecipeInfoViews, RecipeView } from '../model/Recipe.model';
|
||||
import { AuthService } from './auth.service';
|
||||
import { QueryClient } from '@tanstack/angular-query-experimental';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RecipeService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly authService = inject(AuthService);
|
||||
private readonly queryClient = inject(QueryClient);
|
||||
|
||||
public getRecipes(): Promise<Recipe[]> {
|
||||
return firstValueFrom(
|
||||
@ -17,11 +21,29 @@ export class RecipeService {
|
||||
);
|
||||
}
|
||||
|
||||
public async getRecipe(username: string, slug: string): Promise<Recipe> {
|
||||
public async getRecipeView(username: string, slug: string): Promise<RecipeView> {
|
||||
return firstValueFrom(
|
||||
this.http
|
||||
.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`)
|
||||
.pipe(map((recipeView) => recipeView.recipe)),
|
||||
this.http.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`)
|
||||
);
|
||||
}
|
||||
|
||||
private getRecipeUrl(recipeView: RecipeView): string {
|
||||
return `http://localhost:8080/recipes/${recipeView.recipe.owner.username}/${recipeView.recipe.slug}`;
|
||||
}
|
||||
|
||||
public async toggleStar(recipeView: RecipeView) {
|
||||
if (this.authService.accessToken()) {
|
||||
if (recipeView.isStarred) {
|
||||
await lastValueFrom(this.http.delete(this.getRecipeUrl(recipeView) + '/star'));
|
||||
} else {
|
||||
await lastValueFrom(this.http.post(this.getRecipeUrl(recipeView) + '/star', null));
|
||||
}
|
||||
await this.queryClient.invalidateQueries({
|
||||
queryKey: ['recipe', recipeView.recipe.owner.username, recipeView.recipe.slug],
|
||||
})
|
||||
} else {
|
||||
throw new Error('Cannot star a recipe when not logged in.')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user