Compare commits
No commits in common. "479e709b4eb17fd12ba308548868c47205b0d51e" and "55c98299f5b6395efe1c7d3437034e6eef0ebc83" have entirely different histories.
479e709b4e
...
55c98299f5
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
/flake.lock
|
/flake.lock
|
||||||
/go.sum
|
/go.sum
|
||||||
/.env
|
/.env
|
||||||
/*.dump
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ RUN npm install tailwindcss @tailwindcss/cli && npx @tailwindcss/cli -i ./web/st
|
|||||||
|
|
||||||
|
|
||||||
# Build stage
|
# Build stage
|
||||||
FROM golang:latest AS build-stage
|
FROM golang:1.24 AS build-stage
|
||||||
|
|
||||||
COPY --from=tailwind-build-stage /app /app
|
COPY --from=tailwind-build-stage /app /app
|
||||||
|
|
||||||
|
|||||||
@ -27,8 +27,6 @@
|
|||||||
watchman
|
watchman
|
||||||
docker-language-server
|
docker-language-server
|
||||||
dockerfile-language-server-nodejs
|
dockerfile-language-server-nodejs
|
||||||
gcc_multi
|
|
||||||
glibc_multi
|
|
||||||
];
|
];
|
||||||
|
|
||||||
# Define the shell that will be executed.
|
# Define the shell that will be executed.
|
||||||
@ -43,11 +41,6 @@
|
|||||||
export GOPATH="$HOME/.local/go"
|
export GOPATH="$HOME/.local/go"
|
||||||
echo "Settings GOPATH to: $HOME/.local/go "
|
echo "Settings GOPATH to: $HOME/.local/go "
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=amd64
|
|
||||||
export CGO_CFLAGS=-Wno-error=cpp;
|
|
||||||
|
|
||||||
|
|
||||||
# Exec zsh to replace the current shell process with zsh.
|
# Exec zsh to replace the current shell process with zsh.
|
||||||
# This ensures your prompt and zsh configurations load correctly.
|
# This ensures your prompt and zsh configurations load correctly.
|
||||||
exec zsh
|
exec zsh
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
|||||||
module github.com/haydenhargreaves/Potion
|
module github.com/haydenhargreaves/Potion
|
||||||
|
|
||||||
go 1.25.0
|
go 1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/a-h/templ v0.3.920
|
github.com/a-h/templ v0.3.920
|
||||||
|
|||||||
@ -1,47 +1,55 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
|
)
|
||||||
|
|
||||||
// GoogleLogin directs the user to Googles select user login page. Once the user has selected an
|
// 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.
|
// account, they will be directed to the GoogleCallback handler where the main logic resides.
|
||||||
//
|
func GoogleLogin(ctx *gin.Context) {
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||||
// func GoogleLogin(ctx *gin.Context) {
|
url := deps.AuthService.GetGoogleAuthUrl()
|
||||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
|
||||||
// url := deps.AuthService.GetGoogleAuthUrl()
|
ctx.Redirect(http.StatusSeeOther, url)
|
||||||
//
|
}
|
||||||
// ctx.Redirect(http.StatusSeeOther, url)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// GoogleCallback is the callback handler when the user successfully logs in with their Google
|
// 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
|
// 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.
|
// cookies and will be used by protected routes to validate their login status.
|
||||||
//
|
//
|
||||||
|
// TODO: This route does not do the proper handling, need to work on the redirection or handling.
|
||||||
|
//
|
||||||
// We do not need to return all of this data, it is just for testing.
|
// We do not need to return all of this data, it is just for testing.
|
||||||
//
|
func GoogleCallback(ctx *gin.Context) {
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||||
// func GoogleCallback(ctx *gin.Context) {
|
|
||||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
var (
|
||||||
//
|
state string = ctx.Query("state")
|
||||||
// var (
|
code string = ctx.Query("code")
|
||||||
// state string = ctx.Query("state")
|
)
|
||||||
// code string = ctx.Query("code")
|
|
||||||
// )
|
// TODO: Do something real, not just return data
|
||||||
//
|
if jwt, dbUser, googleUserInfo, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
||||||
// if jwt, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
// components.RenderErrorBanner(ctx, err.Error())
|
} else {
|
||||||
// ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
domain.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7)
|
||||||
// } else {
|
// ctx.JSON(http.StatusOK, gin.H{"jwt": jwt, "googleUserInfo": googleUserInfo, "dbUser": dbUser})
|
||||||
// domain.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7)
|
_ = dbUser
|
||||||
// ctx.Redirect(http.StatusSeeOther, "/")
|
_ = googleUserInfo
|
||||||
// }
|
|
||||||
// }
|
ctx.Redirect(http.StatusSeeOther, "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Logout removes the token from the user's browser. Effectively "logging them out." Routes that
|
// 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.
|
// 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.
|
// This route will direct the user back to the home page.
|
||||||
//
|
func Logout(ctx *gin.Context) {
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||||
// func Logout(ctx *gin.Context) {
|
domain.SetCookie(ctx, "search-filters", "", -1)
|
||||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME)
|
||||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
}
|
||||||
// ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME)
|
|
||||||
// }
|
|
||||||
|
|||||||
@ -1,142 +1,141 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
import (
|
||||||
// func EngagementViewRecipe(ctx *gin.Context) {
|
"fmt"
|
||||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
"net/http"
|
||||||
// recipeId, _ := strconv.Atoi(ctx.Param("id"))
|
"strconv"
|
||||||
//
|
|
||||||
// // 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)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
"github.com/gin-gonic/gin"
|
||||||
// func EngagementShareRecipe(ctx *gin.Context) {
|
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
// 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)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
func EngagementViewRecipe(ctx *gin.Context) {
|
||||||
// func EngagementFavoriteRecipe(ctx *gin.Context) {
|
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||||
// 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 {
|
|
||||||
// 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)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
// Ensure user is logged in with a valid account
|
||||||
// func EngagementMakeRecipe(ctx *gin.Context) {
|
user := deps.UserService.GetAuthenicatedUser(ctx)
|
||||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
if user == nil {
|
||||||
//
|
// Log (stale) user out
|
||||||
// // Ensure user is logged in with a valid account
|
domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||||
// user := deps.UserService.GetAuthenicatedUser(ctx)
|
domain.SetCookie(ctx, "search-filters", "", -1)
|
||||||
// if user == nil {
|
}
|
||||||
// // Log (stale) user out
|
|
||||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
if !domain.IsLoggedIn(ctx) || user == nil {
|
||||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
if _, err := deps.EngagementService.ViewRecipe(recipeId); err != nil {
|
||||||
// }
|
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||||
//
|
"status": http.StatusInternalServerError,
|
||||||
// if !domain.IsLoggedIn(ctx) || user == nil {
|
"message": err.Error(),
|
||||||
// ctx.Header("HX-Redirect", domain.WEB_LOGIN)
|
})
|
||||||
// ctx.Status(http.StatusOK)
|
} else {
|
||||||
// return
|
ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId))
|
||||||
// }
|
ctx.Status(http.StatusOK)
|
||||||
//
|
}
|
||||||
// id := ctx.Param("id")
|
return
|
||||||
// recipeId, _ := strconv.Atoi(id)
|
}
|
||||||
//
|
|
||||||
// if _, err := deps.EngagementService.UserMakeRecipe(user.Id, recipeId); err != nil {
|
// We caught nil already, we can assume the user exists
|
||||||
// components.RenderErrorBanner(ctx, err.Error())
|
if _, err := deps.EngagementService.UserViewRecipe(user.Id, recipeId); err != nil {
|
||||||
// ctx.JSON(http.StatusInternalServerError, gin.H{
|
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||||
// "status": http.StatusInternalServerError,
|
"status": http.StatusInternalServerError,
|
||||||
// "message": err.Error(),
|
"message": err.Error(),
|
||||||
// })
|
})
|
||||||
// } else {
|
} else {
|
||||||
// ctx.Status(http.StatusNoContent)
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"status": http.StatusInternalServerError,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,296 +1,299 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
import (
|
||||||
// func LoginPage(ctx *gin.Context) {
|
"encoding/json"
|
||||||
// title := "Potion - Login"
|
"fmt"
|
||||||
// page := pages.LoginPage()
|
"net/http"
|
||||||
//
|
"strconv"
|
||||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
"github.com/a-h/templ"
|
||||||
// func HomePage(ctx *gin.Context) {
|
"github.com/gin-gonic/gin"
|
||||||
// deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies)
|
domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||||
//
|
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
// loggedIn := domain.IsLoggedIn(ctx)
|
domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
//
|
layouts "github.com/haydenhargreaves/Potion/internal/templates/layouts"
|
||||||
// // Ensure user is logged in with a valid account
|
pages "github.com/haydenhargreaves/Potion/internal/templates/pages"
|
||||||
// if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil {
|
templates "github.com/haydenhargreaves/Potion/internal/templates/pages"
|
||||||
// // 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))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
func LoginPage(ctx *gin.Context) {
|
||||||
// func FavoritesPage(ctx *gin.Context) {
|
title := "Potion - Login"
|
||||||
// // If not logged in, direct to the login page
|
page := pages.LoginPage()
|
||||||
// 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))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
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))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
func HomePage(ctx *gin.Context) {
|
||||||
// func ListPage(ctx *gin.Context) {
|
deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies)
|
||||||
// title := "Potion - Shopping List"
|
|
||||||
// page := pages.ListPage()
|
|
||||||
//
|
|
||||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
loggedIn := domain.IsLoggedIn(ctx)
|
||||||
// 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))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
// Ensure user is logged in with a valid account
|
||||||
// func NotFoundPage(ctx *gin.Context) {
|
if user := deps.UserService.GetAuthenicatedUser(ctx); user == nil {
|
||||||
// title := "Potion - Not Found"
|
// Log (stale) user out
|
||||||
// page := pages.NotFoundPage()
|
domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||||
//
|
domain.SetCookie(ctx, "search-filters", "", -1)
|
||||||
// ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
loggedIn = false
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
var page templ.Component
|
||||||
|
if loggedIn {
|
||||||
|
userId := ctx.MustGet("userId").(int)
|
||||||
|
madeRecipes, err := deps.RecipeService.GetUserMadeRecipes(userId, 6)
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
|
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"status": http.StatusInternalServerError,
|
||||||
|
"message": fmt.Sprintf("Error getting made recipes. %s\n", err.Error()),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the recipe of the week
|
||||||
|
recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(&userId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"status": http.StatusInternalServerError,
|
||||||
|
"message": fmt.Sprintf("Error getting made recipes. %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 {
|
||||||
|
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"status": http.StatusInternalServerError,
|
||||||
|
"message": fmt.Sprintf("Error getting made recipes. %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 {
|
||||||
|
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 {
|
||||||
|
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"status": http.StatusInternalServerError,
|
||||||
|
"message": fmt.Sprintf("Error getting recipes. %s\n", err.Error()),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the engagement data, not sure what will happen when errors occur
|
||||||
|
engagements, err := deps.EngagementService.GetUserEngagement(user.Id, 6)
|
||||||
|
if err != nil {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
id := ctx.Param("id")
|
||||||
|
|
||||||
|
// Parse ID
|
||||||
|
parsed, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("ERROR: %s\n", 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 {
|
||||||
|
fmt.Printf("ERROR: %s\n", err.Error())
|
||||||
|
ctx.JSON(400, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user (owner)
|
||||||
|
user, err := deps.UserService.GetUser(recipe.UserId)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("ERROR: %s\n", 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))
|
||||||
|
}
|
||||||
|
|||||||
@ -1,136 +1,142 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
import (
|
||||||
// const CREATE_ERROR_HTML = `
|
"encoding/json"
|
||||||
// <p id="response" class="text-sm text-red-500 px-4 py-1 bg-red-100 rounded-full w-fit">
|
"fmt"
|
||||||
// Uh oh! Something went wrong when creating your recipe. Please try again. %s
|
"net/http"
|
||||||
// </p>
|
"strconv"
|
||||||
// `
|
"time"
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
"github.com/gin-gonic/gin"
|
||||||
// func CreateRecipe(ctx *gin.Context) {
|
domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
//
|
templates "github.com/haydenhargreaves/Potion/internal/templates/pages"
|
||||||
// recipe, err := deps.RecipeService.CreateRecipe(ctx)
|
)
|
||||||
// if err != nil {
|
|
||||||
// components.RenderErrorBanner(ctx, err.Error())
|
const CREATE_ERROR_HTML = `
|
||||||
// ctx.String(http.StatusOK, CREATE_ERROR_HTML, err.Error())
|
<p id="response" class="text-sm text-red-500 px-4 py-1 bg-red-100 rounded-full w-fit">
|
||||||
// return
|
Uh oh! Something went wrong when creating your recipe. Please try again. %s
|
||||||
// }
|
</p>
|
||||||
//
|
`
|
||||||
// // Send HTMX redirection
|
|
||||||
// url := fmt.Sprintf(domain.WEB_RECIPE, recipe.Id)
|
func CreateRecipe(ctx *gin.Context) {
|
||||||
// ctx.Header("HX-Redirect", url)
|
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||||
// ctx.Status(http.StatusCreated)
|
|
||||||
// }
|
recipe, err := deps.RecipeService.CreateRecipe(ctx)
|
||||||
|
if err != nil {
|
||||||
|
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
|
// toBits converts an array of stringified numbers into a single summed value
|
||||||
//
|
func toBits(arr []string) (bits int) {
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
for _, x := range arr {
|
||||||
// func toBits(arr []string) (bits int) {
|
num, _ := strconv.Atoi(x)
|
||||||
// for _, x := range arr {
|
bits += num
|
||||||
// num, _ := strconv.Atoi(x)
|
}
|
||||||
// bits += num
|
return
|
||||||
// }
|
}
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
// 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) {
|
func SearchRecipes(ctx *gin.Context) {
|
||||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
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)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
// create filters
|
||||||
// func SearchRecipesFavorites(ctx *gin.Context) {
|
filters := domainRecipe.SearchFilters{
|
||||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
Search: ctx.PostForm("search"), // string, search query for titles
|
||||||
//
|
MealType: toBits(ctx.PostFormArray("meal")),
|
||||||
// // create filters
|
Time: toBits(ctx.PostFormArray("time")),
|
||||||
// filters := domainRecipe.SearchFilters{
|
Difficulty: toBits(ctx.PostFormArray("difficulty")),
|
||||||
// Search: ctx.PostForm("search"), // string, search query for titles
|
ServingSize: toBits(ctx.PostFormArray("serving")),
|
||||||
// MealType: toBits(ctx.PostFormArray("meal")),
|
}
|
||||||
// Time: toBits(ctx.PostFormArray("time")),
|
|
||||||
// Difficulty: toBits(ctx.PostFormArray("difficulty")),
|
// Set the filters into the cookies, so they can be reloaded
|
||||||
// ServingSize: toBits(ctx.PostFormArray("serving")),
|
if bytes, err := json.Marshal(filters); err == nil {
|
||||||
// }
|
domain.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24)
|
||||||
//
|
// ctx.SetCookie(
|
||||||
// // Set the filters into the cookies, so they can be reloaded
|
// "search-filters",
|
||||||
// if bytes, err := json.Marshal(filters); err == nil {
|
// string(bytes),
|
||||||
// domain.SetCookie(ctx, "search-filters", string(bytes), time.Hour*24)
|
// int(time.Now().Add(24*time.Hour).Sub(time.Now()).Seconds()),
|
||||||
// // ctx.SetCookie(
|
// "/",
|
||||||
// // "search-filters",
|
// "", // TODO: Need an actual domain
|
||||||
// // string(bytes),
|
// false, // TODO: True in prod
|
||||||
// // int(time.Now().Add(24*time.Hour).Sub(time.Now()).Seconds()),
|
// true,
|
||||||
// // "/",
|
// )
|
||||||
// // "", // 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)
|
||||||
// // TODO: Error here if they're not logged in?
|
return
|
||||||
// // Get user data (they should be logged in)
|
}
|
||||||
// if !domain.IsLoggedIn(ctx) {
|
|
||||||
// components.RenderErrorBanner(ctx, "User is not logged in. User will be nil.")
|
// Get user if logged in, so we can get favorite status
|
||||||
// ctx.JSON(http.StatusOK, gin.H{"error": "User is not logged in. User will be nil."})
|
var userId *int = nil
|
||||||
// }
|
if domain.IsLoggedIn(ctx) {
|
||||||
//
|
id := ctx.MustGet("userId").(int)
|
||||||
// userId := ctx.MustGet("userId").(int)
|
userId = &id
|
||||||
//
|
}
|
||||||
// recipes, err := deps.RecipeService.SearchRecipes(filters, &userId, true)
|
|
||||||
// if err != nil {
|
// TODO: Not sure if we need to ensure the user is valid here
|
||||||
// components.RenderErrorBanner(ctx, err.Error())
|
|
||||||
// ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
|
// We don't care about favorite status, so use false
|
||||||
// }
|
recipes, err := deps.RecipeService.SearchRecipes(filters, userId, false)
|
||||||
//
|
if err != nil {
|
||||||
// // Render content as the response
|
ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
|
||||||
// ctx.Status(200)
|
}
|
||||||
// templates.FavoriteList(recipes).Render(ctx.Request.Context(), ctx.Writer)
|
|
||||||
// }
|
// 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) {
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,71 +1,76 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
import (
|
||||||
// const TAG_HTML = `
|
"fmt"
|
||||||
// <li
|
"net/http"
|
||||||
// hx-post="%s"
|
"strings"
|
||||||
// hx-trigger="click"
|
|
||||||
// hx-target="#tag-list"
|
|
||||||
// hx-swap="innerHTML"
|
|
||||||
// hx-include="#tag-list"
|
|
||||||
// hx-vals='{"target": "%s"}'
|
|
||||||
// class="flex text-xs items-center bg-blue-100 w-fit px-2 py-1 rounded-full gap-x-1 select-none cursor-pointer hover:bg-blue-200 duration-300">
|
|
||||||
// × %s
|
|
||||||
// </li>
|
|
||||||
// `
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
"github.com/gin-gonic/gin"
|
||||||
// const TAG_LIST_HTML = `
|
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
// <input
|
)
|
||||||
// hx-swap-oob="outerHTML"
|
|
||||||
// type="hidden"
|
|
||||||
// name="tags"
|
|
||||||
// id="tags"
|
|
||||||
// value="%s"
|
|
||||||
// />
|
|
||||||
// `
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
const TAG_HTML = `
|
||||||
// func NewTag(ctx *gin.Context) {
|
<li
|
||||||
// tag := strings.ToLower(ctx.PostForm("tag"))
|
hx-post="%s"
|
||||||
// tags := strings.Split(ctx.PostForm("tags"), ",")
|
hx-trigger="click"
|
||||||
//
|
hx-target="#tag-list"
|
||||||
// tags = append([]string{tag}, tags...)
|
hx-swap="innerHTML"
|
||||||
//
|
hx-include="#tag-list"
|
||||||
// var html string
|
hx-vals='{"target": "%s"}'
|
||||||
// var cleaned_tags []string
|
class="flex text-xs items-center bg-blue-100 w-fit px-2 py-1 rounded-full gap-x-1 select-none cursor-pointer hover:bg-blue-200 duration-300">
|
||||||
// for _, tag := range tags {
|
× %s
|
||||||
// if tag != "" {
|
</li>
|
||||||
// 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)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
const TAG_LIST_HTML = `
|
||||||
// func DeleteTag(ctx *gin.Context) {
|
<input
|
||||||
// tags := strings.Split(ctx.PostForm("tags"), ",")
|
hx-swap-oob="outerHTML"
|
||||||
// target := ctx.PostForm("target")
|
type="hidden"
|
||||||
//
|
name="tags"
|
||||||
// var html string
|
id="tags"
|
||||||
// var new_tags []string
|
value="%s"
|
||||||
// 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)
|
func NewTag(ctx *gin.Context) {
|
||||||
// }
|
tag := strings.ToLower(ctx.PostForm("tag"))
|
||||||
// }
|
tags := strings.Split(ctx.PostForm("tags"), ",")
|
||||||
//
|
|
||||||
// // Execute OOB swap for the tags
|
tags = append([]string{tag}, tags...)
|
||||||
// html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(new_tags, ","))
|
|
||||||
//
|
var html string
|
||||||
// ctx.String(http.StatusOK, html)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,83 +1,85 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
import (
|
||||||
// func GetUserRecipes(ctx *gin.Context) {
|
"fmt"
|
||||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
"net/http"
|
||||||
//
|
|
||||||
// // 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,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
"github.com/gin-gonic/gin"
|
||||||
// func GetUserFavoriteRecipes(ctx *gin.Context) {
|
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
)
|
||||||
//
|
|
||||||
// // Ensure user is logged in with a valid account
|
func GetUserRecipes(ctx *gin.Context) {
|
||||||
// user := deps.UserService.GetAuthenicatedUser(ctx)
|
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||||
// if user == nil {
|
|
||||||
// // Log (stale) user out
|
// Ensure user is logged in with a valid account
|
||||||
// domain.SetCookie(ctx, "jwt_token", "", -1)
|
user := deps.UserService.GetAuthenicatedUser(ctx)
|
||||||
// domain.SetCookie(ctx, "search-filters", "", -1)
|
if user == nil {
|
||||||
// }
|
// Log (stale) user out
|
||||||
//
|
domain.SetCookie(ctx, "jwt_token", "", -1)
|
||||||
// // Ensure logged in
|
domain.SetCookie(ctx, "search-filters", "", -1)
|
||||||
// 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{
|
// Ensure logged in
|
||||||
// "status": http.StatusUnauthorized,
|
if !domain.IsLoggedIn(ctx) || user == nil {
|
||||||
// "message": "User is not authorized to access this endpoint. Please login to continue.",
|
ctx.JSON(http.StatusUnauthorized, gin.H{
|
||||||
// "recipes": nil,
|
"status": http.StatusUnauthorized,
|
||||||
// })
|
"message": "User is not authorized to access this endpoint. Please login to continue.",
|
||||||
// return
|
"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()))
|
recipes, err := deps.RecipeService.GetUserRecipes(user.Id)
|
||||||
// ctx.JSON(http.StatusBadRequest, gin.H{
|
if err != nil {
|
||||||
// "status": http.StatusBadRequest,
|
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||||
// "message": fmt.Sprintf("Could not get favorite recipes. %s", err.Error()),
|
"status": http.StatusBadRequest,
|
||||||
// "recipes": nil,
|
"message": fmt.Sprintf("Could not get user recipes. %s", err.Error()),
|
||||||
// })
|
"recipes": nil,
|
||||||
// return
|
})
|
||||||
// }
|
return
|
||||||
//
|
}
|
||||||
// ctx.JSON(http.StatusOK, gin.H{
|
|
||||||
// "status": http.StatusOK,
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
// "message": "User recipes successfully retrieved.",
|
"status": http.StatusOK,
|
||||||
// "recipes": recipes,
|
"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 {
|
||||||
|
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 {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,14 +13,12 @@ import (
|
|||||||
|
|
||||||
// DepedencyInjectionMiddleware injects the dependencies into the context set. This is a middleware
|
// DepedencyInjectionMiddleware injects the dependencies into the context set. This is a middleware
|
||||||
// that is used to apply the required services.
|
// that is used to apply the required services.
|
||||||
//
|
func DepedencyInjectionMiddleware(deps *domain.InjectedDependencies) gin.HandlerFunc {
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
return func(ctx *gin.Context) {
|
||||||
// func DepedencyInjectionMiddleware(deps *domain.InjectedDependencies) gin.HandlerFunc {
|
ctx.Set("deps", deps)
|
||||||
// return func(ctx *gin.Context) {
|
ctx.Next()
|
||||||
// ctx.Set("deps", deps)
|
}
|
||||||
// ctx.Next()
|
}
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// JwtAuthMiddleWare handles collection the JWT from the browser's cookies and setting the
|
// 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
|
// appropriate data. If the data is not found, this middleware will do effectively nothing, by not
|
||||||
|
|||||||
@ -1,298 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
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 = `
|
|
||||||
<p id="response" class="text-sm text-red-500 px-4 py-1 bg-red-100 rounded-full w-fit">
|
|
||||||
Uh oh! Something went wrong when creating your recipe. Please try again. %s
|
|
||||||
</p>
|
|
||||||
`
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/a-h/templ/examples/integration-gin/gintemplrenderer"
|
"github.com/a-h/templ/examples/integration-gin/gintemplrenderer"
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/haydenhargreaves/Potion/internal/app/handlers"
|
||||||
"github.com/haydenhargreaves/Potion/internal/app/service"
|
"github.com/haydenhargreaves/Potion/internal/app/service"
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
||||||
@ -22,7 +23,6 @@ type Server struct {
|
|||||||
Router *gin.Engine
|
Router *gin.Engine
|
||||||
config cors.Config
|
config cors.Config
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
deps domain.InjectedDependencies
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the server with the provided port. CORS settings are defined here.
|
// Init initializes the server with the provided port. CORS settings are defined here.
|
||||||
@ -53,7 +53,6 @@ func (s *Server) Start() {
|
|||||||
s.Router.Run(fmt.Sprintf(":%d", s.port))
|
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 {
|
func (s *Server) Setup() *Server {
|
||||||
// SETUP THE ENVIRONMENT CONFIGURATION
|
// SETUP THE ENVIRONMENT CONFIGURATION
|
||||||
cfg, err := domain.LoadEnvironment()
|
cfg, err := domain.LoadEnvironment()
|
||||||
@ -110,7 +109,7 @@ func (s *Server) Setup() *Server {
|
|||||||
recipeService := service.NewRecipeService(recipeRepo, engagementRepo)
|
recipeService := service.NewRecipeService(recipeRepo, engagementRepo)
|
||||||
engagementService := service.NewEngagementService(engagementRepo, recipeRepo)
|
engagementService := service.NewEngagementService(engagementRepo, recipeRepo)
|
||||||
|
|
||||||
s.deps = domain.InjectedDependencies{
|
deps := &domain.InjectedDependencies{
|
||||||
UserService: userService,
|
UserService: userService,
|
||||||
AuthService: authService,
|
AuthService: authService,
|
||||||
RecipeService: recipeService,
|
RecipeService: recipeService,
|
||||||
@ -120,6 +119,7 @@ func (s *Server) Setup() *Server {
|
|||||||
|
|
||||||
// Apply middleware
|
// Apply middleware
|
||||||
s.Router.Use(RecoveryMiddleware())
|
s.Router.Use(RecoveryMiddleware())
|
||||||
|
s.Router.Use(DepedencyInjectionMiddleware(deps))
|
||||||
s.Router.Use(JwtAuthMiddleWare(jwtSecret))
|
s.Router.Use(JwtAuthMiddleWare(jwtSecret))
|
||||||
|
|
||||||
// Redirect index to home page: Update this as needed
|
// Redirect index to home page: Update this as needed
|
||||||
@ -138,41 +138,44 @@ func (s *Server) Setup() *Server {
|
|||||||
|
|
||||||
// API router endpoints
|
// API router endpoints
|
||||||
router_api.GET("/", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "Server is active."}) })
|
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
|
// WEB router endpoints
|
||||||
router_web.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) })
|
router_web.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) })
|
||||||
router_web.GET("/login", s.LoginPageHandler)
|
router_web.GET("/login", handlers.LoginPage)
|
||||||
router_web.GET("/home", s.HomePageHandler)
|
router_web.GET("/home", handlers.HomePage)
|
||||||
router_web.GET("/favorites", s.FavoritesPageHandler)
|
router_web.GET("/favorites", handlers.FavoritesPage)
|
||||||
router_web.GET("/create", s.CreatePageHandler)
|
router_web.GET("/create", handlers.CreatePage)
|
||||||
router_web.GET("/profile", s.ProfilePageHandler)
|
router_web.GET("/profile", handlers.ProfilePage)
|
||||||
router_web.GET("/list", s.ListPageHandler)
|
router_web.GET("/list", handlers.ListPage)
|
||||||
router_web.GET("/recipe/:id", s.RecipePageHandler)
|
router_web.GET("/recipe/:id", handlers.RecipePage)
|
||||||
router_web.GET("/search", s.SearchPageHandler)
|
router_web.GET("/search", handlers.SearchPage)
|
||||||
router_web.GET("/404", s.NotFoundPageHandler)
|
router_web.GET("/404", handlers.NotFoundPage)
|
||||||
|
|
||||||
// WEB state endpoints
|
// WEB state endpoints
|
||||||
router_state.POST("/tags", s.NewTagHandler)
|
router_state.POST("/tags", handlers.NewTag)
|
||||||
router_state.POST("/tags/delete", s.DeleteTagHandler)
|
router_state.POST("/tags/delete", handlers.DeleteTag)
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
router_api.GET("/auth/login", s.GoogleLoginHandler)
|
router_api.GET("/auth/login", handlers.GoogleLogin)
|
||||||
router_api.GET("/auth/callback", s.GoogleCallbackHandler)
|
router_api.GET("/auth/callback", handlers.GoogleCallback)
|
||||||
router_api.GET("/auth/logout", s.LogoutHandler)
|
router_api.GET("/auth/logout", handlers.Logout)
|
||||||
|
|
||||||
// Recipe endpoints
|
// Recipe endpoints
|
||||||
router_api.POST("/recipe", s.CreateRecipeHandler)
|
router_api.POST("/recipe", handlers.CreateRecipe)
|
||||||
router_api.POST("/recipe/search", s.SearchRecipesHandler)
|
router_api.POST("/recipe/search", handlers.SearchRecipes)
|
||||||
router_api.POST("/recipe/search/favorites", s.SearchRecipesFavoritesHandler)
|
router_api.POST("/recipe/search/favorites", handlers.SearchRecipesFavorites)
|
||||||
|
router_api.GET("/user/recipes", handlers.GetUserRecipes)
|
||||||
router_api.GET("/user/recipes", s.GetUserFavoriteRecipesHandler)
|
router_api.GET("/user/favorites", handlers.GetUserFavoriteRecipes)
|
||||||
router_api.GET("/user/favorites", s.GetUserFavoriteRecipesHandler)
|
|
||||||
|
|
||||||
// Engagement endpoints
|
// Engagement endpoints
|
||||||
router_api.POST("/engagement/view/:id", s.EngagementViewRecipeHandler)
|
router_api.POST("/engagement/view/:id", handlers.EngagementViewRecipe)
|
||||||
router_api.POST("/engagement/share/:id", s.EngagementShareRecipeHandler)
|
router_api.POST("/engagement/share/:id", handlers.EngagementShareRecipe)
|
||||||
router_api.POST("/engagement/favorite/:id", s.EngagementFavoriteRecipeHandler)
|
router_api.POST("/engagement/favorite/:id", handlers.EngagementFavoriteRecipe)
|
||||||
router_api.POST("/engagement/make/:id", s.EngagementMakeRecipeHandler)
|
router_api.POST("/engagement/make/:id", handlers.EngagementMakeRecipe)
|
||||||
|
|
||||||
// Catch un-routed URLS
|
// Catch un-routed URLS
|
||||||
s.Router.NoRoute(func(ctx *gin.Context) {
|
s.Router.NoRoute(func(ctx *gin.Context) {
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
const TAG_HTML = `
|
|
||||||
<li
|
|
||||||
hx-post="%s"
|
|
||||||
hx-trigger="click"
|
|
||||||
hx-target="#tag-list"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
hx-include="#tag-list"
|
|
||||||
hx-vals='{"target": "%s"}'
|
|
||||||
class="flex text-xs items-center bg-blue-100 w-fit px-2 py-1 rounded-full gap-x-1 select-none cursor-pointer hover:bg-blue-200 duration-300">
|
|
||||||
× %s
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
|
|
||||||
const TAG_LIST_HTML = `
|
|
||||||
<input
|
|
||||||
hx-swap-oob="outerHTML"
|
|
||||||
type="hidden"
|
|
||||||
name="tags"
|
|
||||||
id="tags"
|
|
||||||
value="%s"
|
|
||||||
/>
|
|
||||||
`
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -60,48 +60,48 @@ func (s *AuthService) GetGoogleAuthUrl() string {
|
|||||||
|
|
||||||
// GoogleAuthSuccess accepts the data from the Google login endpoint and uses it to fetch the users
|
// GoogleAuthSuccess accepts the data from the Google login endpoint and uses it to fetch the users
|
||||||
// data. The data is then used to log the user in or create an account.
|
// data. The data is then used to log the user in or create an account.
|
||||||
func (s *AuthService) GoogleAuthSuccess(state, code string) (string, error) {
|
func (s *AuthService) GoogleAuthSuccess(state, code string) (string, domain.User, domain.GoogleUserInfo, error) {
|
||||||
// Ensure the state matches, prevents M.I.T.M. attacks
|
// Ensure the state matches, prevents M.I.T.M. attacks
|
||||||
if state != "randomstate" {
|
if state != "randomstate" {
|
||||||
return "", fmt.Errorf("States don't match, received %s", state)
|
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("States don't match, received %s", state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get access token from Google
|
// Get access token from Google
|
||||||
token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code)
|
token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Code exchange failed: %s", err.Error())
|
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Code exchange failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the access token to get user data
|
// Use the access token to get user data
|
||||||
googleUserInfo, err := auth.GetUserData(token.AccessToken)
|
googleUserInfo, err := auth.GetUserData(token.AccessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", domain.User{}, domain.GoogleUserInfo{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to get the user, user is nil when they don't exit
|
// Attempt to get the user, user is nil when they don't exit
|
||||||
user, err := s.userRepository.GetGoogleUser(googleUserInfo.Id)
|
user, err := s.userRepository.GetGoogleUser(googleUserInfo.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Failed to get db user: %s", err)
|
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Failed to get db user: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A user was found
|
// A user was found
|
||||||
if user != nil {
|
if user != nil {
|
||||||
jwt, err := generateJwt(user.Id, user.Email, s.jwtSecret)
|
jwt, err := generateJwt(user.Id, user.Email, s.jwtSecret)
|
||||||
return jwt, err
|
return jwt, *user, googleUserInfo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// user did not exist, need to create one
|
// user did not exist, need to create one
|
||||||
newUser, err := s.userRepository.CreateGoogleUser(&googleUserInfo, token.RefreshToken)
|
newUser, err := s.userRepository.CreateGoogleUser(&googleUserInfo, token.RefreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Repository failed to create user: %s", err.Error())
|
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Repository failed to create user: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt, err := generateJwt(newUser.Id, newUser.Email, s.jwtSecret)
|
jwt, err := generateJwt(newUser.Id, newUser.Email, s.jwtSecret)
|
||||||
return jwt, err
|
return jwt, newUser, googleUserInfo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateJwt requires user data and returns a JSON web token which can be stored in the browsers
|
// generateJwt requires user data and returns a JSON web token which can be stored in the browsers
|
||||||
// cookies. This token is used to log a user into the application and allow access to protected
|
// cookies. This token is used to log a user into the application and allow access to protected
|
||||||
// routes.
|
// routes.
|
||||||
func generateJwt(userId int, email string, jwtSecret []byte) (string, error) {
|
func generateJwt(userId int, email string, jwtSecret []byte) (string, error) {
|
||||||
expiration := time.Now().Add(7 * 24 * time.Hour)
|
expiration := time.Now().Add(7 * 24 * time.Hour)
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||||
|
)
|
||||||
|
|
||||||
type AuthService interface {
|
type AuthService interface {
|
||||||
GetGoogleAuthUrl() string
|
GetGoogleAuthUrl() string
|
||||||
GoogleAuthSuccess(state, code string) (string, error)
|
GoogleAuthSuccess(state, code string) (string, domain.User, domain.GoogleUserInfo, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package domain
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@ -131,43 +132,41 @@ func LoadEnvironment() (*EnvironmentConfig, error) {
|
|||||||
//
|
//
|
||||||
// If 0 is provided as the duration, then a session cookie is created, which will be cleared when
|
// If 0 is provided as the duration, then a session cookie is created, which will be cleared when
|
||||||
// the browser is closed.
|
// the browser is closed.
|
||||||
//
|
func SetCookie(ctx *gin.Context, name, value string, duration time.Duration) {
|
||||||
// DEPRECATED: As of September 4th, 2025.
|
deps := ctx.MustGet("deps").(*InjectedDependencies)
|
||||||
// func SetCookie(ctx *gin.Context, name, value string, duration time.Duration) {
|
|
||||||
// deps := ctx.MustGet("deps").(*InjectedDependencies)
|
var (
|
||||||
//
|
path string = "/"
|
||||||
// var (
|
httpOnly bool = true
|
||||||
// path string = "/"
|
maxAge int
|
||||||
// httpOnly bool = true
|
secure bool
|
||||||
// maxAge int
|
domain string
|
||||||
// secure bool
|
)
|
||||||
// domain string
|
|
||||||
// )
|
if duration < 0 {
|
||||||
//
|
// Delete the cookie
|
||||||
// if duration < 0 {
|
maxAge = -1
|
||||||
// // Delete the cookie
|
} else if duration == 0 {
|
||||||
// maxAge = -1
|
// Session cookie, clears when browser is closed
|
||||||
// } else if duration == 0 {
|
maxAge = 0
|
||||||
// // Session cookie, clears when browser is closed
|
} else {
|
||||||
// maxAge = 0
|
// Normal calculation
|
||||||
// } else {
|
maxAge = int(time.Now().Add(duration).Sub(time.Now()).Seconds())
|
||||||
// // Normal calculation
|
}
|
||||||
// maxAge = int(time.Now().Add(duration).Sub(time.Now()).Seconds())
|
|
||||||
// }
|
if deps.EnvironmentConfig.Environment == "prod" {
|
||||||
//
|
secure = true
|
||||||
// if deps.EnvironmentConfig.Environment == "prod" {
|
domain = deps.EnvironmentConfig.Domain
|
||||||
// secure = true
|
|
||||||
// domain = deps.EnvironmentConfig.Domain
|
} else if deps.EnvironmentConfig.Environment == "dev" {
|
||||||
//
|
secure = false
|
||||||
// } else if deps.EnvironmentConfig.Environment == "dev" {
|
domain = deps.EnvironmentConfig.Domain
|
||||||
// secure = false
|
|
||||||
// domain = deps.EnvironmentConfig.Domain
|
} else {
|
||||||
//
|
// Defaults
|
||||||
// } else {
|
secure = false
|
||||||
// // Defaults
|
domain = ""
|
||||||
// secure = false
|
}
|
||||||
// domain = ""
|
|
||||||
// }
|
ctx.SetCookie(name, value, maxAge, path, domain, secure, httpOnly)
|
||||||
//
|
}
|
||||||
// ctx.SetCookie(name, value, maxAge, path, domain, secure, httpOnly)
|
|
||||||
// }
|
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Ensure the environment is sourced
|
|
||||||
source .env
|
|
||||||
|
|
||||||
pg_dump \
|
|
||||||
-h "$PSQL_HOST" \
|
|
||||||
-U "$PSQL_USERNAME" \
|
|
||||||
-Fc -v -d "$PSQL_DATABASE" \
|
|
||||||
-f backup.dump
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Ensure the environment is sourced
|
|
||||||
source .env
|
|
||||||
|
|
||||||
pg_restore \
|
|
||||||
-v -d "$PSQL_DATABASE_BACKUP" \
|
|
||||||
-U "$PSQL_USERNAME" \
|
|
||||||
-h "$PSQL_HOST" \
|
|
||||||
-c -F c backup.dump
|
|
||||||
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/001_create_users_table.sql
|
|
||||||
psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/002_create_meal_enum.sql
|
|
||||||
psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/003_create_recipes_table.sql
|
|
||||||
psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/004_create_fts_index.sql
|
|
||||||
psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/005_create_tags_tables.sql
|
|
||||||
psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/006_create_engagment_enum.sql
|
|
||||||
psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/007_create_engagement_table.sql
|
|
||||||
psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/008_create_favorites_table.sql
|
|
||||||
psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/009_create_recipe_of_the_week_table.sql
|
|
||||||
psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/010_create_recipe_of_the_week_procedure.sql
|
|
||||||
|
|
||||||
@ -369,27 +369,7 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i
|
|||||||
// Create search vector query
|
// Create search vector query
|
||||||
var orderBy string = ""
|
var orderBy string = ""
|
||||||
if filters.Search != "" {
|
if filters.Search != "" {
|
||||||
spl := strings.Split(filters.Search, " ")
|
vector_query := strings.ReplaceAll(filters.Search, " ", " | ")
|
||||||
var cleaned []string
|
|
||||||
|
|
||||||
// Use a string replacer, each word in the query will be passed through this
|
|
||||||
replacer := strings.NewReplacer(
|
|
||||||
"'", "",
|
|
||||||
"-", "",
|
|
||||||
"&", "",
|
|
||||||
"|", "",
|
|
||||||
"!", "",
|
|
||||||
)
|
|
||||||
|
|
||||||
for i := range len(spl) {
|
|
||||||
q := strings.TrimSpace(replacer.Replace(spl[i]))
|
|
||||||
|
|
||||||
if q != "" {
|
|
||||||
cleaned = append(cleaned, q)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vector_query := strings.Join(cleaned, " | ")
|
|
||||||
|
|
||||||
conditions = append(
|
conditions = append(
|
||||||
conditions,
|
conditions,
|
||||||
@ -397,8 +377,8 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i
|
|||||||
)
|
)
|
||||||
|
|
||||||
template := `
|
template := `
|
||||||
ORDER BY
|
ORDER BY
|
||||||
ts_rank(r.search_vector, to_tsquery('english', '%s')) DESC,
|
ts_rank(r.search_vector, to_tsquery('english', '%s')) DESC,
|
||||||
ts_rank_cd(r.search_vector, to_tsquery('english', '%s')) DESC
|
ts_rank_cd(r.search_vector, to_tsquery('english', '%s')) DESC
|
||||||
`
|
`
|
||||||
orderBy = fmt.Sprintf(template, vector_query, vector_query)
|
orderBy = fmt.Sprintf(template, vector_query, vector_query)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package components
|
package components
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package components
|
package components
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package components
|
package components
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
package components
|
|
||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
templ errorIcon() {
|
|
||||||
<svg class="h-6 text-red-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M12 16.99V17M12 7V14M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
|
|
||||||
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ closeButton() {
|
|
||||||
<button onclick="hideError();"
|
|
||||||
class="text-red-500 ml-auto hover:bg-red-200 p-1 rounded-sm transition-all duration-300 cursor-pointer">
|
|
||||||
<svg class="h-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
||||||
d="M19.207 6.207a1 1 0 0 0-1.414-1.414L12 10.586 6.207 4.793a1 1 0 0 0-1.414 1.414L10.586 12l-5.793 5.793a1 1 0 1 0 1.414 1.414L12 13.414l5.793 5.793a1 1 0 0 0 1.414-1.414L13.414 12l5.793-5.793z"
|
|
||||||
fill="currentColor"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ errorBanner(message string) {
|
|
||||||
<div id="error-toast" hx-swap-oob="outerHTML"
|
|
||||||
class="fixed z-20 border border-red-500 rounded-sm right-0 top-0 m-4 p-4 bg-red-100 shadow shadow-red-200 transition-all duration-300 text-sm md:w-1/3">
|
|
||||||
<div class="flex items-center gap-x-2 pb-1">
|
|
||||||
@errorIcon()
|
|
||||||
<h1 class="text-red-500 font-semibold">Error</h1>
|
|
||||||
@closeButton()
|
|
||||||
</div>
|
|
||||||
<div class="ml-8">
|
|
||||||
<p class="text-red-500 text-sm">
|
|
||||||
{ message }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// If this is called from an NON-HTMX request, it will display (and functionality seems to work)
|
|
||||||
// but it will be loaded at an unknown position in the DOM. It will not swap with the proper
|
|
||||||
// element.
|
|
||||||
func RenderErrorBanner(ctx *gin.Context, message string) {
|
|
||||||
ctx.Writer.Header().Set("Content-Type", "text/html")
|
|
||||||
errorBanner(message).Render(ctx.Request.Context(), ctx.Writer)
|
|
||||||
}
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
|
||||||
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, "<svg class=\"h-6 text-red-500\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12 16.99V17M12 7V14M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path></svg>")
|
|
||||||
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, "<button onclick=\"hideError();\" class=\"text-red-500 ml-auto hover:bg-red-200 p-1 rounded-sm transition-all duration-300 cursor-pointer\"><svg class=\"h-4\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M19.207 6.207a1 1 0 0 0-1.414-1.414L12 10.586 6.207 4.793a1 1 0 0 0-1.414 1.414L10.586 12l-5.793 5.793a1 1 0 1 0 1.414 1.414L12 13.414l5.793 5.793a1 1 0 0 0 1.414-1.414L13.414 12l5.793-5.793z\" fill=\"currentColor\"></path></svg></button>")
|
|
||||||
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, "<div id=\"error-toast\" hx-swap-oob=\"outerHTML\" class=\"fixed z-20 border border-red-500 rounded-sm right-0 top-0 m-4 p-4 bg-red-100 shadow shadow-red-200 transition-all duration-300 text-sm md:w-1/3\"><div class=\"flex items-center gap-x-2 pb-1\">")
|
|
||||||
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, "<h1 class=\"text-red-500 font-semibold\">Error</h1>")
|
|
||||||
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, "</div><div class=\"ml-8\"><p class=\"text-red-500 text-sm\">")
|
|
||||||
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: 34, Col: 15}
|
|
||||||
}
|
|
||||||
_, 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, "</p></div></div>")
|
|
||||||
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.
|
|
||||||
//
|
|
||||||
// If this is called from an NON-HTMX request, it will display (and functionality seems to work)
|
|
||||||
// but it will be loaded at an unknown position in the DOM. It will not swap with the proper
|
|
||||||
// element.
|
|
||||||
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
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package components
|
package components
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package components
|
package components
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -3,24 +3,20 @@ package templates
|
|||||||
// AppLayout is the main application layout, this does not contain any content other than
|
// 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.
|
// meta data, links, scripts and whatever is passed into it as a component.
|
||||||
templ AppLayout(title string, child templ.Component) {
|
templ AppLayout(title string, child templ.Component) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8"/>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta charset="UTF-8" />
|
||||||
<title>{ title }</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="/v1/web/static/css/tailwind.css"/>
|
<title>{ title }</title>
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
<link rel="stylesheet" href="/v1/web/static/css/tailwind.css" />
|
||||||
</head>
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
<body class="bg-gray-100">
|
</head>
|
||||||
<div id="error-toast"></div>
|
|
||||||
@child
|
<body class="bg-gray-100">
|
||||||
<script>
|
@child
|
||||||
function hideError() {
|
</body>
|
||||||
const err = document.getElementById("error-toast");
|
|
||||||
err.classList.add("hidden");
|
</html>
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
@ -38,13 +38,13 @@ func AppLayout(title string, child templ.Component) templ.Component {
|
|||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/layouts/app_layout.templ`, Line: 11, Col: 17}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/layouts/app_layout.templ`, Line: 12, Col: 16}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/v1/web/static/css/tailwind.css\"><script src=\"https://unpkg.com/htmx.org@2.0.4\"></script></head><body class=\"bg-gray-100\"><div id=\"error-toast\"></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/v1/web/static/css/tailwind.css\"><script src=\"https://unpkg.com/htmx.org@2.0.4\"></script></head><body class=\"bg-gray-100\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ func AppLayout(title string, child templ.Component) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<script>\n function hideError() {\n const err = document.getElementById(\"error-toast\");\n err.classList.add(\"hidden\");\n }\n </script></body></html>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</body></html>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.943
|
// templ: version: v0.3.937
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
--color-red-100: oklch(93.6% 0.032 17.717);
|
--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-red-500: oklch(63.7% 0.237 25.331);
|
||||||
--color-green-500: oklch(72.3% 0.219 149.579);
|
--color-green-500: oklch(72.3% 0.219 149.579);
|
||||||
--color-blue-50: oklch(97% 0.014 254.604);
|
--color-blue-50: oklch(97% 0.014 254.604);
|
||||||
@ -236,27 +235,18 @@
|
|||||||
.absolute {
|
.absolute {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
.fixed {
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
.relative {
|
.relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.static {
|
.static {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
.top-0 {
|
|
||||||
top: calc(var(--spacing) * 0);
|
|
||||||
}
|
|
||||||
.top-1\/2 {
|
.top-1\/2 {
|
||||||
top: calc(1/2 * 100%);
|
top: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
.top-\[100\%\] {
|
.top-\[100\%\] {
|
||||||
top: 100%;
|
top: 100%;
|
||||||
}
|
}
|
||||||
.right-0 {
|
|
||||||
right: calc(var(--spacing) * 0);
|
|
||||||
}
|
|
||||||
.left-0 {
|
.left-0 {
|
||||||
left: calc(var(--spacing) * 0);
|
left: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
@ -272,9 +262,6 @@
|
|||||||
.z-20 {
|
.z-20 {
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
.m-4 {
|
|
||||||
margin: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
.mx-2 {
|
.mx-2 {
|
||||||
margin-inline: calc(var(--spacing) * 2);
|
margin-inline: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@ -350,12 +337,6 @@
|
|||||||
.mb-16 {
|
.mb-16 {
|
||||||
margin-bottom: calc(var(--spacing) * 16);
|
margin-bottom: calc(var(--spacing) * 16);
|
||||||
}
|
}
|
||||||
.ml-8 {
|
|
||||||
margin-left: calc(var(--spacing) * 8);
|
|
||||||
}
|
|
||||||
.ml-auto {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@ -713,9 +694,6 @@
|
|||||||
--tw-gradient-to: var(--color-purple-200);
|
--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));
|
--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 {
|
.p-2 {
|
||||||
padding: calc(var(--spacing) * 2);
|
padding: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@ -961,12 +939,6 @@
|
|||||||
--tw-shadow-color: color-mix(in oklab, var(--color-gray-300) var(--tw-shadow-alpha), transparent);
|
--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 {
|
||||||
outline-style: var(--tw-outline-style);
|
outline-style: var(--tw-outline-style);
|
||||||
outline-width: 1px;
|
outline-width: 1px;
|
||||||
@ -1169,13 +1141,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:bg-red-200 {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
background-color: var(--color-red-200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hover\:text-blue-400 {
|
.hover\:text-blue-400 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user