diff --git a/flake.nix b/flake.nix index 1ca34fc..3ad3ac2 100644 --- a/flake.nix +++ b/flake.nix @@ -27,6 +27,8 @@ watchman docker-language-server dockerfile-language-server-nodejs + gcc_multi + glibc_multi ]; # Define the shell that will be executed. @@ -41,6 +43,9 @@ export GOPATH="$HOME/.local/go" echo "Settings GOPATH to: $HOME/.local/go " + export GOOS=linux + export GOARCH=amd64 + # Exec zsh to replace the current shell process with zsh. # This ensures your prompt and zsh configurations load correctly. exec zsh diff --git a/go.mod b/go.mod index 31e6e1b..d938e8a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/haydenhargreaves/Potion -go 1.24.3 +go 1.25.0 require ( github.com/a-h/templ v0.3.920 diff --git a/internal/app/handlers/auth_handler.go b/internal/app/handlers/auth_handler.go index 3c37be0..50d48df 100644 --- a/internal/app/handlers/auth_handler.go +++ b/internal/app/handlers/auth_handler.go @@ -1,50 +1,47 @@ package handlers -import ( - "net/http" - "time" - - "github.com/gin-gonic/gin" - domain "github.com/haydenhargreaves/Potion/internal/domain/server" - "github.com/haydenhargreaves/Potion/internal/templates/components" -) - // GoogleLogin directs the user to Googles select user login page. Once the user has selected an // account, they will be directed to the GoogleCallback handler where the main logic resides. -func GoogleLogin(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - url := deps.AuthService.GetGoogleAuthUrl() - - ctx.Redirect(http.StatusSeeOther, url) -} +// +// DEPRECATED: As of September 4th, 2025. +// func GoogleLogin(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// url := deps.AuthService.GetGoogleAuthUrl() +// +// ctx.Redirect(http.StatusSeeOther, url) +// } // GoogleCallback is the callback handler when the user successfully logs in with their Google // account. They will be directed here and a JWT is generated. This JWT is stored in the users // cookies and will be used by protected routes to validate their login status. // // We do not need to return all of this data, it is just for testing. -func GoogleCallback(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - - var ( - state string = ctx.Query("state") - code string = ctx.Query("code") - ) - - if jwt, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil { - components.RenderErrorBanner(ctx, err.Error()) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - } else { - domain.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7) - ctx.Redirect(http.StatusSeeOther, "/") - } -} +// +// DEPRECATED: As of September 4th, 2025. +// func GoogleCallback(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// +// var ( +// state string = ctx.Query("state") +// code string = ctx.Query("code") +// ) +// +// if jwt, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil { +// components.RenderErrorBanner(ctx, err.Error()) +// ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) +// } else { +// domain.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7) +// ctx.Redirect(http.StatusSeeOther, "/") +// } +// } // Logout removes the token from the user's browser. Effectively "logging them out." Routes that // require authentication will require the user to sign back in before accessing them again. // This route will direct the user back to the home page. -func Logout(ctx *gin.Context) { - domain.SetCookie(ctx, "jwt_token", "", -1) - domain.SetCookie(ctx, "search-filters", "", -1) - ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) -} +// +// DEPRECATED: As of September 4th, 2025. +// func Logout(ctx *gin.Context) { +// domain.SetCookie(ctx, "jwt_token", "", -1) +// domain.SetCookie(ctx, "search-filters", "", -1) +// ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) +// } diff --git a/internal/app/handlers/engagement_handler.go b/internal/app/handlers/engagement_handler.go index 571d6a3..3620d04 100644 --- a/internal/app/handlers/engagement_handler.go +++ b/internal/app/handlers/engagement_handler.go @@ -1,148 +1,142 @@ package handlers -import ( - "fmt" - "net/http" - "strconv" +// DEPRECATED: As of September 4th, 2025. +// func EngagementViewRecipe(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// recipeId, _ := strconv.Atoi(ctx.Param("id")) +// +// // Ensure user is logged in with a valid account +// user := deps.UserService.GetAuthenicatedUser(ctx) +// if user == nil { +// // Log (stale) user out +// domain.SetCookie(ctx, "jwt_token", "", -1) +// domain.SetCookie(ctx, "search-filters", "", -1) +// } +// +// if !domain.IsLoggedIn(ctx) || user == nil { +// if _, err := deps.EngagementService.ViewRecipe(recipeId); err != nil { +// components.RenderErrorBanner(ctx, err.Error()) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": err.Error(), +// }) +// } else { +// ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId)) +// ctx.Status(http.StatusOK) +// } +// return +// } +// +// // We caught nil already, we can assume the user exists +// if _, err := deps.EngagementService.UserViewRecipe(user.Id, recipeId); err != nil { +// components.RenderErrorBanner(ctx, err.Error()) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": err.Error(), +// }) +// } else { +// ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId)) +// ctx.Status(http.StatusOK) +// } +// } - "github.com/gin-gonic/gin" - domain "github.com/haydenhargreaves/Potion/internal/domain/server" - "github.com/haydenhargreaves/Potion/internal/templates/components" -) +// DEPRECATED: As of September 4th, 2025. +// func EngagementShareRecipe(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// recipeId, _ := strconv.Atoi(ctx.Param("id")) +// +// // Ensure user is logged in with a valid account +// user := deps.UserService.GetAuthenicatedUser(ctx) +// if user == nil { +// // Log (stale) user out +// domain.SetCookie(ctx, "jwt_token", "", -1) +// domain.SetCookie(ctx, "search-filters", "", -1) +// } +// +// if !domain.IsLoggedIn(ctx) || user == nil { +// if _, err := deps.EngagementService.ShareRecipe(recipeId); err != nil { +// components.RenderErrorBanner(ctx, err.Error()) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": err.Error(), +// }) +// } else { +// ctx.Status(http.StatusNoContent) +// } +// return +// } +// +// if _, err := deps.EngagementService.UserShareRecipe(user.Id, recipeId); err != nil { +// components.RenderErrorBanner(ctx, err.Error()) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": err.Error(), +// }) +// } else { +// ctx.Status(http.StatusNoContent) +// } +// } -func EngagementViewRecipe(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - recipeId, _ := strconv.Atoi(ctx.Param("id")) +// DEPRECATED: As of September 4th, 2025. +// func EngagementFavoriteRecipe(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// +// // Ensure user is logged in with a valid account +// user := deps.UserService.GetAuthenicatedUser(ctx) +// if user == nil { +// // Log (stale) user out +// domain.SetCookie(ctx, "jwt_token", "", -1) +// domain.SetCookie(ctx, "search-filters", "", -1) +// } +// +// if !domain.IsLoggedIn(ctx) || user == nil { +// ctx.Header("HX-Redirect", domain.WEB_LOGIN) +// ctx.Status(http.StatusOK) +// return +// } +// +// id := ctx.Param("id") +// recipeId, _ := strconv.Atoi(id) +// +// if _, err := deps.EngagementService.UserFavoriteRecipe(user.Id, recipeId); err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("Something went wrong. %s.", err.Error())) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": err.Error(), +// }) +// } else { +// ctx.Status(http.StatusNoContent) +// } +// } - // Ensure user is logged in with a valid account - user := deps.UserService.GetAuthenicatedUser(ctx) - if user == nil { - // Log (stale) user out - domain.SetCookie(ctx, "jwt_token", "", -1) - domain.SetCookie(ctx, "search-filters", "", -1) - } - - if !domain.IsLoggedIn(ctx) || user == nil { - if _, err := deps.EngagementService.ViewRecipe(recipeId); err != nil { - components.RenderErrorBanner(ctx, err.Error()) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": err.Error(), - }) - } else { - ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId)) - ctx.Status(http.StatusOK) - } - return - } - - // We caught nil already, we can assume the user exists - if _, err := deps.EngagementService.UserViewRecipe(user.Id, recipeId); err != nil { - components.RenderErrorBanner(ctx, err.Error()) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": err.Error(), - }) - } else { - ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId)) - ctx.Status(http.StatusOK) - } -} - -func EngagementShareRecipe(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - recipeId, _ := strconv.Atoi(ctx.Param("id")) - - // Ensure user is logged in with a valid account - user := deps.UserService.GetAuthenicatedUser(ctx) - if user == nil { - // Log (stale) user out - domain.SetCookie(ctx, "jwt_token", "", -1) - domain.SetCookie(ctx, "search-filters", "", -1) - } - - if !domain.IsLoggedIn(ctx) || user == nil { - if _, err := deps.EngagementService.ShareRecipe(recipeId); err != nil { - components.RenderErrorBanner(ctx, err.Error()) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": err.Error(), - }) - } else { - ctx.Status(http.StatusNoContent) - } - return - } - - if _, err := deps.EngagementService.UserShareRecipe(user.Id, recipeId); err != nil { - components.RenderErrorBanner(ctx, err.Error()) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": err.Error(), - }) - } else { - ctx.Status(http.StatusNoContent) - } -} - -func EngagementFavoriteRecipe(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - - // Ensure user is logged in with a valid account - user := deps.UserService.GetAuthenicatedUser(ctx) - if user == nil { - // Log (stale) user out - domain.SetCookie(ctx, "jwt_token", "", -1) - domain.SetCookie(ctx, "search-filters", "", -1) - } - - if !domain.IsLoggedIn(ctx) || user == nil { - ctx.Header("HX-Redirect", domain.WEB_LOGIN) - ctx.Status(http.StatusOK) - return - } - - id := ctx.Param("id") - recipeId, _ := strconv.Atoi(id) - - if _, err := deps.EngagementService.UserFavoriteRecipe(user.Id, recipeId); err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("Something went wrong. %s.", err.Error())) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": err.Error(), - }) - } else { - ctx.Status(http.StatusNoContent) - } -} - -func EngagementMakeRecipe(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - - // Ensure user is logged in with a valid account - user := deps.UserService.GetAuthenicatedUser(ctx) - if user == nil { - // Log (stale) user out - domain.SetCookie(ctx, "jwt_token", "", -1) - domain.SetCookie(ctx, "search-filters", "", -1) - } - - if !domain.IsLoggedIn(ctx) || user == nil { - ctx.Header("HX-Redirect", domain.WEB_LOGIN) - ctx.Status(http.StatusOK) - return - } - - id := ctx.Param("id") - recipeId, _ := strconv.Atoi(id) - - if _, err := deps.EngagementService.UserMakeRecipe(user.Id, recipeId); err != nil { - components.RenderErrorBanner(ctx, err.Error()) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": err.Error(), - }) - } else { - ctx.Status(http.StatusNoContent) - } -} +// DEPRECATED: As of September 4th, 2025. +// func EngagementMakeRecipe(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// +// // Ensure user is logged in with a valid account +// user := deps.UserService.GetAuthenicatedUser(ctx) +// if user == nil { +// // Log (stale) user out +// domain.SetCookie(ctx, "jwt_token", "", -1) +// domain.SetCookie(ctx, "search-filters", "", -1) +// } +// +// if !domain.IsLoggedIn(ctx) || user == nil { +// ctx.Header("HX-Redirect", domain.WEB_LOGIN) +// ctx.Status(http.StatusOK) +// return +// } +// +// id := ctx.Param("id") +// recipeId, _ := strconv.Atoi(id) +// +// if _, err := deps.EngagementService.UserMakeRecipe(user.Id, recipeId); err != nil { +// components.RenderErrorBanner(ctx, err.Error()) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": err.Error(), +// }) +// } else { +// ctx.Status(http.StatusNoContent) +// } +// } diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index 8c84971..1165ecd 100755 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -1,306 +1,296 @@ package handlers -import ( - "encoding/json" - "fmt" - "net/http" - "strconv" +// DEPRECATED: As of September 4th, 2025. +// func LoginPage(ctx *gin.Context) { +// title := "Potion - Login" +// page := pages.LoginPage() +// +// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +// } - "github.com/a-h/templ" - "github.com/gin-gonic/gin" - domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" - domain "github.com/haydenhargreaves/Potion/internal/domain/server" - domainServer "github.com/haydenhargreaves/Potion/internal/domain/server" - "github.com/haydenhargreaves/Potion/internal/templates/components" - layouts "github.com/haydenhargreaves/Potion/internal/templates/layouts" - pages "github.com/haydenhargreaves/Potion/internal/templates/pages" - templates "github.com/haydenhargreaves/Potion/internal/templates/pages" -) +// DEPRECATED: As of September 4th, 2025. +// func HomePage(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) +// +// loggedIn := domain.IsLoggedIn(ctx) +// +// // Ensure user is logged in with a valid account +// if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil { +// // Log (stale) user out +// domain.SetCookie(ctx, "jwt_token", "", -1) +// domain.SetCookie(ctx, "search-filters", "", -1) +// loggedIn = false +// } +// +// var page templ.Component +// if loggedIn { +// userId := ctx.MustGet("userId").(int) +// madeRecipes, err := deps.RecipeService.GetUserMadeRecipes(userId, 6) +// if err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting made recipes. %s\n", err.Error())) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": fmt.Sprintf("Error getting made recipes. %s\n", err.Error()), +// }) +// return +// } +// viewedRecipes, err := deps.RecipeService.GetUserViewedRecipes(userId, 6) +// if err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error())) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error()), +// }) +// return +// } +// +// // Get the recipe of the week +// recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(&userId) +// if err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error())) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), +// }) +// return +// } +// +// if bytes, err := ctx.Cookie("search-filters"); err != nil { +// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) +// page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, nil) +// } else { +// var filters domainRecipe.SearchFilters +// if err := json.Unmarshal([]byte(bytes), &filters); err != nil { +// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) +// page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, nil) +// } else { +// page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, &filters) +// } +// } +// } else { +// // Get the recipe of the week +// recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(nil) +// if err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error())) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), +// }) +// return +// } +// +// if bytes, err := ctx.Cookie("search-filters"); err != nil { +// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) +// page = templates.HomePage(false, nil, nil, recipeOfTheWeek, nil) +// } else { +// var filters domainRecipe.SearchFilters +// if err := json.Unmarshal([]byte(bytes), &filters); err != nil { +// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) +// page = templates.HomePage(false, nil, nil, recipeOfTheWeek, nil) +// } else { +// page = templates.HomePage(false, nil, nil, recipeOfTheWeek, &filters) +// } +// } +// } +// +// title := "Potion - Home" +// +// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +// } -func LoginPage(ctx *gin.Context) { - title := "Potion - Login" - page := pages.LoginPage() +// DEPRECATED: As of September 4th, 2025. +// func FavoritesPage(ctx *gin.Context) { +// // If not logged in, direct to the login page +// if !domainServer.IsLoggedIn(ctx) { +// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) +// return +// } +// +// title := "Potion - Favorites" +// var page templ.Component +// +// // Get filters from cookies +// if bytes, err := ctx.Cookie("search-filters"); err != nil { +// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) +// page = pages.FavoritesPage(nil) +// } else { +// var filters domainRecipe.SearchFilters +// if err := json.Unmarshal([]byte(bytes), &filters); err != nil { +// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) +// page = pages.FavoritesPage(nil) +// } else { +// page = pages.FavoritesPage(&filters) +// } +// } +// +// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +// } +// +// func CreatePage(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) +// +// // If not logged in, direct to the login page +// if !domainServer.IsLoggedIn(ctx) { +// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) +// return +// } +// +// // Ensure user is logged in with a valid account +// if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil { +// // Log (stale) user out +// domain.SetCookie(ctx, "jwt_token", "", -1) +// domain.SetCookie(ctx, "search-filters", "", -1) +// +// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) +// return +// } +// +// title := "Potion - Create" +// page := pages.CreatePage() +// +// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +// } - ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -} +// DEPRECATED: As of September 4th, 2025. +// func ProfilePage(ctx *gin.Context) { +// // If not logged in, direct to the login page +// if !domainServer.IsLoggedIn(ctx) { +// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) +// return +// } +// +// // Else, get the user data +// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) +// user := deps.UserService.GetAuthenicatedUser(ctx) +// if user == nil { +// // User is failing to be found, direct to the login page +// ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) +// return +// } +// +// recipes, err := deps.RecipeService.GetUserRecipes(user.Id) +// if err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("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 +// } +// +// favorites, err := deps.RecipeService.GetUserFavoriteRecipes(user.Id) +// if err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting favorite recipes. %s\n", err.Error())) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": fmt.Sprintf("Error getting favorite 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 { +// components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting user engagements. %s\n", err.Error())) +// ctx.JSON(http.StatusInternalServerError, gin.H{ +// "status": http.StatusInternalServerError, +// "message": fmt.Sprintf("Error getting user engagements. %s\n", err.Error()), +// }) +// return +// } +// +// title := "Potion - Profile" +// page := pages.ProfilePage(*user, recipes, favorites, engagements) +// +// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +// } -func HomePage(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) +// DEPRECATED: As of September 4th, 2025. +// func ListPage(ctx *gin.Context) { +// title := "Potion - Shopping List" +// page := pages.ListPage() +// +// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +// } - loggedIn := domain.IsLoggedIn(ctx) +// DEPRECATED: As of September 4th, 2025. +// func RecipePage(ctx *gin.Context) { +// // Call recipe service to get via ID +// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) +// id := ctx.Param("id") +// +// // Parse ID +// parsed, err := strconv.Atoi(id) +// if err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) +// ctx.JSON(400, err.Error()) +// return +// } +// +// // Get signed in user, if they exist +// var userId *int = nil +// var loggedIn = domainServer.IsLoggedIn(ctx) +// +// // Ensure user is logged in with a valid account +// if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil { +// // Log (stale) user out +// domain.SetCookie(ctx, "jwt_token", "", -1) +// domain.SetCookie(ctx, "search-filters", "", -1) +// loggedIn = false +// } +// +// if loggedIn { +// storeId := ctx.MustGet("userId").(int) +// userId = &storeId +// } +// +// // Get recipe +// recipe, err := deps.RecipeService.GetRecipe(parsed, userId) +// if err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) +// ctx.JSON(400, err.Error()) +// return +// } +// +// // Get user (owner) +// user, err := deps.UserService.GetUser(recipe.UserId) +// if err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) +// ctx.JSON(400, err.Error()) +// return +// } +// +// title := "Potion - View Recipe" +// page := pages.RecipePage(*recipe, *user, loggedIn, deps.EnvironmentConfig.Domain) +// +// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +// } +// +// func SearchPage(ctx *gin.Context) { +// var page templ.Component +// // Get filters from cookies +// if bytes, err := ctx.Cookie("search-filters"); err != nil { +// fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) +// page = pages.SearchPage(nil, false) +// } else { +// var filters domainRecipe.SearchFilters +// if err := json.Unmarshal([]byte(bytes), &filters); err != nil { +// fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) +// page = pages.SearchPage(nil, false) +// } else { +// page = pages.SearchPage(&filters, true) +// } +// } +// +// title := "Potion - Recipe Search" +// +// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +// } - // Ensure user is logged in with a valid account - if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil { - // Log (stale) user out - domain.SetCookie(ctx, "jwt_token", "", -1) - domain.SetCookie(ctx, "search-filters", "", -1) - loggedIn = false - } - - var page templ.Component - if loggedIn { - userId := ctx.MustGet("userId").(int) - madeRecipes, err := deps.RecipeService.GetUserMadeRecipes(userId, 6) - if err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting made recipes. %s\n", err.Error())) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": fmt.Sprintf("Error getting made recipes. %s\n", err.Error()), - }) - return - } - viewedRecipes, err := deps.RecipeService.GetUserViewedRecipes(userId, 6) - if err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error())) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error()), - }) - return - } - - // Get the recipe of the week - recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(&userId) - if err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error())) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), - }) - return - } - - if bytes, err := ctx.Cookie("search-filters"); err != nil { - fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) - page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, nil) - } else { - var filters domainRecipe.SearchFilters - if err := json.Unmarshal([]byte(bytes), &filters); err != nil { - fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) - page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, nil) - } else { - page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, &filters) - } - } - } else { - // Get the recipe of the week - recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(nil) - if err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error())) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), - }) - return - } - - if bytes, err := ctx.Cookie("search-filters"); err != nil { - fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) - page = templates.HomePage(false, nil, nil, recipeOfTheWeek, nil) - } else { - var filters domainRecipe.SearchFilters - if err := json.Unmarshal([]byte(bytes), &filters); err != nil { - fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) - page = templates.HomePage(false, nil, nil, recipeOfTheWeek, nil) - } else { - page = templates.HomePage(false, nil, nil, recipeOfTheWeek, &filters) - } - } - } - - title := "Potion - Home" - - ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -} - -func FavoritesPage(ctx *gin.Context) { - // If not logged in, direct to the login page - if !domainServer.IsLoggedIn(ctx) { - ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) - return - } - - title := "Potion - Favorites" - var page templ.Component - - // Get filters from cookies - if bytes, err := ctx.Cookie("search-filters"); err != nil { - fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) - page = pages.FavoritesPage(nil) - } else { - var filters domainRecipe.SearchFilters - if err := json.Unmarshal([]byte(bytes), &filters); err != nil { - fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) - page = pages.FavoritesPage(nil) - } else { - page = pages.FavoritesPage(&filters) - } - } - - ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -} - -func CreatePage(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) - - // If not logged in, direct to the login page - if !domainServer.IsLoggedIn(ctx) { - ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) - return - } - - // Ensure user is logged in with a valid account - if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil { - // Log (stale) user out - domain.SetCookie(ctx, "jwt_token", "", -1) - domain.SetCookie(ctx, "search-filters", "", -1) - - ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) - return - } - - title := "Potion - Create" - page := pages.CreatePage() - - ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -} - -func ProfilePage(ctx *gin.Context) { - // If not logged in, direct to the login page - if !domainServer.IsLoggedIn(ctx) { - ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) - return - } - - // Else, get the user data - deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) - user := deps.UserService.GetAuthenicatedUser(ctx) - if user == nil { - // User is failing to be found, direct to the login page - ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) - return - } - - recipes, err := deps.RecipeService.GetUserRecipes(user.Id) - if err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("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 - } - - favorites, err := deps.RecipeService.GetUserFavoriteRecipes(user.Id) - if err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting favorite recipes. %s\n", err.Error())) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": fmt.Sprintf("Error getting favorite 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 { - components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting user engagements. %s\n", err.Error())) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, - "message": fmt.Sprintf("Error getting user engagements. %s\n", err.Error()), - }) - return - } - - title := "Potion - Profile" - page := pages.ProfilePage(*user, recipes, favorites, engagements) - - ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -} - -func ListPage(ctx *gin.Context) { - title := "Potion - Shopping List" - page := pages.ListPage() - - ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -} - -func RecipePage(ctx *gin.Context) { - // Call recipe service to get via ID - deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) - id := ctx.Param("id") - - // Parse ID - parsed, err := strconv.Atoi(id) - if err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) - ctx.JSON(400, err.Error()) - return - } - - // Get signed in user, if they exist - var userId *int = nil - var loggedIn = domainServer.IsLoggedIn(ctx) - - // Ensure user is logged in with a valid account - if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil { - // Log (stale) user out - domain.SetCookie(ctx, "jwt_token", "", -1) - domain.SetCookie(ctx, "search-filters", "", -1) - loggedIn = false - } - - if loggedIn { - storeId := ctx.MustGet("userId").(int) - userId = &storeId - } - - // Get recipe - recipe, err := deps.RecipeService.GetRecipe(parsed, userId) - if err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) - ctx.JSON(400, err.Error()) - return - } - - // Get user (owner) - user, err := deps.UserService.GetUser(recipe.UserId) - if err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) - ctx.JSON(400, err.Error()) - return - } - - title := "Potion - View Recipe" - page := pages.RecipePage(*recipe, *user, loggedIn, deps.EnvironmentConfig.Domain) - - ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -} - -func SearchPage(ctx *gin.Context) { - var page templ.Component - // Get filters from cookies - if bytes, err := ctx.Cookie("search-filters"); err != nil { - fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) - page = pages.SearchPage(nil, false) - } else { - var filters domainRecipe.SearchFilters - if err := json.Unmarshal([]byte(bytes), &filters); err != nil { - fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) - page = pages.SearchPage(nil, false) - } else { - page = pages.SearchPage(&filters, true) - } - } - - title := "Potion - Recipe Search" - - ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -} - -func NotFoundPage(ctx *gin.Context) { - title := "Potion - Not Found" - page := pages.NotFoundPage() - - ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) -} +// DEPRECATED: As of September 4th, 2025. +// func NotFoundPage(ctx *gin.Context) { +// title := "Potion - Not Found" +// page := pages.NotFoundPage() +// +// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +// } diff --git a/internal/app/handlers/recipe_handler.go b/internal/app/handlers/recipe_handler.go index 770c25c..868de1b 100644 --- a/internal/app/handlers/recipe_handler.go +++ b/internal/app/handlers/recipe_handler.go @@ -1,147 +1,136 @@ package handlers -import ( - "encoding/json" - "fmt" - "net/http" - "strconv" - "time" +// DEPRECATED: As of September 4th, 2025. +// const CREATE_ERROR_HTML = ` +//

