diff --git a/internal/app/server/recipe_handler_v2.go b/internal/app/server/recipe_handler_v2.go index ec45e6a..0da4937 100644 --- a/internal/app/server/recipe_handler_v2.go +++ b/internal/app/server/recipe_handler_v2.go @@ -131,6 +131,37 @@ func (s *Server) CreateRecipeHandlerV2(ctx *gin.Context) { }) } +func (s *Server) EditRecipeHandlerV2(ctx *gin.Context) { + s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domainUser.User) { + id := ctx.Param("id") + parsedId, err := strconv.Atoi(id) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to parse ID parameter. %s", err.Error()), + }) + return + } + + recipe, err := s.deps.RecipeService.EditRecipe(ctx, parsedId, user.Id) + + _, err = s.deps.EngagementService.UserEditRecipe(user.Id, recipe.Id) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to create recipe engagement. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully updated recipe.", + "recipe": recipe, + }) + }) +} + func (s *Server) DeleteRecipeHandlerV2(ctx *gin.Context) { s.withAuthenticatedUser(ctx, func(ctx *gin.Context, user *domainUser.User) { id := ctx.Param("id") diff --git a/internal/app/server/server.go b/internal/app/server/server.go index 6f99457..889d925 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -259,10 +259,11 @@ func (s *Server) Setup() *Server { // ---- VERSION 2 ROUTES ---- // router_api_v2 := router_v2.Group(domain.API) + router_api_v2.POST("/recipe", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.CreateRecipeHandlerV2) router_api_v2.GET("/recipe/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeHandlerV2) + router_api_v2.PUT("/recipe/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EditRecipeHandlerV2) router_api_v2.GET("/recipe/of-the-week", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeOfTheWeekHandlerV2) router_api_v2.POST("/recipe/search", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.SearchRecipeHandlerV2) - router_api_v2.POST("/recipe", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.CreateRecipeHandlerV2) router_api_v2.DELETE("/recipe/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.DeleteRecipeHandlerV2) router_api_v2.GET("/recipe/:id/is-owner", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.IsRecipeOwnerV2) diff --git a/internal/app/service/engagement_service.go b/internal/app/service/engagement_service.go index fa89e00..3d1d4d9 100644 --- a/internal/app/service/engagement_service.go +++ b/internal/app/service/engagement_service.go @@ -150,6 +150,20 @@ func (s *EngagementService) UserDeleteRecipe(userId, recipeId int) (domain.Engag return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementDeleted) } +// UserEditRecipe requires a user ID and a recipe ID to create an engagement record in the database. +// A message will be generated using the recipe data and then used to add a make engagement to the +// database. +func (s *EngagementService) UserEditRecipe(userId, recipeId int) (domain.Engagement, error) { + recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId) + if err != nil { + return domain.Engagement{}, err + } + + message := fmt.Sprintf("Edited \"%s\"", recipe.Title) + + return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementEdited) +} + // GetUserEngagement returns a list of the users most recent engagement entries. The number of records // is determined by the limit passed into this function. The results are sorted, newest-to-oldest. func (s *EngagementService) GetUserEngagement(userId, limit int) ([]domain.Engagement, error) { diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index 1bc9d8c..e455bf4 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -80,6 +80,44 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) { return &recipe, nil } +func (s *RecipeService) EditRecipe(ctx *gin.Context, recipeId, userId int) (*domain.Recipe, error) { + var req domain.EditRecipeRequest + + if err := ctx.ShouldBindJSON(&req); err != nil { + return nil, err + } + + if recipeId != req.Id { + return nil, fmt.Errorf("[ERROR] Mismatched recipe IDs provided. Given %d and %d.", recipeId, req.Id) + } + + recipe := domain.Recipe{ + Id: recipeId, + Title: req.Title, + Description: req.Description, + Instructions: req.Instructions, + Serves: req.Serves, + Difficulty: req.Difficulty, + Duration: req.Duration, + Category: req.Category, + Ingredients: req.Ingredients, + Sections: req.Sections, + } + + if err := s.recipeRepository.EditRecipe(&recipe, userId); err != nil { + return &recipe, err + } + + // Update the tags + if len(req.Tags) > 0 { + if err := s.recipeRepository.UpdateRecipeTags(recipe, req.Tags); err != nil { + return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error()) + } + } + + return &recipe, nil +} + // DeleteRecipe deletes a recipe in the database using the recipe repository. This function requires // the userId of the requesting user - to ensure the user is the owner of the recipe. Any errors will // be returned to caller when/if they occur. diff --git a/internal/domain/engagement/engagement.go b/internal/domain/engagement/engagement.go index 7be27ce..92bd413 100644 --- a/internal/domain/engagement/engagement.go +++ b/internal/domain/engagement/engagement.go @@ -16,6 +16,7 @@ const ( EngagementRated EngagementType = "rated" EngagementCreated EngagementType = "created" EngagementDeleted EngagementType = "deleted" + EngagementEdited EngagementType = "edited" ) // Engagement is the database model of a user engagement. There is no need to map to a different diff --git a/internal/domain/engagement/service.go b/internal/domain/engagement/service.go index bbf4492..1fb1c1d 100644 --- a/internal/domain/engagement/service.go +++ b/internal/domain/engagement/service.go @@ -9,5 +9,6 @@ type EngagementService interface { UserShareRecipe(userId, recipeId int) (Engagement, error) UserCreateRecipe(userId, recipeId int) (Engagement, error) UserDeleteRecipe(userId, recipeId int) (Engagement, error) + UserEditRecipe(userId, recipeId int) (Engagement, error) GetUserEngagement(userId, limit int) ([]Engagement, error) } diff --git a/internal/domain/recipe/recipe.go b/internal/domain/recipe/recipe.go index 49fa8aa..c4cdcb5 100644 --- a/internal/domain/recipe/recipe.go +++ b/internal/domain/recipe/recipe.go @@ -156,7 +156,7 @@ type RecipeTag struct { Created time.Time } -// TODO: Comment +// TODO: Document type CreateRecipeRequest struct { Title string Description string @@ -169,3 +169,18 @@ type CreateRecipeRequest struct { Sections []RecipeIngredientSection Tags []string } + +// TODO Document +type EditRecipeRequest struct { + Id int + Title string + Description string + Instructions []RecipeInstruction + Serves int + Difficulty int + Duration RecipeDuration + Category RecipeMeal + Ingredients []RecipeIngredient + Sections []RecipeIngredientSection + Tags []string +} diff --git a/internal/domain/recipe/repository.go b/internal/domain/recipe/repository.go index c91a616..9d4d305 100644 --- a/internal/domain/recipe/repository.go +++ b/internal/domain/recipe/repository.go @@ -2,11 +2,13 @@ package domain type RecipeRepository interface { CreateRecipe(recipe *Recipe) error + EditRecipe(recipe *Recipe, userId int) error DeleteRecipe(recipeId int) error GetRecipe(id int, userId *int) (*Recipe, error) GetRecipes(ids []int, userId *int) ([]Recipe, error) SearchRecipes(filters SearchFilters, userId *int, favorites bool) ([]int, error) CreateRecipeTags(recipe Recipe, tags []string) error + UpdateRecipeTags(recipe Recipe, tags []string) error GetUserRecipesIds(userId int) ([]int, error) GetUserFavoriteRecipesIds(userId int) ([]int, error) GetRecipeTags(recipe *Recipe) error diff --git a/internal/domain/recipe/service.go b/internal/domain/recipe/service.go index c7f67b9..c89f6d0 100644 --- a/internal/domain/recipe/service.go +++ b/internal/domain/recipe/service.go @@ -6,6 +6,7 @@ import ( type RecipeService interface { CreateRecipe(ctx *gin.Context) (*Recipe, error) + EditRecipe(ctx *gin.Context, recipeId, userId int) (*Recipe, error) DeleteRecipe(userId, recipeId int) error GetRecipe(id int, userId *int) (*Recipe, error) SearchRecipes(filters SearchFilters, userId *int, favorites bool) ([]Recipe, error) diff --git a/internal/infrastructure/database/migrations/015_update_engagement_enum.sql b/internal/infrastructure/database/migrations/015_update_engagement_enum.sql new file mode 100644 index 0000000..d7b7b8e --- /dev/null +++ b/internal/infrastructure/database/migrations/015_update_engagement_enum.sql @@ -0,0 +1,10 @@ +-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com) +-- Desc: Updated the E_ENGAGEMENT enum to contain edited. +-- Date: 02/01/2026 + +BEGIN; + +ALTER TYPE E_ENGAGEMENT + ADD VALUE IF NOT EXISTS 'edited'; -- edited recipe + +COMMIT; diff --git a/internal/infrastructure/database/migrations/100_init_database.sh b/internal/infrastructure/database/migrations/100_init_database.sh index 9a415ab..edd071d 100755 --- a/internal/infrastructure/database/migrations/100_init_database.sh +++ b/internal/infrastructure/database/migrations/100_init_database.sh @@ -14,4 +14,5 @@ psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infra psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/012_update_recipes_table_deleted_column.sql psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/013_update_recipes_allow_large_servings.sql psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/014_create_logs_table.sql +psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/015_update_engagement_enum.sql diff --git a/internal/infrastructure/database/repository/engagement_repository.go b/internal/infrastructure/database/repository/engagement_repository.go index 906bd5e..879105f 100644 --- a/internal/infrastructure/database/repository/engagement_repository.go +++ b/internal/infrastructure/database/repository/engagement_repository.go @@ -31,7 +31,6 @@ func NewEngagementRepository(db *sql.DB) domain.EngagementRepository { func (r *EngagementRepository) AddUserEngagement(userId int, message string, engagementType domain.EngagementType) (domain.Engagement, error) { tx, err := r.db.Begin() if err != nil { - tx.Rollback() return domain.Engagement{}, err } @@ -58,7 +57,6 @@ func (r *EngagementRepository) AddUserEngagement(userId int, message string, eng } if err := tx.Commit(); err != nil { - tx.Rollback() return domain.Engagement{}, err } @@ -80,7 +78,6 @@ func (r *EngagementRepository) AddUserEngagement(userId int, message string, eng func (r *EngagementRepository) AddUserEntityEngagement(userId, entityId int, message string, engagementType domain.EngagementType) (domain.Engagement, error) { tx, err := r.db.Begin() if err != nil { - tx.Rollback() return domain.Engagement{}, err } @@ -107,7 +104,6 @@ func (r *EngagementRepository) AddUserEntityEngagement(userId, entityId int, mes } if err := tx.Commit(); err != nil { - tx.Rollback() return domain.Engagement{}, err } @@ -141,7 +137,6 @@ func (r *EngagementRepository) AddEngagement(message string, engagementType doma tx, err := r.db.Begin() if err != nil { - tx.Rollback() return domain.Engagement{}, err } @@ -168,7 +163,6 @@ func (r *EngagementRepository) AddEngagement(message string, engagementType doma } if err := tx.Commit(); err != nil { - tx.Rollback() return domain.Engagement{}, err } @@ -202,7 +196,6 @@ func (r *EngagementRepository) AddEntityEngagement(entityId int, message string, tx, err := r.db.Begin() if err != nil { - tx.Rollback() return domain.Engagement{}, err } @@ -229,7 +222,6 @@ func (r *EngagementRepository) AddEntityEngagement(entityId int, message string, } if err := tx.Commit(); err != nil { - tx.Rollback() return domain.Engagement{}, err } @@ -339,7 +331,6 @@ func (r *EngagementRepository) GetUserEngagementFiltered(userId, limit int, enga func (r *EngagementRepository) UserFavoriteRecipeToggle(userId, recipeId int) (bool, error) { tx, err := r.db.Begin() if err != nil { - tx.Rollback() return false, err } diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go index 096ec16..f229284 100644 --- a/internal/infrastructure/database/repository/recipe_repository.go +++ b/internal/infrastructure/database/repository/recipe_repository.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "strings" + "time" domain "github.com/haydenhargreaves/Potion/internal/domain/recipe" "github.com/lib/pq" @@ -33,7 +34,6 @@ func NewRecipeRepository(db *sql.DB) domain.RecipeRepository { func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error { tx, err := r.db.Begin() if err != nil { - tx.Rollback() return err } @@ -92,7 +92,6 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error { } if err := tx.Commit(); err != nil { - tx.Rollback() return err } @@ -102,6 +101,97 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error { return nil } +// EditRecipe updates a recipe in the database. The recipe provided must contain an ID, otherwise this +// function will fail - it will not know what recipe to edit. +func (r *RecipeRepository) EditRecipe(recipe *domain.Recipe, userId int) error { + if recipe.Id <= 0 { + return fmt.Errorf("[ERROR] Recipe must contain an ID. Cannot edit unknown recipe.") + } + + tx, err := r.db.Begin() + if err != nil { + return err + } + + // This query will ensure the userId matches the owner. + query := `UPDATE recipes SET + title = $1, + description = $2, + instructions = $3, + serves = $4, + difficulty = $5, + duration = $6, + category = $7, + ingredients = $8, + modified = $9 + WHERE id = $10 + AND userid = $11;` + + // NOTE: Data steps + // cast duration to JSON + // convert ingredients to store type + // cast store type to JSON + // extract string instructions from type + // cast category to string + // use nil for the modified time + + durationJSON, err := json.Marshal(recipe.Duration) + if err != nil { + return err + } + + ingredientsStore := domain.RecipeIngredientStore{ + Sections: recipe.Sections, + Ingredients: recipe.Ingredients, + } + + ingredientsJSON, err := json.Marshal(ingredientsStore) + if err != nil { + return err + } + + instructions := make([]string, len(recipe.Instructions)) + for i, instruction := range recipe.Instructions { + instructions[i] = instruction.Content + } + + result, err := tx.Exec( + query, + recipe.Title, + recipe.Description, + pq.Array(instructions), + recipe.Serves, + recipe.Difficulty, + durationJSON, + string(recipe.Category), + ingredientsJSON, + time.Now().UTC(), + recipe.Id, + userId, + ) + if err != nil { + tx.Rollback() + return err + } + + rows, err := result.RowsAffected() + if err != nil { + tx.Rollback() + return err + } + + if rows != 1 { + tx.Rollback() + return fmt.Errorf("[ERROR] Modified an unexpected number of rows. Expected 1, modified %d.", rows) + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} + // DeleteRecipe deletes a recipe in the database. This is done by setting the deleted field to true. // This will create a "soft delete" effect. This function does not validate that the user is the owner, // so the caller should validate the owner. If any errors occur, they will be returned to the caller. @@ -437,7 +527,6 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string) error { tx, err := r.db.Begin() if err != nil { - tx.Rollback() return err } @@ -487,6 +576,82 @@ func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string) return nil } +// UpdateRecipeTags replaces all existing tags for a recipe with a new list of tags. +// It removes all current tag associations, creates any new tags that don't exist, +// and creates new associations for the provided tags. The recipe object must contain +// a valid ID. Any errors will be bubbled to the caller. +func (r *RecipeRepository) UpdateRecipeTags(recipe domain.Recipe, tags []string) error { + tx, err := r.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() // Rollback if we don't commit + + if recipe.Id <= 0 { + return fmt.Errorf("[ERROR] Recipe must have a valid ID") + } + + // Step 1: Delete all existing tag associations for this recipe + deleteQuery := `DELETE FROM RecipeTags WHERE RecipeId = $1;` + if _, err := tx.Exec(deleteQuery, recipe.Id); err != nil { + return fmt.Errorf("[ERROR] Failed to delete existing recipe tags: %w", err) + } + + // Step 2: Normalize the tag names (lower case with trimmed space) + normalized := make(map[string]struct{}) // Use map to disallow duplicates + for _, tag := range tags { + trimmed := strings.ToLower(strings.TrimSpace(tag)) + if trimmed != "" { + normalized[trimmed] = struct{}{} + } + } + + // If no tags provided, we're done (all tags removed) + if len(normalized) == 0 { + if err := tx.Commit(); err != nil { + return err + } + return nil + } + + // Step 3: Insert the tags into the DB and return their IDs into the tag ID list + var tagIds []int + for tag := range normalized { + var tagId int + query := ` + INSERT INTO tags (name) VALUES ($1) + ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name + RETURNING id; + ` + err := tx.QueryRow(query, tag).Scan(&tagId) + if err != nil { + return fmt.Errorf("[ERROR] Failed to retrieve or create tag: %w", err) + } + tagIds = append(tagIds, tagId) + } + + // Step 4: Insert the new tag associations + // Use a single prepared statement for all inserts + stmt, err := tx.Prepare("INSERT INTO RecipeTags (RecipeId, TagId) VALUES ($1, $2);") + if err != nil { + return fmt.Errorf("[ERROR] Failed to create statement for recipe tag mapping: %w", err) + } + defer stmt.Close() + + for _, id := range tagIds { + if _, err := stmt.Exec(recipe.Id, id); err != nil { + return fmt.Errorf("[ERROR] Failed to insert tag-recipe mapping: %w", err) + } + } + + // Commit the transaction + if err := tx.Commit(); err != nil { + return err + } + + return nil +} + // GetUserRecipes gets a list of a users owned recipes. This function does not ensure the user is // authenticated or exists. If nothing is found, a blank slice will be returned. The resulting list // is sorted by the created dates, newest first. Any errors will be bubbled to the caller. diff --git a/internal/infrastructure/database/repository/user_repository.go b/internal/infrastructure/database/repository/user_repository.go index ee198b5..12098b9 100644 --- a/internal/infrastructure/database/repository/user_repository.go +++ b/internal/infrastructure/database/repository/user_repository.go @@ -32,7 +32,6 @@ func NewUserRepository(db *sql.DB) domain.UserRepository { func (r *UserRepository) CreateGoogleUser(googleUserInfo *domain.GoogleUserInfo, googleRefreshToken string) (domain.User, error) { tx, err := r.db.Begin() if err != nil { - tx.Rollback() return domain.User{}, err } @@ -66,7 +65,6 @@ func (r *UserRepository) CreateGoogleUser(googleUserInfo *domain.GoogleUserInfo, } if err := tx.Commit(); err != nil { - tx.Rollback() return domain.User{}, err } diff --git a/web/src/components/buttons/DeleteButton.tsx b/web/src/components/buttons/DeleteButton.tsx index 7f62528..8389834 100644 --- a/web/src/components/buttons/DeleteButton.tsx +++ b/web/src/components/buttons/DeleteButton.tsx @@ -6,7 +6,7 @@ interface DeleteButtonProps { export default function DeleteButton({ clickHandler }: DeleteButtonProps) { return ( - diff --git a/web/src/components/buttons/EditButton.tsx b/web/src/components/buttons/EditButton.tsx new file mode 100644 index 0000000..14477b4 --- /dev/null +++ b/web/src/components/buttons/EditButton.tsx @@ -0,0 +1,15 @@ +import EditIconSmall from "../icons/EditIconSmall"; + +interface EditButtonProps { + clickHandler: () => void +} + +export default function EditButton({ clickHandler }: EditButtonProps) { + return ( + + ); + +} diff --git a/web/src/components/buttons/FavoriteButton.tsx b/web/src/components/buttons/FavoriteButton.tsx index d7a71be..9faaa7e 100644 --- a/web/src/components/buttons/FavoriteButton.tsx +++ b/web/src/components/buttons/FavoriteButton.tsx @@ -32,7 +32,7 @@ export default function FavoriteButton({ favorite, id }: FavoriteButtonProps) { const result = await EngagementFavoriteRecipe(id); if (isApiError(result)) { console.error(result.message); - } + } } useEffect(() => { @@ -43,7 +43,7 @@ export default function FavoriteButton({ favorite, id }: FavoriteButtonProps) { return _favorite ? ( diff --git a/web/src/pages/Recipe.tsx b/web/src/pages/Recipe.tsx index a55ad8f..469ce18 100644 --- a/web/src/pages/Recipe.tsx +++ b/web/src/pages/Recipe.tsx @@ -18,6 +18,7 @@ import type { User } from "../types/user"; import DeleteButton from "../components/buttons/DeleteButton"; import ROUTE_CONSTANTS from "../types/routes"; import ConfirmRecipeDeleteModal from "../components/modals/ConfirmRecipeDeleteModal"; +import EditButton from "../components/buttons/EditButton"; export default function RecipePage() { // Url params @@ -54,7 +55,7 @@ export default function RecipePage() { const getIsAuthor = async () => { if (!recipe) return; - const response = await IsRecipeOwner(recipe.Id) + const response = await IsRecipeOwner(recipe.Id); if (isApiError(response)) { setError(response.message); return; @@ -80,6 +81,12 @@ export default function RecipePage() { setIsDeleting(true); } + const editHandler = () => { + if (!recipe || !isAuthor) return; + const route = ROUTE_CONSTANTS.Edit(Number(recipe.Id)); + void navigate(route); + } + // Effects useEffect(() => { void getRecipe(Number(id)); @@ -102,9 +109,9 @@ export default function RecipePage() { return recipe ? ( <> - {isDeleting && - setIsDeleting(false)} + {isDeleting && + setIsDeleting(false)} deleteHandler={() => void deleteRecipe(recipe.Id)} />} @@ -121,6 +128,7 @@ export default function RecipePage() { {isAuthor && } + {isAuthor && }

About this recipe

diff --git a/web/src/services/RecipeService.ts b/web/src/services/RecipeService.ts index 3fb9c82..551ef8f 100644 --- a/web/src/services/RecipeService.ts +++ b/web/src/services/RecipeService.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import type { CreateRecipeRequest, CreateRecipeResponse, DeleteRecipeResponse, GetRecipeOfTheWeekResponse, GetRecipeResponse, IsRecipeOwnerResponse, SearchRecipesResponse } from "../types/api/recipe"; +import type { CreateRecipeRequest, CreateRecipeResponse, DeleteRecipeResponse, EditRecipeRequest, GetRecipeOfTheWeekResponse, GetRecipeResponse, IsRecipeOwnerResponse, SearchRecipesResponse, EditRecipeResponse } from "../types/api/recipe"; import type { Recipe } from "../types/recipe"; import type { ApiError } from "../types/api/error"; import type { SearchFilters } from "../types/search"; @@ -64,6 +64,20 @@ export async function CreateRecipe(data: CreateRecipeRequest): Promise { + const response = await axios.put(`${BACKEND_URL}/v2/api/recipe/${data.Id}`, data); + + if (response.status !== 200 || response.data.recipe === undefined) { + const err: ApiError = { + status: response.data.status, + message: response.data.message + }; + return err; + } + + return response.data.recipe; +} + export async function DeleteRecipe(id: number): Promise { const response = await axios.delete(`${BACKEND_URL}/v2/api/recipe/${id}`); diff --git a/web/src/types/api/recipe.ts b/web/src/types/api/recipe.ts index 2f0d548..915dcd9 100644 --- a/web/src/types/api/recipe.ts +++ b/web/src/types/api/recipe.ts @@ -24,6 +24,12 @@ export interface CreateRecipeResponse { recipe?: Recipe; } +export interface EditRecipeResponse { + status: number; + message: string; + recipe?: Recipe; +} + export interface CreateRecipeRequest { Title: string; Description: string; @@ -37,6 +43,20 @@ export interface CreateRecipeRequest { Tags: string[]; } +export interface EditRecipeRequest { + Id: number; + Title: string; + Description: string; + Instructions: RecipeInstruction[]; + Serves: number; + Difficulty: number; + Duration: RecipeDuration; + Category: RecipeMeal; + Ingredients: RecipeIngredient[]; + Sections: RecipeIngredientSection[]; + Tags: string[]; +} + export interface DeleteRecipeResponse { status: number; message: string; diff --git a/web/src/types/engagement.ts b/web/src/types/engagement.ts index 233e6c9..a787f1f 100644 --- a/web/src/types/engagement.ts +++ b/web/src/types/engagement.ts @@ -1,5 +1,5 @@ -export type EngagementType = "made" | "liked" | "viewed" | "shared" | "reviewed" | "rated"; +export type EngagementType = "made" | "liked" | "viewed" | "shared" | "reviewed" | "rated" | "created" | "deleted" | "edited"; export interface Engagement { Id: number; diff --git a/web/src/types/routes.ts b/web/src/types/routes.ts index bea2f70..e5d9104 100644 --- a/web/src/types/routes.ts +++ b/web/src/types/routes.ts @@ -10,6 +10,7 @@ const ROUTE_CONSTANTS: { History: string; Search: string; Recipe: (id: number) => string; + Edit: (id: number) => string; } = { Home: `${VERSION_FLAG}/web/home`, Favorites: `${VERSION_FLAG}/web/favorites`, @@ -20,6 +21,7 @@ const ROUTE_CONSTANTS: { History: `${VERSION_FLAG}/web/history`, Search: `${VERSION_FLAG}/web/search`, Recipe: (id: number) => `${VERSION_FLAG}/web/recipe/${id}`, + Edit: (id: number) => `${VERSION_FLAG}/web/create?edit=${id}`, }; export default ROUTE_CONSTANTS;