diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go
index b8b2885..d375b78 100644
--- a/internal/app/handlers/page_handler.go
+++ b/internal/app/handlers/page_handler.go
@@ -58,9 +58,18 @@ func ProfilePage(ctx *gin.Context) {
// Else, get the user data
deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies)
user := deps.UserService.GetAuthenicatedUser(ctx)
+ recipes, err := deps.RecipeService.GetUserRecipes(user.Id)
+ if err != nil {
+ fmt.Printf("Error getting recipes. %s\n", err.Error())
+ ctx.JSON(http.StatusInternalServerError, gin.H{
+ "status": http.StatusInternalServerError,
+ "message": fmt.Sprintf("Error getting recipes. %s\n", err.Error()),
+ })
+ return
+ }
title := "Potion - Profile"
- page := pages.ProfilePage(user)
+ page := pages.ProfilePage(user, recipes)
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 5ac8282..261e199 100644
--- a/internal/app/handlers/user_handler.go
+++ b/internal/app/handlers/user_handler.go
@@ -1 +1,49 @@
package handlers
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ domain "github.com/haydenhargreaves/Potion/internal/domain/server"
+)
+
+func GetUserRecipes(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.GetUserRecipes(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 f46e8bd..406169b 100644
--- a/internal/app/server/server.go
+++ b/internal/app/server/server.go
@@ -181,6 +181,7 @@ func (s *Server) Setup() *Server {
// Recipe endpoints
router_api.POST("/recipe", handlers.CreateRecipe)
router_api.POST("/recipe/search", handlers.SearchRecipes)
+ router_api.GET("/user/recipes", handlers.GetUserRecipes)
// Catch un-routed URLS
s.Router.NoRoute(func(ctx *gin.Context) {
diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go
index 82e9696..870f053 100644
--- a/internal/app/service/recipe_service.go
+++ b/internal/app/service/recipe_service.go
@@ -134,8 +134,8 @@ func (s *RecipeService) GetRecipe(id int) (*domain.Recipe, error) {
}
// SearchRecipes will search the database using the filters provided. The recipes can be passed into
-// a template and displayed in the UI as the search result. A more detailed definition of the
-// filters is provided below.
+// a template and displayed in the UI as the search result. A more detailed definition of the
+// filters is provided below.
//
// Each input is given a bit value (e.g., 00001 for 1) and will be passed
// back to this handler as an array. The values are then added together
@@ -150,3 +150,7 @@ func (s *RecipeService) GetRecipe(id int) (*domain.Recipe, error) {
func (s *RecipeService) SearchRecipes(filters domain.SearchFilters) ([]domain.Recipe, error) {
return s.recipeRepository.SearchRecipes(filters)
}
+
+func (s *RecipeService) GetUserRecipes(id int) ([]domain.Recipe, error) {
+ return s.recipeRepository.GetUserRecipes(id)
+}
diff --git a/internal/domain/recipe/repository.go b/internal/domain/recipe/repository.go
index b47ccd9..a5c57d9 100644
--- a/internal/domain/recipe/repository.go
+++ b/internal/domain/recipe/repository.go
@@ -5,4 +5,6 @@ type RecipeRepository interface {
GetRecipe(id int) (*Recipe, error)
SearchRecipes(filters SearchFilters) ([]Recipe, error)
CreateRecipeTags(recipe Recipe, tags []string) error
+ GetUserRecipes(id int) ([]Recipe, error)
+ GetRecipeTags(recipe *Recipe) error
}
diff --git a/internal/domain/recipe/service.go b/internal/domain/recipe/service.go
index cf6723e..b3466f4 100644
--- a/internal/domain/recipe/service.go
+++ b/internal/domain/recipe/service.go
@@ -6,4 +6,5 @@ type RecipeService interface {
CreateRecipe(ctx *gin.Context) (*Recipe, error)
GetRecipe(id int) (*Recipe, error)
SearchRecipes(filters SearchFilters) ([]Recipe, error)
+ GetUserRecipes(id int) ([]Recipe, error)
}
diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go
index 293f9c8..e92739a 100644
--- a/internal/infrastructure/database/repository/recipe_repository.go
+++ b/internal/infrastructure/database/repository/recipe_repository.go
@@ -128,34 +128,6 @@ func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) {
return nil, fmt.Errorf("Failed to location recipe in database: %s", err.Error())
}
- // Get tags from external tables
- query = `
- SELECT t.* FROM tags t
- JOIN recipetags rt ON rt.tagid = t.id
- WHERE rt.recipeid = $1;
- `
- rows, err := tx.Query(query, recipe.Id)
- if err != nil {
- return nil, fmt.Errorf("Failed to get tags for recipe. %s\n", err.Error())
- }
- defer rows.Close()
-
- for rows.Next() {
- var tag domain.Tag
-
- err := rows.Scan(&tag.Id, &tag.Name, &tag.Created)
- if err != nil {
- return nil, fmt.Errorf("Failed to scan tag onto domain model. %s\n", err.Error())
- }
-
- recipe.Tags = append(recipe.Tags, tag)
- }
-
- if err := tx.Commit(); err != nil {
- tx.Rollback()
- return nil, err
- }
-
// Parse duration
if len(durationBytes) > 0 {
var duration domain.RecipeDuration
@@ -180,6 +152,14 @@ func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) {
recipe.Ingredients = []domain.RecipeIngredient{}
}
+ // Add tags
+ r.GetRecipeTags(&recipe)
+
+ if err := tx.Commit(); err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+
return &recipe, nil
}
@@ -387,6 +367,9 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain
recipe.Ingredients = []domain.RecipeIngredient{}
}
+ // Add tags
+ r.GetRecipeTags(&recipe)
+
recipes = append(recipes, recipe)
}
@@ -456,3 +439,135 @@ func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string)
return nil
}
+
+// GetUserRecipes gets a list of a users owned recipes. This function does not ensure the user is
+// authenticated or exists. If nothing is found, a blank slice will be returned. The resulting list
+// is sorted by the created dates, newest first. Any errors will be bubbled to the caller.
+func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) {
+ tx, err := r.db.Begin()
+ if err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+
+ query := `
+ SELECT id, title, description, instructions, serves, difficulty, duration, category, ingredients,
+ userid, modified, created
+ FROM recipes
+ WHERE userid = $1
+ ORDER BY 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()
+
+ // 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())
+ }
+
+ // 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
+ r.GetRecipeTags(&recipe)
+
+ 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.
+func (r *RecipeRepository) GetRecipeTags(recipe *domain.Recipe) error {
+ tx, err := r.db.Begin()
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ recipe.Tags = []domain.Tag{}
+
+ query := `
+ SELECT t.* FROM tags t
+ JOIN recipetags rt ON rt.tagid = t.id
+ WHERE rt.recipeid = $1;
+ `
+ rows, err := tx.Query(query, recipe.Id)
+ if err != nil {
+ return fmt.Errorf("Failed to get tags for recipe. %s\n", err.Error())
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var tag domain.Tag
+
+ err := rows.Scan(&tag.Id, &tag.Name, &tag.Created)
+ if err != nil {
+ return fmt.Errorf("Failed to scan tag onto domain model. %s\n", err.Error())
+ }
+
+ recipe.Tags = append(recipe.Tags, tag)
+ }
+
+ if err := tx.Commit(); err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ return nil
+}
diff --git a/internal/templates/pages/profile.templ b/internal/templates/pages/profile.templ
index 51a406b..c631efa 100644
--- a/internal/templates/pages/profile.templ
+++ b/internal/templates/pages/profile.templ
@@ -1,10 +1,38 @@
package templates
import "github.com/haydenhargreaves/Potion/internal/templates/components"
+import "fmt"
+import "strings"
import domain "github.com/haydenhargreaves/Potion/internal/domain/server"
-import domain_user "github.com/haydenhargreaves/Potion/internal/domain/user"
+import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
+import domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
-templ userDetailsSection(user domain_user.User) {
+func displayDifficulty(diff int) string {
+ switch diff {
+ case 1:
+ return "Beginner"
+ case 2:
+ return "Easy"
+ case 3:
+ return "Intermediate"
+ case 4:
+ return "Challenging"
+ case 5:
+ return "Extreme"
+ default:
+ return ""
+ }
+}
+
+func displayTags(tags []domainRecipe.Tag) string {
+ names := make([]string, 0, len(tags))
+ for _, tag := range tags {
+ names = append(names, tag.Name)
+ }
+ return strings.Join(names, ", ")
+}
+
+templ userDetailsSection(user domainUser.User, recipeCount int) {
10 recipes 14 favorites { recipeCount } recipes 0 favorites
-
- My Awesome Chili Recipe
+
+ { recipe.Title }
- Difficulty: Medium
- | Duration: 60 min
- | Category: Dinner
+ Difficulty: { displayDifficulty(recipe.Difficulty) }
+ | Duration: { recipe.Duration.Total } min
+ | Category: { recipe.Category }
- Difficulty: Medium
+ Difficulty: { displayDifficulty(recipe.Difficulty) }
- Duration: 60 min
+ Duration: { recipe.Duration.Total } min
- Category: Dinner
-
- Tags: comfort food, spicy, beef
+ Category: { recipe.Category }
+ Tags: { displayTags(recipe.Tags) }
+ { user.Email }
My Recipes
- @recipeListItem()
- @recipeListItem()
- @recipeListItem()
- @recipeListItem()
+ if len(recipes) <= 4 {
+ for _, recipe := range recipes {
+ @recipeListItem(recipe)
+ }
+ } else {
+ for _, recipe := range recipes[:4] {
+ @recipeListItem(recipe)
+ }
+ }
My Favorites
- @recipeListItem()
- @recipeListItem()
- @recipeListItem()
- @recipeListItem()
+ if len(recipes) <= 4 {
+ for _, recipe := range recipes {
+ @recipeListItem(recipe)
+ }
+ } else {
+ for _, recipe := range recipes[:4] {
+ @recipeListItem(recipe)
+ }
+ }
My Favorites
+ Recent Activity
@activityListItem()
@activityListItem()
@@ -84,30 +122,32 @@ templ activitySection() {
10 recipes
14 favorites
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(recipeCount) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 48, Col: 72} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " recipes
0 favorites
Difficulty: Medium | Duration: 60 min | Category: Dinner
Difficulty: Medium
Duration: 60 min
Category: Dinner
Tags: comfort food, spicy, beef
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: 133, 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, 16, " | 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: 134, 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, 17, " 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: 135, 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, 18, "
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: 138, 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, 19, "
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: 141, 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, 20, " 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: 144, 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, 21, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(recipe.Tags) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "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: 148, 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, 23, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Rated \"Spicy Chicken Wings\"
2 days ago
Rated \"Spicy Chicken Wings\"
2 days ago