+// Uh oh! Something went wrong when creating your recipe. Please try again. %s +//

+// ` - "github.com/gin-gonic/gin" - domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" - domain "github.com/haydenhargreaves/Potion/internal/domain/server" - "github.com/haydenhargreaves/Potion/internal/templates/components" - templates "github.com/haydenhargreaves/Potion/internal/templates/pages" -) - -const CREATE_ERROR_HTML = ` -

- Uh oh! Something went wrong when creating your recipe. Please try again. %s -

-` - -func CreateRecipe(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - - recipe, err := deps.RecipeService.CreateRecipe(ctx) - if err != nil { - components.RenderErrorBanner(ctx, err.Error()) - ctx.String(http.StatusOK, CREATE_ERROR_HTML, err.Error()) - return - } - - // Send HTMX redirection - url := fmt.Sprintf(domain.WEB_RECIPE, recipe.Id) - ctx.Header("HX-Redirect", url) - ctx.Status(http.StatusCreated) -} +// DEPRECATED: As of September 4th, 2025. +// func CreateRecipe(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// +// recipe, err := deps.RecipeService.CreateRecipe(ctx) +// if err != nil { +// components.RenderErrorBanner(ctx, err.Error()) +// ctx.String(http.StatusOK, CREATE_ERROR_HTML, err.Error()) +// return +// } +// +// // Send HTMX redirection +// url := fmt.Sprintf(domain.WEB_RECIPE, recipe.Id) +// ctx.Header("HX-Redirect", url) +// ctx.Status(http.StatusCreated) +// } // toBits converts an array of stringified numbers into a single summed value -func toBits(arr []string) (bits int) { - for _, x := range arr { - num, _ := strconv.Atoi(x) - bits += num - } - return -} +// +// DEPRECATED: As of September 4th, 2025. +// func toBits(arr []string) (bits int) { +// for _, x := range arr { +// num, _ := strconv.Atoi(x) +// bits += num +// } +// return +// } -// TODO: I don't love doing all of this here, but it seems to be the only way to get it to work... -func SearchRecipes(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// DEPRECATED: As of September 4th, 2025. +// func SearchRecipes(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// +// // create filters +// filters := domainRecipe.SearchFilters{ +// Search: ctx.PostForm("search"), // string, search query for titles +// MealType: toBits(ctx.PostFormArray("meal")), +// Time: toBits(ctx.PostFormArray("time")), +// Difficulty: toBits(ctx.PostFormArray("difficulty")), +// ServingSize: toBits(ctx.PostFormArray("serving")), +// } +// +// // Set the filters into the cookies, so they can be reloaded +// if bytes, err := json.Marshal(filters); err == nil { +// domain.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24) +// // ctx.SetCookie( +// // "search-filters", +// // string(bytes), +// // int(time.Now().Add(24*time.Hour).Sub(time.Now()).Seconds()), +// // "/", +// // true, +// // ) +// } +// +// redirect := ctx.PostForm("redirect") +// if redirect == "true" { +// ctx.Header("HX-Redirect", domain.WEB_SEARCH) +// ctx.Status(http.StatusOK) +// return +// } +// +// // Get user if logged in, so we can get favorite status +// var userId *int = nil +// if domain.IsLoggedIn(ctx) { +// id := ctx.MustGet("userId").(int) +// userId = &id +// } +// +// // TODO: Not sure if we need to ensure the user is valid here +// +// // We don't care about favorite status, so use false +// recipes, err := deps.RecipeService.SearchRecipes(filters, userId, false) +// if err != nil { +// components.RenderErrorBanner(ctx, err.Error()) +// ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) +// } +// +// // Render content as the response +// ctx.Status(200) +// templates.ResultList(recipes).Render(ctx.Request.Context(), ctx.Writer) +// } - // create filters - filters := domainRecipe.SearchFilters{ - Search: ctx.PostForm("search"), // string, search query for titles - MealType: toBits(ctx.PostFormArray("meal")), - Time: toBits(ctx.PostFormArray("time")), - Difficulty: toBits(ctx.PostFormArray("difficulty")), - ServingSize: toBits(ctx.PostFormArray("serving")), - } - - // Set the filters into the cookies, so they can be reloaded - if bytes, err := json.Marshal(filters); err == nil { - domain.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24) - // ctx.SetCookie( - // "search-filters", - // string(bytes), - // int(time.Now().Add(24*time.Hour).Sub(time.Now()).Seconds()), - // "/", - // "", // TODO: Need an actual domain - // false, // TODO: True in prod - // true, - // ) - } - - redirect := ctx.PostForm("redirect") - if redirect == "true" { - ctx.Header("HX-Redirect", domain.WEB_SEARCH) - ctx.Status(http.StatusOK) - return - } - - // Get user if logged in, so we can get favorite status - var userId *int = nil - if domain.IsLoggedIn(ctx) { - id := ctx.MustGet("userId").(int) - userId = &id - } - - // TODO: Not sure if we need to ensure the user is valid here - - // We don't care about favorite status, so use false - recipes, err := deps.RecipeService.SearchRecipes(filters, userId, false) - if err != nil { - components.RenderErrorBanner(ctx, err.Error()) - ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) - } - - // Render content as the response - ctx.Status(200) - templates.ResultList(recipes).Render(ctx.Request.Context(), ctx.Writer) -} - -func SearchRecipesFavorites(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - - // create filters - filters := domainRecipe.SearchFilters{ - Search: ctx.PostForm("search"), // string, search query for titles - MealType: toBits(ctx.PostFormArray("meal")), - Time: toBits(ctx.PostFormArray("time")), - Difficulty: toBits(ctx.PostFormArray("difficulty")), - ServingSize: toBits(ctx.PostFormArray("serving")), - } - - // Set the filters into the cookies, so they can be reloaded - if bytes, err := json.Marshal(filters); err == nil { - domain.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24) - // ctx.SetCookie( - // "search-filters", - // string(bytes), - // int(time.Now().Add(24*time.Hour).Sub(time.Now()).Seconds()), - // "/", - // "", // TODO: Need an actual domain - // false, // TODO: True in prod - // true, - // ) - } - - // TODO: Error here if they're not logged in? - // Get user data (they should be logged in) - if !domain.IsLoggedIn(ctx) { - components.RenderErrorBanner(ctx, "User is not logged in. User will be nil.") - ctx.JSON(http.StatusOK, gin.H{"error": "User is not logged in. User will be nil."}) - } - - userId := ctx.MustGet("userId").(int) - - recipes, err := deps.RecipeService.SearchRecipes(filters, &userId, true) - if err != nil { - components.RenderErrorBanner(ctx, err.Error()) - ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) - } - - // Render content as the response - ctx.Status(200) - templates.FavoriteList(recipes).Render(ctx.Request.Context(), ctx.Writer) -} +// DEPRECATED: As of September 4th, 2025. +// func SearchRecipesFavorites(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// +// // create filters +// filters := domainRecipe.SearchFilters{ +// Search: ctx.PostForm("search"), // string, search query for titles +// MealType: toBits(ctx.PostFormArray("meal")), +// Time: toBits(ctx.PostFormArray("time")), +// Difficulty: toBits(ctx.PostFormArray("difficulty")), +// ServingSize: toBits(ctx.PostFormArray("serving")), +// } +// +// // Set the filters into the cookies, so they can be reloaded +// if bytes, err := json.Marshal(filters); err == nil { +// domain.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24) +// // ctx.SetCookie( +// // "search-filters", +// // string(bytes), +// // int(time.Now().Add(24*time.Hour).Sub(time.Now()).Seconds()), +// // "/", +// // "", // TODO: Need an actual domain +// // false, // TODO: True in prod +// // true, +// // ) +// } +// +// // TODO: Error here if they're not logged in? +// // Get user data (they should be logged in) +// if !domain.IsLoggedIn(ctx) { +// components.RenderErrorBanner(ctx, "User is not logged in. User will be nil.") +// ctx.JSON(http.StatusOK, gin.H{"error": "User is not logged in. User will be nil."}) +// } +// +// userId := ctx.MustGet("userId").(int) +// +// recipes, err := deps.RecipeService.SearchRecipes(filters, &userId, true) +// if err != nil { +// components.RenderErrorBanner(ctx, err.Error()) +// ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) +// } +// +// // Render content as the response +// ctx.Status(200) +// templates.FavoriteList(recipes).Render(ctx.Request.Context(), ctx.Writer) +// } diff --git a/internal/app/handlers/state_handler.go b/internal/app/handlers/state_handler.go index 91c2de3..d2c55b5 100644 --- a/internal/app/handlers/state_handler.go +++ b/internal/app/handlers/state_handler.go @@ -1,76 +1,71 @@ package handlers -import ( - "fmt" - "net/http" - "strings" +// DEPRECATED: As of September 4th, 2025. +// const TAG_HTML = ` +//
  • +// × %s +//
  • +// ` - "github.com/gin-gonic/gin" - domain "github.com/haydenhargreaves/Potion/internal/domain/server" -) +// DEPRECATED: As of September 4th, 2025. +// const TAG_LIST_HTML = ` +// +// ` -const TAG_HTML = ` -
  • - × %s -
  • -` +// DEPRECATED: As of September 4th, 2025. +// func NewTag(ctx *gin.Context) { +// tag := strings.ToLower(ctx.PostForm("tag")) +// tags := strings.Split(ctx.PostForm("tags"), ",") +// +// tags = append([]string{tag}, tags...) +// +// var html string +// var cleaned_tags []string +// for _, tag := range tags { +// if tag != "" { +// html += fmt.Sprintf(TAG_HTML, domain.STATE_TAGS_DELETE, tag, tag) +// +// // Ensure that the list provided does not contain blank spaces. +// // This is another measure to ensure this state is bulletproof. +// cleaned_tags = append(cleaned_tags, tag) +// } +// } +// +// // Execute OOB swap for the tags +// html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(cleaned_tags, ",")) +// +// ctx.String(http.StatusOK, html) +// } -const TAG_LIST_HTML = ` - -` - -func NewTag(ctx *gin.Context) { - tag := strings.ToLower(ctx.PostForm("tag")) - tags := strings.Split(ctx.PostForm("tags"), ",") - - tags = append([]string{tag}, tags...) - - var html string - var cleaned_tags []string - for _, tag := range tags { - if tag != "" { - html += fmt.Sprintf(TAG_HTML, domain.STATE_TAGS_DELETE, tag, tag) - - // Ensure that the list provided does not contain blank spaces. - // This is another measure to ensure this state is bulletproof. - cleaned_tags = append(cleaned_tags, tag) - } - } - - // Execute OOB swap for the tags - html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(cleaned_tags, ",")) - - ctx.String(http.StatusOK, html) -} - -func DeleteTag(ctx *gin.Context) { - tags := strings.Split(ctx.PostForm("tags"), ",") - target := ctx.PostForm("target") - - var html string - var new_tags []string - for _, tag := range tags { - if tag != target && tag != "" { - html += fmt.Sprintf(TAG_HTML, domain.STATE_TAGS_DELETE ,tag, tag) - new_tags = append(new_tags, tag) - } - } - - // Execute OOB swap for the tags - html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(new_tags, ",")) - - ctx.String(http.StatusOK, html) -} +// DEPRECATED: As of September 4th, 2025. +// func DeleteTag(ctx *gin.Context) { +// tags := strings.Split(ctx.PostForm("tags"), ",") +// target := ctx.PostForm("target") +// +// var html string +// var new_tags []string +// for _, tag := range tags { +// if tag != target && tag != "" { +// html += fmt.Sprintf(TAG_HTML, domain.STATE_TAGS_DELETE ,tag, tag) +// new_tags = append(new_tags, tag) +// } +// } +// +// // Execute OOB swap for the tags +// html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(new_tags, ",")) +// +// ctx.String(http.StatusOK, html) +// } diff --git a/internal/app/handlers/user_handler.go b/internal/app/handlers/user_handler.go index 4561f87..d63dd61 100644 --- a/internal/app/handlers/user_handler.go +++ b/internal/app/handlers/user_handler.go @@ -1,90 +1,83 @@ package handlers -import ( - "fmt" - "net/http" +// DEPRECATED: As of September 4th, 2025. +// func GetUserRecipes(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// +// // Ensure user is logged in with a valid account +// user := deps.UserService.GetAuthenicatedUser(ctx) +// if user == nil { +// // Log (stale) user out +// domain.SetCookie(ctx, "jwt_token", "", -1) +// domain.SetCookie(ctx, "search-filters", "", -1) +// } +// +// // Ensure logged in +// if !domain.IsLoggedIn(ctx) || user == nil { +// components.RenderErrorBanner(ctx, "User is not authorized to access this endpoint. Please login to continue.") +// 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 +// } +// +// recipes, err := deps.RecipeService.GetUserRecipes(user.Id) +// if err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get user recipes. %s", err.Error())) +// 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, +// }) +// } - "github.com/gin-gonic/gin" - domain "github.com/haydenhargreaves/Potion/internal/domain/server" - "github.com/haydenhargreaves/Potion/internal/templates/components" -) - -func GetUserRecipes(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - - // Ensure user is logged in with a valid account - user := deps.UserService.GetAuthenicatedUser(ctx) - if user == nil { - // Log (stale) user out - domain.SetCookie(ctx, "jwt_token", "", -1) - domain.SetCookie(ctx, "search-filters", "", -1) - } - - // Ensure logged in - if !domain.IsLoggedIn(ctx) || user == nil { - components.RenderErrorBanner(ctx, "User is not authorized to access this endpoint. Please login to continue.") - 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 - } - - recipes, err := deps.RecipeService.GetUserRecipes(user.Id) - if err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get user recipes. %s", err.Error())) - 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, - }) -} - -func GetUserFavoriteRecipes(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - - // Ensure user is logged in with a valid account - user := deps.UserService.GetAuthenicatedUser(ctx) - if user == nil { - // Log (stale) user out - domain.SetCookie(ctx, "jwt_token", "", -1) - domain.SetCookie(ctx, "search-filters", "", -1) - } - - // Ensure logged in - if !domain.IsLoggedIn(ctx) || user == nil { - components.RenderErrorBanner(ctx, "User is not authorized to access this endpoint. Please login to continue.") - 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 - } - - recipes, err := deps.RecipeService.GetUserFavoriteRecipes(user.Id) - if err != nil { - components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get favorite recipes. %s", err.Error())) - ctx.JSON(http.StatusBadRequest, gin.H{ - "status": http.StatusBadRequest, - "message": fmt.Sprintf("Could not get favorite recipes. %s", err.Error()), - "recipes": nil, - }) - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "status": http.StatusOK, - "message": "User recipes successfully retrieved.", - "recipes": recipes, - }) -} +// DEPRECATED: As of September 4th, 2025. +// func GetUserFavoriteRecipes(ctx *gin.Context) { +// deps := ctx.MustGet("deps").(*domain.InjectedDependencies) +// +// // Ensure user is logged in with a valid account +// user := deps.UserService.GetAuthenicatedUser(ctx) +// if user == nil { +// // Log (stale) user out +// domain.SetCookie(ctx, "jwt_token", "", -1) +// domain.SetCookie(ctx, "search-filters", "", -1) +// } +// +// // Ensure logged in +// if !domain.IsLoggedIn(ctx) || user == nil { +// components.RenderErrorBanner(ctx, "User is not authorized to access this endpoint. Please login to continue.") +// 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 +// } +// +// recipes, err := deps.RecipeService.GetUserFavoriteRecipes(user.Id) +// if err != nil { +// components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get favorite recipes. %s", err.Error())) +// ctx.JSON(http.StatusBadRequest, gin.H{ +// "status": http.StatusBadRequest, +// "message": fmt.Sprintf("Could not get favorite 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/auth_handler.go b/internal/app/server/auth_handler.go new file mode 100644 index 0000000..16ffbb4 --- /dev/null +++ b/internal/app/server/auth_handler.go @@ -0,0 +1,45 @@ +package server + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" +) + +// GoogleLogin directs the user to Googles select user login page. Once the user has selected an +// account, they will be directed to the GoogleCallback handler where the main logic resides. +func (s *Server) GoogleLoginHandler(ctx *gin.Context) { + url := s.deps.AuthService.GetGoogleAuthUrl() + ctx.Redirect(http.StatusSeeOther, url) +} + +// GoogleCallback is the callback handler when the user successfully logs in with their Google +// account. They will be directed here and a JWT is generated. This JWT is stored in the users +// cookies and will be used by protected routes to validate their login status. +func (s *Server) GoogleCallbackHandler(ctx *gin.Context) { + var ( + state string = ctx.Query("state") + code string = ctx.Query("code") + ) + + if jwt, err := s.deps.AuthService.GoogleAuthSuccess(state, code); err != nil { + components.RenderErrorBanner(ctx, err.Error()) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } else { + s.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7) + ctx.Redirect(http.StatusSeeOther, "/") + } +} + +// Logout removes the token from the user's browser. Effectively "logging them out." Routes that +// require authentication will require the user to sign back in before accessing them again. +// This route will direct the user back to the home page. +func (s *Server) LogoutHandler(ctx *gin.Context) { + s.SetCookie(ctx, "jwt_token", "", -1) + s.SetCookie(ctx, "search-filters", "", -1) + ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) +} + diff --git a/internal/app/server/cookies.go b/internal/app/server/cookies.go new file mode 100644 index 0000000..8cb46b3 --- /dev/null +++ b/internal/app/server/cookies.go @@ -0,0 +1,53 @@ +package server + +import ( + "time" + + "github.com/gin-gonic/gin" +) + +// SetCookie sets a cookie value with a duration provided. This function handles setting the security +// configuration as well as the domain. These values are based on the EnvironmentConfig, therefore +// the value should be set. Nothing is returned by this function, but the cookie will be set. +// +// This function can also be used to clear cookies, if a blank value ("") and invalid duration (-1) +// is provided. +// +// If 0 is provided as the duration, then a session cookie is created, which will be cleared when +// the browser is closed. +func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.Duration) { + var ( + path string = "/" + httpOnly bool = true + maxAge int + secure bool + domain string + ) + + if duration < 0 { + // Delete the cookie + maxAge = -1 + } else if duration == 0 { + // Session cookie, clears when browser is closed + maxAge = 0 + } else { + // Normal calculation + maxAge = int(time.Now().Add(duration).Sub(time.Now()).Seconds()) + } + + if s.deps.EnvironmentConfig.Environment == "prod" { + secure = true + domain = s.deps.EnvironmentConfig.Domain + + } else if s.deps.EnvironmentConfig.Environment == "dev" { + secure = false + domain = s.deps.EnvironmentConfig.Domain + + } else { + // Defaults + secure = false + domain = "" + } + + ctx.SetCookie(name, value, maxAge, path, domain, secure, httpOnly) +} diff --git a/internal/app/server/engagement_handler.go b/internal/app/server/engagement_handler.go new file mode 100644 index 0000000..f8619b8 --- /dev/null +++ b/internal/app/server/engagement_handler.go @@ -0,0 +1,142 @@ +package server + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" +) + +func (s *Server) EngagementViewRecipeHandler(ctx *gin.Context) { + recipeId, _ := strconv.Atoi(ctx.Param("id")) + + // Ensure user is logged in with a valid account + user := s.deps.UserService.GetAuthenicatedUser(ctx) + if user == nil { + // Log (stale) user out + s.SetCookie(ctx, "jwt_token", "", -1) + s.SetCookie(ctx, "search-filters", "", -1) + } + + if !domain.IsLoggedIn(ctx) || user == nil { + if _, err := s.deps.EngagementService.ViewRecipe(recipeId); err != nil { + components.RenderErrorBanner(ctx, err.Error()) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId)) + ctx.Status(http.StatusOK) + } + return + } + + // We caught nil already, we can assume the user exists + if _, err := s.deps.EngagementService.UserViewRecipe(user.Id, recipeId); err != nil { + components.RenderErrorBanner(ctx, err.Error()) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId)) + ctx.Status(http.StatusOK) + } +} + +func (s *Server) EngagementShareRecipeHandler(ctx *gin.Context) { + recipeId, _ := strconv.Atoi(ctx.Param("id")) + + // Ensure user is logged in with a valid account + user := s.deps.UserService.GetAuthenicatedUser(ctx) + if user == nil { + // Log (stale) user out + s.SetCookie(ctx, "jwt_token", "", -1) + s.SetCookie(ctx, "search-filters", "", -1) + } + + if !domain.IsLoggedIn(ctx) || user == nil { + if _, err := s.deps.EngagementService.ShareRecipe(recipeId); err != nil { + components.RenderErrorBanner(ctx, err.Error()) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Status(http.StatusNoContent) + } + return + } + + if _, err := s.deps.EngagementService.UserShareRecipe(user.Id, recipeId); err != nil { + components.RenderErrorBanner(ctx, err.Error()) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Status(http.StatusNoContent) + } +} + +func (s *Server) EngagementFavoriteRecipeHandler(ctx *gin.Context) { + // Ensure user is logged in with a valid account + user := s.deps.UserService.GetAuthenicatedUser(ctx) + if user == nil { + // Log (stale) user out + s.SetCookie(ctx, "jwt_token", "", -1) + s.SetCookie(ctx, "search-filters", "", -1) + } + + if !domain.IsLoggedIn(ctx) || user == nil { + ctx.Header("HX-Redirect", domain.WEB_LOGIN) + ctx.Status(http.StatusOK) + return + } + + id := ctx.Param("id") + recipeId, _ := strconv.Atoi(id) + + if _, err := s.deps.EngagementService.UserFavoriteRecipe(user.Id, recipeId); err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("Something went wrong. %s.", err.Error())) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Status(http.StatusNoContent) + } +} + +func (s *Server) EngagementMakeRecipeHandler(ctx *gin.Context) { + // Ensure user is logged in with a valid account + user := s.deps.UserService.GetAuthenicatedUser(ctx) + if user == nil { + // Log (stale) user out + s.SetCookie(ctx, "jwt_token", "", -1) + s.SetCookie(ctx, "search-filters", "", -1) + } + + if !domain.IsLoggedIn(ctx) || user == nil { + ctx.Header("HX-Redirect", domain.WEB_LOGIN) + ctx.Status(http.StatusOK) + return + } + + id := ctx.Param("id") + recipeId, _ := strconv.Atoi(id) + + if _, err := s.deps.EngagementService.UserMakeRecipe(user.Id, recipeId); err != nil { + components.RenderErrorBanner(ctx, err.Error()) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Status(http.StatusNoContent) + } +} diff --git a/internal/app/server/middleware.go b/internal/app/server/middleware.go index a22acdd..16b0966 100644 --- a/internal/app/server/middleware.go +++ b/internal/app/server/middleware.go @@ -13,12 +13,14 @@ import ( // DepedencyInjectionMiddleware injects the dependencies into the context set. This is a middleware // that is used to apply the required services. -func DepedencyInjectionMiddleware(deps *domain.InjectedDependencies) gin.HandlerFunc { - return func(ctx *gin.Context) { - ctx.Set("deps", deps) - ctx.Next() - } -} +// +// DEPRECATED: As of September 4th, 2025. +// func DepedencyInjectionMiddleware(deps *domain.InjectedDependencies) gin.HandlerFunc { +// return func(ctx *gin.Context) { +// ctx.Set("deps", deps) +// ctx.Next() +// } +// } // JwtAuthMiddleWare handles collection the JWT from the browser's cookies and setting the // appropriate data. If the data is not found, this middleware will do effectively nothing, by not diff --git a/internal/app/server/page_handler.go b/internal/app/server/page_handler.go new file mode 100644 index 0000000..e9805dc --- /dev/null +++ b/internal/app/server/page_handler.go @@ -0,0 +1,298 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/a-h/templ" + "github.com/gin-gonic/gin" + domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" + domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" + layouts "github.com/haydenhargreaves/Potion/internal/templates/layouts" + pages "github.com/haydenhargreaves/Potion/internal/templates/pages" +) + +func (s *Server) LoginPageHandler(ctx *gin.Context) { + title := "Potion - Login" + page := pages.LoginPage() + + ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +} + +func (s *Server) HomePageHandler(ctx *gin.Context) { + loggedIn := domain.IsLoggedIn(ctx) + + // Ensure user is logged in with a valid account + if user := s.deps.UserService.GetAuthenicatedUser(ctx); user == nil { + // Log (stale) user out + s.SetCookie(ctx, "jwt_token", "", -1) + s.SetCookie(ctx, "search-filters", "", -1) + loggedIn = false + } + + var page templ.Component + if loggedIn { + userId := ctx.MustGet("userId").(int) + madeRecipes, err := s.deps.RecipeService.GetUserMadeRecipes(userId, 6) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting made recipes. %s\n", err.Error())) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": fmt.Sprintf("Error getting made recipes. %s\n", err.Error()), + }) + return + } + viewedRecipes, err := s.deps.RecipeService.GetUserViewedRecipes(userId, 6) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error())) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error()), + }) + return + } + + // Get the recipe of the week + recipeOfTheWeek, err := s.deps.RecipeService.GetRecipeOfTheWeek(&userId) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error())) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), + }) + return + } + + if bytes, err := ctx.Cookie("search-filters"); err != nil { + fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) + page = pages.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, nil) + } else { + var filters domainRecipe.SearchFilters + if err := json.Unmarshal([]byte(bytes), &filters); err != nil { + fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) + page = pages.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, nil) + } else { + page = pages.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek, &filters) + } + } + } else { + // Get the recipe of the week + recipeOfTheWeek, err := s.deps.RecipeService.GetRecipeOfTheWeek(nil) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error())) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), + }) + return + } + + if bytes, err := ctx.Cookie("search-filters"); err != nil { + fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) + page = pages.HomePage(false, nil, nil, recipeOfTheWeek, nil) + } else { + var filters domainRecipe.SearchFilters + if err := json.Unmarshal([]byte(bytes), &filters); err != nil { + fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) + page = pages.HomePage(false, nil, nil, recipeOfTheWeek, nil) + } else { + page = pages.HomePage(false, nil, nil, recipeOfTheWeek, &filters) + } + } + } + + title := "Potion - Home" + + ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +} + +func (s *Server) FavoritesPageHandler(ctx *gin.Context) { + // If not logged in, direct to the login page + if !domain.IsLoggedIn(ctx) { + ctx.Redirect(http.StatusSeeOther, domain.WEB_LOGIN) + return + } + + title := "Potion - Favorites" + var page templ.Component + + // Get filters from cookies + if bytes, err := ctx.Cookie("search-filters"); err != nil { + fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) + page = pages.FavoritesPage(nil) + } else { + var filters domainRecipe.SearchFilters + if err := json.Unmarshal([]byte(bytes), &filters); err != nil { + fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) + page = pages.FavoritesPage(nil) + } else { + page = pages.FavoritesPage(&filters) + } + } + + ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +} + +func (s *Server) CreatePageHandler(ctx *gin.Context) { + // If not logged in, direct to the login page + if !domain.IsLoggedIn(ctx) { + ctx.Redirect(http.StatusSeeOther, domain.WEB_LOGIN) + return + } + + // Ensure user is logged in with a valid account + if user := s.deps.UserService.GetAuthenicatedUser(ctx); user == nil { + // Log (stale) user out + s.SetCookie(ctx, "jwt_token", "", -1) + s.SetCookie(ctx, "search-filters", "", -1) + + ctx.Redirect(http.StatusSeeOther, domain.WEB_LOGIN) + return + } + + title := "Potion - Create" + page := pages.CreatePage() + + ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +} + +func (s *Server) ProfilePageHandler(ctx *gin.Context) { + // If not logged in, direct to the login page + if !domain.IsLoggedIn(ctx) { + ctx.Redirect(http.StatusSeeOther, domain.WEB_LOGIN) + return + } + + // Else, get the user data + user := s.deps.UserService.GetAuthenicatedUser(ctx) + if user == nil { + // User is failing to be found, direct to the login page + ctx.Redirect(http.StatusSeeOther, domain.WEB_LOGIN) + return + } + + recipes, err := s.deps.RecipeService.GetUserRecipes(user.Id) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("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 + } + + favorites, err := s.deps.RecipeService.GetUserFavoriteRecipes(user.Id) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting favorite recipes. %s\n", err.Error())) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": fmt.Sprintf("Error getting favorite recipes. %s\n", err.Error()), + }) + return + } + + // Get the engagement data, not sure what will happen when errors occur + engagements, err := s.deps.EngagementService.GetUserEngagement(user.Id, 6) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("Error getting user engagements. %s\n", err.Error())) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": fmt.Sprintf("Error getting user engagements. %s\n", err.Error()), + }) + return + } + + title := "Potion - Profile" + page := pages.ProfilePage(*user, recipes, favorites, engagements) + + ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +} + +func (s *Server) ListPageHandler(ctx *gin.Context) { + title := "Potion - Shopping List" + page := pages.ListPage() + + ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +} + +func (s *Server) RecipePageHandler(ctx *gin.Context) { + // Call recipe service to get via ID + id := ctx.Param("id") + + // Parse ID + parsed, err := strconv.Atoi(id) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) + ctx.JSON(400, err.Error()) + return + } + + // Get signed in user, if they exist + var userId *int = nil + var loggedIn = domain.IsLoggedIn(ctx) + + // Ensure user is logged in with a valid account + if user := s.deps.UserService.GetAuthenicatedUser(ctx); user == nil { + // Log (stale) user out + s.SetCookie(ctx, "jwt_token", "", -1) + s.SetCookie(ctx, "search-filters", "", -1) + loggedIn = false + } + + if loggedIn { + storeId := ctx.MustGet("userId").(int) + userId = &storeId + } + + // Get recipe + recipe, err := s.deps.RecipeService.GetRecipe(parsed, userId) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) + ctx.JSON(400, err.Error()) + return + } + + // Get user (owner) + user, err := s.deps.UserService.GetUser(recipe.UserId) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) + ctx.JSON(400, err.Error()) + return + } + + title := "Potion - View Recipe" + page := pages.RecipePage(*recipe, *user, loggedIn, s.deps.EnvironmentConfig.Domain) + + ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +} + +func (s *Server) SearchPageHandler(ctx *gin.Context) { + var page templ.Component + // Get filters from cookies + if bytes, err := ctx.Cookie("search-filters"); err != nil { + fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error()) + page = pages.SearchPage(nil, false) + } else { + var filters domainRecipe.SearchFilters + if err := json.Unmarshal([]byte(bytes), &filters); err != nil { + fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error()) + page = pages.SearchPage(nil, false) + } else { + page = pages.SearchPage(&filters, true) + } + } + + title := "Potion - Recipe Search" + + ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +} + +func (s *Server) NotFoundPageHandler(ctx *gin.Context) { + title := "Potion - Not Found" + page := pages.NotFoundPage() + + ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) +} diff --git a/internal/app/server/recipe_handler.go b/internal/app/server/recipe_handler.go new file mode 100644 index 0000000..f45b746 --- /dev/null +++ b/internal/app/server/recipe_handler.go @@ -0,0 +1,121 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" + domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" + templates "github.com/haydenhargreaves/Potion/internal/templates/pages" +) + +const CREATE_ERROR_HTML = ` +

    + Uh oh! Something went wrong when creating your recipe. Please try again. %s +

    +` + +func (s *Server) CreateRecipeHandler(ctx *gin.Context) { + recipe, err := s.deps.RecipeService.CreateRecipe(ctx) + if err != nil { + components.RenderErrorBanner(ctx, err.Error()) + ctx.String(http.StatusOK, CREATE_ERROR_HTML, err.Error()) + return + } + + // Send HTMX redirection + url := fmt.Sprintf(domain.WEB_RECIPE, recipe.Id) + ctx.Header("HX-Redirect", url) + ctx.Status(http.StatusCreated) +} + +// toBits converts an array of stringified numbers into a single summed value +func toBits(arr []string) (bits int) { + for _, x := range arr { + num, _ := strconv.Atoi(x) + bits += num + } + return +} + +// TODO: (7/06/2025) I don't love doing all of this here, but it seems to be the only way to get it to work... +func (s *Server) SearchRecipesHandler(ctx *gin.Context) { + // create filters + filters := domainRecipe.SearchFilters{ + Search: ctx.PostForm("search"), // string, search query for titles + MealType: toBits(ctx.PostFormArray("meal")), + Time: toBits(ctx.PostFormArray("time")), + Difficulty: toBits(ctx.PostFormArray("difficulty")), + ServingSize: toBits(ctx.PostFormArray("serving")), + } + + // Set the filters into the cookies, so they can be reloaded + if bytes, err := json.Marshal(filters); err == nil { + s.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24) + } + + redirect := ctx.PostForm("redirect") + if redirect == "true" { + ctx.Header("HX-Redirect", domain.WEB_SEARCH) + ctx.Status(http.StatusOK) + return + } + + // Get user if logged in, so we can get favorite status + var userId *int = nil + if domain.IsLoggedIn(ctx) { + id := ctx.MustGet("userId").(int) + userId = &id + } + + // TODO: Not sure if we need to ensure the user is valid here + + // We don't care about favorite status, so use false + recipes, err := s.deps.RecipeService.SearchRecipes(filters, userId, false) + if err != nil { + components.RenderErrorBanner(ctx, err.Error()) + ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) + } + + // Render content as the response + ctx.Status(200) + templates.ResultList(recipes).Render(ctx.Request.Context(), ctx.Writer) +} + +func (s *Server) SearchRecipesFavoritesHandler(ctx *gin.Context) { + // create filters + filters := domainRecipe.SearchFilters{ + Search: ctx.PostForm("search"), // string, search query for titles + MealType: toBits(ctx.PostFormArray("meal")), + Time: toBits(ctx.PostFormArray("time")), + Difficulty: toBits(ctx.PostFormArray("difficulty")), + ServingSize: toBits(ctx.PostFormArray("serving")), + } + + // Set the filters into the cookies, so they can be reloaded + if bytes, err := json.Marshal(filters); err == nil { + s.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24) + } + + if !domain.IsLoggedIn(ctx) { + components.RenderErrorBanner(ctx, "User is not logged in. User will be nil.") + ctx.JSON(http.StatusOK, gin.H{"error": "User is not logged in. User will be nil."}) + } + + userId := ctx.MustGet("userId").(int) + + recipes, err := s.deps.RecipeService.SearchRecipes(filters, &userId, true) + if err != nil { + components.RenderErrorBanner(ctx, err.Error()) + ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) + } + + // Render content as the response + ctx.Status(200) + templates.FavoriteList(recipes).Render(ctx.Request.Context(), ctx.Writer) +} diff --git a/internal/app/server/server.go b/internal/app/server/server.go index 07b9af6..3e8f85f 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -9,7 +9,6 @@ import ( "github.com/a-h/templ/examples/integration-gin/gintemplrenderer" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "github.com/haydenhargreaves/Potion/internal/app/handlers" "github.com/haydenhargreaves/Potion/internal/app/service" domain "github.com/haydenhargreaves/Potion/internal/domain/server" "github.com/haydenhargreaves/Potion/internal/infrastructure/auth" @@ -23,6 +22,7 @@ type Server struct { Router *gin.Engine config cors.Config DB *sql.DB + deps domain.InjectedDependencies } // Init initializes the server with the provided port. CORS settings are defined here. @@ -53,6 +53,7 @@ func (s *Server) Start() { s.Router.Run(fmt.Sprintf(":%d", s.port)) } +// TODO: (9/4/2025) Abstract these functions and cleanup. This is fucking messy... func (s *Server) Setup() *Server { // SETUP THE ENVIRONMENT CONFIGURATION cfg, err := domain.LoadEnvironment() @@ -109,7 +110,7 @@ func (s *Server) Setup() *Server { recipeService := service.NewRecipeService(recipeRepo, engagementRepo) engagementService := service.NewEngagementService(engagementRepo, recipeRepo) - deps := &domain.InjectedDependencies{ + s.deps = domain.InjectedDependencies{ UserService: userService, AuthService: authService, RecipeService: recipeService, @@ -119,7 +120,6 @@ func (s *Server) Setup() *Server { // Apply middleware s.Router.Use(RecoveryMiddleware()) - s.Router.Use(DepedencyInjectionMiddleware(deps)) s.Router.Use(JwtAuthMiddleWare(jwtSecret)) // Redirect index to home page: Update this as needed @@ -138,44 +138,41 @@ func (s *Server) Setup() *Server { // API router endpoints router_api.GET("/", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "Server is active."}) }) - router_api.GET("/tmp", func(ctx *gin.Context) { - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - ctx.JSON(200, gin.H{"config": deps.EnvironmentConfig}) - }) // WEB router endpoints router_web.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) }) - router_web.GET("/login", handlers.LoginPage) - router_web.GET("/home", handlers.HomePage) - router_web.GET("/favorites", handlers.FavoritesPage) - router_web.GET("/create", handlers.CreatePage) - router_web.GET("/profile", handlers.ProfilePage) - router_web.GET("/list", handlers.ListPage) - router_web.GET("/recipe/:id", handlers.RecipePage) - router_web.GET("/search", handlers.SearchPage) - router_web.GET("/404", handlers.NotFoundPage) + router_web.GET("/login", s.LoginPageHandler) + router_web.GET("/home", s.HomePageHandler) + router_web.GET("/favorites", s.FavoritesPageHandler) + router_web.GET("/create", s.CreatePageHandler) + router_web.GET("/profile", s.ProfilePageHandler) + router_web.GET("/list", s.ListPageHandler) + router_web.GET("/recipe/:id", s.RecipePageHandler) + router_web.GET("/search", s.SearchPageHandler) + router_web.GET("/404", s.NotFoundPageHandler) // WEB state endpoints - router_state.POST("/tags", handlers.NewTag) - router_state.POST("/tags/delete", handlers.DeleteTag) + router_state.POST("/tags", s.NewTagHandler) + router_state.POST("/tags/delete", s.DeleteTagHandler) // Authentication - router_api.GET("/auth/login", handlers.GoogleLogin) - router_api.GET("/auth/callback", handlers.GoogleCallback) - router_api.GET("/auth/logout", handlers.Logout) + router_api.GET("/auth/login", s.GoogleLoginHandler) + router_api.GET("/auth/callback", s.GoogleCallbackHandler) + router_api.GET("/auth/logout", s.LogoutHandler) // Recipe endpoints - router_api.POST("/recipe", handlers.CreateRecipe) - router_api.POST("/recipe/search", handlers.SearchRecipes) - router_api.POST("/recipe/search/favorites", handlers.SearchRecipesFavorites) - router_api.GET("/user/recipes", handlers.GetUserRecipes) - router_api.GET("/user/favorites", handlers.GetUserFavoriteRecipes) + router_api.POST("/recipe", s.CreateRecipeHandler) + router_api.POST("/recipe/search", s.SearchRecipesHandler) + router_api.POST("/recipe/search/favorites", s.SearchRecipesFavoritesHandler) + + router_api.GET("/user/recipes", s.GetUserFavoriteRecipesHandler) + router_api.GET("/user/favorites", s.GetUserFavoriteRecipesHandler) // 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) + router_api.POST("/engagement/view/:id", s.EngagementViewRecipeHandler) + router_api.POST("/engagement/share/:id", s.EngagementShareRecipeHandler) + router_api.POST("/engagement/favorite/:id", s.EngagementFavoriteRecipeHandler) + router_api.POST("/engagement/make/:id", s.EngagementMakeRecipeHandler) // Catch un-routed URLS s.Router.NoRoute(func(ctx *gin.Context) { diff --git a/internal/app/server/state_handler.go b/internal/app/server/state_handler.go new file mode 100644 index 0000000..139985f --- /dev/null +++ b/internal/app/server/state_handler.go @@ -0,0 +1,76 @@ +package server + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/server" +) + +const TAG_HTML = ` +
  • + × %s +
  • +` + +const TAG_LIST_HTML = ` + +` + +func (s *Server) NewTagHandler(ctx *gin.Context) { + tag := strings.ToLower(ctx.PostForm("tag")) + tags := strings.Split(ctx.PostForm("tags"), ",") + + tags = append([]string{tag}, tags...) + + var html string + var cleaned_tags []string + for _, tag := range tags { + if tag != "" { + html += fmt.Sprintf(TAG_HTML, domain.STATE_TAGS_DELETE, tag, tag) + + // Ensure that the list provided does not contain blank spaces. + // This is another measure to ensure this state is bulletproof. + cleaned_tags = append(cleaned_tags, tag) + } + } + + // Execute OOB swap for the tags + html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(cleaned_tags, ",")) + + ctx.String(http.StatusOK, html) +} + +func (s *Server) DeleteTagHandler(ctx *gin.Context) { + tags := strings.Split(ctx.PostForm("tags"), ",") + target := ctx.PostForm("target") + + var html string + var new_tags []string + for _, tag := range tags { + if tag != target && tag != "" { + html += fmt.Sprintf(TAG_HTML, domain.STATE_TAGS_DELETE, tag, tag) + new_tags = append(new_tags, tag) + } + } + + // Execute OOB swap for the tags + html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(new_tags, ",")) + + ctx.String(http.StatusOK, html) +} diff --git a/internal/app/server/user_handler.go b/internal/app/server/user_handler.go new file mode 100644 index 0000000..5e08316 --- /dev/null +++ b/internal/app/server/user_handler.go @@ -0,0 +1,87 @@ +package server + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" +) + +func (s *Server) GetUserRecipesHandler(ctx *gin.Context) { + // Ensure user is logged in with a valid account + user := s.deps.UserService.GetAuthenicatedUser(ctx) + if user == nil { + // Log (stale) user out + s.SetCookie(ctx, "jwt_token", "", -1) + s.SetCookie(ctx, "search-filters", "", -1) + } + + // Ensure logged in + if !domain.IsLoggedIn(ctx) || user == nil { + components.RenderErrorBanner(ctx, "User is not authorized to access this endpoint. Please login to continue.") + 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 + } + + recipes, err := s.deps.RecipeService.GetUserRecipes(user.Id) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get user recipes. %s", err.Error())) + 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, + }) +} + +func (s *Server) GetUserFavoriteRecipesHandler(ctx *gin.Context) { + // Ensure user is logged in with a valid account + user := s.deps.UserService.GetAuthenicatedUser(ctx) + if user == nil { + // Log (stale) user out + s.SetCookie(ctx, "jwt_token", "", -1) + s.SetCookie(ctx, "search-filters", "", -1) + } + + // Ensure logged in + if !domain.IsLoggedIn(ctx) || user == nil { + components.RenderErrorBanner(ctx, "User is not authorized to access this endpoint. Please login to continue.") + 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 + } + + recipes, err := s.deps.RecipeService.GetUserFavoriteRecipes(user.Id) + if err != nil { + components.RenderErrorBanner(ctx, fmt.Sprintf("Could not get favorite recipes. %s", err.Error())) + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("Could not get favorite 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/domain/server/server.go b/internal/domain/server/server.go index b9a8210..da17824 100644 --- a/internal/domain/server/server.go +++ b/internal/domain/server/server.go @@ -3,7 +3,6 @@ package domain import ( "fmt" "os" - "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" @@ -132,41 +131,43 @@ func LoadEnvironment() (*EnvironmentConfig, error) { // // If 0 is provided as the duration, then a session cookie is created, which will be cleared when // the browser is closed. -func SetCookie(ctx *gin.Context, name, value string, duration time.Duration) { - deps := ctx.MustGet("deps").(*InjectedDependencies) - - var ( - path string = "/" - httpOnly bool = true - maxAge int - secure bool - domain string - ) - - if duration < 0 { - // Delete the cookie - maxAge = -1 - } else if duration == 0 { - // Session cookie, clears when browser is closed - maxAge = 0 - } else { - // Normal calculation - maxAge = int(time.Now().Add(duration).Sub(time.Now()).Seconds()) - } - - if deps.EnvironmentConfig.Environment == "prod" { - secure = true - domain = deps.EnvironmentConfig.Domain - - } else if deps.EnvironmentConfig.Environment == "dev" { - secure = false - domain = deps.EnvironmentConfig.Domain - - } else { - // Defaults - secure = false - domain = "" - } - - ctx.SetCookie(name, value, maxAge, path, domain, secure, httpOnly) -} +// +// DEPRECATED: As of September 4th, 2025. +// func SetCookie(ctx *gin.Context, name, value string, duration time.Duration) { +// deps := ctx.MustGet("deps").(*InjectedDependencies) +// +// var ( +// path string = "/" +// httpOnly bool = true +// maxAge int +// secure bool +// domain string +// ) +// +// if duration < 0 { +// // Delete the cookie +// maxAge = -1 +// } else if duration == 0 { +// // Session cookie, clears when browser is closed +// maxAge = 0 +// } else { +// // Normal calculation +// maxAge = int(time.Now().Add(duration).Sub(time.Now()).Seconds()) +// } +// +// if deps.EnvironmentConfig.Environment == "prod" { +// secure = true +// domain = deps.EnvironmentConfig.Domain +// +// } else if deps.EnvironmentConfig.Environment == "dev" { +// secure = false +// domain = deps.EnvironmentConfig.Domain +// +// } else { +// // Defaults +// secure = false +// domain = "" +// } +// +// ctx.SetCookie(name, value, maxAge, path, domain, secure, httpOnly) +// } diff --git a/internal/templates/components/banner_templ.go b/internal/templates/components/banner_templ.go index 9c9d198..8d5dc82 100644 --- a/internal/templates/components/banner_templ.go +++ b/internal/templates/components/banner_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/components/cards_templ.go b/internal/templates/components/cards_templ.go index da80511..4fcd054 100644 --- a/internal/templates/components/cards_templ.go +++ b/internal/templates/components/cards_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/components/dropdowns_templ.go b/internal/templates/components/dropdowns_templ.go index b73df5a..5f39d15 100644 --- a/internal/templates/components/dropdowns_templ.go +++ b/internal/templates/components/dropdowns_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/components/error_templ.go b/internal/templates/components/error_templ.go index 5f93b89..1bfb31b 100644 --- a/internal/templates/components/error_templ.go +++ b/internal/templates/components/error_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/components/navbar_templ.go b/internal/templates/components/navbar_templ.go index 2365195..ba562fd 100644 --- a/internal/templates/components/navbar_templ.go +++ b/internal/templates/components/navbar_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/components/search_bar_templ.go b/internal/templates/components/search_bar_templ.go index 958d685..2e78e1e 100644 --- a/internal/templates/components/search_bar_templ.go +++ b/internal/templates/components/search_bar_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package components //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/layouts/app_layout_templ.go b/internal/templates/layouts/app_layout_templ.go index 2bacf07..0608ea8 100644 --- a/internal/templates/layouts/app_layout_templ.go +++ b/internal/templates/layouts/app_layout_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/create_templ.go b/internal/templates/pages/create_templ.go index 6b1c315..28b33db 100644 --- a/internal/templates/pages/create_templ.go +++ b/internal/templates/pages/create_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/favorites_templ.go b/internal/templates/pages/favorites_templ.go index 7788752..e888fc2 100644 --- a/internal/templates/pages/favorites_templ.go +++ b/internal/templates/pages/favorites_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/home_templ.go b/internal/templates/pages/home_templ.go index eb134b0..447abab 100644 --- a/internal/templates/pages/home_templ.go +++ b/internal/templates/pages/home_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/list_templ.go b/internal/templates/pages/list_templ.go index 9218f27..4a91ce0 100644 --- a/internal/templates/pages/list_templ.go +++ b/internal/templates/pages/list_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/login_templ.go b/internal/templates/pages/login_templ.go index 1c378a8..1cb6d29 100644 --- a/internal/templates/pages/login_templ.go +++ b/internal/templates/pages/login_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/notFound_templ.go b/internal/templates/pages/notFound_templ.go index a94b872..9ebcde1 100644 --- a/internal/templates/pages/notFound_templ.go +++ b/internal/templates/pages/notFound_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/profile_templ.go b/internal/templates/pages/profile_templ.go index 7b8b061..a99005c 100644 --- a/internal/templates/pages/profile_templ.go +++ b/internal/templates/pages/profile_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/recipe_templ.go b/internal/templates/pages/recipe_templ.go index 8f60f3e..7a3ba58 100644 --- a/internal/templates/pages/recipe_templ.go +++ b/internal/templates/pages/recipe_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/internal/templates/pages/search_templ.go b/internal/templates/pages/search_templ.go index 80f9942..2e7d66b 100644 --- a/internal/templates/pages/search_templ.go +++ b/internal/templates/pages/search_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.937 +// templ: version: v0.3.943 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present.