diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 9579269..c52a99c 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -4,6 +4,7 @@ import { RecipesPage } from './pages/recipes-page/recipes-page';
import { RecipesSearchPage } from './pages/recipes-search-page/recipes-search-page';
import { RecipeUploadPage } from './pages/recipe-upload-page/recipe-upload-page';
import { authGuard } from './shared/guards/auth-guard';
+import { RecipeEditPage } from './pages/recipe-edit-page/recipe-edit-page';
export const routes: Routes = [
{
@@ -23,4 +24,8 @@ export const routes: Routes = [
path: 'recipes/:username/:slug',
component: RecipePage,
},
+ {
+ path: 'recipes/:username/:slug/edit',
+ component: RecipeEditPage,
+ },
];
diff --git a/src/app/pages/recipe-edit-page/recipe-edit-page.css b/src/app/pages/recipe-edit-page/recipe-edit-page.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/pages/recipe-edit-page/recipe-edit-page.html b/src/app/pages/recipe-edit-page/recipe-edit-page.html
new file mode 100644
index 0000000..f778f37
--- /dev/null
+++ b/src/app/pages/recipe-edit-page/recipe-edit-page.html
@@ -0,0 +1,22 @@
+
Edit Recipe
+@if (loadingRecipe()) {
+
+} @else if (loadRecipeError()) {
+ There was an error loading the recipe: {{ loadRecipeError() }}
+} @else {
+ @let recipe = recipeView()!.recipe;
+
+
Created: {{ recipe.created | date: "short" }}
+ @if (recipe.modified) {
+
Last modified: {{ recipe.modified | date: "short" }}
+ }
+
+
+ @if (submittingRecipe()) {
+
+ }
+}
diff --git a/src/app/pages/recipe-edit-page/recipe-edit-page.spec.ts b/src/app/pages/recipe-edit-page/recipe-edit-page.spec.ts
new file mode 100644
index 0000000..baa8682
--- /dev/null
+++ b/src/app/pages/recipe-edit-page/recipe-edit-page.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RecipeEditPage } from './recipe-edit-page';
+
+describe('RecipeEditPage', () => {
+ let component: RecipeEditPage;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [RecipeEditPage],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(RecipeEditPage);
+ component = fixture.componentInstance;
+ await fixture.whenStable();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/recipe-edit-page/recipe-edit-page.ts b/src/app/pages/recipe-edit-page/recipe-edit-page.ts
new file mode 100644
index 0000000..de93a7b
--- /dev/null
+++ b/src/app/pages/recipe-edit-page/recipe-edit-page.ts
@@ -0,0 +1,73 @@
+import { Component, inject, OnInit, signal } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { FullRecipeViewWrapper } from '../../shared/models/Recipe.model';
+import { RecipeService } from '../../shared/services/RecipeService';
+import { Spinner } from '../../shared/components/spinner/spinner';
+import { DatePipe } from '@angular/common';
+import { RecipeEditForm } from '../../shared/components/recipe-edit-form/recipe-edit-form';
+import { RecipeEditFormSubmitEvent } from '../../shared/components/recipe-edit-form/RecipeEditFormSubmitEvent';
+import { ToastrService } from 'ngx-toastr';
+
+@Component({
+ selector: 'app-recipe-edit-page',
+ imports: [Spinner, DatePipe, RecipeEditForm],
+ templateUrl: './recipe-edit-page.html',
+ styleUrl: './recipe-edit-page.css',
+})
+export class RecipeEditPage implements OnInit {
+ private readonly activatedRoute = inject(ActivatedRoute);
+ private readonly recipeService = inject(RecipeService);
+ private readonly router = inject(Router);
+ private readonly toastrService = inject(ToastrService);
+
+ protected readonly loadingRecipe = signal(false);
+ protected readonly loadRecipeError = signal(null);
+ protected readonly recipeView = signal(null);
+
+ protected readonly submittingRecipe = signal(false);
+
+ public ngOnInit(): void {
+ this.activatedRoute.paramMap.subscribe((paramMap) => {
+ const username = paramMap.get('username')!;
+ const slug = paramMap.get('slug')!;
+ this.loadingRecipe.set(true);
+ this.recipeService.getRecipeView2(username, slug, true).subscribe({
+ next: (recipeView) => {
+ this.loadingRecipe.set(false);
+ this.recipeView.set(recipeView);
+ },
+ error: (e) => {
+ this.loadingRecipe.set(false);
+ this.loadRecipeError.set(e);
+ },
+ });
+ });
+ }
+
+ public onRecipeSubmit(event: RecipeEditFormSubmitEvent): void {
+ this.submittingRecipe.set(true);
+ const baseRecipe = this.recipeView()!.recipe;
+ this.recipeService
+ .updateRecipe(baseRecipe.owner.username, baseRecipe.slug, {
+ ...event,
+ mainImage: event.mainImage
+ ? {
+ username: event.mainImage.owner.username,
+ filename: event.mainImage.filename,
+ }
+ : undefined,
+ })
+ .subscribe({
+ next: async () => {
+ this.submittingRecipe.set(false);
+ await this.router.navigate(['recipes', baseRecipe.owner.username, baseRecipe.slug]);
+ this.toastrService.success('Recipe updated');
+ },
+ error: (e) => {
+ this.submittingRecipe.set(false);
+ console.error(e);
+ this.toastrService.error('Error submitting recipe');
+ },
+ });
+ }
+}
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 26dd8c9..05b7f64 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
@@ -23,6 +23,7 @@
+
}
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 bf7084b..7d4773a 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
@@ -59,6 +59,11 @@ export class RecipePageContent {
private readonly dialog = inject(MatDialog);
private readonly toastrService = inject(ToastrService);
+ protected async onRecipeEdit(): Promise {
+ const recipe = this.recipeView().recipe;
+ await this.router.navigate(['recipes', recipe.owner.username, recipe.slug, 'edit']);
+ }
+
protected onRecipeDelete(): void {
const dialogRef = this.dialog.open(ConfirmationDialog, {
data: {
diff --git a/src/app/shared/bodies/RecipeUpdateBody.ts b/src/app/shared/bodies/RecipeUpdateBody.ts
new file mode 100644
index 0000000..f9576df
--- /dev/null
+++ b/src/app/shared/bodies/RecipeUpdateBody.ts
@@ -0,0 +1,21 @@
+export interface RecipeUpdateBody {
+ title?: string | null;
+ preparationTime?: number | null;
+ cookingTime?: number | null;
+ totalTime?: number | null;
+ ingredients?: IngredientUpdateBody[] | null;
+ rawText?: string | null;
+ isPublic?: boolean | null;
+ mainImage?: MainImageUpdateBody | null;
+}
+
+export interface IngredientUpdateBody {
+ amount?: string | null;
+ name: string;
+ notes?: string | null;
+}
+
+export interface MainImageUpdateBody {
+ username: string;
+ filename: string;
+}
diff --git a/src/app/shared/components/recipe-edit-form/recipe-edit-form.html b/src/app/shared/components/recipe-edit-form/recipe-edit-form.html
index 5c21b09..da207be 100644
--- a/src/app/shared/components/recipe-edit-form/recipe-edit-form.html
+++ b/src/app/shared/components/recipe-edit-form/recipe-edit-form.html
@@ -123,5 +123,5 @@
(input)="onRecipeTextChange($event)"
>
-
+
diff --git a/src/app/shared/components/recipe-edit-form/recipe-edit-form.ts b/src/app/shared/components/recipe-edit-form/recipe-edit-form.ts
index 95dbef8..4a3ff86 100644
--- a/src/app/shared/components/recipe-edit-form/recipe-edit-form.ts
+++ b/src/app/shared/components/recipe-edit-form/recipe-edit-form.ts
@@ -73,6 +73,7 @@ import { FullRecipeView } from '../../models/Recipe.model';
})
export class RecipeEditForm implements OnInit {
public readonly recipe = input.required();
+ public readonly editSlugDisabled = input(false);
public readonly submitRecipe = output();
public readonly deleteDraft = output();
@@ -105,6 +106,9 @@ export class RecipeEditForm implements OnInit {
private readonly injector = inject(Injector);
public ngOnInit(): void {
+ if (this.editSlugDisabled()) {
+ this.recipeFormGroup.controls.slug.disable();
+ }
const draft = this.recipe();
this.recipeFormGroup.patchValue({
title: draft.title ?? '',
@@ -122,6 +126,9 @@ export class RecipeEditForm implements OnInit {
})),
);
}
+ if (draft.mainImage) {
+ this.mainImage.set(draft.mainImage);
+ }
runInInjectionContext(this.injector, () => {
afterNextRender({
mixedReadWrite: () => {
diff --git a/src/app/shared/services/RecipeService.ts b/src/app/shared/services/RecipeService.ts
index dec28f6..ec0124c 100644
--- a/src/app/shared/services/RecipeService.ts
+++ b/src/app/shared/services/RecipeService.ts
@@ -10,6 +10,7 @@ import { EndpointService } from './EndpointService';
import { SliceView } from '../models/SliceView.model';
import { WithStringDates } from '../util';
import { ImageService } from './ImageService';
+import { RecipeUpdateBody } from '../bodies/RecipeUpdateBody';
@Injectable({
providedIn: 'root',
@@ -32,6 +33,19 @@ export class RecipeService {
};
}
+ private hydrateFullRecipeViewWrapper(raw: WithStringDates): FullRecipeViewWrapper {
+ return {
+ ...raw,
+ recipe: {
+ ...raw.recipe,
+ created: new Date(raw.recipe.created),
+ modified: raw.recipe.modified ? new Date(raw.recipe.modified) : undefined,
+ ingredients: raw.recipe.ingredients!, // TODO: investigate why we need this
+ mainImage: raw.recipe.mainImage ? this.imageService.hydrateImageView(raw.recipe.mainImage) : undefined,
+ },
+ };
+ }
+
public getRecipes(): Observable> {
return this.http.get>>(this.endpointService.getUrl('recipes')).pipe(
map((sliceView) => ({
@@ -47,6 +61,34 @@ export class RecipeService {
);
}
+ public getRecipeView2(
+ username: string,
+ slug: string,
+ includeRawText: boolean = false,
+ ): Observable {
+ return this.http
+ .get>(
+ this.endpointService.getUrl('recipes', [username, slug], {
+ custom: {
+ includeRawText,
+ },
+ }),
+ )
+ .pipe(map((raw) => this.hydrateFullRecipeViewWrapper(raw)));
+ }
+
+ public updateRecipe(
+ username: string,
+ slug: string,
+ recipeUpdateBody: RecipeUpdateBody,
+ ): Observable {
+ return this.http
+ .put<
+ WithStringDates
+ >(this.endpointService.getUrl('recipes', [username, slug]), recipeUpdateBody)
+ .pipe(map((raw) => this.hydrateFullRecipeViewWrapper(raw)));
+ }
+
private getRecipeBaseUrl(recipeView: FullRecipeViewWrapper): string {
return this.endpointService.getUrl('recipes', [recipeView.recipe.owner.username, recipeView.recipe.slug]);
}