(FIX): So so so much has been migrated.
But this includes templ builds also... Needed for compilation. Search is the last broken piece, I believe.
This commit is contained in:
parent
ce6d748731
commit
90b3b7b1b0
@ -12,8 +12,9 @@ import (
|
|||||||
// GetRecipeOfTheWeekHandler fetchs the current recipe of the week and returns it.
|
// GetRecipeOfTheWeekHandler fetchs the current recipe of the week and returns it.
|
||||||
// If an error occurs, it will be returned and a recipe will not be returned.
|
// If an error occurs, it will be returned and a recipe will not be returned.
|
||||||
//
|
//
|
||||||
// Until auth is reimplemented, there is no way to determine what user is making the
|
// BUG: Until auth is reimplemented, there is no way to determine what user is making the
|
||||||
// call.
|
// call.
|
||||||
|
// NOTE: I believe this issue has been resolved
|
||||||
func (s *Server) GetRecipeOfTheWeekHandlerV2(ctx *gin.Context) {
|
func (s *Server) GetRecipeOfTheWeekHandlerV2(ctx *gin.Context) {
|
||||||
userId := getUserId(ctx)
|
userId := getUserId(ctx)
|
||||||
recipe, err := s.deps.RecipeService.GetRecipeOfTheWeek(userId)
|
recipe, err := s.deps.RecipeService.GetRecipeOfTheWeek(userId)
|
||||||
@ -93,3 +94,20 @@ func (s *Server) SearchRecipeHandlerV2(ctx *gin.Context) {
|
|||||||
"recipes": recipes,
|
"recipes": recipes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) CreateRecipeHandlerV2(ctx *gin.Context) {
|
||||||
|
recipe, err := s.deps.RecipeService.CreateRecipe(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"status": http.StatusBadRequest,
|
||||||
|
"message": fmt.Sprintf("[ERROR] Failed to create recipe. %s", err.Error()),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": http.StatusOK,
|
||||||
|
"message": "[OK] Successfully created new recipe.",
|
||||||
|
"recipe": recipe,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -204,6 +204,7 @@ func (s *Server) Setup() *Server {
|
|||||||
router_api_v2.GET("/recipe/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeHandlerV2)
|
router_api_v2.GET("/recipe/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeHandlerV2)
|
||||||
router_api_v2.GET("/recipe/of-the-week", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeOfTheWeekHandlerV2)
|
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/search", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.SearchRecipeHandlerV2)
|
||||||
|
router_api_v2.POST("/recipe", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.CreateRecipeHandlerV2)
|
||||||
|
|
||||||
router_api_v2.GET("/auth/login", s.GetGoogleAuthUrlHandlerV2)
|
router_api_v2.GET("/auth/login", s.GetGoogleAuthUrlHandlerV2)
|
||||||
router_api_v2.GET("/auth/callback", s.GoogleCallbackHandlerV2)
|
router_api_v2.GET("/auth/callback", s.GoogleCallbackHandlerV2)
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -45,66 +41,25 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
|
|||||||
return nil, fmt.Errorf("User is not logged in.")
|
return nil, fmt.Errorf("User is not logged in.")
|
||||||
}
|
}
|
||||||
|
|
||||||
title := ctx.PostForm("title")
|
|
||||||
description := ctx.PostForm("description")
|
|
||||||
preparation := ctx.PostForm("preparation-time")
|
|
||||||
cook := ctx.PostForm("cook-time")
|
|
||||||
serving := ctx.PostForm("serving-size")
|
|
||||||
category := ctx.PostForm("category")
|
|
||||||
difficulty := ctx.PostForm("difficulty")
|
|
||||||
ingredients := ctx.PostFormArray("ingredients")
|
|
||||||
quantity := ctx.PostFormArray("quantity")
|
|
||||||
instructions := ctx.PostFormArray("instructions")
|
|
||||||
tags := strings.Split(ctx.PostForm("tags"), ",")
|
|
||||||
userId := ctx.MustGet("userId").(int)
|
userId := ctx.MustGet("userId").(int)
|
||||||
|
var req domain.CreateRecipeRequest
|
||||||
|
|
||||||
// Have to get the image differently
|
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||||
image, err := ctx.FormFile("image")
|
return nil, err
|
||||||
if err != nil && !errors.Is(err, http.ErrMissingFile) {
|
|
||||||
// Error getting image
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to proper values
|
|
||||||
servingInt, _ := strconv.Atoi(serving)
|
|
||||||
difficultyInt, _ := strconv.Atoi(difficulty)
|
|
||||||
prepInt, _ := strconv.Atoi(preparation)
|
|
||||||
cookInt, _ := strconv.Atoi(cook)
|
|
||||||
|
|
||||||
var ingredientSlice []domain.RecipeIngredient
|
|
||||||
for i := range len(ingredients) {
|
|
||||||
if strings.TrimSpace(ingredients[i]) != "" {
|
|
||||||
ins := domain.RecipeIngredient{
|
|
||||||
Name: ingredients[i],
|
|
||||||
Quantity: quantity[i],
|
|
||||||
}
|
|
||||||
|
|
||||||
ingredientSlice = append(ingredientSlice, ins)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var instructionSlice []string
|
|
||||||
for _, ins := range instructions {
|
|
||||||
if ins != "" {
|
|
||||||
instructionSlice = append(instructionSlice, ins)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the recipe
|
|
||||||
recipe := domain.Recipe{
|
recipe := domain.Recipe{
|
||||||
Title: title,
|
Title: req.Title,
|
||||||
Description: description,
|
Description: req.Description,
|
||||||
Instructions: instructionSlice,
|
Instructions: req.Instructions,
|
||||||
Serves: servingInt,
|
Serves: req.Serves,
|
||||||
Difficulty: difficultyInt,
|
Difficulty: req.Difficulty,
|
||||||
Duration: domain.RecipeDuration{
|
Duration: req.Duration,
|
||||||
Total: prepInt + cookInt,
|
Category: req.Category,
|
||||||
Prep: prepInt,
|
Ingredients: req.Ingredients,
|
||||||
Cook: cookInt,
|
Sections: req.Sections,
|
||||||
},
|
UserId: userId,
|
||||||
Category: domain.RecipeMeal(category),
|
Created: time.Now(),
|
||||||
Ingredients: ingredientSlice,
|
|
||||||
UserId: userId,
|
|
||||||
Created: time.Now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.recipeRepository.CreateRecipe(&recipe); err != nil {
|
if err := s.recipeRepository.CreateRecipe(&recipe); err != nil {
|
||||||
@ -112,17 +67,96 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Upload the image
|
// TODO: Upload the image
|
||||||
if image != nil {
|
// if req.image != nil {
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Create the tags
|
// Create the tags
|
||||||
if len(tags) > 0 {
|
if len(req.Tags) > 0 {
|
||||||
if err := s.recipeRepository.CreateRecipeTags(recipe, tags); err != nil {
|
if err := s.recipeRepository.CreateRecipeTags(recipe, req.Tags); err != nil {
|
||||||
return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error())
|
return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &recipe, nil
|
return &recipe, nil
|
||||||
|
|
||||||
|
// title := ctx.PostForm("title")
|
||||||
|
// description := ctx.PostForm("description")
|
||||||
|
// preparation := ctx.PostForm("preparation-time")
|
||||||
|
// cook := ctx.PostForm("cook-time")
|
||||||
|
// serving := ctx.PostForm("serving-size")
|
||||||
|
// category := ctx.PostForm("category")
|
||||||
|
// difficulty := ctx.PostForm("difficulty")
|
||||||
|
// ingredients := ctx.PostFormArray("ingredients")
|
||||||
|
// quantity := ctx.PostFormArray("quantity")
|
||||||
|
// instructions := ctx.PostFormArray("instructions")
|
||||||
|
// tags := strings.Split(ctx.PostForm("tags"), ",")
|
||||||
|
// userId := ctx.MustGet("userId").(int)
|
||||||
|
//
|
||||||
|
// // Have to get the image differently
|
||||||
|
// image, err := ctx.FormFile("image")
|
||||||
|
// if err != nil && !errors.Is(err, http.ErrMissingFile) {
|
||||||
|
// // Error getting image
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Convert to proper values
|
||||||
|
// servingInt, _ := strconv.Atoi(serving)
|
||||||
|
// difficultyInt, _ := strconv.Atoi(difficulty)
|
||||||
|
// prepInt, _ := strconv.Atoi(preparation)
|
||||||
|
// cookInt, _ := strconv.Atoi(cook)
|
||||||
|
//
|
||||||
|
// var ingredientSlice []domain.RecipeIngredient
|
||||||
|
// for i := range len(ingredients) {
|
||||||
|
// if strings.TrimSpace(ingredients[i]) != "" {
|
||||||
|
// ins := domain.RecipeIngredient{
|
||||||
|
// Name: ingredients[i],
|
||||||
|
// Quantity: quantity[i],
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// ingredientSlice = append(ingredientSlice, ins)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var instructionSlice []string
|
||||||
|
// for _, ins := range instructions {
|
||||||
|
// if ins != "" {
|
||||||
|
// instructionSlice = append(instructionSlice, ins)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Create the recipe
|
||||||
|
// recipe := domain.Recipe{
|
||||||
|
// Title: title,
|
||||||
|
// Description: description,
|
||||||
|
// Instructions: instructionSlice,
|
||||||
|
// Serves: servingInt,
|
||||||
|
// Difficulty: difficultyInt,
|
||||||
|
// Duration: domain.RecipeDuration{
|
||||||
|
// Total: prepInt + cookInt,
|
||||||
|
// Prep: prepInt,
|
||||||
|
// Cook: cookInt,
|
||||||
|
// },
|
||||||
|
// Category: domain.RecipeMeal(category),
|
||||||
|
// Ingredients: ingredientSlice,
|
||||||
|
// UserId: userId,
|
||||||
|
// Created: time.Now(),
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if err := s.recipeRepository.CreateRecipe(&recipe); err != nil {
|
||||||
|
// return &recipe, err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // TODO: Upload the image
|
||||||
|
// if image != nil {
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Create the tags
|
||||||
|
// if len(tags) > 0 {
|
||||||
|
// if err := s.recipeRepository.CreateRecipeTags(recipe, tags); err != nil {
|
||||||
|
// return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return &recipe, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore,
|
// GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore,
|
||||||
@ -135,7 +169,7 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
|
|||||||
func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
||||||
recipe, err := s.recipeRepository.GetRecipe(id, userId)
|
recipe, err := s.recipeRepository.GetRecipe(id, userId)
|
||||||
|
|
||||||
if recipe == nil {
|
if recipe == nil && err == nil {
|
||||||
return nil, fmt.Errorf("Recipe does not exist or has been relocated. Please try again.")
|
return nil, fmt.Errorf("Recipe does not exist or has been relocated. Please try again.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,5 +245,14 @@ func (s *RecipeService) GetUserMadeRecipes(userId, limit int) ([]domain.Recipe,
|
|||||||
// GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value,
|
// GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value,
|
||||||
// the recipe will be nil. Any errors will be bubbled to the caller.
|
// the recipe will be nil. Any errors will be bubbled to the caller.
|
||||||
func (s *RecipeService) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) {
|
func (s *RecipeService) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) {
|
||||||
return s.recipeRepository.GetRecipeOfTheWeek(userId)
|
id, err := s.recipeRepository.GetRecipeOfTheWeekId(userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == nil {
|
||||||
|
return nil, fmt.Errorf("[ERROR] Recipe of the week ID could not be found. It may not exist.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.recipeRepository.GetRecipe(*id, userId)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,11 +46,62 @@ func ParseMeal(meal int) RecipeMeal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Comment
|
||||||
|
type RecipeIngredientUnit string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Blank RecipeIngredientUnit = ""
|
||||||
|
Tsp RecipeIngredientUnit = "tsp"
|
||||||
|
Tbsp RecipeIngredientUnit = "tbsp"
|
||||||
|
FlOz RecipeIngredientUnit = "fl oz"
|
||||||
|
Cup RecipeIngredientUnit = "cup"
|
||||||
|
Ml RecipeIngredientUnit = "ml"
|
||||||
|
Litre RecipeIngredientUnit = "l"
|
||||||
|
Pint RecipeIngredientUnit = "pt"
|
||||||
|
Quart RecipeIngredientUnit = "qt"
|
||||||
|
Gallon RecipeIngredientUnit = "gal"
|
||||||
|
Gram RecipeIngredientUnit = "g"
|
||||||
|
Kilogram RecipeIngredientUnit = "kg"
|
||||||
|
Ounce RecipeIngredientUnit = "oz"
|
||||||
|
Pound RecipeIngredientUnit = "lb"
|
||||||
|
Piece RecipeIngredientUnit = "piece"
|
||||||
|
Clove RecipeIngredientUnit = "clove"
|
||||||
|
Slice RecipeIngredientUnit = "slice"
|
||||||
|
Stick RecipeIngredientUnit = "stick"
|
||||||
|
Bunch RecipeIngredientUnit = "bunch"
|
||||||
|
Pinch RecipeIngredientUnit = "pinch"
|
||||||
|
Dash RecipeIngredientUnit = "dash"
|
||||||
|
Splash RecipeIngredientUnit = "splash"
|
||||||
|
ToTaste RecipeIngredientUnit = "to taste"
|
||||||
|
)
|
||||||
|
|
||||||
// RecipeIngredient is a single ingredients in a recipe. These have JSON tags which allow them
|
// RecipeIngredient is a single ingredients in a recipe. These have JSON tags which allow them
|
||||||
// to be marshaled into a JSON array and stored in the database (JSONB).
|
// to be marshaled into a JSON array and stored in the database (JSONB).
|
||||||
type RecipeIngredient struct {
|
type RecipeIngredient struct {
|
||||||
Name string `json:"Name"`
|
Id string `json:"Id"`
|
||||||
Quantity string `json:"Quantity"`
|
SectionId string `json:"SectionId"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Amount float64 `json:"Amount"`
|
||||||
|
Unit RecipeIngredientUnit `json:"Unit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Comment
|
||||||
|
type RecipeInstruction struct {
|
||||||
|
Id string `json:"Id"`
|
||||||
|
Content string `json:"Content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Comment
|
||||||
|
type RecipeIngredientSection struct {
|
||||||
|
Id string `json:"Id"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecipeIngredientStore is the struct stored in the database Ingredients column. It is simply a
|
||||||
|
// combindation of the sections and the ingredients so they can be stored together.
|
||||||
|
type RecipeIngredientStore struct {
|
||||||
|
Sections []RecipeIngredientSection `json:"Sections"`
|
||||||
|
Ingredients []RecipeIngredient `json:"Ingredients"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recipe is the database model of a recipe. There is no need to map to a different model so
|
// Recipe is the database model of a recipe. There is no need to map to a different model so
|
||||||
@ -61,12 +112,13 @@ type Recipe struct {
|
|||||||
Id int
|
Id int
|
||||||
Title string
|
Title string
|
||||||
Description string
|
Description string
|
||||||
Instructions []string
|
Instructions []RecipeInstruction
|
||||||
Serves int
|
Serves int
|
||||||
Difficulty int
|
Difficulty int
|
||||||
Duration RecipeDuration
|
Duration RecipeDuration
|
||||||
Category RecipeMeal
|
Category RecipeMeal
|
||||||
Ingredients []RecipeIngredient // Just a list of ingredients
|
Ingredients []RecipeIngredient
|
||||||
|
Sections []RecipeIngredientSection
|
||||||
UserId int
|
UserId int
|
||||||
Modified *time.Time // Pointer to allow null
|
Modified *time.Time // Pointer to allow null
|
||||||
Created time.Time
|
Created time.Time
|
||||||
@ -79,11 +131,11 @@ type Recipe struct {
|
|||||||
// details can be found in the SearchRecipes service function.
|
// details can be found in the SearchRecipes service function.
|
||||||
type SearchFilters struct {
|
type SearchFilters struct {
|
||||||
Search string `json:"Search"`
|
Search string `json:"Search"`
|
||||||
MealType int `json:"MealType"`
|
MealType int `json:"MealType"`
|
||||||
Time int `json:"Time"`
|
Time int `json:"Time"`
|
||||||
Difficulty int `json:"Difficulty"`
|
Difficulty int `json:"Difficulty"`
|
||||||
ServingSize int `json:"ServingSize"`
|
ServingSize int `json:"ServingSize"`
|
||||||
Favorites bool `json:"Favorites"`
|
Favorites bool `json:"Favorites"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tag is a model which represents a single tag in the Tags table. A tag is mapped to a recipe
|
// Tag is a model which represents a single tag in the Tags table. A tag is mapped to a recipe
|
||||||
@ -102,3 +154,17 @@ type RecipeTag struct {
|
|||||||
TagId int
|
TagId int
|
||||||
Created time.Time
|
Created time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Comment
|
||||||
|
type CreateRecipeRequest struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Instructions []RecipeInstruction
|
||||||
|
Serves int
|
||||||
|
Difficulty int
|
||||||
|
Duration RecipeDuration
|
||||||
|
Category RecipeMeal
|
||||||
|
Ingredients []RecipeIngredient
|
||||||
|
Sections []RecipeIngredientSection
|
||||||
|
Tags []string
|
||||||
|
}
|
||||||
|
|||||||
@ -10,5 +10,5 @@ type RecipeRepository interface {
|
|||||||
GetUserFavoriteRecipes(id int) ([]Recipe, error)
|
GetUserFavoriteRecipes(id int) ([]Recipe, error)
|
||||||
GetRecipeTags(recipe *Recipe) error
|
GetRecipeTags(recipe *Recipe) error
|
||||||
GetRecipeFavorite(recipe *Recipe, userId int) error
|
GetRecipeFavorite(recipe *Recipe, userId int) error
|
||||||
GetRecipeOfTheWeek(userId *int) (*Recipe, error)
|
GetRecipeOfTheWeekId(userId *int) (*int, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,9 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
|||||||
|
|
||||||
// NOTE: Data steps
|
// NOTE: Data steps
|
||||||
// cast duration to JSON
|
// cast duration to JSON
|
||||||
// cast ingredients to JSON
|
// convert ingredients to store type
|
||||||
|
// cast store type to JSON
|
||||||
|
// extract string instructions from type
|
||||||
// cast category to string
|
// cast category to string
|
||||||
// use nil for the modified time
|
// use nil for the modified time
|
||||||
|
|
||||||
@ -55,17 +57,27 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ingredientsJSON, err := json.Marshal(recipe.Ingredients)
|
ingredientsStore := domain.RecipeIngredientStore{
|
||||||
|
Sections: recipe.Sections,
|
||||||
|
Ingredients: recipe.Ingredients,
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientsJSON, err := json.Marshal(ingredientsStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
instructions := make([]string, len(recipe.Instructions))
|
||||||
|
for i, instruction := range recipe.Instructions {
|
||||||
|
instructions[i] = instruction.Content
|
||||||
|
}
|
||||||
|
|
||||||
var id int
|
var id int
|
||||||
if err = tx.QueryRow(
|
if err = tx.QueryRow(
|
||||||
query,
|
query,
|
||||||
recipe.Title,
|
recipe.Title,
|
||||||
recipe.Description,
|
recipe.Description,
|
||||||
pq.Array(recipe.Instructions),
|
pq.Array(instructions),
|
||||||
recipe.Serves,
|
recipe.Serves,
|
||||||
recipe.Difficulty,
|
recipe.Difficulty,
|
||||||
durationJSON,
|
durationJSON,
|
||||||
@ -94,8 +106,7 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
|||||||
// for added safety. The repository will not check for a nil result, instead the service will. Callers
|
// for added safety. The repository will not check for a nil result, instead the service will. Callers
|
||||||
// are responsible for protecting against double nil results. Any errors will be bubbled to the caller.
|
// are responsible for protecting against double nil results. Any errors will be bubbled to the caller.
|
||||||
func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
||||||
query := `
|
query := ` SELECT
|
||||||
SELECT
|
|
||||||
id, title, description, instructions, serves, difficulty, duration, category, ingredients,
|
id, title, description, instructions, serves, difficulty, duration, category, ingredients,
|
||||||
userid, modified, created
|
userid, modified, created
|
||||||
FROM recipes
|
FROM recipes
|
||||||
@ -103,6 +114,7 @@ func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error
|
|||||||
`
|
`
|
||||||
|
|
||||||
var durationBytes []byte
|
var durationBytes []byte
|
||||||
|
var instructions pq.StringArray
|
||||||
var ingredientBytes []byte
|
var ingredientBytes []byte
|
||||||
|
|
||||||
var recipe domain.Recipe
|
var recipe domain.Recipe
|
||||||
@ -110,7 +122,8 @@ func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error
|
|||||||
&recipe.Id,
|
&recipe.Id,
|
||||||
&recipe.Title,
|
&recipe.Title,
|
||||||
&recipe.Description,
|
&recipe.Description,
|
||||||
pq.Array(&recipe.Instructions),
|
// pq.Array(&instructions),
|
||||||
|
&instructions,
|
||||||
&recipe.Serves,
|
&recipe.Serves,
|
||||||
&recipe.Difficulty,
|
&recipe.Difficulty,
|
||||||
&durationBytes,
|
&durationBytes,
|
||||||
@ -137,16 +150,23 @@ func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error
|
|||||||
|
|
||||||
// Parse ingredient
|
// Parse ingredient
|
||||||
if len(ingredientBytes) > 0 {
|
if len(ingredientBytes) > 0 {
|
||||||
var ingredients []domain.RecipeIngredient
|
var store domain.RecipeIngredientStore
|
||||||
if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
|
if err := json.Unmarshal(ingredientBytes, &store); err != nil {
|
||||||
|
// Check for unmarshal to support backwards compatability
|
||||||
return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
|
return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
recipe.Ingredients = ingredients
|
recipe.Ingredients = store.Ingredients
|
||||||
|
recipe.Sections = store.Sections
|
||||||
} else {
|
} else {
|
||||||
recipe.Ingredients = []domain.RecipeIngredient{}
|
recipe.Ingredients = []domain.RecipeIngredient{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add instructions
|
||||||
|
for _, instruction := range instructions {
|
||||||
|
recipe.Instructions = append(recipe.Instructions, domain.RecipeInstruction{Content: instruction})
|
||||||
|
}
|
||||||
|
|
||||||
// Add tags
|
// Add tags
|
||||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
if err := r.GetRecipeTags(&recipe); err != nil {
|
||||||
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
||||||
@ -169,83 +189,18 @@ func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error
|
|||||||
// will. Callers are responsible for protecting against double nil results. Any errors will be bubbled
|
// will. Callers are responsible for protecting against double nil results. Any errors will be bubbled
|
||||||
// to the caller.
|
// to the caller.
|
||||||
func (r *RecipeRepository) GetRecipes(ids []int, userId *int) ([]domain.Recipe, error) {
|
func (r *RecipeRepository) GetRecipes(ids []int, userId *int) ([]domain.Recipe, error) {
|
||||||
query := `
|
|
||||||
SELECT
|
|
||||||
id, title, description, instructions, serves, difficulty, duration, category, ingredients, userid, modified, created
|
|
||||||
FROM recipes
|
|
||||||
WHERE id = ANY($1)
|
|
||||||
ORDER BY array_position($1, id);
|
|
||||||
`
|
|
||||||
|
|
||||||
var recipes []domain.Recipe
|
var recipes []domain.Recipe
|
||||||
|
|
||||||
rows, err := r.db.Query(query, pq.Array(ids))
|
for _, id := range ids {
|
||||||
if err != nil {
|
recipe, err := r.GetRecipe(id, userId)
|
||||||
return nil, fmt.Errorf("Failed to get recipes. %s", err.Error())
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var recipe domain.Recipe
|
|
||||||
var durationBytes []byte
|
|
||||||
var ingredientBytes []byte
|
|
||||||
|
|
||||||
if err := rows.Scan(
|
|
||||||
&recipe.Id,
|
|
||||||
&recipe.Title,
|
|
||||||
&recipe.Description,
|
|
||||||
pq.Array(&recipe.Instructions),
|
|
||||||
&recipe.Serves,
|
|
||||||
&recipe.Difficulty,
|
|
||||||
&durationBytes,
|
|
||||||
&recipe.Category,
|
|
||||||
&ingredientBytes,
|
|
||||||
&recipe.UserId,
|
|
||||||
&recipe.Modified,
|
|
||||||
&recipe.Created,
|
|
||||||
); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to scan recipe from database: %s", err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse duration
|
// Skip any un-found recipes...?
|
||||||
if len(durationBytes) > 0 {
|
if recipe != nil {
|
||||||
var duration domain.RecipeDuration
|
recipes = append(recipes, *recipe)
|
||||||
if err := json.Unmarshal(durationBytes, &duration); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
recipe.Duration = duration
|
|
||||||
} else {
|
|
||||||
recipe.Duration = domain.RecipeDuration{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse ingredient
|
|
||||||
if len(ingredientBytes) > 0 {
|
|
||||||
var ingredients []domain.RecipeIngredient
|
|
||||||
if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
recipe.Ingredients = ingredients
|
|
||||||
} else {
|
|
||||||
recipe.Ingredients = []domain.RecipeIngredient{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tags
|
|
||||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
|
||||||
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get favorite status, if user id is provided
|
|
||||||
if userId != nil {
|
|
||||||
if err := r.GetRecipeFavorite(&recipe, *userId); err != nil {
|
|
||||||
fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
recipe.Favorite = false
|
|
||||||
}
|
|
||||||
|
|
||||||
recipes = append(recipes, recipe)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return recipes, nil
|
return recipes, nil
|
||||||
@ -264,6 +219,8 @@ func isBitActive(bits, pos int) bool {
|
|||||||
// The favorites parameter is used to only return filters favorited by the userId provided.
|
// The favorites parameter is used to only return filters favorited by the userId provided.
|
||||||
//
|
//
|
||||||
// TODO: Pagination is required, to provide infinite scroll.
|
// TODO: Pagination is required, to provide infinite scroll.
|
||||||
|
//
|
||||||
|
// TODO: This does not work in the current build, the DB does not return valid values.
|
||||||
func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) {
|
func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) {
|
||||||
// Compute meals type filters (there are 7 bits)
|
// Compute meals type filters (there are 7 bits)
|
||||||
var mealConditions []string
|
var mealConditions []string
|
||||||
@ -569,10 +526,11 @@ func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string)
|
|||||||
// GetUserRecipes gets a list of a users owned recipes. This function does not ensure the user is
|
// 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
|
// 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.
|
// is sorted by the created dates, newest first. Any errors will be bubbled to the caller.
|
||||||
|
//
|
||||||
|
// TODO: This should just return the IDs
|
||||||
func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) {
|
func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, title, description, instructions, serves, difficulty, duration, category, ingredients,
|
SELECT id
|
||||||
userid, modified, created
|
|
||||||
FROM recipes
|
FROM recipes
|
||||||
WHERE userid = $1
|
WHERE userid = $1
|
||||||
ORDER BY created DESC;
|
ORDER BY created DESC;
|
||||||
@ -584,69 +542,21 @@ func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) {
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
// Prepare statement for tag query
|
|
||||||
// tagQuery := `
|
|
||||||
// `
|
|
||||||
|
|
||||||
var recipes []domain.Recipe
|
var recipes []domain.Recipe
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var recipe domain.Recipe
|
var r_id int
|
||||||
var durationBytes []byte
|
if err := rows.Scan(&r_id); err != nil {
|
||||||
var ingredientBytes []byte
|
return []domain.Recipe{}, fmt.Errorf("Failed to scan ID from db. %s\n", err.Error())
|
||||||
|
|
||||||
// Scan results from recipe query onto recipe object
|
|
||||||
if err := rows.Scan(
|
|
||||||
&recipe.Id,
|
|
||||||
&recipe.Title,
|
|
||||||
&recipe.Description,
|
|
||||||
pq.Array(&recipe.Instructions),
|
|
||||||
&recipe.Serves,
|
|
||||||
&recipe.Difficulty,
|
|
||||||
&durationBytes,
|
|
||||||
&recipe.Category,
|
|
||||||
&ingredientBytes,
|
|
||||||
&recipe.UserId,
|
|
||||||
&recipe.Modified,
|
|
||||||
&recipe.Created,
|
|
||||||
); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to scan row onto recipe object. %s\n", err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse duration
|
recipe, err := r.GetRecipe(r_id, &id)
|
||||||
if len(durationBytes) > 0 {
|
if err != nil {
|
||||||
var duration domain.RecipeDuration
|
return []domain.Recipe{}, err
|
||||||
if err := json.Unmarshal(durationBytes, &duration); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
recipe.Duration = duration
|
|
||||||
} else {
|
|
||||||
recipe.Duration = domain.RecipeDuration{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse ingredient
|
if recipe != nil {
|
||||||
if len(ingredientBytes) > 0 {
|
recipes = append(recipes, *recipe)
|
||||||
var ingredients []domain.RecipeIngredient
|
|
||||||
if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
recipe.Ingredients = ingredients
|
|
||||||
} else {
|
|
||||||
recipe.Ingredients = []domain.RecipeIngredient{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tags
|
|
||||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
|
||||||
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get favorite status
|
|
||||||
if err := r.GetRecipeFavorite(&recipe, id); err != nil {
|
|
||||||
fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
recipes = append(recipes, recipe)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return recipes, nil
|
return recipes, nil
|
||||||
@ -655,10 +565,11 @@ func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) {
|
|||||||
// GetUserRecipes gets a list of a users favorited recipes. This function does not ensure the user is
|
// GetUserRecipes gets a list of a users favorited 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
|
// 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.
|
// is sorted by the created dates, newest first. Any errors will be bubbled to the caller.
|
||||||
|
//
|
||||||
|
// TODO: This should just return the IDs
|
||||||
func (r *RecipeRepository) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) {
|
func (r *RecipeRepository) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT r.id, r.title, r.description, r.instructions, r.serves, r.difficulty, r.duration, r.category, r.ingredients, r.
|
SELECT r.id
|
||||||
userid, r.modified, r.created
|
|
||||||
FROM favorites f
|
FROM favorites f
|
||||||
JOIN recipes r ON r.id = f.recipeid
|
JOIN recipes r ON r.id = f.recipeid
|
||||||
WHERE f.userid = $1
|
WHERE f.userid = $1
|
||||||
@ -672,61 +583,19 @@ func (r *RecipeRepository) GetUserFavoriteRecipes(id int) ([]domain.Recipe, erro
|
|||||||
|
|
||||||
var recipes []domain.Recipe
|
var recipes []domain.Recipe
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var recipe domain.Recipe
|
var r_id int
|
||||||
var durationBytes []byte
|
if err := rows.Scan(&r_id); err != nil {
|
||||||
var ingredientBytes []byte
|
return []domain.Recipe{}, fmt.Errorf("Failed to scan ID from db. %s\n", err.Error())
|
||||||
|
|
||||||
// Scan results from recipe query onto recipe object
|
|
||||||
if err := rows.Scan(
|
|
||||||
&recipe.Id,
|
|
||||||
&recipe.Title,
|
|
||||||
&recipe.Description,
|
|
||||||
pq.Array(&recipe.Instructions),
|
|
||||||
&recipe.Serves,
|
|
||||||
&recipe.Difficulty,
|
|
||||||
&durationBytes,
|
|
||||||
&recipe.Category,
|
|
||||||
&ingredientBytes,
|
|
||||||
&recipe.UserId,
|
|
||||||
&recipe.Modified,
|
|
||||||
&recipe.Created,
|
|
||||||
); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to scan row onto recipe object. %s\n", err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse duration
|
recipe, err := r.GetRecipe(r_id, &id)
|
||||||
if len(durationBytes) > 0 {
|
if err != nil {
|
||||||
var duration domain.RecipeDuration
|
return []domain.Recipe{}, err
|
||||||
if err := json.Unmarshal(durationBytes, &duration); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
recipe.Duration = duration
|
|
||||||
} else {
|
|
||||||
recipe.Duration = domain.RecipeDuration{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse ingredient
|
if recipe != nil {
|
||||||
if len(ingredientBytes) > 0 {
|
recipes = append(recipes, *recipe)
|
||||||
var ingredients []domain.RecipeIngredient
|
|
||||||
if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
recipe.Ingredients = ingredients
|
|
||||||
} else {
|
|
||||||
recipe.Ingredients = []domain.RecipeIngredient{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tags
|
|
||||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
|
||||||
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set favorite status (they're always true!)
|
|
||||||
recipe.Favorite = true
|
|
||||||
|
|
||||||
recipes = append(recipes, recipe)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return recipes, nil
|
return recipes, nil
|
||||||
@ -793,82 +662,27 @@ func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value,
|
// GetRecipeOfTheWeekId searches for the most recent recipe of the week. If there is not a value,
|
||||||
// the recipe will be nil. This function simply collects the most recent entry in the recipeoftheweek
|
// the recipe will be nil. This function simply collects the most recent entry in the recipeoftheweek
|
||||||
// table and return it. If there is no entry, nil will be returned Any errors will be bubbled to
|
// table and return it. If there is no entry, nil will be returned. Any errors will be bubbled to
|
||||||
// the caller.
|
// the caller. All that is returned is the recipe ID, that way the caller can handle the fetching.
|
||||||
func (r *RecipeRepository) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) {
|
func (r *RecipeRepository) GetRecipeOfTheWeekId(userId *int) (*int, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT
|
SELECT
|
||||||
r.id, r.title, r.description, r.instructions, r.serves, r.difficulty, r.duration, r.category,
|
r.id
|
||||||
r.ingredients, r.userid, r.modified, r.created
|
|
||||||
FROM recipes r
|
FROM recipes r
|
||||||
JOIN recipeoftheweek rw ON rw.recipeid = r.id
|
JOIN recipeoftheweek rw ON rw.recipeid = r.id
|
||||||
ORDER BY created DESC
|
ORDER BY rw.created DESC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
var durationBytes []byte
|
var id int
|
||||||
var ingredientBytes []byte
|
if err := r.db.QueryRow(query).Scan(&id); err != nil {
|
||||||
|
|
||||||
var recipe domain.Recipe
|
|
||||||
if err := r.db.QueryRow(query).Scan(
|
|
||||||
&recipe.Id,
|
|
||||||
&recipe.Title,
|
|
||||||
&recipe.Description,
|
|
||||||
pq.Array(&recipe.Instructions),
|
|
||||||
&recipe.Serves,
|
|
||||||
&recipe.Difficulty,
|
|
||||||
&durationBytes,
|
|
||||||
&recipe.Category,
|
|
||||||
&ingredientBytes,
|
|
||||||
&recipe.UserId,
|
|
||||||
&recipe.Modified,
|
|
||||||
&recipe.Created,
|
|
||||||
); err != nil {
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("Failed to location recipe in database: %s", err.Error())
|
return nil, fmt.Errorf("Failed to location recipe in database: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse duration
|
return &id, nil
|
||||||
if len(durationBytes) > 0 {
|
|
||||||
var duration domain.RecipeDuration
|
|
||||||
if err := json.Unmarshal(durationBytes, &duration); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
recipe.Duration = duration
|
|
||||||
} else {
|
|
||||||
recipe.Duration = domain.RecipeDuration{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse ingredient
|
|
||||||
if len(ingredientBytes) > 0 {
|
|
||||||
var ingredients []domain.RecipeIngredient
|
|
||||||
if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
recipe.Ingredients = ingredients
|
|
||||||
} else {
|
|
||||||
recipe.Ingredients = []domain.RecipeIngredient{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tags
|
|
||||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
|
||||||
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get favorite status, if user id is provided
|
|
||||||
if userId != nil {
|
|
||||||
if err := r.GetRecipeFavorite(&recipe, *userId); err != nil {
|
|
||||||
fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
recipe.Favorite = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return &recipe, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package components
|
package components
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package components
|
package components
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package components
|
package components
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package components
|
package components
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package components
|
package components
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package components
|
package components
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -112,7 +112,7 @@ templ ingredientList(ingredients []domain.RecipeIngredient) {
|
|||||||
<hr class="text-gray-300"/>
|
<hr class="text-gray-300"/>
|
||||||
<ul class="text-lg my-4 text-gray-700">
|
<ul class="text-lg my-4 text-gray-700">
|
||||||
for _, ingredient := range ingredients {
|
for _, ingredient := range ingredients {
|
||||||
@ingredientListItem(ingredient.Name, ingredient.Quantity)
|
@ingredientListItem(ingredient.Name, "")
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -308,7 +308,7 @@ templ RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool, doma
|
|||||||
<p class="text-gray-700">{ recipe.Description }</p>
|
<p class="text-gray-700">{ recipe.Description }</p>
|
||||||
</div>
|
</div>
|
||||||
@ingredientList(recipe.Ingredients)
|
@ingredientList(recipe.Ingredients)
|
||||||
@instructionList(recipe.Instructions)
|
@instructionList([]string{})
|
||||||
@tagList(recipe.Tags, recipe.Created, recipe.Modified)
|
@tagList(recipe.Tags, recipe.Created, recipe.Modified)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
@ -268,7 +268,7 @@ func ingredientList(ingredients []domain.RecipeIngredient) templ.Component {
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
for _, ingredient := range ingredients {
|
for _, ingredient := range ingredients {
|
||||||
templ_7745c5c3_Err = ingredientListItem(ingredient.Name, ingredient.Quantity).Render(ctx, templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = ingredientListItem(ingredient.Name, "").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -875,7 +875,7 @@ func RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool, domai
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = instructionList(recipe.Instructions).Render(ctx, templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = instructionList([]string{}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.960
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export default function IngredientItem({ classes, ingredient, onChange, removeIn
|
|||||||
className="select-none p-2 flex gap-2 flex-col"
|
className="select-none p-2 flex gap-2 flex-col"
|
||||||
>
|
>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex-col md:flex-row flex-grow flex gap-2">
|
<div className="flex-col md:flex-row flex-grow flex gap-2 flex-wrap">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@ -29,8 +29,8 @@ export default function IngredientSection({ section, onChange, removeIngredientS
|
|||||||
type="text"
|
type="text"
|
||||||
value={section.Name}
|
value={section.Name}
|
||||||
onChange={(e) => onChange(section.Id, e.target.value)}
|
onChange={(e) => onChange(section.Id, e.target.value)}
|
||||||
placeholder="Section title"
|
placeholder="Group label"
|
||||||
className="mx-2 px-2 py-1 border border-gray-300 flex-grow rounded-sm"
|
className="mx-2 px-2 py-1 border border-gray-300 flex-grow rounded-sm min-w-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex gap-x-2 items-center">
|
<div className="flex gap-x-2 items-center">
|
||||||
|
|||||||
@ -1,33 +1,46 @@
|
|||||||
import type { RecipeIngredient } from "../../types/recipe";
|
import { Fragment } from "react/jsx-runtime";
|
||||||
|
import type { RecipeIngredient, RecipeIngredientSection } from "../../types/recipe";
|
||||||
|
|
||||||
interface IngredientListProps {
|
interface IngredientListProps {
|
||||||
|
sections: RecipeIngredientSection[];
|
||||||
ingredients: RecipeIngredient[];
|
ingredients: RecipeIngredient[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function IngredientList({ ingredients }: IngredientListProps) {
|
export default function IngredientList({ sections, ingredients }: IngredientListProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="px-4 py-8 md:px-8">
|
<div className="px-4 py-8 md:px-8">
|
||||||
<h2 className="text-2xl text-gray-800 font-semibold mb-2">Ingredients</h2>
|
<h2 className="text-2xl text-gray-800 font-semibold mb-2">Ingredients</h2>
|
||||||
<hr className="text-gray-300" />
|
<hr className="text-gray-300" />
|
||||||
<ul className="text-lg my-4 text-gray-700">
|
{sections?.map(section => (
|
||||||
{ingredients?.map(ingredient => (
|
<Fragment key={section.Id}>
|
||||||
<li key={ingredient.Name} className="p-2 hover:bg-gray-100 transition-all duration-300 rounded-sm flex items-center justify-start odd:bg-[#f8f8f8]">
|
{/* NOTE: If there is a only one section, do not display a name. */}
|
||||||
<span className="mr-4">
|
{sections.length > 1 && (
|
||||||
<svg className="h-4 text-gray-400" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<h3 className="text-xl text-gray-800 font-semibold my-4">{section.Name}</h3>
|
||||||
<path
|
)}
|
||||||
d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
|
<ul className="text-lg my-2 text-gray-700">
|
||||||
stroke="currentColor"
|
{ingredients?.filter(x => x.SectionId === section.Id).map(ingredient => (
|
||||||
strokeWidth="2"
|
<li key={ingredient.Id} className="p-2 hover:bg-gray-100 transition-all duration-300 rounded-sm flex items-center justify-start odd:bg-[#f8f8f8]">
|
||||||
strokeLinecap="round"
|
<span className="mr-4">
|
||||||
strokeLinejoin="round"
|
<svg className="h-4 text-gray-400" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
></path>
|
<path
|
||||||
</svg>
|
d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
|
||||||
</span>
|
stroke="currentColor"
|
||||||
<span className="font-semibold mr-2">{ingredient.Amount}: </span> {ingredient.Name}
|
strokeWidth="2"
|
||||||
</li>
|
strokeLinecap="round"
|
||||||
))}
|
strokeLinejoin="round"
|
||||||
</ul>
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold mr-2">
|
||||||
|
{ingredient.Amount > 0 ? ingredient.Amount : null} {ingredient.Unit}
|
||||||
|
</span>
|
||||||
|
{ingredient.Name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
import type { RecipeInstruction } from "../../types/recipe";
|
||||||
|
|
||||||
interface InstructionListProps {
|
interface InstructionListProps {
|
||||||
instructions: string[];
|
instructions: RecipeInstruction[];
|
||||||
}
|
}
|
||||||
export default function InstructionList({ instructions }: InstructionListProps) {
|
export default function InstructionList({ instructions }: InstructionListProps) {
|
||||||
return (
|
return (
|
||||||
@ -9,11 +11,11 @@ export default function InstructionList({ instructions }: InstructionListProps)
|
|||||||
<hr className="text-gray-300" />
|
<hr className="text-gray-300" />
|
||||||
<ul className="text-lg my-4 text-gray-700">
|
<ul className="text-lg my-4 text-gray-700">
|
||||||
{instructions?.map((instruction, i) => (
|
{instructions?.map((instruction, i) => (
|
||||||
<li key={instruction} className="p-4 flex items-start gap-x-4 odd:bg-[#f8f8f8]">
|
<li key={instruction.Id || crypto.randomUUID()} className="p-4 flex items-start gap-x-4 odd:bg-[#f8f8f8]">
|
||||||
<div className="size-8 md:size-10 bg-blue-50 rounded-full flex items-center justify-center flex-shrink-0">
|
<div className="size-8 md:size-10 bg-blue-50 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
<h3 className="text-base md:text-xl text-blue-600 font-semibold">{i + 1}</h3>
|
<h3 className="text-base md:text-xl text-blue-600 font-semibold">{i + 1}</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-base">{instruction}</p>
|
<p className="text-base">{instruction.Content}</p>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Banner from "../components/Banner";
|
import Banner from "../components/Banner";
|
||||||
import { type RecipeInstruction } from "../types/recipe";
|
import { isRecipeMeal, type RecipeInstruction } from "../types/recipe";
|
||||||
import InstructionList from "../components/forms/InstructionList";
|
import InstructionList from "../components/forms/InstructionList";
|
||||||
import ValidationErrorList from "../components/forms/ValidationErrorList";
|
import ValidationErrorList from "../components/forms/ValidationErrorList";
|
||||||
import IngredientSection from "../components/forms/IngredientSection";
|
import IngredientSection from "../components/forms/IngredientSection";
|
||||||
@ -13,6 +13,10 @@ import RecipeCreateFormWrapper from "../components/inputs/RecipeCreateFormWrappe
|
|||||||
import RecipeCreateFormTagsInputs from "../components/inputs/RecipeCreateFormTagsInput";
|
import RecipeCreateFormTagsInputs from "../components/inputs/RecipeCreateFormTagsInput";
|
||||||
import { useIngredients } from "../hooks/useIngredients";
|
import { useIngredients } from "../hooks/useIngredients";
|
||||||
import { validateCreateRecipeForm } from "../hooks/validation";
|
import { validateCreateRecipeForm } from "../hooks/validation";
|
||||||
|
import { CreateRecipe } from "../services/RecipeService";
|
||||||
|
import type { CreateRecipeRequest } from "../types/api/recipe";
|
||||||
|
import { isApiError } from "../types/api/error";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
// TODO: Move these
|
// TODO: Move these
|
||||||
export interface RecipeValidationEntry {
|
export interface RecipeValidationEntry {
|
||||||
@ -118,6 +122,45 @@ export default function Create() {
|
|||||||
instructions: {},
|
instructions: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
const createRecipe = async (): Promise<void> => {
|
||||||
|
console.log({ title, description, tags, prepTime, cookTime, servingSize, category, difficulty, sections, ingredients, instructions });
|
||||||
|
|
||||||
|
// Exit if not valid recipe meal
|
||||||
|
if (!isRecipeMeal(category)) {
|
||||||
|
console.error("[ERROR] Recipe meal is invalid.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipe: CreateRecipeRequest = {
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
Instructions: instructions,
|
||||||
|
Serves: Number(servingSize),
|
||||||
|
Difficulty: Number(difficulty),
|
||||||
|
Duration: {
|
||||||
|
Prep: Number(prepTime),
|
||||||
|
Cook: Number(cookTime),
|
||||||
|
Total: Number(prepTime) + Number(cookTime)
|
||||||
|
},
|
||||||
|
Category: category,
|
||||||
|
Ingredients: ingredients,
|
||||||
|
Sections: sections,
|
||||||
|
Tags: tags,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const response = await CreateRecipe(recipe);
|
||||||
|
if (isApiError(response)) {
|
||||||
|
console.error(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: Success toast!
|
||||||
|
await navigate(`/web/recipe/${response.Id}`);
|
||||||
|
};
|
||||||
|
|
||||||
// Import ingredients
|
// Import ingredients
|
||||||
const {
|
const {
|
||||||
sections,
|
sections,
|
||||||
@ -201,6 +244,8 @@ export default function Create() {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void createRecipe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -381,7 +426,7 @@ export default function Create() {
|
|||||||
<RecipeCreateFormWrapper
|
<RecipeCreateFormWrapper
|
||||||
label="Ingredients"
|
label="Ingredients"
|
||||||
name="ingredients"
|
name="ingredients"
|
||||||
desc="Please provide a list of ingredients and their quantities."
|
desc="Please provide a list of ingredients and their quantities. Ingredients can be grouped together by item using the group headers. If only a single group is defined, the name will be ignored and they will be displayed without a heading."
|
||||||
required
|
required
|
||||||
parentClasses="my-4"
|
parentClasses="my-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export default function RecipePage() {
|
|||||||
<h3 className="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3>
|
<h3 className="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3>
|
||||||
<p className="text-gray-700">{recipe.Description}</p>
|
<p className="text-gray-700">{recipe.Description}</p>
|
||||||
</div>
|
</div>
|
||||||
<IngredientList ingredients={recipe.Ingredients} />
|
<IngredientList sections={recipe.Sections} ingredients={recipe.Ingredients} />
|
||||||
<InstructionList instructions={recipe.Instructions} />
|
<InstructionList instructions={recipe.Instructions} />
|
||||||
<TagList tags={recipe.Tags} created={recipe.Created} modified={recipe.Modified} />
|
<TagList tags={recipe.Tags} created={recipe.Created} modified={recipe.Modified} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import type { GetRecipeOfTheWeekResponse, GetRecipeResponse, SearchRecipesResponse } from "../types/api/recipe";
|
import type { CreateRecipeRequest, CreateRecipeResponse, GetRecipeOfTheWeekResponse, GetRecipeResponse, SearchRecipesResponse } from "../types/api/recipe";
|
||||||
import type { Recipe } from "../types/recipe";
|
import type { Recipe } from "../types/recipe";
|
||||||
import type { ApiError } from "../types/api/error";
|
import type { ApiError } from "../types/api/error";
|
||||||
import type { SearchFilters } from "../types/search";
|
import type { SearchFilters } from "../types/search";
|
||||||
@ -46,3 +46,17 @@ export async function SearchRecipes(filters: SearchFilters): Promise<Recipe[] |
|
|||||||
|
|
||||||
return response.data.recipes;
|
return response.data.recipes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function CreateRecipe(data: CreateRecipeRequest): Promise<Recipe | ApiError> {
|
||||||
|
const response = await axios.post<CreateRecipeResponse>("http://localhost:3000/v2/api/recipe", 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;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { Recipe } from "../recipe";
|
import type { Recipe, RecipeDuration, RecipeIngredient, RecipeIngredientSection, RecipeInstruction, RecipeMeal } from "../recipe";
|
||||||
|
|
||||||
export interface GetRecipeOfTheWeekResponse {
|
export interface GetRecipeOfTheWeekResponse {
|
||||||
status: number;
|
status: number;
|
||||||
@ -17,3 +17,22 @@ export interface SearchRecipesResponse {
|
|||||||
message: string;
|
message: string;
|
||||||
recipes?: Recipe[];
|
recipes?: Recipe[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateRecipeResponse {
|
||||||
|
status: number;
|
||||||
|
message: string;
|
||||||
|
recipe?: Recipe;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateRecipeRequest {
|
||||||
|
Title: string;
|
||||||
|
Description: string;
|
||||||
|
Instructions: RecipeInstruction[];
|
||||||
|
Serves: number;
|
||||||
|
Difficulty: number;
|
||||||
|
Duration: RecipeDuration;
|
||||||
|
Category: RecipeMeal;
|
||||||
|
Ingredients: RecipeIngredient[];
|
||||||
|
Sections: RecipeIngredientSection[];
|
||||||
|
Tags: string[];
|
||||||
|
}
|
||||||
|
|||||||
@ -81,12 +81,13 @@ export interface Recipe {
|
|||||||
Id: number;
|
Id: number;
|
||||||
Title: string;
|
Title: string;
|
||||||
Description: string;
|
Description: string;
|
||||||
Instructions: string[];
|
Instructions: RecipeInstruction[];
|
||||||
Serves: number;
|
Serves: number;
|
||||||
Difficulty: number;
|
Difficulty: number;
|
||||||
Duration: RecipeDuration;
|
Duration: RecipeDuration;
|
||||||
Category: RecipeMeal;
|
Category: RecipeMeal;
|
||||||
Ingredients: RecipeIngredient[];
|
Ingredients: RecipeIngredient[];
|
||||||
|
Sections: RecipeIngredientSection[];
|
||||||
UserId: number;
|
UserId: number;
|
||||||
Modified: Date;
|
Modified: Date;
|
||||||
Created: Date;
|
Created: Date;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user