diff --git a/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.css b/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.css index 4e77b2b..20bb869 100644 --- a/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.css +++ b/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.css @@ -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; +} diff --git a/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.html b/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.html index 07a1d5f..26dd8c9 100644 --- a/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.html +++ b/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.html @@ -53,6 +53,26 @@ [width]="recipe.mainImage!.width" /> } + + @if (recipe.ingredients?.length) { +

Ingredients

+ + } + +

Instructions

+ diff --git a/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.ts b/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.ts index a97bdda..bf7084b 100644 --- a/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.ts +++ b/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.ts @@ -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(); + public recipeView = input.required(); private readonly imageService = inject(ImageService); private readonly recipeService = inject(RecipeService); diff --git a/src/app/pages/recipe-upload-page/steps/enter-recipe-data/enter-recipe-data.spec.ts b/src/app/pages/recipe-upload-page/steps/enter-recipe-data/enter-recipe-data.spec.ts index fa5a92c..b695f73 100644 --- a/src/app/pages/recipe-upload-page/steps/enter-recipe-data/enter-recipe-data.spec.ts +++ b/src/app/pages/recipe-upload-page/steps/enter-recipe-data/enter-recipe-data.spec.ts @@ -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, ), ], diff --git a/src/app/pages/recipes-page/recipes-page.html b/src/app/pages/recipes-page/recipes-page.html index 935f60d..cf69ce0 100644 --- a/src/app/pages/recipes-page/recipes-page.html +++ b/src/app/pages/recipes-page/recipes-page.html @@ -1,8 +1,8 @@

Recipes

-@if (recipes.isSuccess()) { - -} @else if (recipes.isLoading()) { -

Loading...

-} @else if (recipes.isError()) { -

{{ recipes.error().message }}

+@if (loadingRecipes()) { + +} @else if (loadRecipesError()) { +

There was an error loading recipes: {{ loadRecipesError() }}

+} @else { + } diff --git a/src/app/pages/recipes-page/recipes-page.ts b/src/app/pages/recipes-page/recipes-page.ts index 9fc2314..0c9f0cf 100644 --- a/src/app/pages/recipes-page/recipes-page.ts +++ b/src/app/pages/recipes-page/recipes-page.ts @@ -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(null); + protected readonly recipes = signal([]); + + 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); + }, + }); + } } diff --git a/src/app/shared/components/recipe-card-grid/recipe-card-grid.ts b/src/app/shared/components/recipe-card-grid/recipe-card-grid.ts index 63ebc78..c11b0a6 100644 --- a/src/app/shared/components/recipe-card-grid/recipe-card-grid.ts +++ b/src/app/shared/components/recipe-card-grid/recipe-card-grid.ts @@ -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(); + public readonly recipes = input.required(); } diff --git a/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts b/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts index f8e4086..3737c6c 100644 --- a/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts +++ b/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts @@ -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(); + public recipe = input.required(); protected readonly recipePageLink = computed(() => { const recipe = this.recipe(); diff --git a/src/app/shared/models/ImageView.model.ts b/src/app/shared/models/ImageView.model.ts index fe5e021..4185d95 100644 --- a/src/app/shared/models/ImageView.model.ts +++ b/src/app/shared/models/ImageView.model.ts @@ -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; diff --git a/src/app/shared/models/Recipe.model.ts b/src/app/shared/models/Recipe.model.ts index 6b2e0be..c851e55 100644 --- a/src/app/shared/models/Recipe.model.ts +++ b/src/app/shared/models/Recipe.model.ts @@ -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; } diff --git a/src/app/shared/models/RecipeComment.model.ts b/src/app/shared/models/RecipeComment.model.ts index a162870..e2a57d7 100644 --- a/src/app/shared/models/RecipeComment.model.ts +++ b/src/app/shared/models/RecipeComment.model.ts @@ -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; } diff --git a/src/app/shared/models/RecipeDraftView.model.ts b/src/app/shared/models/RecipeDraftView.model.ts index 64f25e5..9486ad2 100644 --- a/src/app/shared/models/RecipeDraftView.model.ts +++ b/src/app/shared/models/RecipeDraftView.model.ts @@ -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; } diff --git a/src/app/shared/models/ResourceOwner.model.ts b/src/app/shared/models/UserInfoView.model.ts similarity index 54% rename from src/app/shared/models/ResourceOwner.model.ts rename to src/app/shared/models/UserInfoView.model.ts index 1fba2b2..a04ff52 100644 --- a/src/app/shared/models/ResourceOwner.model.ts +++ b/src/app/shared/models/UserInfoView.model.ts @@ -1,4 +1,4 @@ -export interface ResourceOwner { +export interface UserInfoView { id: number; username: string; } diff --git a/src/app/shared/services/RecipeDraftService.ts b/src/app/shared/services/RecipeDraftService.ts index a7d636c..8477e05 100644 --- a/src/app/shared/services/RecipeDraftService.ts +++ b/src/app/shared/services/RecipeDraftService.ts @@ -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 { + public publish(id: string): Promise { return firstValueFrom( - this.http.post(this.endpointService.getUrl('recipeDrafts', [id, 'publish']), null), + this.http.post(this.endpointService.getUrl('recipeDrafts', [id, 'publish']), null), ); } diff --git a/src/app/shared/services/RecipeService.ts b/src/app/shared/services/RecipeService.ts index 2b0ff32..dec28f6 100644 --- a/src/app/shared/services/RecipeService.ts +++ b/src/app/shared/services/RecipeService.ts @@ -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 { - return firstValueFrom( - this.http.get(this.endpointService.getUrl('recipes')).pipe(map((res) => res.content)), + private hydrateRecipeInfoView(withStringDates: WithStringDates): 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> { + return this.http.get>>(this.endpointService.getUrl('recipes')).pipe( + map((sliceView) => ({ + ...sliceView, + content: sliceView.content.map((withStringDates) => this.hydrateRecipeInfoView(withStringDates)), + })), ); } - public getRecipeView(username: string, slug: string): Promise { - return firstValueFrom(this.http.get(this.endpointService.getUrl('recipes', [username, slug]))); + public getRecipeView(username: string, slug: string): Promise { + return firstValueFrom( + this.http.get(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 { + public async toggleStar(recipeView: FullRecipeViewWrapper): Promise { 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 { + public async aiSearch(prompt: string): Promise { 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,