(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.
|
||||
// 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.
|
||||
// NOTE: I believe this issue has been resolved
|
||||
func (s *Server) GetRecipeOfTheWeekHandlerV2(ctx *gin.Context) {
|
||||
userId := getUserId(ctx)
|
||||
recipe, err := s.deps.RecipeService.GetRecipeOfTheWeek(userId)
|
||||
@ -93,3 +94,20 @@ func (s *Server) SearchRecipeHandlerV2(ctx *gin.Context) {
|
||||
"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/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.GET("/auth/login", s.GetGoogleAuthUrlHandlerV2)
|
||||
router_api_v2.GET("/auth/callback", s.GoogleCallbackHandlerV2)
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -45,64 +41,23 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
|
||||
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)
|
||||
var req domain.CreateRecipeRequest
|
||||
|
||||
// Have to get the image differently
|
||||
image, err := ctx.FormFile("image")
|
||||
if err != nil && !errors.Is(err, http.ErrMissingFile) {
|
||||
// Error getting image
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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,
|
||||
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,
|
||||
UserId: userId,
|
||||
Created: time.Now(),
|
||||
}
|
||||
@ -112,17 +67,96 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
|
||||
}
|
||||
|
||||
// TODO: Upload the image
|
||||
if image != nil {
|
||||
}
|
||||
// if req.image != nil {
|
||||
// }
|
||||
|
||||
// Create the tags
|
||||
if len(tags) > 0 {
|
||||
if err := s.recipeRepository.CreateRecipeTags(recipe, tags); err != nil {
|
||||
if len(req.Tags) > 0 {
|
||||
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, 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,
|
||||
@ -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) {
|
||||
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.")
|
||||
}
|
||||
|
||||
@ -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,
|
||||
// the recipe will be nil. Any errors will be bubbled to the caller.
|
||||
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
|
||||
// to be marshaled into a JSON array and stored in the database (JSONB).
|
||||
type RecipeIngredient struct {
|
||||
Id string `json:"Id"`
|
||||
SectionId string `json:"SectionId"`
|
||||
Name string `json:"Name"`
|
||||
Quantity string `json:"Quantity"`
|
||||
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
|
||||
@ -61,12 +112,13 @@ type Recipe struct {
|
||||
Id int
|
||||
Title string
|
||||
Description string
|
||||
Instructions []string
|
||||
Instructions []RecipeInstruction
|
||||
Serves int
|
||||
Difficulty int
|
||||
Duration RecipeDuration
|
||||
Category RecipeMeal
|
||||
Ingredients []RecipeIngredient // Just a list of ingredients
|
||||
Ingredients []RecipeIngredient
|
||||
Sections []RecipeIngredientSection
|
||||
UserId int
|
||||
Modified *time.Time // Pointer to allow null
|
||||
Created time.Time
|
||||
@ -102,3 +154,17 @@ type RecipeTag struct {
|
||||
TagId int
|
||||
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)
|
||||
GetRecipeTags(recipe *Recipe) 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
|
||||
// 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
|
||||
// use nil for the modified time
|
||||
|
||||
@ -55,17 +57,27 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
instructions := make([]string, len(recipe.Instructions))
|
||||
for i, instruction := range recipe.Instructions {
|
||||
instructions[i] = instruction.Content
|
||||
}
|
||||
|
||||
var id int
|
||||
if err = tx.QueryRow(
|
||||
query,
|
||||
recipe.Title,
|
||||
recipe.Description,
|
||||
pq.Array(recipe.Instructions),
|
||||
pq.Array(instructions),
|
||||
recipe.Serves,
|
||||
recipe.Difficulty,
|
||||
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
|
||||
// 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) {
|
||||
query := `
|
||||
SELECT
|
||||
query := ` SELECT
|
||||
id, title, description, instructions, serves, difficulty, duration, category, ingredients,
|
||||
userid, modified, created
|
||||
FROM recipes
|
||||
@ -103,6 +114,7 @@ func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error
|
||||
`
|
||||
|
||||
var durationBytes []byte
|
||||
var instructions pq.StringArray
|
||||
var ingredientBytes []byte
|
||||
|
||||
var recipe domain.Recipe
|
||||
@ -110,7 +122,8 @@ func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error
|
||||
&recipe.Id,
|
||||
&recipe.Title,
|
||||
&recipe.Description,
|
||||
pq.Array(&recipe.Instructions),
|
||||
// pq.Array(&instructions),
|
||||
&instructions,
|
||||
&recipe.Serves,
|
||||
&recipe.Difficulty,
|
||||
&durationBytes,
|
||||
@ -137,16 +150,23 @@ func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error
|
||||
|
||||
// Parse ingredient
|
||||
if len(ingredientBytes) > 0 {
|
||||
var ingredients []domain.RecipeIngredient
|
||||
if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
|
||||
var store domain.RecipeIngredientStore
|
||||
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())
|
||||
}
|
||||
|
||||
recipe.Ingredients = ingredients
|
||||
recipe.Ingredients = store.Ingredients
|
||||
recipe.Sections = store.Sections
|
||||
} else {
|
||||
recipe.Ingredients = []domain.RecipeIngredient{}
|
||||
}
|
||||
|
||||
// Add instructions
|
||||
for _, instruction := range instructions {
|
||||
recipe.Instructions = append(recipe.Instructions, domain.RecipeInstruction{Content: instruction})
|
||||
}
|
||||
|
||||
// Add tags
|
||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
||||
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
|
||||
// to the caller.
|
||||
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
|
||||
|
||||
rows, err := r.db.Query(query, pq.Array(ids))
|
||||
for _, id := range ids {
|
||||
recipe, err := r.GetRecipe(id, userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get recipes. %s", err.Error())
|
||||
}
|
||||
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())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse duration
|
||||
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())
|
||||
// Skip any un-found recipes...?
|
||||
if recipe != nil {
|
||||
recipes = append(recipes, *recipe)
|
||||
}
|
||||
|
||||
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
|
||||
@ -264,6 +219,8 @@ func isBitActive(bits, pos int) bool {
|
||||
// The favorites parameter is used to only return filters favorited by the userId provided.
|
||||
//
|
||||
// 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) {
|
||||
// Compute meals type filters (there are 7 bits)
|
||||
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
|
||||
// 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.
|
||||
//
|
||||
// TODO: This should just return the IDs
|
||||
func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) {
|
||||
query := `
|
||||
SELECT id, title, description, instructions, serves, difficulty, duration, category, ingredients,
|
||||
userid, modified, created
|
||||
SELECT id
|
||||
FROM recipes
|
||||
WHERE userid = $1
|
||||
ORDER BY created DESC;
|
||||
@ -584,69 +542,21 @@ func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) {
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Prepare statement for tag query
|
||||
// tagQuery := `
|
||||
// `
|
||||
|
||||
var recipes []domain.Recipe
|
||||
for rows.Next() {
|
||||
var recipe domain.Recipe
|
||||
var durationBytes []byte
|
||||
var ingredientBytes []byte
|
||||
|
||||
// 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())
|
||||
var r_id int
|
||||
if err := rows.Scan(&r_id); err != nil {
|
||||
return []domain.Recipe{}, fmt.Errorf("Failed to scan ID from db. %s\n", err.Error())
|
||||
}
|
||||
|
||||
// Parse duration
|
||||
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, err := r.GetRecipe(r_id, &id)
|
||||
if err != nil {
|
||||
return []domain.Recipe{}, err
|
||||
}
|
||||
|
||||
recipe.Duration = duration
|
||||
} else {
|
||||
recipe.Duration = domain.RecipeDuration{}
|
||||
if recipe != nil {
|
||||
recipes = append(recipes, *recipe)
|
||||
}
|
||||
|
||||
// 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 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
|
||||
@ -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
|
||||
// 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.
|
||||
//
|
||||
// TODO: This should just return the IDs
|
||||
func (r *RecipeRepository) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) {
|
||||
query := `
|
||||
SELECT r.id, r.title, r.description, r.instructions, r.serves, r.difficulty, r.duration, r.category, r.ingredients, r.
|
||||
userid, r.modified, r.created
|
||||
SELECT r.id
|
||||
FROM favorites f
|
||||
JOIN recipes r ON r.id = f.recipeid
|
||||
WHERE f.userid = $1
|
||||
@ -672,61 +583,19 @@ func (r *RecipeRepository) GetUserFavoriteRecipes(id int) ([]domain.Recipe, erro
|
||||
|
||||
var recipes []domain.Recipe
|
||||
for rows.Next() {
|
||||
var recipe domain.Recipe
|
||||
var durationBytes []byte
|
||||
var ingredientBytes []byte
|
||||
|
||||
// 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())
|
||||
var r_id int
|
||||
if err := rows.Scan(&r_id); err != nil {
|
||||
return []domain.Recipe{}, fmt.Errorf("Failed to scan ID from db. %s\n", err.Error())
|
||||
}
|
||||
|
||||
// Parse duration
|
||||
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, err := r.GetRecipe(r_id, &id)
|
||||
if err != nil {
|
||||
return []domain.Recipe{}, err
|
||||
}
|
||||
|
||||
recipe.Duration = duration
|
||||
} else {
|
||||
recipe.Duration = domain.RecipeDuration{}
|
||||
if recipe != nil {
|
||||
recipes = append(recipes, *recipe)
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
// Set favorite status (they're always true!)
|
||||
recipe.Favorite = true
|
||||
|
||||
recipes = append(recipes, recipe)
|
||||
}
|
||||
|
||||
return recipes, nil
|
||||
@ -793,82 +662,27 @@ func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int)
|
||||
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
|
||||
// table and return it. If there is no entry, nil will be returned Any errors will be bubbled to
|
||||
// the caller.
|
||||
func (r *RecipeRepository) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) {
|
||||
// table and return it. If there is no entry, nil will be returned. Any errors will be bubbled to
|
||||
// the caller. All that is returned is the recipe ID, that way the caller can handle the fetching.
|
||||
func (r *RecipeRepository) GetRecipeOfTheWeekId(userId *int) (*int, error) {
|
||||
query := `
|
||||
SELECT
|
||||
r.id, r.title, r.description, r.instructions, r.serves, r.difficulty, r.duration, r.category,
|
||||
r.ingredients, r.userid, r.modified, r.created
|
||||
r.id
|
||||
FROM recipes r
|
||||
JOIN recipeoftheweek rw ON rw.recipeid = r.id
|
||||
ORDER BY created DESC
|
||||
ORDER BY rw.created DESC
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
var durationBytes []byte
|
||||
var ingredientBytes []byte
|
||||
|
||||
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 {
|
||||
var id int
|
||||
if err := r.db.QueryRow(query).Scan(&id); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Failed to location recipe in database: %s", err.Error())
|
||||
}
|
||||
|
||||
// Parse duration
|
||||
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
|
||||
return &id, nil
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package components
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package components
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package components
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package components
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package components
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package components
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package templates
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package templates
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package templates
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package templates
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package templates
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package templates
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package templates
|
||||
|
||||
//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.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package templates
|
||||
|
||||
//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"/>
|
||||
<ul class="text-lg my-4 text-gray-700">
|
||||
for _, ingredient := range ingredients {
|
||||
@ingredientListItem(ingredient.Name, ingredient.Quantity)
|
||||
@ingredientListItem(ingredient.Name, "")
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@ -308,7 +308,7 @@ templ RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool, doma
|
||||
<p class="text-gray-700">{ recipe.Description }</p>
|
||||
</div>
|
||||
@ingredientList(recipe.Ingredients)
|
||||
@instructionList(recipe.Instructions)
|
||||
@instructionList([]string{})
|
||||
@tagList(recipe.Tags, recipe.Created, recipe.Modified)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package templates
|
||||
|
||||
//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
|
||||
}
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -875,7 +875,7 @@ func RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool, domai
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.943
|
||||
// templ: version: v0.3.960
|
||||
package templates
|
||||
|
||||
//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"
|
||||
>
|
||||
<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">
|
||||
<input
|
||||
type="number"
|
||||
|
||||
@ -29,8 +29,8 @@ export default function IngredientSection({ section, onChange, removeIngredientS
|
||||
type="text"
|
||||
value={section.Name}
|
||||
onChange={(e) => onChange(section.Id, e.target.value)}
|
||||
placeholder="Section title"
|
||||
className="mx-2 px-2 py-1 border border-gray-300 flex-grow rounded-sm"
|
||||
placeholder="Group label"
|
||||
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">
|
||||
|
||||
@ -1,18 +1,26 @@
|
||||
import type { RecipeIngredient } from "../../types/recipe";
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
import type { RecipeIngredient, RecipeIngredientSection } from "../../types/recipe";
|
||||
|
||||
interface IngredientListProps {
|
||||
sections: RecipeIngredientSection[];
|
||||
ingredients: RecipeIngredient[];
|
||||
}
|
||||
|
||||
export default function IngredientList({ ingredients }: IngredientListProps) {
|
||||
export default function IngredientList({ sections, ingredients }: IngredientListProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="px-4 py-8 md:px-8">
|
||||
<h2 className="text-2xl text-gray-800 font-semibold mb-2">Ingredients</h2>
|
||||
<hr className="text-gray-300" />
|
||||
<ul className="text-lg my-4 text-gray-700">
|
||||
{ingredients?.map(ingredient => (
|
||||
<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]">
|
||||
{sections?.map(section => (
|
||||
<Fragment key={section.Id}>
|
||||
{/* NOTE: If there is a only one section, do not display a name. */}
|
||||
{sections.length > 1 && (
|
||||
<h3 className="text-xl text-gray-800 font-semibold my-4">{section.Name}</h3>
|
||||
)}
|
||||
<ul className="text-lg my-2 text-gray-700">
|
||||
{ingredients?.filter(x => x.SectionId === section.Id).map(ingredient => (
|
||||
<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]">
|
||||
<span className="mr-4">
|
||||
<svg className="h-4 text-gray-400" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -24,10 +32,15 @@ export default function IngredientList({ ingredients }: IngredientListProps) {
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="font-semibold mr-2">{ingredient.Amount}: </span> {ingredient.Name}
|
||||
<span className="font-semibold mr-2">
|
||||
{ingredient.Amount > 0 ? ingredient.Amount : null} {ingredient.Unit}
|
||||
</span>
|
||||
{ingredient.Name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import type { RecipeInstruction } from "../../types/recipe";
|
||||
|
||||
interface InstructionListProps {
|
||||
instructions: string[];
|
||||
instructions: RecipeInstruction[];
|
||||
}
|
||||
export default function InstructionList({ instructions }: InstructionListProps) {
|
||||
return (
|
||||
@ -9,11 +11,11 @@ export default function InstructionList({ instructions }: InstructionListProps)
|
||||
<hr className="text-gray-300" />
|
||||
<ul className="text-lg my-4 text-gray-700">
|
||||
{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">
|
||||
<h3 className="text-base md:text-xl text-blue-600 font-semibold">{i + 1}</h3>
|
||||
</div>
|
||||
<p className="text-base">{instruction}</p>
|
||||
<p className="text-base">{instruction.Content}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
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 ValidationErrorList from "../components/forms/ValidationErrorList";
|
||||
import IngredientSection from "../components/forms/IngredientSection";
|
||||
@ -13,6 +13,10 @@ import RecipeCreateFormWrapper from "../components/inputs/RecipeCreateFormWrappe
|
||||
import RecipeCreateFormTagsInputs from "../components/inputs/RecipeCreateFormTagsInput";
|
||||
import { useIngredients } from "../hooks/useIngredients";
|
||||
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
|
||||
export interface RecipeValidationEntry {
|
||||
@ -118,6 +122,45 @@ export default function Create() {
|
||||
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
|
||||
const {
|
||||
sections,
|
||||
@ -201,6 +244,8 @@ export default function Create() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
void createRecipe();
|
||||
}
|
||||
|
||||
|
||||
@ -381,7 +426,7 @@ export default function Create() {
|
||||
<RecipeCreateFormWrapper
|
||||
label="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
|
||||
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>
|
||||
<p className="text-gray-700">{recipe.Description}</p>
|
||||
</div>
|
||||
<IngredientList ingredients={recipe.Ingredients} />
|
||||
<IngredientList sections={recipe.Sections} ingredients={recipe.Ingredients} />
|
||||
<InstructionList instructions={recipe.Instructions} />
|
||||
<TagList tags={recipe.Tags} created={recipe.Created} modified={recipe.Modified} />
|
||||
</>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { ApiError } from "../types/api/error";
|
||||
import type { SearchFilters } from "../types/search";
|
||||
@ -46,3 +46,17 @@ export async function SearchRecipes(filters: SearchFilters): Promise<Recipe[] |
|
||||
|
||||
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 {
|
||||
status: number;
|
||||
@ -17,3 +17,22 @@ export interface SearchRecipesResponse {
|
||||
message: string;
|
||||
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;
|
||||
Title: string;
|
||||
Description: string;
|
||||
Instructions: string[];
|
||||
Instructions: RecipeInstruction[];
|
||||
Serves: number;
|
||||
Difficulty: number;
|
||||
Duration: RecipeDuration;
|
||||
Category: RecipeMeal;
|
||||
Ingredients: RecipeIngredient[];
|
||||
Sections: RecipeIngredientSection[];
|
||||
UserId: number;
|
||||
Modified: Date;
|
||||
Created: Date;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user