From 2a33edc8f6ca5eacab05fb8cd2a4dfc9b136ac5b Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Tue, 15 Jul 2025 21:19:47 -0700 Subject: [PATCH] (FEAT): Implemented API for the share engagement. This includes user and no user routes! Now wired to the frontend, however, it will still create an engagement even if it fails... --- internal/app/handlers/engagement_handler.go | 49 +++++++-- internal/app/handlers/page_handler.go | 2 - internal/app/server/server.go | 1 + internal/app/service/engagement_service.go | 28 ++++++ internal/domain/engagement/service.go | 2 + internal/domain/server/routes.go | 1 + internal/templates/pages/recipe.templ | 31 +++--- internal/templates/pages/recipe_templ.go | 105 +++++++++++--------- 8 files changed, 148 insertions(+), 71 deletions(-) diff --git a/internal/app/handlers/engagement_handler.go b/internal/app/handlers/engagement_handler.go index d57bc6b..dcca6d5 100644 --- a/internal/app/handlers/engagement_handler.go +++ b/internal/app/handlers/engagement_handler.go @@ -8,24 +8,55 @@ import ( domain "github.com/haydenhargreaves/Potion/internal/domain/server" ) - - func EngagementViewRecipe(ctx *gin.Context) { deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - id := ctx.Param("id") + recipeId, _ := strconv.Atoi(ctx.Param("id")) if !domain.IsLoggedIn(ctx) { - // TODO: Anon view - ctx.Status(http.StatusNoContent) + if _, err := deps.EngagementService.ViewRecipe(recipeId); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Status(http.StatusNoContent) + } return } - recipeId, _ := strconv.Atoi(id) userId := ctx.MustGet("userId").(int) if _, err := deps.EngagementService.UserViewRecipe(userId, recipeId); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Status(http.StatusNoContent) + } +} + +func EngagementShareRecipe(ctx *gin.Context) { + deps := ctx.MustGet("deps").(*domain.InjectedDependencies) + recipeId, _ := strconv.Atoi(ctx.Param("id")) + + if !domain.IsLoggedIn(ctx) { + if _, err := deps.EngagementService.ShareRecipe(recipeId); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Status(http.StatusNoContent) + } + return + } + + userId := ctx.MustGet("userId").(int) + + if _, err := deps.EngagementService.UserShareRecipe(userId, recipeId); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, "message": err.Error(), }) } else { @@ -48,7 +79,7 @@ func EngagementFavoriteRecipe(ctx *gin.Context) { if _, err := deps.EngagementService.UserFavoriteRecipe(userId, recipeId); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, + "status": http.StatusInternalServerError, "message": err.Error(), }) } else { @@ -71,7 +102,7 @@ func EngagementMakeRecipe(ctx *gin.Context) { if _, err := deps.EngagementService.UserMakeRecipe(userId, recipeId); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, + "status": http.StatusInternalServerError, "message": err.Error(), }) } else { diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index 7c5c48d..3b36eb4 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -130,14 +130,12 @@ func RecipePage(ctx *gin.Context) { // Add engagement if loggedIn { - fmt.Println("CALLING USER VIEW") if _, err = deps.EngagementService.UserViewRecipe(*userId, recipe.Id); err != nil { fmt.Printf("ERROR: %s\n", err.Error()) ctx.JSON(400, err.Error()) return } } else { - fmt.Println("CALLING VIEW") if _, err = deps.EngagementService.ViewRecipe(recipe.Id); err != nil { fmt.Printf("ERROR: %s\n", err.Error()) ctx.JSON(400, err.Error()) diff --git a/internal/app/server/server.go b/internal/app/server/server.go index 226aa43..091e464 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -188,6 +188,7 @@ func (s *Server) Setup() *Server { // Engagement endpoints router_api.POST("/engagement/view/:id", handlers.EngagementViewRecipe) + router_api.POST("/engagement/share/:id", handlers.EngagementShareRecipe) router_api.POST("/engagement/favorite/:id", handlers.EngagementFavoriteRecipe) router_api.POST("/engagement/make/:id", handlers.EngagementMakeRecipe) diff --git a/internal/app/service/engagement_service.go b/internal/app/service/engagement_service.go index 9e7bf92..54ec6a9 100644 --- a/internal/app/service/engagement_service.go +++ b/internal/app/service/engagement_service.go @@ -39,6 +39,20 @@ func (s *EngagementService) ViewRecipe(recipeId int) (domain.Engagement, error) return s.engagementRepository.AddEntityEngagement(recipeId, message, domain.EngagementViewed) } +// ShareRecipe requires a user ID and a recipe ID to create an engagement record in the database. +// A message will be generated using the recipe data and then used to add a view engagement to the +// database. +func (s *EngagementService) ShareRecipe(recipeId int) (domain.Engagement, error) { + recipe, err := s.recipeRepository.GetRecipe(recipeId, nil) + if err != nil { + return domain.Engagement{}, err + } + + message := fmt.Sprintf("Shared \"%s\"", recipe.Title) + + return s.engagementRepository.AddEntityEngagement(recipeId, message, domain.EngagementShared) +} + // UserViewRecipe requires a user ID and a recipe ID to create an engagement record in the database. // A message will be generated using the recipe data and then used to add a view engagement to the // database. @@ -94,6 +108,20 @@ func (s *EngagementService) UserMakeRecipe(userId, recipeId int) (domain.Engagem return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementMade) } +// UserShareRecipe requires a user ID and a recipe ID to create an engagement record in the database. +// A message will be generated using the recipe data and then used to add a make engagement to the +// database. +func (s *EngagementService) UserShareRecipe(userId, recipeId int) (domain.Engagement, error) { + recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId) + if err != nil { + return domain.Engagement{}, err + } + + message := fmt.Sprintf("Shared \"%s\"", recipe.Title) + + return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementShared) +} + // GetUserEngagement returns a list of the users most recent engagement entries. The number of records // is determined by the limit passed into this function. The results are sorted, newest-to-oldest. func (s *EngagementService) GetUserEngagement(userId, limit int) ([]domain.Engagement, error) { diff --git a/internal/domain/engagement/service.go b/internal/domain/engagement/service.go index a4dd0fc..4c3a0ff 100644 --- a/internal/domain/engagement/service.go +++ b/internal/domain/engagement/service.go @@ -2,8 +2,10 @@ package domain type EngagementService interface { ViewRecipe(recipeId int) (Engagement, error) + ShareRecipe(recipeId int) (Engagement, error) UserViewRecipe(userId, recipeId int) (Engagement, error) UserFavoriteRecipe(userId, recipeId int) (Engagement, error) UserMakeRecipe(userId, recipeId int) (Engagement, error) + UserShareRecipe(userId, recipeId int) (Engagement, error) GetUserEngagement(userId, limit int) ([]Engagement, error) } diff --git a/internal/domain/server/routes.go b/internal/domain/server/routes.go index a6f2fab..b6823da 100644 --- a/internal/domain/server/routes.go +++ b/internal/domain/server/routes.go @@ -26,6 +26,7 @@ const API_CREATE_RECIPE = VERSION + API + "/recipe" const API_SEARCH_RECIPES = VERSION + API + "/recipe/search" const API_ENGAGEMENT_VIEW = VERSION + API + "/engagement/view/%d" +const API_ENGAGEMENT_SHARE = VERSION + API + "/engagement/share/%d" const API_ENGAGEMENT_FAVORITE = VERSION + API + "/engagement/favorite/%d" const API_ENGAGEMENT_MAKE = VERSION + API + "/engagement/make/%d" diff --git a/internal/templates/pages/recipe.templ b/internal/templates/pages/recipe.templ index f853b55..3ce0723 100644 --- a/internal/templates/pages/recipe.templ +++ b/internal/templates/pages/recipe.templ @@ -189,11 +189,11 @@ templ favoriteButton(favorited bool, id int, loggedIn bool) { hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id) } hx-trigger="click" hx-swap="none" - if loggedIn { - hx-on:click="favoriteButtonHandler();" - } + if loggedIn { + hx-on:click="favoriteButtonHandler();" + } class="flex items-center justify-center gap-x-1 rounded-lg border border-blue-300 bg-blue-50 text-gray-800 px-6 py-3 flex-grow hover:bg-blue-100 hover:border-blue-500 duration-300" - id="favorite-button" + id="favorite-button" > } -templ shareButton() { +templ shareButton(id int) {
\"\"

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 297, Col: 75} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

Author: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

\"\"

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 298, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 300, Col: 75} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "

