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;
|
||||
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"
|
||||
/>
|
||||
}
|
||||
|
||||
@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>
|
||||
|
||||
<app-recipe-comments-list [recipeUsername]="recipe.owner.username" [recipeSlug]="recipe.slug" />
|
||||
</article>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { ImageService } from '../../../shared/services/ImageService';
|
||||
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',
|
||||
})
|
||||
export class RecipePageContent {
|
||||
public recipeView = input.required<RecipeView>();
|
||||
public recipeView = input.required<FullRecipeViewWrapper>();
|
||||
|
||||
private readonly imageService = inject(ImageService);
|
||||
private readonly recipeService = inject(RecipeService);
|
||||
|
||||
@ -3,7 +3,7 @@ import { EnterRecipeData } from './enter-recipe-data';
|
||||
import { provideQueryClient, QueryClient } from '@tanstack/angular-query-experimental';
|
||||
import { inputBinding } from '@angular/core';
|
||||
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 { of } from 'rxjs';
|
||||
import { SliceView, SliceViewMeta } from '../../../../shared/models/SliceView.model';
|
||||
@ -54,7 +54,7 @@ describe('EnterRecipeData', () => {
|
||||
id: 'test-id',
|
||||
created: new Date(),
|
||||
state: 'ENTER_DATA',
|
||||
owner: {} as ResourceOwner,
|
||||
owner: {} as UserInfoView,
|
||||
}) satisfies RecipeDraftViewModel,
|
||||
),
|
||||
],
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<h1>Recipes</h1>
|
||||
@if (recipes.isSuccess()) {
|
||||
<app-recipe-card-grid [recipes]="recipes.data()" />
|
||||
} @else if (recipes.isLoading()) {
|
||||
<p>Loading...</p>
|
||||
} @else if (recipes.isError()) {
|
||||
<p>{{ recipes.error().message }}</p>
|
||||
@if (loadingRecipes()) {
|
||||
<app-spinner></app-spinner>
|
||||
} @else if (loadRecipesError()) {
|
||||
<p>There was an error loading recipes: {{ loadRecipesError() }}</p>
|
||||
} @else {
|
||||
<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 { injectQuery } from '@tanstack/angular-query-experimental';
|
||||
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({
|
||||
selector: 'app-recipes-page',
|
||||
imports: [RecipeCardGrid],
|
||||
imports: [RecipeCardGrid, Spinner],
|
||||
templateUrl: './recipes-page.html',
|
||||
styleUrl: './recipes-page.css',
|
||||
})
|
||||
export class RecipesPage {
|
||||
export class RecipesPage implements OnInit {
|
||||
private readonly recipeService = inject(RecipeService);
|
||||
|
||||
protected readonly recipes = injectQuery(() => ({
|
||||
queryKey: ['recipes'],
|
||||
queryFn: () => this.recipeService.getRecipes(),
|
||||
}));
|
||||
protected readonly loadingRecipes = signal(false);
|
||||
protected readonly loadRecipesError = signal<Error | null>(null);
|
||||
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 { RecipeCard } from './recipe-card/recipe-card';
|
||||
import { Recipe } from '../../models/Recipe.model';
|
||||
import { RecipeInfoView } from '../../models/Recipe.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recipe-card-grid',
|
||||
@ -9,5 +9,5 @@ import { Recipe } from '../../models/Recipe.model';
|
||||
styleUrl: './recipe-card-grid.css',
|
||||
})
|
||||
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 { Recipe } from '../../../models/Recipe.model';
|
||||
import { RecipeInfoView } from '../../../models/Recipe.model';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { CreateQueryOptions, injectQuery } from '@tanstack/angular-query-experimental';
|
||||
import { ImageService } from '../../../services/ImageService';
|
||||
@ -16,7 +16,7 @@ import { Spinner } from '../../spinner/spinner';
|
||||
styleUrl: './recipe-card.css',
|
||||
})
|
||||
export class RecipeCard {
|
||||
public recipe = input.required<Recipe>();
|
||||
public recipe = input.required<RecipeInfoView>();
|
||||
|
||||
protected readonly recipePageLink = computed(() => {
|
||||
const recipe = this.recipe();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ResourceOwner } from './ResourceOwner.model';
|
||||
import { UserInfoView } from './UserInfoView.model';
|
||||
|
||||
export interface ImageView {
|
||||
url: string;
|
||||
@ -8,7 +8,7 @@ export interface ImageView {
|
||||
mimeType: string;
|
||||
alt?: string | null;
|
||||
caption?: string | null;
|
||||
owner: ResourceOwner;
|
||||
owner: UserInfoView;
|
||||
isPublic?: boolean;
|
||||
height: number | null;
|
||||
width: number | null;
|
||||
|
||||
@ -1,27 +1,48 @@
|
||||
import { ResourceOwner } from './ResourceOwner.model';
|
||||
import { UserInfoView } from './UserInfoView.model';
|
||||
import { ImageView } from './ImageView.model';
|
||||
|
||||
export interface RecipeInfoViews {
|
||||
slice: {
|
||||
number: number;
|
||||
size: number;
|
||||
};
|
||||
content: Recipe[];
|
||||
export interface RecipeInfoView {
|
||||
id: number;
|
||||
created: Date;
|
||||
modified?: Date | null;
|
||||
slug: string;
|
||||
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;
|
||||
isStarred: boolean | null;
|
||||
recipe: Recipe;
|
||||
recipe: FullRecipeView;
|
||||
}
|
||||
|
||||
export interface Recipe {
|
||||
export interface FullRecipeView {
|
||||
id: number;
|
||||
isPublic: boolean;
|
||||
mainImage?: ImageView | null;
|
||||
owner: ResourceOwner;
|
||||
created: Date;
|
||||
modified?: Date | null;
|
||||
slug: string;
|
||||
starCount: number;
|
||||
text: 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 {
|
||||
id: number;
|
||||
@ -6,6 +6,6 @@ export interface RecipeComment {
|
||||
modified: string | null;
|
||||
text: string;
|
||||
rawText: string | null;
|
||||
owner: ResourceOwner;
|
||||
owner: UserInfoView;
|
||||
recipeId: number;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ResourceOwner } from './ResourceOwner.model';
|
||||
import { UserInfoView } from './UserInfoView.model';
|
||||
import { ImageView } from './ImageView.model';
|
||||
|
||||
export interface RecipeDraftViewModel {
|
||||
@ -13,7 +13,7 @@ export interface RecipeDraftViewModel {
|
||||
totalTime?: number | null;
|
||||
rawText?: string | null;
|
||||
ingredients?: IngredientDraft[] | null;
|
||||
owner: ResourceOwner;
|
||||
owner: UserInfoView;
|
||||
mainImage?: ImageView | null;
|
||||
lastInference?: RecipeDraftInferenceView | null;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export interface ResourceOwner {
|
||||
export interface UserInfoView {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
||||
@ -6,7 +6,7 @@ import { RecipeUploadStep } from '../client-models/RecipeUploadStep';
|
||||
import { RecipeDraftViewModel } from '../models/RecipeDraftView.model';
|
||||
import { EndpointService } from './EndpointService';
|
||||
import { WithStringDates } from '../util';
|
||||
import { Recipe } from '../models/Recipe.model';
|
||||
import { FullRecipeView } from '../models/Recipe.model';
|
||||
import { ImageView } from '../models/ImageView.model';
|
||||
import { SetImageBody } from '../models/SetImageBody';
|
||||
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(
|
||||
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 { HttpClient } from '@angular/common/http';
|
||||
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 { QueryClient } from '@tanstack/angular-query-experimental';
|
||||
import { RecipeComment } from '../models/RecipeComment.model';
|
||||
import { QueryParams } from '../models/Query.model';
|
||||
import { EndpointService } from './EndpointService';
|
||||
import { SliceView } from '../models/SliceView.model';
|
||||
import { WithStringDates } from '../util';
|
||||
import { ImageService } from './ImageService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@ -17,22 +19,39 @@ export class RecipeService {
|
||||
private readonly authService = inject(AuthService);
|
||||
private readonly queryClient = inject(QueryClient);
|
||||
private readonly endpointService = inject(EndpointService);
|
||||
private readonly imageService = inject(ImageService);
|
||||
|
||||
public getRecipes(): Promise<Recipe[]> {
|
||||
return firstValueFrom(
|
||||
this.http.get<RecipeInfoViews>(this.endpointService.getUrl('recipes')).pipe(map((res) => res.content)),
|
||||
private hydrateRecipeInfoView(withStringDates: WithStringDates<RecipeInfoView>): RecipeInfoView {
|
||||
return {
|
||||
...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> {
|
||||
return firstValueFrom(this.http.get<RecipeView>(this.endpointService.getUrl('recipes', [username, slug])));
|
||||
public getRecipeView(username: string, slug: string): Promise<FullRecipeViewWrapper> {
|
||||
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]);
|
||||
}
|
||||
|
||||
public async toggleStar(recipeView: RecipeView): Promise<void> {
|
||||
public async toggleStar(recipeView: FullRecipeViewWrapper): Promise<void> {
|
||||
if (this.authService.accessToken()) {
|
||||
if (recipeView.isStarred) {
|
||||
await lastValueFrom(this.http.delete(this.getRecipeBaseUrl(recipeView) + '/star'));
|
||||
@ -67,9 +86,9 @@ export class RecipeService {
|
||||
return comment;
|
||||
}
|
||||
|
||||
public async aiSearch(prompt: string): Promise<Recipe[]> {
|
||||
public async aiSearch(prompt: string): Promise<FullRecipeView[]> {
|
||||
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',
|
||||
data: {
|
||||
prompt,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user