MME-9 Add ingredients to recipe page. Small refactorings and model changes.
This commit is contained in:
parent
09fb9a3d98
commit
ac000de6a5
@ -36,3 +36,16 @@ article {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ingredients-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingredient {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 5px;
|
||||||
|
}
|
||||||
|
|||||||
@ -53,6 +53,26 @@
|
|||||||
[width]="recipe.mainImage!.width"
|
[width]="recipe.mainImage!.width"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (recipe.ingredients?.length) {
|
||||||
|
<h3>Ingredients</h3>
|
||||||
|
<ul class="ingredients-list">
|
||||||
|
@for (ingredient of recipe.ingredients; track $index) {
|
||||||
|
<li class="ingredient">
|
||||||
|
@if (ingredient.amount) {
|
||||||
|
<span>{{ ingredient.amount }}</span>
|
||||||
|
}
|
||||||
|
<span>{{ ingredient.name }}</span>
|
||||||
|
@if (ingredient.notes) {
|
||||||
|
<span>{{ ingredient.notes }}</span>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Instructions</h3>
|
||||||
<div [innerHTML]="recipe.text"></div>
|
<div [innerHTML]="recipe.text"></div>
|
||||||
|
|
||||||
<app-recipe-comments-list [recipeUsername]="recipe.owner.username" [recipeSlug]="recipe.slug" />
|
<app-recipe-comments-list [recipeUsername]="recipe.owner.username" [recipeSlug]="recipe.slug" />
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Component, computed, inject, input } from '@angular/core';
|
import { Component, computed, inject, input } from '@angular/core';
|
||||||
import { RecipeView } from '../../../shared/models/Recipe.model';
|
import { FullRecipeViewWrapper } from '../../../shared/models/Recipe.model';
|
||||||
import { CreateQueryOptions, injectMutation, injectQuery } from '@tanstack/angular-query-experimental';
|
import { CreateQueryOptions, injectMutation, injectQuery } from '@tanstack/angular-query-experimental';
|
||||||
import { ImageService } from '../../../shared/services/ImageService';
|
import { ImageService } from '../../../shared/services/ImageService';
|
||||||
import { faEllipsis, faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons';
|
import { faEllipsis, faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons';
|
||||||
@ -24,7 +24,7 @@ import { ToastrService } from 'ngx-toastr';
|
|||||||
styleUrl: './recipe-page-content.css',
|
styleUrl: './recipe-page-content.css',
|
||||||
})
|
})
|
||||||
export class RecipePageContent {
|
export class RecipePageContent {
|
||||||
public recipeView = input.required<RecipeView>();
|
public recipeView = input.required<FullRecipeViewWrapper>();
|
||||||
|
|
||||||
private readonly imageService = inject(ImageService);
|
private readonly imageService = inject(ImageService);
|
||||||
private readonly recipeService = inject(RecipeService);
|
private readonly recipeService = inject(RecipeService);
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { EnterRecipeData } from './enter-recipe-data';
|
|||||||
import { provideQueryClient, QueryClient } from '@tanstack/angular-query-experimental';
|
import { provideQueryClient, QueryClient } from '@tanstack/angular-query-experimental';
|
||||||
import { inputBinding } from '@angular/core';
|
import { inputBinding } from '@angular/core';
|
||||||
import { RecipeDraftViewModel } from '../../../../shared/models/RecipeDraftView.model';
|
import { RecipeDraftViewModel } from '../../../../shared/models/RecipeDraftView.model';
|
||||||
import { ResourceOwner } from '../../../../shared/models/ResourceOwner.model';
|
import { UserInfoView } from '../../../../shared/models/UserInfoView.model';
|
||||||
import { ImageService } from '../../../../shared/services/ImageService';
|
import { ImageService } from '../../../../shared/services/ImageService';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { SliceView, SliceViewMeta } from '../../../../shared/models/SliceView.model';
|
import { SliceView, SliceViewMeta } from '../../../../shared/models/SliceView.model';
|
||||||
@ -54,7 +54,7 @@ describe('EnterRecipeData', () => {
|
|||||||
id: 'test-id',
|
id: 'test-id',
|
||||||
created: new Date(),
|
created: new Date(),
|
||||||
state: 'ENTER_DATA',
|
state: 'ENTER_DATA',
|
||||||
owner: {} as ResourceOwner,
|
owner: {} as UserInfoView,
|
||||||
}) satisfies RecipeDraftViewModel,
|
}) satisfies RecipeDraftViewModel,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<h1>Recipes</h1>
|
<h1>Recipes</h1>
|
||||||
@if (recipes.isSuccess()) {
|
@if (loadingRecipes()) {
|
||||||
<app-recipe-card-grid [recipes]="recipes.data()" />
|
<app-spinner></app-spinner>
|
||||||
} @else if (recipes.isLoading()) {
|
} @else if (loadRecipesError()) {
|
||||||
<p>Loading...</p>
|
<p>There was an error loading recipes: {{ loadRecipesError() }}</p>
|
||||||
} @else if (recipes.isError()) {
|
} @else {
|
||||||
<p>{{ recipes.error().message }}</p>
|
<app-recipe-card-grid [recipes]="recipes()"></app-recipe-card-grid>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,33 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||||
import { RecipeService } from '../../shared/services/RecipeService';
|
import { RecipeService } from '../../shared/services/RecipeService';
|
||||||
import { injectQuery } from '@tanstack/angular-query-experimental';
|
|
||||||
import { RecipeCardGrid } from '../../shared/components/recipe-card-grid/recipe-card-grid';
|
import { RecipeCardGrid } from '../../shared/components/recipe-card-grid/recipe-card-grid';
|
||||||
|
import { RecipeInfoView } from '../../shared/models/Recipe.model';
|
||||||
|
import { Spinner } from '../../shared/components/spinner/spinner';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-recipes-page',
|
selector: 'app-recipes-page',
|
||||||
imports: [RecipeCardGrid],
|
imports: [RecipeCardGrid, Spinner],
|
||||||
templateUrl: './recipes-page.html',
|
templateUrl: './recipes-page.html',
|
||||||
styleUrl: './recipes-page.css',
|
styleUrl: './recipes-page.css',
|
||||||
})
|
})
|
||||||
export class RecipesPage {
|
export class RecipesPage implements OnInit {
|
||||||
private readonly recipeService = inject(RecipeService);
|
private readonly recipeService = inject(RecipeService);
|
||||||
|
|
||||||
protected readonly recipes = injectQuery(() => ({
|
protected readonly loadingRecipes = signal(false);
|
||||||
queryKey: ['recipes'],
|
protected readonly loadRecipesError = signal<Error | null>(null);
|
||||||
queryFn: () => this.recipeService.getRecipes(),
|
protected readonly recipes = signal<RecipeInfoView[]>([]);
|
||||||
}));
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.loadingRecipes.set(true);
|
||||||
|
this.recipeService.getRecipes().subscribe({
|
||||||
|
next: (sliceView) => {
|
||||||
|
this.loadingRecipes.set(false);
|
||||||
|
this.recipes.set(sliceView.content);
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.loadingRecipes.set(false);
|
||||||
|
this.loadRecipesError.set(e);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Component, input } from '@angular/core';
|
import { Component, input } from '@angular/core';
|
||||||
import { RecipeCard } from './recipe-card/recipe-card';
|
import { RecipeCard } from './recipe-card/recipe-card';
|
||||||
import { Recipe } from '../../models/Recipe.model';
|
import { RecipeInfoView } from '../../models/Recipe.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-recipe-card-grid',
|
selector: 'app-recipe-card-grid',
|
||||||
@ -9,5 +9,5 @@ import { Recipe } from '../../models/Recipe.model';
|
|||||||
styleUrl: './recipe-card-grid.css',
|
styleUrl: './recipe-card-grid.css',
|
||||||
})
|
})
|
||||||
export class RecipeCardGrid {
|
export class RecipeCardGrid {
|
||||||
public readonly recipes = input.required<Recipe[]>();
|
public readonly recipes = input.required<RecipeInfoView[]>();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Component, computed, inject, input } from '@angular/core';
|
import { Component, computed, inject, input } from '@angular/core';
|
||||||
import { Recipe } from '../../../models/Recipe.model';
|
import { RecipeInfoView } from '../../../models/Recipe.model';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { CreateQueryOptions, injectQuery } from '@tanstack/angular-query-experimental';
|
import { CreateQueryOptions, injectQuery } from '@tanstack/angular-query-experimental';
|
||||||
import { ImageService } from '../../../services/ImageService';
|
import { ImageService } from '../../../services/ImageService';
|
||||||
@ -16,7 +16,7 @@ import { Spinner } from '../../spinner/spinner';
|
|||||||
styleUrl: './recipe-card.css',
|
styleUrl: './recipe-card.css',
|
||||||
})
|
})
|
||||||
export class RecipeCard {
|
export class RecipeCard {
|
||||||
public recipe = input.required<Recipe>();
|
public recipe = input.required<RecipeInfoView>();
|
||||||
|
|
||||||
protected readonly recipePageLink = computed(() => {
|
protected readonly recipePageLink = computed(() => {
|
||||||
const recipe = this.recipe();
|
const recipe = this.recipe();
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ResourceOwner } from './ResourceOwner.model';
|
import { UserInfoView } from './UserInfoView.model';
|
||||||
|
|
||||||
export interface ImageView {
|
export interface ImageView {
|
||||||
url: string;
|
url: string;
|
||||||
@ -8,7 +8,7 @@ export interface ImageView {
|
|||||||
mimeType: string;
|
mimeType: string;
|
||||||
alt?: string | null;
|
alt?: string | null;
|
||||||
caption?: string | null;
|
caption?: string | null;
|
||||||
owner: ResourceOwner;
|
owner: UserInfoView;
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
height: number | null;
|
height: number | null;
|
||||||
width: number | null;
|
width: number | null;
|
||||||
|
|||||||
@ -1,27 +1,48 @@
|
|||||||
import { ResourceOwner } from './ResourceOwner.model';
|
import { UserInfoView } from './UserInfoView.model';
|
||||||
import { ImageView } from './ImageView.model';
|
import { ImageView } from './ImageView.model';
|
||||||
|
|
||||||
export interface RecipeInfoViews {
|
export interface RecipeInfoView {
|
||||||
slice: {
|
id: number;
|
||||||
number: number;
|
created: Date;
|
||||||
size: number;
|
modified?: Date | null;
|
||||||
};
|
slug: string;
|
||||||
content: Recipe[];
|
title: string;
|
||||||
|
preparationTime?: number | null;
|
||||||
|
cookingTime?: number | null;
|
||||||
|
totalTime?: number | null;
|
||||||
|
owner: UserInfoView;
|
||||||
|
isPublic: boolean;
|
||||||
|
starCount: number;
|
||||||
|
mainImage?: ImageView | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecipeView {
|
export interface FullRecipeViewWrapper {
|
||||||
isOwner: boolean | null;
|
isOwner: boolean | null;
|
||||||
isStarred: boolean | null;
|
isStarred: boolean | null;
|
||||||
recipe: Recipe;
|
recipe: FullRecipeView;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Recipe {
|
export interface FullRecipeView {
|
||||||
id: number;
|
id: number;
|
||||||
isPublic: boolean;
|
created: Date;
|
||||||
mainImage?: ImageView | null;
|
modified?: Date | null;
|
||||||
owner: ResourceOwner;
|
|
||||||
slug: string;
|
slug: string;
|
||||||
starCount: number;
|
|
||||||
text: string;
|
|
||||||
title: string;
|
title: string;
|
||||||
|
preparationTime?: number | null;
|
||||||
|
cookingTime?: number | null;
|
||||||
|
totalTime?: number | null;
|
||||||
|
ingredients?: Ingredient[];
|
||||||
|
text: string;
|
||||||
|
rawText?: string | null;
|
||||||
|
owner: UserInfoView;
|
||||||
|
starCount: number;
|
||||||
|
viewerCount: number;
|
||||||
|
mainImage?: ImageView | null;
|
||||||
|
isPublic: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ingredient {
|
||||||
|
amount?: string | null;
|
||||||
|
name: string;
|
||||||
|
notes?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ResourceOwner } from './ResourceOwner.model';
|
import { UserInfoView } from './UserInfoView.model';
|
||||||
|
|
||||||
export interface RecipeComment {
|
export interface RecipeComment {
|
||||||
id: number;
|
id: number;
|
||||||
@ -6,6 +6,6 @@ export interface RecipeComment {
|
|||||||
modified: string | null;
|
modified: string | null;
|
||||||
text: string;
|
text: string;
|
||||||
rawText: string | null;
|
rawText: string | null;
|
||||||
owner: ResourceOwner;
|
owner: UserInfoView;
|
||||||
recipeId: number;
|
recipeId: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ResourceOwner } from './ResourceOwner.model';
|
import { UserInfoView } from './UserInfoView.model';
|
||||||
import { ImageView } from './ImageView.model';
|
import { ImageView } from './ImageView.model';
|
||||||
|
|
||||||
export interface RecipeDraftViewModel {
|
export interface RecipeDraftViewModel {
|
||||||
@ -13,7 +13,7 @@ export interface RecipeDraftViewModel {
|
|||||||
totalTime?: number | null;
|
totalTime?: number | null;
|
||||||
rawText?: string | null;
|
rawText?: string | null;
|
||||||
ingredients?: IngredientDraft[] | null;
|
ingredients?: IngredientDraft[] | null;
|
||||||
owner: ResourceOwner;
|
owner: UserInfoView;
|
||||||
mainImage?: ImageView | null;
|
mainImage?: ImageView | null;
|
||||||
lastInference?: RecipeDraftInferenceView | null;
|
lastInference?: RecipeDraftInferenceView | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export interface ResourceOwner {
|
export interface UserInfoView {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
@ -6,7 +6,7 @@ import { RecipeUploadStep } from '../client-models/RecipeUploadStep';
|
|||||||
import { RecipeDraftViewModel } from '../models/RecipeDraftView.model';
|
import { RecipeDraftViewModel } from '../models/RecipeDraftView.model';
|
||||||
import { EndpointService } from './EndpointService';
|
import { EndpointService } from './EndpointService';
|
||||||
import { WithStringDates } from '../util';
|
import { WithStringDates } from '../util';
|
||||||
import { Recipe } from '../models/Recipe.model';
|
import { FullRecipeView } from '../models/Recipe.model';
|
||||||
import { ImageView } from '../models/ImageView.model';
|
import { ImageView } from '../models/ImageView.model';
|
||||||
import { SetImageBody } from '../models/SetImageBody';
|
import { SetImageBody } from '../models/SetImageBody';
|
||||||
import { ImageService } from './ImageService';
|
import { ImageService } from './ImageService';
|
||||||
@ -112,9 +112,9 @@ export class RecipeDraftService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public publish(id: string): Promise<Recipe> {
|
public publish(id: string): Promise<FullRecipeView> {
|
||||||
return firstValueFrom(
|
return firstValueFrom(
|
||||||
this.http.post<Recipe>(this.endpointService.getUrl('recipeDrafts', [id, 'publish']), null),
|
this.http.post<FullRecipeView>(this.endpointService.getUrl('recipeDrafts', [id, 'publish']), null),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
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 { firstValueFrom, lastValueFrom, map, Observable } from 'rxjs';
|
||||||
import { Recipe, RecipeInfoViews, RecipeView } 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';
|
||||||
import { RecipeComment } from '../models/RecipeComment.model';
|
import { RecipeComment } from '../models/RecipeComment.model';
|
||||||
import { QueryParams } from '../models/Query.model';
|
import { QueryParams } from '../models/Query.model';
|
||||||
import { EndpointService } from './EndpointService';
|
import { EndpointService } from './EndpointService';
|
||||||
import { SliceView } from '../models/SliceView.model';
|
import { SliceView } from '../models/SliceView.model';
|
||||||
|
import { WithStringDates } from '../util';
|
||||||
|
import { ImageService } from './ImageService';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@ -17,22 +19,39 @@ export class RecipeService {
|
|||||||
private readonly authService = inject(AuthService);
|
private readonly authService = inject(AuthService);
|
||||||
private readonly queryClient = inject(QueryClient);
|
private readonly queryClient = inject(QueryClient);
|
||||||
private readonly endpointService = inject(EndpointService);
|
private readonly endpointService = inject(EndpointService);
|
||||||
|
private readonly imageService = inject(ImageService);
|
||||||
|
|
||||||
public getRecipes(): Promise<Recipe[]> {
|
private hydrateRecipeInfoView(withStringDates: WithStringDates<RecipeInfoView>): RecipeInfoView {
|
||||||
return firstValueFrom(
|
return {
|
||||||
this.http.get<RecipeInfoViews>(this.endpointService.getUrl('recipes')).pipe(map((res) => res.content)),
|
...withStringDates,
|
||||||
|
created: new Date(withStringDates.created),
|
||||||
|
modified: withStringDates.modified ? new Date(withStringDates.modified) : undefined,
|
||||||
|
mainImage: withStringDates.mainImage
|
||||||
|
? this.imageService.hydrateImageView(withStringDates.mainImage)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRecipes(): Observable<SliceView<RecipeInfoView>> {
|
||||||
|
return this.http.get<SliceView<WithStringDates<RecipeInfoView>>>(this.endpointService.getUrl('recipes')).pipe(
|
||||||
|
map((sliceView) => ({
|
||||||
|
...sliceView,
|
||||||
|
content: sliceView.content.map((withStringDates) => this.hydrateRecipeInfoView(withStringDates)),
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRecipeView(username: string, slug: string): Promise<RecipeView> {
|
public getRecipeView(username: string, slug: string): Promise<FullRecipeViewWrapper> {
|
||||||
return firstValueFrom(this.http.get<RecipeView>(this.endpointService.getUrl('recipes', [username, slug])));
|
return firstValueFrom(
|
||||||
|
this.http.get<FullRecipeViewWrapper>(this.endpointService.getUrl('recipes', [username, slug])),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRecipeBaseUrl(recipeView: RecipeView): string {
|
private getRecipeBaseUrl(recipeView: FullRecipeViewWrapper): string {
|
||||||
return this.endpointService.getUrl('recipes', [recipeView.recipe.owner.username, recipeView.recipe.slug]);
|
return this.endpointService.getUrl('recipes', [recipeView.recipe.owner.username, recipeView.recipe.slug]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async toggleStar(recipeView: RecipeView): Promise<void> {
|
public async toggleStar(recipeView: FullRecipeViewWrapper): Promise<void> {
|
||||||
if (this.authService.accessToken()) {
|
if (this.authService.accessToken()) {
|
||||||
if (recipeView.isStarred) {
|
if (recipeView.isStarred) {
|
||||||
await lastValueFrom(this.http.delete(this.getRecipeBaseUrl(recipeView) + '/star'));
|
await lastValueFrom(this.http.delete(this.getRecipeBaseUrl(recipeView) + '/star'));
|
||||||
@ -67,9 +86,9 @@ export class RecipeService {
|
|||||||
return comment;
|
return comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async aiSearch(prompt: string): Promise<Recipe[]> {
|
public async aiSearch(prompt: string): Promise<FullRecipeView[]> {
|
||||||
const recipeInfoViews = await firstValueFrom(
|
const recipeInfoViews = await firstValueFrom(
|
||||||
this.http.post<{ results: Recipe[] }>(this.endpointService.getUrl('recipes'), {
|
this.http.post<{ results: FullRecipeView[] }>(this.endpointService.getUrl('recipes'), {
|
||||||
type: 'AI_PROMPT',
|
type: 'AI_PROMPT',
|
||||||
data: {
|
data: {
|
||||||
prompt,
|
prompt,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user