Category: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "

Author: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category) + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 299, Col: 69} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 301, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "

Category: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 302, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -841,20 +854,20 @@ func RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool) templ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "

About this recipe

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "

About this recipe

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var32 string - templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Description) + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 305, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 308, Col: 49} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -870,7 +883,7 @@ func RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool) templ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -898,24 +911,24 @@ func scripts(id int) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var33 := templ.GetChildren(ctx) - if templ_7745c5c3_Var33 == nil { - templ_7745c5c3_Var33 = templ.NopComponent + templ_7745c5c3_Var34 := templ.GetChildren(ctx) + if templ_7745c5c3_Var34 == nil { + templ_7745c5c3_Var34 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\"\n navigator.clipboard.writeText(url).then(() => {\n button.outerHTML = `\n \n `;\n\n setTimeout(() => {\n const newButton = document.getElementById(\"share-button\");\n newButton.outerHTML = before;\n }, 2000);\n });\n } else {\n console.warn(\"Clipboard API not available.\");\n\n button.outerHTML = `\n \n `;\n\n setTimeout(() => {\n const newButton = document.getElementById(\"share-button\");\n newButton.outerHTML = before;\n }, 2000);\n }\n }\n\n function makeButtonHandler() {\n const button = document.getElementById(\"make-button\");\n\n button.outerHTML = `\n \n \n \n \n \n \n Made This!\n \n `;\n }\n\n function favoriteButtonHandler() {\n const button = document.getElementById(\"favorite-button\");\n\n console.log(button.classList);\n console.log(button.classList.contains(\"border-blue-300\"));\n\n const toggleClasses = [\n \"border-gray-300\", \"hover:bg-gray-50\", \"hover:border-blue-300\",\n \"border-blue-300\", \"bg-blue-50\", \"hover:bg-blue-100\", \"hover:border-blue-500\"\n ];\n\n for (const cls of toggleClasses) {\n console.log(\"toggling class \" + cls);\n button.classList.toggle(cls);\n }\n\n if (!button.classList.contains(\"border-blue-300\")) {\n button.innerHTML = `\n \n \n \n Favorite\n `;\n\n } else {\n button.innerHTML = `\n \n \n \n Unfavorite\n `;\n }\n\n }\n\n") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }