From 47b838684437b653c2f93e19b974893f97af24df Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Wed, 3 Sep 2025 22:22:23 -0700 Subject: [PATCH] (FEAT): Errors can now be displayed using the RenderErrorBanner fx. This is found in the components domain. Make sure only HTMX routes call it. Although, I think I put this into the page handlers WHICH IS WRONG. However, it does work, ish. But it does not load into the DOM properly. But it seems to display just fine. --- internal/app/handlers/engagement_handler.go | 2 + internal/app/handlers/page_handler.go | 23 ++- internal/templates/components/error.templ | 58 ++++++++ internal/templates/components/error_templ.go | 137 ++++++++++++++++++ internal/templates/layouts/app_layout.templ | 36 +++-- .../templates/layouts/app_layout_templ.go | 6 +- web/static/css/tailwind.css | 35 +++++ 7 files changed, 270 insertions(+), 27 deletions(-) create mode 100644 internal/templates/components/error.templ create mode 100644 internal/templates/components/error_templ.go diff --git a/internal/app/handlers/engagement_handler.go b/internal/app/handlers/engagement_handler.go index 9e35182..1c75b7c 100644 --- a/internal/app/handlers/engagement_handler.go +++ b/internal/app/handlers/engagement_handler.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" ) func EngagementViewRecipe(ctx *gin.Context) { @@ -101,6 +102,7 @@ func EngagementFavoriteRecipe(ctx *gin.Context) { 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(), diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index a29b705..8c84971 100755 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -11,6 +11,7 @@ import ( 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" @@ -41,6 +42,7 @@ func HomePage(ctx *gin.Context) { 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()), @@ -49,9 +51,10 @@ func HomePage(ctx *gin.Context) { } 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 made recipes. %s\n", err.Error()), + "message": fmt.Sprintf("Error getting viewed recipes. %s\n", err.Error()), }) return } @@ -59,9 +62,10 @@ func HomePage(ctx *gin.Context) { // 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 made recipes. %s\n", err.Error()), + "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), }) return } @@ -82,9 +86,10 @@ func HomePage(ctx *gin.Context) { // 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 made recipes. %s\n", err.Error()), + "message": fmt.Sprintf("Error getting recipe of the week. %s\n", err.Error()), }) return } @@ -178,6 +183,7 @@ func ProfilePage(ctx *gin.Context) { 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()), @@ -187,9 +193,10 @@ func ProfilePage(ctx *gin.Context) { 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 recipes. %s\n", err.Error()), + "message": fmt.Sprintf("Error getting favorite recipes. %s\n", err.Error()), }) return } @@ -197,6 +204,7 @@ func ProfilePage(ctx *gin.Context) { // 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()), @@ -217,7 +225,6 @@ func ListPage(ctx *gin.Context) { ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) } -// TODO: Figure out how to handle errors, think we just need a simple display. func RecipePage(ctx *gin.Context) { // Call recipe service to get via ID deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) @@ -226,7 +233,7 @@ func RecipePage(ctx *gin.Context) { // Parse ID parsed, err := strconv.Atoi(id) if err != nil { - fmt.Printf("ERROR: %s\n", err.Error()) + components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) ctx.JSON(400, err.Error()) return } @@ -251,7 +258,7 @@ func RecipePage(ctx *gin.Context) { // Get recipe recipe, err := deps.RecipeService.GetRecipe(parsed, userId) if err != nil { - fmt.Printf("ERROR: %s\n", err.Error()) + components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) ctx.JSON(400, err.Error()) return } @@ -259,7 +266,7 @@ func RecipePage(ctx *gin.Context) { // Get user (owner) user, err := deps.UserService.GetUser(recipe.UserId) if err != nil { - fmt.Printf("ERROR: %s\n", err.Error()) + components.RenderErrorBanner(ctx, fmt.Sprintf("ERROR: %s", err.Error())) ctx.JSON(400, err.Error()) return } diff --git a/internal/templates/components/error.templ b/internal/templates/components/error.templ new file mode 100644 index 0000000..7c75b50 --- /dev/null +++ b/internal/templates/components/error.templ @@ -0,0 +1,58 @@ +package components + +import "github.com/gin-gonic/gin" + +templ errorIcon() { + + + +} + +templ closeButton() { + +} + +templ errorBanner(message string) { +
+
+ @errorIcon() +

Error

+ @closeButton() +
+
+

+ { message } +

+
+
+} + +// RenderErrorBanner renders the error banner. However, this function must ONLY be called by an +// HTMX route. Otherwise, there is no promise it will work (may result in undefined behavior). +// Just writes a piece of content to the response. +func RenderErrorBanner(ctx *gin.Context, message string) { + ctx.Writer.Header().Set("Content-Type", "text/html") + errorBanner(message).Render(ctx.Request.Context(), ctx.Writer) +} diff --git a/internal/templates/components/error_templ.go b/internal/templates/components/error_templ.go new file mode 100644 index 0000000..607c8e9 --- /dev/null +++ b/internal/templates/components/error_templ.go @@ -0,0 +1,137 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.937 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "github.com/gin-gonic/gin" + +func errorIcon() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func closeButton() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func errorBanner(message string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = errorIcon().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

