Add starring ability.
This commit is contained in:
parent
1a51a8f751
commit
28222e0655
@ -1,6 +1,21 @@
|
|||||||
|
@let recipe = recipeView().recipe;
|
||||||
|
<article>
|
||||||
|
<div id="recipe-header">
|
||||||
<h1>{{ recipe.title }}</h1>
|
<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()) {
|
@if (mainImageUrl.isSuccess()) {
|
||||||
<!--suppress AngularNgOptimizedImage -->
|
|
||||||
<img
|
<img
|
||||||
[src]="mainImageUrl.data()"
|
[src]="mainImageUrl.data()"
|
||||||
[alt]="recipe.mainImage.alt"
|
[alt]="recipe.mainImage.alt"
|
||||||
@ -9,3 +24,4 @@
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<div [innerHTML]="recipe.text"></div>
|
<div [innerHTML]="recipe.text"></div>
|
||||||
|
</article>
|
||||||
|
|||||||
@ -1,22 +1,42 @@
|
|||||||
import { Component, inject, Input } from '@angular/core';
|
import { Component, computed, inject, input, Input } from '@angular/core';
|
||||||
import { Recipe } from '../../model/Recipe.model';
|
import { RecipeView } from '../../model/Recipe.model';
|
||||||
import { injectQuery } from '@tanstack/angular-query-experimental';
|
import { injectMutation, injectQuery } from '@tanstack/angular-query-experimental';
|
||||||
import { ImageService } from '../../service/image.service';
|
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({
|
@Component({
|
||||||
selector: 'app-recipe-page-content',
|
selector: 'app-recipe-page-content',
|
||||||
imports: [],
|
imports: [
|
||||||
|
FaIconComponent
|
||||||
|
],
|
||||||
templateUrl: './recipe-page-content.html',
|
templateUrl: './recipe-page-content.html',
|
||||||
styleUrl: './recipe-page-content.css',
|
styleUrl: './recipe-page-content.css',
|
||||||
})
|
})
|
||||||
export class RecipePageContent {
|
export class RecipePageContent {
|
||||||
@Input({ required: true })
|
|
||||||
public recipe!: Recipe;
|
public recipeView = input.required<RecipeView>();
|
||||||
|
|
||||||
private readonly imageService = inject(ImageService);
|
private readonly imageService = inject(ImageService);
|
||||||
|
private readonly recipeService = inject(RecipeService);
|
||||||
|
private readonly authService = inject(AuthService);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
protected mainImageUrl = injectQuery(() => ({
|
|
||||||
queryKey: ['images', this.recipe.mainImage.owner.username, this.recipe.mainImage.filename],
|
|
||||||
queryFn: () => this.imageService.getImage(this.recipe.mainImage.url),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
@if (recipe.isLoading()) {
|
@if (recipeView.isLoading()) {
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
} @else if (recipe.isSuccess()) {
|
} @else if (recipeView.isSuccess()) {
|
||||||
<app-recipe-page-content [recipe]="recipe.data()"></app-recipe-page-content>
|
<app-recipe-page-content [recipeView]="recipeView.data()"></app-recipe-page-content>
|
||||||
} @else if (recipe.error(); as error) {
|
} @else if (recipeView.error(); as error) {
|
||||||
<p>{{ error.message }}</p>
|
<p>{{ error.message }}</p>
|
||||||
} @else {
|
} @else {
|
||||||
<p>There was an error loading the recipe.</p>
|
<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 username = this.route.snapshot.paramMap.get('username') as string;
|
||||||
private slug = this.route.snapshot.paramMap.get('slug') as string;
|
private slug = this.route.snapshot.paramMap.get('slug') as string;
|
||||||
|
|
||||||
protected recipe = injectQuery(() => ({
|
protected recipeView = injectQuery(() => ({
|
||||||
queryKey: ['recipe', this.username, this.slug],
|
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;
|
max-height: 200px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
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>
|
<article>
|
||||||
<a [routerLink]="recipePageLink()">
|
<a [routerLink]="recipePageLink()">
|
||||||
@if (mainImage.isSuccess()) {
|
@if (mainImage.isSuccess()) {
|
||||||
<!--suppress AngularNgOptimizedImage -->
|
<img [src]="mainImage.data()" id="recipe-card-image" [alt]="recipe.mainImage.alt" />
|
||||||
<img [src]="mainImage.data()" class="recipe-card-image" [alt]="recipe.mainImage.alt" />
|
|
||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<div id="title-and-visibility">
|
||||||
<a [routerLink]="recipePageLink()">
|
<a [routerLink]="recipePageLink()">
|
||||||
<h1>{{ recipe.title }}</h1>
|
<h1 id="recipe-title">{{ recipe.title }}</h1>
|
||||||
</a>
|
</a>
|
||||||
@if (recipe.isPublic) {
|
@if (recipe.isPublic) {
|
||||||
<fa-icon [icon]="faGlobe" />
|
<fa-icon [icon]="faGlobe" />
|
||||||
@ -15,7 +14,7 @@
|
|||||||
<fa-icon [icon]="faLock" />
|
<fa-icon [icon]="faLock" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div id="user-and-stars">
|
||||||
<span><fa-icon [icon]="faUser" /> {{ recipe.owner.username }}</span>
|
<span><fa-icon [icon]="faUser" /> {{ recipe.owner.username }}</span>
|
||||||
<span><fa-icon [icon]="faStar" /> {{ recipe.starCount }}</span>
|
<span><fa-icon [icon]="faStar" /> {{ recipe.starCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
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, map } from 'rxjs';
|
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 { QueryClient } from '@tanstack/angular-query-experimental';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class RecipeService {
|
export class RecipeService {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly authService = inject(AuthService);
|
||||||
|
private readonly queryClient = inject(QueryClient);
|
||||||
|
|
||||||
public getRecipes(): Promise<Recipe[]> {
|
public getRecipes(): Promise<Recipe[]> {
|
||||||
return firstValueFrom(
|
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(
|
return firstValueFrom(
|
||||||
this.http
|
this.http.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`)
|
||||||
.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`)
|
|
||||||
.pipe(map((recipeView) => recipeView.recipe)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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