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
+
+ @for (ingredient of recipe.ingredients; track $index) {
+ -
+ @if (ingredient.amount) {
+ {{ ingredient.amount }}
+ }
+ {{ ingredient.name }}
+ @if (ingredient.notes) {
+ {{ ingredient.notes }}
+ }
+
+ }
+
+ }
+
+ 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,