Error

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = closeButton().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(message) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/error.templ`, Line: 46, Col: 13} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// RenderErrorBanner renders the error banner. However, this function must ONLY be called by an +// HTMX route. Otherwise, there is no promise it will work (may result in undefined behavior). +// Just writes a piece of content to the response. +func RenderErrorBanner(ctx *gin.Context, message string) { + ctx.Writer.Header().Set("Content-Type", "text/html") + errorBanner(message).Render(ctx.Request.Context(), ctx.Writer) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/templates/layouts/app_layout.templ b/internal/templates/layouts/app_layout.templ index 45f9a04..6076747 100644 --- a/internal/templates/layouts/app_layout.templ +++ b/internal/templates/layouts/app_layout.templ @@ -3,20 +3,24 @@ package templates // AppLayout is the main application layout, this does not contain any content other than // meta data, links, scripts and whatever is passed into it as a component. templ AppLayout(title string, child templ.Component) { - - - - - - - { title } - - - - - - @child - - - + + + + + + { title } + + + + +
+ @child + + + } diff --git a/internal/templates/layouts/app_layout_templ.go b/internal/templates/layouts/app_layout_templ.go index 8e40f3e..2bacf07 100644 --- a/internal/templates/layouts/app_layout_templ.go +++ b/internal/templates/layouts/app_layout_templ.go @@ -38,13 +38,13 @@ func AppLayout(title string, child templ.Component) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/layouts/app_layout.templ`, Line: 12, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/layouts/app_layout.templ`, Line: 11, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -52,7 +52,7 @@ func AppLayout(title string, child templ.Component) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index f7bd052..b2e20b3 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -8,6 +8,7 @@ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; --color-red-100: oklch(93.6% 0.032 17.717); + --color-red-200: oklch(88.5% 0.062 18.334); --color-red-500: oklch(63.7% 0.237 25.331); --color-green-500: oklch(72.3% 0.219 149.579); --color-blue-50: oklch(97% 0.014 254.604); @@ -235,18 +236,27 @@ .absolute { position: absolute; } + .fixed { + position: fixed; + } .relative { position: relative; } .static { position: static; } + .top-0 { + top: calc(var(--spacing) * 0); + } .top-1\/2 { top: calc(1/2 * 100%); } .top-\[100\%\] { top: 100%; } + .right-0 { + right: calc(var(--spacing) * 0); + } .left-0 { left: calc(var(--spacing) * 0); } @@ -262,6 +272,9 @@ .z-20 { z-index: 20; } + .m-4 { + margin: calc(var(--spacing) * 4); + } .mx-2 { margin-inline: calc(var(--spacing) * 2); } @@ -337,6 +350,12 @@ .mb-16 { margin-bottom: calc(var(--spacing) * 16); } + .ml-8 { + margin-left: calc(var(--spacing) * 8); + } + .ml-auto { + margin-left: auto; + } .block { display: block; } @@ -694,6 +713,9 @@ --tw-gradient-to: var(--color-purple-200); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } + .p-1 { + padding: calc(var(--spacing) * 1); + } .p-2 { padding: calc(var(--spacing) * 2); } @@ -939,6 +961,12 @@ --tw-shadow-color: color-mix(in oklab, var(--color-gray-300) var(--tw-shadow-alpha), transparent); } } + .shadow-red-200 { + --tw-shadow-color: oklch(88.5% 0.062 18.334); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, var(--color-red-200) var(--tw-shadow-alpha), transparent); + } + } .outline { outline-style: var(--tw-outline-style); outline-width: 1px; @@ -1141,6 +1169,13 @@ } } } + .hover\:bg-red-200 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-200); + } + } + } .hover\:text-blue-400 { &:hover { @media (hover: hover) {