diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go
index cadcee1..512d537 100644
--- a/internal/app/handlers/page_handler.go
+++ b/internal/app/handlers/page_handler.go
@@ -67,6 +67,15 @@ func ProfilePage(ctx *gin.Context) {
return
}
+ favorites, err := deps.RecipeService.GetUserFavoriteRecipes(user.Id)
+ if err != nil {
+ ctx.JSON(http.StatusInternalServerError, gin.H{
+ "status": http.StatusInternalServerError,
+ "message": fmt.Sprintf("Error getting recipes. %s\n", err.Error()),
+ })
+ return
+ }
+
// Get the engagement data, not sure what will happen when errors occur
engagements, err := deps.EngagementService.GetUserEngagement(user.Id, 6)
if err != nil {
@@ -78,7 +87,7 @@ func ProfilePage(ctx *gin.Context) {
}
title := "Potion - Profile"
- page := pages.ProfilePage(user, recipes, engagements)
+ page := pages.ProfilePage(user, recipes, favorites, engagements)
ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
}
diff --git a/internal/app/handlers/user_handler.go b/internal/app/handlers/user_handler.go
index 261e199..c5508bf 100644
--- a/internal/app/handlers/user_handler.go
+++ b/internal/app/handlers/user_handler.go
@@ -47,3 +47,43 @@ func GetUserRecipes(ctx *gin.Context) {
"recipes": recipes,
})
}
+
+func GetUserFavoriteRecipes(ctx *gin.Context) {
+ deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
+
+ // Ensure logged in
+ if !domain.IsLoggedIn(ctx) {
+ ctx.JSON(http.StatusUnauthorized, gin.H{
+ "status": http.StatusUnauthorized,
+ "message": "User is not authorized to access this endpoint. Please login to continue.",
+ "recipes": nil,
+ })
+ return
+ }
+
+ userId, ok := ctx.MustGet("userId").(int)
+ if !ok {
+ ctx.JSON(http.StatusInternalServerError, gin.H{
+ "status": http.StatusInternalServerError,
+ "message": "Unable to access user id from store.",
+ "recipes": nil,
+ })
+ return
+ }
+
+ recipes, err := deps.RecipeService.GetUserFavoriteRecipes(userId)
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, gin.H{
+ "status": http.StatusBadRequest,
+ "message": fmt.Sprintf("Could not get user recipes. %s", err.Error()),
+ "recipes": nil,
+ })
+ return
+ }
+
+ ctx.JSON(http.StatusOK, gin.H{
+ "status": http.StatusOK,
+ "message": "User recipes successfully retrieved.",
+ "recipes": recipes,
+ })
+}
diff --git a/internal/app/server/server.go b/internal/app/server/server.go
index 091e464..479ec37 100644
--- a/internal/app/server/server.go
+++ b/internal/app/server/server.go
@@ -185,6 +185,7 @@ func (s *Server) Setup() *Server {
router_api.POST("/recipe", handlers.CreateRecipe)
router_api.POST("/recipe/search", handlers.SearchRecipes)
router_api.GET("/user/recipes", handlers.GetUserRecipes)
+ router_api.GET("/user/favorites", handlers.GetUserFavoriteRecipes)
// Engagement endpoints
router_api.POST("/engagement/view/:id", handlers.EngagementViewRecipe)
diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go
index be4ba73..22fe6a1 100644
--- a/internal/app/service/recipe_service.go
+++ b/internal/app/service/recipe_service.go
@@ -155,6 +155,14 @@ func (s *RecipeService) SearchRecipes(filters domain.SearchFilters) ([]domain.Re
return s.recipeRepository.SearchRecipes(filters)
}
+// GetUserRecipes returns a list of the recipes that the user has created. The user's
+// ID should be provided. Any errors will be bubbled to the caller.
func (s *RecipeService) GetUserRecipes(id int) ([]domain.Recipe, error) {
return s.recipeRepository.GetUserRecipes(id)
}
+
+// GetUserFavoriteRecipes returns a list of the recipes that the user has marked as a
+// favorite. The user's ID should be provided. Any errors will be bubbled to the caller.
+func (s *RecipeService) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) {
+ return s.recipeRepository.GetUserFavoriteRecipes(id)
+}
diff --git a/internal/domain/recipe/repository.go b/internal/domain/recipe/repository.go
index d1a3a6e..639d21c 100644
--- a/internal/domain/recipe/repository.go
+++ b/internal/domain/recipe/repository.go
@@ -6,6 +6,7 @@ type RecipeRepository interface {
SearchRecipes(filters SearchFilters) ([]Recipe, error)
CreateRecipeTags(recipe Recipe, tags []string) error
GetUserRecipes(id int) ([]Recipe, error)
+ GetUserFavoriteRecipes(id int) ([]Recipe, error)
GetRecipeTags(recipe *Recipe) error
GetRecipeFavorite(recipe *Recipe, userId int) error
}
diff --git a/internal/domain/recipe/service.go b/internal/domain/recipe/service.go
index 33d919a..425b83e 100644
--- a/internal/domain/recipe/service.go
+++ b/internal/domain/recipe/service.go
@@ -7,4 +7,5 @@ type RecipeService interface {
GetRecipe(id int, userId *int) (*Recipe, error)
SearchRecipes(filters SearchFilters) ([]Recipe, error)
GetUserRecipes(id int) ([]Recipe, error)
+ GetUserFavoriteRecipes(id int) ([]Recipe, error)
}
diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go
index a0a578d..4347ce4 100644
--- a/internal/infrastructure/database/repository/recipe_repository.go
+++ b/internal/infrastructure/database/repository/recipe_repository.go
@@ -548,6 +548,94 @@ func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) {
return recipes, nil
}
+func (r *RecipeRepository) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) {
+ tx, err := r.db.Begin()
+ if err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+
+ 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
+ FROM favorites f
+ JOIN recipes r ON r.id = f.recipeid
+ WHERE f.userid = $1
+ ORDER BY f.created DESC;
+ `
+ rows, err := tx.Query(query, id)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to query DB for user recipes. %s\n", err.Error())
+ }
+ defer rows.Close()
+
+ 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())
+ }
+
+ // 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())
+ }
+
+ // Set favorite status (they're always true!)
+ recipe.Favorite = true
+
+ recipes = append(recipes, recipe)
+ }
+
+ if err := tx.Commit(); err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+
+ return recipes, nil
+}
+
// GetRecipeTags requires a recipe to be filled with at least an ID. This function will use the ID
// defined in the provided recipe to fill the Tags array with the recipe's tags from the database.
// The recipe is modified in place and is not returned. Any errors will be bubbled to the caller.
diff --git a/internal/templates/pages/profile.templ b/internal/templates/pages/profile.templ
index c13d33e..c53031e 100644
--- a/internal/templates/pages/profile.templ
+++ b/internal/templates/pages/profile.templ
@@ -44,7 +44,7 @@ templ userDetailsSection(user domainUser.User, recipeCount int) {
} else {
}
Favorites section is under construction!
+- { recipe.Title } +
+ { recipe.Title }
Difficulty: { displayDifficulty(recipe.Difficulty) } @@ -167,13 +184,13 @@ templ logoutSection() { } -templ ProfilePage(user domainUser.User, recipes []domainRecipe.Recipe, engagement []domainEngagement.Engagement) { +templ ProfilePage(user domainUser.User, recipes []domainRecipe.Recipe, favorites []domainRecipe.Recipe, engagement []domainEngagement.Engagement) { @components.Navbar(" profile")
Favorites section is under construction!
Activity section is under construction!
Activity section is under construction!
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"text-base md:text-lg hover:text-blue-600 duration-100 cursor-pointer\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 122, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 139, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
Difficulty: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
Difficulty: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(displayDifficulty(recipe.Difficulty)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 125, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 142, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " | Duration: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " | Duration: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Duration.Total) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 126, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 143, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " min | Category: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " min | Category: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 127, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 144, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
Difficulty: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
Difficulty: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(displayDifficulty(recipe.Difficulty)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 130, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 147, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
Duration: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
Duration: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var17 string templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Duration.Total) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 133, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 150, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " min
Category: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " min
Category: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 136, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 153, Col: 58} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(recipe.Tags) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Tags: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
Tags: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var19 string templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(displayTags(recipe.Tags)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 140, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 157, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var21 string templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(engagement.Message) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 151, Col: 23} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 168, Col: 23} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var22 string templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(engagement.Created.Format("01/02/2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 154, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 171, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "