Merge pull request '(DB/FEAT): Began the implementation of the user engagement!' (#18) from feature/engagement into master
Reviewed-on: #18
This commit is contained in:
commit
d191902448
@ -291,18 +291,19 @@ found in **OTHER** section.
|
||||
- [x] GoogleRefreshToken () text
|
||||
- [x] Created (Required) date/time stamp
|
||||
|
||||
- [ ] Engagements: Represents a single engagement from a single user.
|
||||
- [ ] ID (PK) Serial
|
||||
- [ ] Message () text (Used to store any relevant notes, if needed)
|
||||
- [ ] Entity (Serial) Serial (Used to relate an entity, if needed)
|
||||
- [ ] UserId (FK: User.Id, Required) Serial
|
||||
- [ ] Created (Required) date/time stamp
|
||||
- [x] Engagements: Represents a single engagement from a single user.
|
||||
- [x] ID (PK) Serial
|
||||
- [x] Type (Required) E_Engagement
|
||||
- [x] Message () text (Used to store any relevant notes, if needed)
|
||||
- [x] Entity (Serial) Serial (Used to relate an entity, if needed)
|
||||
- [x] UserId (FK: User.Id) Serial, optional for not logged in users
|
||||
- [x] Created (Required) date/time stamp
|
||||
|
||||
- [ ] Likes: **Many-to-many** table to represent a list of recipes liked by a user.
|
||||
- [ ] ID (PK) *Composite key***
|
||||
- [ ] UserId (FK: User.Id, Required) Serial
|
||||
- [ ] RecipeId (FK: Recipe.Id, Required) Serial
|
||||
- [ ] Created (Required) date/time stamp
|
||||
- [x] Favorites: **Many-to-many** table to represent a list of recipes favorites by a user.
|
||||
- [x] ID (PK) *Composite key***
|
||||
- [x] UserId (FK: User.Id, Required) Serial
|
||||
- [x] RecipeId (FK: Recipe.Id, Required) Serial
|
||||
- [x] Created (Required) date/time stamp
|
||||
|
||||
- [x] Tags: Represents a single tag that can be had by many recipes.
|
||||
- [x] ID (PK) Serial
|
||||
@ -368,10 +369,10 @@ Various tables will reference these types.
|
||||
- [ ] like: string
|
||||
- [ ] system: string
|
||||
|
||||
- [ ] E_Engagement: Type to represent a type of user engagement.
|
||||
- [ ] made: string
|
||||
- [ ] liked: string
|
||||
- [ ] viewed: string
|
||||
- [ ] shared: string
|
||||
- [ ] reviewed: string
|
||||
- [ ] rated: string
|
||||
- [x] E_Engagement: Type to represent a type of user engagement.
|
||||
- [x] made: string
|
||||
- [x] liked: string
|
||||
- [x] viewed: string
|
||||
- [x] shared: string
|
||||
- [x] reviewed: string
|
||||
- [x] rated: string
|
||||
|
||||
114
internal/app/handlers/engagement_handler.go
Normal file
114
internal/app/handlers/engagement_handler.go
Normal file
@ -0,0 +1,114 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
)
|
||||
|
||||
func EngagementViewRecipe(ctx *gin.Context) {
|
||||
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
recipeId, _ := strconv.Atoi(ctx.Param("id"))
|
||||
|
||||
if !domain.IsLoggedIn(ctx) {
|
||||
if _, err := deps.EngagementService.ViewRecipe(recipeId); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
userId := ctx.MustGet("userId").(int)
|
||||
|
||||
if _, err := deps.EngagementService.UserViewRecipe(userId, recipeId); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": http.StatusInternalServerError,
|
||||
"message": err.Error(),
|
||||
})
|
||||
} else {
|
||||
ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId))
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func EngagementShareRecipe(ctx *gin.Context) {
|
||||
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
recipeId, _ := strconv.Atoi(ctx.Param("id"))
|
||||
|
||||
if !domain.IsLoggedIn(ctx) {
|
||||
if _, err := deps.EngagementService.ShareRecipe(recipeId); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": http.StatusInternalServerError,
|
||||
"message": err.Error(),
|
||||
})
|
||||
} else {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
userId := ctx.MustGet("userId").(int)
|
||||
|
||||
if _, err := deps.EngagementService.UserShareRecipe(userId, recipeId); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": http.StatusInternalServerError,
|
||||
"message": err.Error(),
|
||||
})
|
||||
} else {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func EngagementFavoriteRecipe(ctx *gin.Context) {
|
||||
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||
|
||||
if !domain.IsLoggedIn(ctx) {
|
||||
ctx.Header("HX-Redirect", domain.WEB_LOGIN)
|
||||
ctx.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
id := ctx.Param("id")
|
||||
recipeId, _ := strconv.Atoi(id)
|
||||
userId := ctx.MustGet("userId").(int)
|
||||
|
||||
if _, err := deps.EngagementService.UserFavoriteRecipe(userId, 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)
|
||||
|
||||
if !domain.IsLoggedIn(ctx) {
|
||||
ctx.Header("HX-Redirect", domain.WEB_LOGIN)
|
||||
ctx.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
id := ctx.Param("id")
|
||||
recipeId, _ := strconv.Atoi(id)
|
||||
userId := ctx.MustGet("userId").(int)
|
||||
|
||||
if _, err := deps.EngagementService.UserMakeRecipe(userId, recipeId); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": http.StatusInternalServerError,
|
||||
"message": err.Error(),
|
||||
})
|
||||
} else {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
@ -60,7 +60,6 @@ func ProfilePage(ctx *gin.Context) {
|
||||
user := deps.UserService.GetAuthenicatedUser(ctx)
|
||||
recipes, err := deps.RecipeService.GetUserRecipes(user.Id)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting recipes. %s\n", err.Error())
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": http.StatusInternalServerError,
|
||||
"message": fmt.Sprintf("Error getting recipes. %s\n", err.Error()),
|
||||
@ -68,8 +67,18 @@ func ProfilePage(ctx *gin.Context) {
|
||||
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)
|
||||
page := pages.ProfilePage(user, recipes, engagements)
|
||||
|
||||
ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
||||
}
|
||||
@ -95,15 +104,23 @@ func RecipePage(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get signed in user, if they exist
|
||||
var userId *int = nil
|
||||
var loggedIn = domainServer.IsLoggedIn(ctx)
|
||||
if loggedIn {
|
||||
storeId := ctx.MustGet("userId").(int)
|
||||
userId = &storeId
|
||||
}
|
||||
|
||||
// Get recipe
|
||||
recipe, err := deps.RecipeService.GetRecipe(parsed)
|
||||
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
|
||||
// Get user (owner)
|
||||
user, err := deps.UserService.GetUser(recipe.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: %s\n", err.Error())
|
||||
@ -111,8 +128,24 @@ func RecipePage(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Add engagement
|
||||
// BUG: Don't want to do this here
|
||||
// if loggedIn {
|
||||
// if _, err = deps.EngagementService.UserViewRecipe(*userId, recipe.Id); err != nil {
|
||||
// fmt.Printf("ERROR: %s\n", err.Error())
|
||||
// ctx.JSON(400, err.Error())
|
||||
// return
|
||||
// }
|
||||
// } else {
|
||||
// if _, err = deps.EngagementService.ViewRecipe(recipe.Id); err != nil {
|
||||
// fmt.Printf("ERROR: %s\n", err.Error())
|
||||
// ctx.JSON(400, err.Error())
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
title := "Potion - View Recipe"
|
||||
page := pages.RecipePage(*recipe, *user)
|
||||
page := pages.RecipePage(*recipe, *user, loggedIn)
|
||||
|
||||
ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
|
||||
}
|
||||
|
||||
@ -115,14 +115,17 @@ func (s *Server) Setup() *Server {
|
||||
// Initialize and inject dependencies
|
||||
userRepo := repository.NewUserRepository(s.DB)
|
||||
recipeRepo := repository.NewRecipeRepository(s.DB)
|
||||
engagementRepo := repository.NewEngagementRepository(s.DB)
|
||||
userService := service.NewUserService(userRepo)
|
||||
authService := service.NewAuthService(userRepo, jwtSecret)
|
||||
recipeService := service.NewRecipeService(recipeRepo)
|
||||
engagementService := service.NewEngagementService(engagementRepo, recipeRepo)
|
||||
|
||||
deps := &domain.InjectedDependencies{
|
||||
UserService: userService,
|
||||
AuthService: authService,
|
||||
RecipeService: recipeService,
|
||||
EngagementService: engagementService,
|
||||
}
|
||||
|
||||
// Apply middleware
|
||||
@ -183,6 +186,12 @@ func (s *Server) Setup() *Server {
|
||||
router_api.POST("/recipe/search", handlers.SearchRecipes)
|
||||
router_api.GET("/user/recipes", handlers.GetUserRecipes)
|
||||
|
||||
// Engagement endpoints
|
||||
router_api.POST("/engagement/view/:id", handlers.EngagementViewRecipe)
|
||||
router_api.POST("/engagement/share/:id", handlers.EngagementShareRecipe)
|
||||
router_api.POST("/engagement/favorite/:id", handlers.EngagementFavoriteRecipe)
|
||||
router_api.POST("/engagement/make/:id", handlers.EngagementMakeRecipe)
|
||||
|
||||
// Catch un-routed URLS
|
||||
s.Router.NoRoute(func(ctx *gin.Context) {
|
||||
path := ctx.Request.URL.Path
|
||||
|
||||
129
internal/app/service/engagement_service.go
Normal file
129
internal/app/service/engagement_service.go
Normal file
@ -0,0 +1,129 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
||||
domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type EngagementService struct {
|
||||
engagementRepository domain.EngagementRepository
|
||||
recipeRepository domainRecipe.RecipeRepository
|
||||
}
|
||||
|
||||
// Compile-time check to ensure the EngagementService implements domain.EngagementService
|
||||
var _ domain.EngagementService = (*EngagementService)(nil)
|
||||
|
||||
// NewUserRepository creates a user repository object which is used by the user service to access
|
||||
// the database. Any user related database operations will take place in this repository.
|
||||
func NewEngagementService(engagementRepository domain.EngagementRepository, recipeRepository domainRecipe.RecipeRepository) domain.EngagementService {
|
||||
return &EngagementService{
|
||||
engagementRepository: engagementRepository,
|
||||
recipeRepository: recipeRepository,
|
||||
}
|
||||
}
|
||||
|
||||
// ViewRecipe requires a user ID and a recipe ID to create an engagement record in the database.
|
||||
// A message will be generated using the recipe data and then used to add a view engagement to the
|
||||
// database.
|
||||
func (s *EngagementService) ViewRecipe(recipeId int) (domain.Engagement, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId, nil)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Viewed \"%s\"", recipe.Title)
|
||||
|
||||
return s.engagementRepository.AddEntityEngagement(recipeId, message, domain.EngagementViewed)
|
||||
}
|
||||
|
||||
// ShareRecipe requires a user ID and a recipe ID to create an engagement record in the database.
|
||||
// A message will be generated using the recipe data and then used to add a view engagement to the
|
||||
// database.
|
||||
func (s *EngagementService) ShareRecipe(recipeId int) (domain.Engagement, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId, nil)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Shared \"%s\"", recipe.Title)
|
||||
|
||||
return s.engagementRepository.AddEntityEngagement(recipeId, message, domain.EngagementShared)
|
||||
}
|
||||
|
||||
// UserViewRecipe requires a user ID and a recipe ID to create an engagement record in the database.
|
||||
// A message will be generated using the recipe data and then used to add a view engagement to the
|
||||
// database.
|
||||
func (s *EngagementService) UserViewRecipe(userId, recipeId int) (domain.Engagement, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Viewed \"%s\"", recipe.Title)
|
||||
|
||||
return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementViewed)
|
||||
}
|
||||
|
||||
// UserFavoriteRecipe requires a user ID and a recipe ID to create an engagement record in the database.
|
||||
// A message will be generated using the recipe data and then used to add a like engagement to the
|
||||
// database.
|
||||
func (s *EngagementService) UserFavoriteRecipe(userId, recipeId int) (domain.Engagement, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
// Update the favorites DB
|
||||
liked, err := s.engagementRepository.UserFavoriteRecipeToggle(userId, recipeId)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
// Determine if this like is a saving or unsaving
|
||||
var message string
|
||||
if liked {
|
||||
message = fmt.Sprintf("Saved \"%s\"", recipe.Title)
|
||||
} else {
|
||||
message = fmt.Sprintf("Unsaved \"%s\"", recipe.Title)
|
||||
}
|
||||
|
||||
return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementLiked)
|
||||
|
||||
}
|
||||
|
||||
// UserMakeRecipe requires a user ID and a recipe ID to create an engagement record in the database.
|
||||
// A message will be generated using the recipe data and then used to add a make engagement to the
|
||||
// database.
|
||||
func (s *EngagementService) UserMakeRecipe(userId, recipeId int) (domain.Engagement, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Made \"%s\"", recipe.Title)
|
||||
|
||||
return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementMade)
|
||||
}
|
||||
|
||||
// UserShareRecipe requires a user ID and a recipe ID to create an engagement record in the database.
|
||||
// A message will be generated using the recipe data and then used to add a make engagement to the
|
||||
// database.
|
||||
func (s *EngagementService) UserShareRecipe(userId, recipeId int) (domain.Engagement, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Shared \"%s\"", recipe.Title)
|
||||
|
||||
return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementShared)
|
||||
}
|
||||
|
||||
// GetUserEngagement returns a list of the users most recent engagement entries. The number of records
|
||||
// is determined by the limit passed into this function. The results are sorted, newest-to-oldest.
|
||||
func (s *EngagementService) GetUserEngagement(userId, limit int) ([]domain.Engagement, error) {
|
||||
return s.engagementRepository.GetUserEngagement(userId, limit)
|
||||
}
|
||||
@ -33,7 +33,7 @@ func NewRecipeService(recipeRepository domain.RecipeRepository) domain.RecipeSer
|
||||
// occur.
|
||||
//
|
||||
// TODO: Implement validation in the API.
|
||||
// TODO: Implement image creation and tag creation.
|
||||
// TODO: Implement image creation.
|
||||
func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
|
||||
// Ensure user is logged in
|
||||
if !domainServer.IsLoggedIn(ctx) {
|
||||
@ -110,7 +110,7 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
|
||||
if image != nil {
|
||||
}
|
||||
|
||||
// TODO: Create the tags in the database
|
||||
// Create the tags
|
||||
if len(tags) > 0 {
|
||||
if err := s.recipeRepository.CreateRecipeTags(recipe, tags); err != nil {
|
||||
return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error())
|
||||
@ -123,8 +123,12 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
|
||||
// GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore,
|
||||
// if the recipe is nil, an error will be returned, so the caller does not need to check for a nil
|
||||
// recipe (e.g., if the error is nil the recipe exists)
|
||||
func (s *RecipeService) GetRecipe(id int) (*domain.Recipe, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(id)
|
||||
//
|
||||
// A userId should be provided to allow the favorite status to be updated. Without a userId (nil),
|
||||
// the favorite status will return false, not because its not a favorite, but because it cannot find
|
||||
// out!
|
||||
func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(id, userId)
|
||||
|
||||
if recipe == nil {
|
||||
return nil, fmt.Errorf("Failed to get recipe from database. Nil result.")
|
||||
|
||||
28
internal/domain/engagement/engagement.go
Normal file
28
internal/domain/engagement/engagement.go
Normal file
@ -0,0 +1,28 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// EngagementType is the database enum E_ENGAGEMENT which defines the type of a user engagement
|
||||
// of a recipe. Postgres enums are case sensitive so these must match the values in the database
|
||||
// exactly.
|
||||
type EngagementType string
|
||||
|
||||
const (
|
||||
EngagementMade EngagementType = "made"
|
||||
EngagementLiked EngagementType = "liked"
|
||||
EngagementViewed EngagementType = "viewed"
|
||||
EngagementShared EngagementType = "shared"
|
||||
EngagementReviewed EngagementType = "reviewed"
|
||||
EngagementRated EngagementType = "rated"
|
||||
)
|
||||
|
||||
// Engagement is the database model of a user engagement. There is no need to map to a different
|
||||
// model so this will remain in the domain.
|
||||
type Engagement struct {
|
||||
Id int
|
||||
Type EngagementType
|
||||
Message string
|
||||
Entity int
|
||||
UserId int
|
||||
Created time.Time
|
||||
}
|
||||
10
internal/domain/engagement/repository.go
Normal file
10
internal/domain/engagement/repository.go
Normal file
@ -0,0 +1,10 @@
|
||||
package domain
|
||||
|
||||
type EngagementRepository interface {
|
||||
AddUserEngagement(userId int, message string, engagementType EngagementType) (Engagement, error)
|
||||
AddUserEntityEngagement(userId, entityId int, message string, engagementType EngagementType) (Engagement, error)
|
||||
AddEngagement(message string, engagementType EngagementType) (Engagement, error)
|
||||
AddEntityEngagement(entityId int, message string, engagementType EngagementType) (Engagement, error)
|
||||
GetUserEngagement(userId, limit int) ([]Engagement, error)
|
||||
UserFavoriteRecipeToggle(userId, recipeId int) (bool, error)
|
||||
}
|
||||
11
internal/domain/engagement/service.go
Normal file
11
internal/domain/engagement/service.go
Normal file
@ -0,0 +1,11 @@
|
||||
package domain
|
||||
|
||||
type EngagementService interface {
|
||||
ViewRecipe(recipeId int) (Engagement, error)
|
||||
ShareRecipe(recipeId int) (Engagement, error)
|
||||
UserViewRecipe(userId, recipeId int) (Engagement, error)
|
||||
UserFavoriteRecipe(userId, recipeId int) (Engagement, error)
|
||||
UserMakeRecipe(userId, recipeId int) (Engagement, error)
|
||||
UserShareRecipe(userId, recipeId int) (Engagement, error)
|
||||
GetUserEngagement(userId, limit int) ([]Engagement, error)
|
||||
}
|
||||
@ -55,7 +55,8 @@ type RecipeIngredient struct {
|
||||
|
||||
// Recipe is the database model of a recipe. There is no need to map to a different model so
|
||||
// this will remain in the domain. The Tags field should be loaded from the external Tags table,
|
||||
// but is still attached to this domain object.
|
||||
// but is still attached to this domain object. The Favorite field should also be loaded from
|
||||
// the external favorites table, these are user specific.
|
||||
type Recipe struct {
|
||||
Id int
|
||||
Title string
|
||||
@ -70,6 +71,7 @@ type Recipe struct {
|
||||
Modified *time.Time // Pointer to allow null
|
||||
Created time.Time
|
||||
Tags []Tag
|
||||
Favorite bool // Per requesting user
|
||||
}
|
||||
|
||||
// SearchFilters is a model which represents the required filters to complete a recipe search.
|
||||
|
||||
@ -2,9 +2,10 @@ package domain
|
||||
|
||||
type RecipeRepository interface {
|
||||
CreateRecipe(recipe *Recipe) error
|
||||
GetRecipe(id int) (*Recipe, error)
|
||||
GetRecipe(id int, userId *int) (*Recipe, error)
|
||||
SearchRecipes(filters SearchFilters) ([]Recipe, error)
|
||||
CreateRecipeTags(recipe Recipe, tags []string) error
|
||||
GetUserRecipes(id int) ([]Recipe, error)
|
||||
GetRecipeTags(recipe *Recipe) error
|
||||
GetRecipeFavorite(recipe *Recipe, userId int) error
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import "github.com/gin-gonic/gin"
|
||||
|
||||
type RecipeService interface {
|
||||
CreateRecipe(ctx *gin.Context) (*Recipe, error)
|
||||
GetRecipe(id int) (*Recipe, error)
|
||||
GetRecipe(id int, userId *int) (*Recipe, error)
|
||||
SearchRecipes(filters SearchFilters) ([]Recipe, error)
|
||||
GetUserRecipes(id int) ([]Recipe, error)
|
||||
}
|
||||
|
||||
@ -25,6 +25,11 @@ const API_AUTH_LOGOUT = VERSION + API + "/auth/logout"
|
||||
const API_CREATE_RECIPE = VERSION + API + "/recipe"
|
||||
const API_SEARCH_RECIPES = VERSION + API + "/recipe/search"
|
||||
|
||||
const API_ENGAGEMENT_VIEW = VERSION + API + "/engagement/view/%d"
|
||||
const API_ENGAGEMENT_SHARE = VERSION + API + "/engagement/share/%d"
|
||||
const API_ENGAGEMENT_FAVORITE = VERSION + API + "/engagement/favorite/%d"
|
||||
const API_ENGAGEMENT_MAKE = VERSION + API + "/engagement/make/%d"
|
||||
|
||||
// State prefixed routes
|
||||
const STATE_TAGS_CREATE = VERSION + WEB + STATE + "/tags"
|
||||
const STATE_TAGS_DELETE = VERSION + WEB + STATE + "/tags/delete"
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
|
||||
domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
||||
domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||
domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||
)
|
||||
@ -14,6 +15,7 @@ type InjectedDependencies struct {
|
||||
UserService domainUser.UserService
|
||||
AuthService domainAuth.AuthService
|
||||
RecipeService domainRecipe.RecipeService
|
||||
EngagementService domainEngagement.EngagementService
|
||||
}
|
||||
|
||||
// JwtClaims is the data stored in the JSON web token. All that is needed is the users ID and their
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com)
|
||||
-- Desc: Create the E_ENGAGEMENT enum.
|
||||
-- Date: 07/13/2025
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TYPE E_ENGAGEMENT AS ENUM(
|
||||
'made',
|
||||
'liked', -- this is the same as saved/favorited
|
||||
'viewed',
|
||||
'shared',
|
||||
'reviewed',
|
||||
'rated'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@ -0,0 +1,16 @@
|
||||
-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com)
|
||||
-- Desc: Create the user engagement table.
|
||||
-- Date: 07/13/2025
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Engagements (
|
||||
Id SERIAL PRIMARY KEY NOT NULL,
|
||||
Type E_ENGAGEMENT NOT NULL,
|
||||
Message TEXT,
|
||||
Entity INT, -- Used to map to other DB objects, recipes, users, etc...
|
||||
UserId INTEGER REFERENCES users(id), -- Can be null, when users aren't logged in
|
||||
Created TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@ -0,0 +1,14 @@
|
||||
-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com)
|
||||
-- Desc: Create the favorites table.
|
||||
-- Date: 07/14/2025
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Favorites (
|
||||
Id SERIAL PRIMARY KEY NOT NULL,
|
||||
UserId INTEGER NOT NULL REFERENCES users(id),
|
||||
RecipeId INTEGER NOT NULL REFERENCES recipes(id),
|
||||
Created TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@ -0,0 +1,355 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type EngagementRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Compile-time check to ensure the EngagementRepository implements domain.EngagementRepository
|
||||
var _ domain.EngagementRepository = (*EngagementRepository)(nil)
|
||||
|
||||
// NewUserRepository creates a user repository object which is used by the user service to access
|
||||
// the database. Any user related database operations will take place in this repository.
|
||||
func NewEngagementRepository(db *sql.DB) domain.EngagementRepository {
|
||||
return &EngagementRepository{db: db}
|
||||
}
|
||||
|
||||
// AddUserEngagement creates an engagement record in the database with the user ID provided. This
|
||||
// function does not accept an entity ID as it should be used when there is no need to reference
|
||||
// an entity. The message should be provided, but a blank string ("") is acceptable. The engagement
|
||||
// type parameter determines the labeling of the engagement in the database. Any errors will be
|
||||
// bubbled to the caller.
|
||||
func (r *EngagementRepository) AddUserEngagement(userId int, message string, engagementType domain.EngagementType) (domain.Engagement, error) {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO Engagements (
|
||||
type, message, entity, userid, created
|
||||
) VALUES (
|
||||
$1, $2, NULL, $3, $4
|
||||
) RETURNING *;
|
||||
`
|
||||
|
||||
var engagement domain.Engagement
|
||||
var engUserId sql.NullInt32
|
||||
if err := tx.QueryRow(query, engagementType, message, userId, time.Now()).Scan(
|
||||
&engagement.Id,
|
||||
&engagement.Type,
|
||||
&engagement.Message,
|
||||
&engagement.Entity,
|
||||
&engUserId,
|
||||
&engagement.Created,
|
||||
); err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, fmt.Errorf("Failed to insert engagement into database. %s", err.Error())
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
// Is user is valid
|
||||
if engUserId.Valid {
|
||||
engagement.UserId = int(engUserId.Int32)
|
||||
}
|
||||
|
||||
return engagement, nil
|
||||
}
|
||||
|
||||
// AddUserEntityEngagement creates an engagement record in the database with the user ID provided. This
|
||||
// function requires an entity ID as it should be used when there is a reference to external an
|
||||
// entity. The message should be provided, but a blank string ("") is acceptable. The engagement
|
||||
// type parameter determines the labeling of the engagement in the database. Any errors will be
|
||||
// bubbled to the caller.
|
||||
//
|
||||
// TODO: Disallow users to "make" the same recipe more than once a day
|
||||
func (r *EngagementRepository) AddUserEntityEngagement(userId, entityId int, message string, engagementType domain.EngagementType) (domain.Engagement, error) {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO Engagements (
|
||||
type, message, entity, userid, created
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5
|
||||
) RETURNING *;
|
||||
`
|
||||
|
||||
var engagement domain.Engagement
|
||||
var engUserId sql.NullInt32
|
||||
if err := tx.QueryRow(query, engagementType, message, entityId, userId, time.Now()).Scan(
|
||||
&engagement.Id,
|
||||
&engagement.Type,
|
||||
&engagement.Message,
|
||||
&engagement.Entity,
|
||||
&engUserId,
|
||||
&engagement.Created,
|
||||
); err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, fmt.Errorf("Failed to insert engagement into database. %s", err.Error())
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
// Is user is valid
|
||||
if engUserId.Valid {
|
||||
engagement.UserId = int(engUserId.Int32)
|
||||
}
|
||||
|
||||
return engagement, nil
|
||||
}
|
||||
|
||||
// AddEngagement creates an engagement record in the database without any user. This function does
|
||||
// not accept an entity ID as it should be used when there is no need to reference an entity or user.
|
||||
// The message should be provided, but a blank string ("") is acceptable. The engagement type
|
||||
// parameter determines the labeling of the engagement in the database. Any errors will be bubbled
|
||||
// to the caller.
|
||||
//
|
||||
// List of allowed engagements: viewed, shared
|
||||
func (r *EngagementRepository) AddEngagement(message string, engagementType domain.EngagementType) (domain.Engagement, error) {
|
||||
// Prevent invalid engagement types
|
||||
switch engagementType {
|
||||
case domain.EngagementViewed:
|
||||
case domain.EngagementShared:
|
||||
break
|
||||
case domain.EngagementMade:
|
||||
case domain.EngagementLiked:
|
||||
case domain.EngagementReviewed:
|
||||
case domain.EngagementRated:
|
||||
return domain.Engagement{}, fmt.Errorf("Attempting to use disallowed anonymous engagement type.")
|
||||
}
|
||||
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO Engagements (
|
||||
type, message, entity, userid, created
|
||||
) VALUES (
|
||||
$1, $2, NULL, NULL, $3
|
||||
) RETURNING *;
|
||||
`
|
||||
|
||||
var engagement domain.Engagement
|
||||
var userId sql.NullInt32
|
||||
if err := tx.QueryRow(query, engagementType, message, time.Now()).Scan(
|
||||
&engagement.Id,
|
||||
&engagement.Type,
|
||||
&engagement.Message,
|
||||
&engagement.Entity,
|
||||
&userId,
|
||||
&engagement.Created,
|
||||
); err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, fmt.Errorf("Failed to insert engagement into database. %s", err.Error())
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
// Is user is valid
|
||||
if userId.Valid {
|
||||
engagement.UserId = int(userId.Int32)
|
||||
}
|
||||
|
||||
return engagement, nil
|
||||
}
|
||||
|
||||
// AddEntityEngagement creates an engagement record in the database without any user. This function
|
||||
// requires an entity ID as it should be used when there is a reference to external an entity.
|
||||
// The message should be provided, but a blank string ("") is acceptable. The engagement type
|
||||
// parameter determines the labeling of the engagement in the database. Any errors will be
|
||||
// bubbled to the caller.
|
||||
//
|
||||
// List of allowed engagements: viewed, shared
|
||||
func (r *EngagementRepository) AddEntityEngagement(entityId int, message string, engagementType domain.EngagementType) (domain.Engagement, error) {
|
||||
// Prevent invalid engagement types
|
||||
switch engagementType {
|
||||
case domain.EngagementViewed:
|
||||
case domain.EngagementShared:
|
||||
break
|
||||
case domain.EngagementMade:
|
||||
case domain.EngagementLiked:
|
||||
case domain.EngagementReviewed:
|
||||
case domain.EngagementRated:
|
||||
return domain.Engagement{}, fmt.Errorf("Attempting to use disallowed anonymous engagement type.")
|
||||
}
|
||||
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO Engagements (
|
||||
type, message, entity, userid, created
|
||||
) VALUES (
|
||||
$1, $2, $3, NULL, $4
|
||||
) RETURNING *;
|
||||
`
|
||||
|
||||
var engagement domain.Engagement
|
||||
var userId sql.NullInt32
|
||||
if err := tx.QueryRow(query, engagementType, message, entityId, time.Now()).Scan(
|
||||
&engagement.Id,
|
||||
&engagement.Type,
|
||||
&engagement.Message,
|
||||
&engagement.Entity,
|
||||
&userId,
|
||||
&engagement.Created,
|
||||
); err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, fmt.Errorf("Failed to insert engagement into database. %s", err.Error())
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
// Is user is valid
|
||||
if userId.Valid {
|
||||
engagement.UserId = int(userId.Int32)
|
||||
}
|
||||
|
||||
return engagement, nil
|
||||
}
|
||||
|
||||
// GetUserEngagement returns a list of the users most recent engagement entries. The number of records
|
||||
// is determined by the limit passed into this function. The results are sorted, newest-to-oldest.
|
||||
func (r *EngagementRepository) GetUserEngagement(userId, limit int) ([]domain.Engagement, error) {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return []domain.Engagement{}, err
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT * FROM Engagements
|
||||
WHERE Userid = $1
|
||||
ORDER BY created DESC LIMIT $2;
|
||||
`
|
||||
|
||||
rows, err := tx.Query(query, userId, limit)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return []domain.Engagement{}, fmt.Errorf("Failed to get user engagements. %s", err.Error())
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var engagements []domain.Engagement
|
||||
for rows.Next() {
|
||||
var engagement domain.Engagement
|
||||
var engUserId sql.NullInt32
|
||||
if err := rows.Scan(
|
||||
&engagement.Id,
|
||||
&engagement.Type,
|
||||
&engagement.Message,
|
||||
&engagement.Entity,
|
||||
&engUserId,
|
||||
&engagement.Created,
|
||||
); err != nil {
|
||||
tx.Rollback()
|
||||
return []domain.Engagement{}, fmt.Errorf("Failed to scan user engagement. %s", err.Error())
|
||||
}
|
||||
|
||||
// Add user if valid
|
||||
if engUserId.Valid {
|
||||
engagement.UserId = int(engUserId.Int32)
|
||||
}
|
||||
|
||||
engagements = append(engagements, engagement)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
return []domain.Engagement{}, err
|
||||
}
|
||||
|
||||
return engagements, err
|
||||
}
|
||||
|
||||
// UserFavoriteRecipeToggle toggles the status of a users favorite of a recipe. If the user has already
|
||||
// favorited the provided recipe, the database entry will be delete, hence removing the favorite. Otherwise,
|
||||
// an entry will be created. The NEW status of the users favorite will be returned as the boolean. Any
|
||||
// errors will be bubbled to the caller.
|
||||
func (r *EngagementRepository) UserFavoriteRecipeToggle(userId, recipeId int) (bool, error) {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id
|
||||
FROM favorites
|
||||
WHERE userid = $1 AND recipeid = $2
|
||||
`
|
||||
|
||||
var id int
|
||||
|
||||
err = tx.QueryRow(query, userId, recipeId).Scan(&id)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
tx.Rollback()
|
||||
return false, fmt.Errorf("Failed to get recipe favorite. %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Means we should create
|
||||
var success bool
|
||||
if id == 0 {
|
||||
createQuery := "INSERT INTO favorites (userid, recipeid, created) VALUES ($1, $2, $3);"
|
||||
|
||||
if result, err := tx.Exec(createQuery, userId, recipeId, time.Now()); err != nil {
|
||||
tx.Rollback()
|
||||
return false, fmt.Errorf("Failed to create recipe favorite. %s", err.Error())
|
||||
} else {
|
||||
rows, _ := result.RowsAffected()
|
||||
success = rows == 1
|
||||
}
|
||||
} else {
|
||||
deleteQuery := "DELETE FROM favorites WHERE id = $1;"
|
||||
|
||||
if result, err := tx.Exec(deleteQuery, id); err != nil {
|
||||
tx.Rollback()
|
||||
return false, fmt.Errorf("Failed to remove recipe favorite. %s", err.Error())
|
||||
} else {
|
||||
rows, _ := result.RowsAffected()
|
||||
success = rows == 1
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
return success, nil
|
||||
}
|
||||
@ -92,7 +92,7 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
||||
// GetRecipe gets a recipe from the database via its ID. The operation is wrapped in a transaction
|
||||
// for added safety. The repository will not check for a nil result, instead the service will. Callers
|
||||
// are responsible for protecting against double nil results. Any errors will be bubbled to the caller.
|
||||
func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) {
|
||||
func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
@ -153,7 +153,18 @@ func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) {
|
||||
}
|
||||
|
||||
// Add tags
|
||||
r.GetRecipeTags(&recipe)
|
||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
||||
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
||||
}
|
||||
|
||||
// Get favorite status, if user id is provided
|
||||
if userId != nil {
|
||||
if err := r.GetRecipeFavorite(&recipe, *userId); err != nil {
|
||||
fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error())
|
||||
}
|
||||
} else {
|
||||
recipe.Favorite = false
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
@ -240,8 +251,6 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Title search somehow...
|
||||
|
||||
// Merge condition strings
|
||||
mealString := fmt.Sprintf("(%s)", strings.Join(mealConditions, " OR "))
|
||||
timeString := fmt.Sprintf("(%s)", strings.Join(timeConditions, " OR "))
|
||||
@ -368,7 +377,9 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain
|
||||
}
|
||||
|
||||
// Add tags
|
||||
r.GetRecipeTags(&recipe)
|
||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
||||
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
||||
}
|
||||
|
||||
recipes = append(recipes, recipe)
|
||||
}
|
||||
@ -517,7 +528,14 @@ func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) {
|
||||
}
|
||||
|
||||
// Add tags
|
||||
r.GetRecipeTags(&recipe)
|
||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
||||
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
||||
}
|
||||
|
||||
// Get favorite status
|
||||
if err := r.GetRecipeFavorite(&recipe, id); err != nil {
|
||||
fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error())
|
||||
}
|
||||
|
||||
recipes = append(recipes, recipe)
|
||||
}
|
||||
@ -571,3 +589,30 @@ func (r *RecipeRepository) GetRecipeTags(recipe *domain.Recipe) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRecipeFavorite requires a recipe to be filled with at least an ID. This function will use the
|
||||
// ID defined in the provided recipe to fill the favorite status of the recipe, based on the provided
|
||||
// userId. The recipe is modified in place and is not returned. Any errors will be bubbled to the caller.
|
||||
func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT COUNT(*)
|
||||
FROM favorites
|
||||
WHERE recipeid = $1 AND userid = $2;
|
||||
`
|
||||
|
||||
var count int
|
||||
if err := tx.QueryRow(query, recipe.Id, userId).Scan(&count); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("Failed to get recipe favorite. %s", err.Error())
|
||||
}
|
||||
|
||||
recipe.Favorite = count > 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -3,13 +3,13 @@ package templates
|
||||
import "github.com/haydenhargreaves/Potion/internal/templates/components"
|
||||
|
||||
templ FavoritesPage() {
|
||||
@components.Navbar("favorites")
|
||||
<div class="w-full h-screen flex justify-center">
|
||||
@components.Navbar("favorites")
|
||||
<div class="w-full h-screen flex justify-center">
|
||||
<div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white">
|
||||
<div class="flex flex-col items-center justify-center h-full gap-y-2">
|
||||
<h1 class="text-4xl text-gray-800 font-semibold">Page Under Construction </h1>
|
||||
<h1 class="text-4xl text-gray-800 font-semibold text-center">Page Under Construction </h1>
|
||||
<p class="text-gray-700">Sit tight, this page is coming soon!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ func FavoritesPage() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full h-screen flex justify-center\"><div class=\"mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white\"><div class=\"flex flex-col items-center justify-center h-full gap-y-2\"><h1 class=\"text-4xl text-gray-800 font-semibold\">Page Under Construction </h1><p class=\"text-gray-700\">Sit tight, this page is coming soon!</p></div></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full h-screen flex justify-center\"><div class=\"mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white\"><div class=\"flex flex-col items-center justify-center h-full gap-y-2\"><h1 class=\"text-4xl text-gray-800 font-semibold text-center\">Page Under Construction </h1><p class=\"text-gray-700\">Sit tight, this page is coming soon!</p></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@ -5,15 +5,13 @@ import "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||
|
||||
templ introSection() {
|
||||
<section class="w-full h-fit mb-16">
|
||||
<section class="w-full h-fit mb-16">
|
||||
<div class="relative">
|
||||
<video class="" autoplay loop muted playsinline>
|
||||
<source src="/v1/web/static/img/salmon_video.mp4" type="video/mp4"/>
|
||||
<source src="/v1/web/static/img/salmon_video.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<h1
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center
|
||||
text-white text-3xl w-4/5 font-bold z-10"
|
||||
>
|
||||
<h1 class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center
|
||||
text-white text-3xl w-4/5 font-bold z-10">
|
||||
Discover Your Next Favorite Meal
|
||||
</h1>
|
||||
</div>
|
||||
@ -23,19 +21,21 @@ templ introSection() {
|
||||
all at your fingertips. Find exactly what you're craving with our powerful search and intuitive filters, or
|
||||
browse our trending dishes for fresh ideas.
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
}
|
||||
|
||||
templ searchSection() {
|
||||
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
|
||||
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
|
||||
@components.BannerText("Craving Something Specific?")
|
||||
<div class="w-full md:w-3/4">
|
||||
@components.SearchBar(domainRecipe.SearchFilters{}, true, false)
|
||||
</div>
|
||||
<div class="hidden" id="result-list"></div>
|
||||
</section>
|
||||
</section>
|
||||
}
|
||||
|
||||
templ highlightSection(liked bool) {
|
||||
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
|
||||
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
|
||||
@components.BannerText("Recipe of the Week!")
|
||||
<p class="leading-relaxed p-4 my-8">
|
||||
Our 'Recipe of the Week' is the cream of the crop! We handpick it by looking at what recipes
|
||||
@ -47,11 +47,11 @@ templ highlightSection(liked bool) {
|
||||
<div class="flex items-center justify-center w-full">
|
||||
@components.RecipeCardLarge(false)
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
}
|
||||
|
||||
templ listsSection() {
|
||||
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
|
||||
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
|
||||
@components.BannerText("Take Another Look.")
|
||||
<div class="w-full">
|
||||
<h3 class="text-lg mt-8 mx-4">Recently viewed</h3>
|
||||
@ -73,13 +73,12 @@ templ listsSection() {
|
||||
@components.RecipeCardSmall("Classic Butter Chicken", "Dinner - 60 min", "Hayden Hargreaves", false)
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
}
|
||||
|
||||
templ ctaSection() {
|
||||
<section
|
||||
class="w-full flex flex-col items-center justify-center mt-16 py-8 md:py-12 bg-gradient-to-br from-blue-100 to-purple-100 text-center"
|
||||
>
|
||||
<section
|
||||
class="w-full flex flex-col items-center justify-center mt-16 py-8 md:py-12 bg-gradient-to-br from-blue-100 to-purple-100 text-center">
|
||||
<h2 class="text-2xl md:text-3xl font-extrabold text-gray-800 mb-6 px-4">
|
||||
Unleash Your Inner Chef!
|
||||
</h2>
|
||||
@ -87,22 +86,19 @@ templ ctaSection() {
|
||||
Have a unique recipe idea? Want to share your culinary masterpiece with the world?
|
||||
It's time to bring your creations to life!
|
||||
</p>
|
||||
<a
|
||||
href={ domain.WEB_CREATE }
|
||||
class="flex items-center justify-center
|
||||
<a href={ domain.WEB_CREATE } class="flex items-center justify-center
|
||||
bg-gradient-to-r from-blue-400 to-blue-600 text-white
|
||||
px-12 py-5 rounded-full shadow-sm hover:shadow-md
|
||||
transition-all duration-300 ease-in-out shadow-blue-700
|
||||
text-lg md:text-2xl font-bold uppercase tracking-wide"
|
||||
>
|
||||
text-lg md:text-2xl font-bold uppercase tracking-wide">
|
||||
Create Your Recipe!
|
||||
</a>
|
||||
</section>
|
||||
</section>
|
||||
}
|
||||
|
||||
templ HomePage() {
|
||||
@components.Navbar("home")
|
||||
<div class="w-full h-fit flex justify-center">
|
||||
@components.Navbar("home")
|
||||
<div class="w-full h-fit flex justify-center">
|
||||
<div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white">
|
||||
@introSection()
|
||||
@searchSection()
|
||||
@ -110,5 +106,5 @@ templ HomePage() {
|
||||
@listsSection()
|
||||
@ctaSection()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -70,11 +70,15 @@ func searchSection() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"w-full md:w-3/4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.SearchBar(domainRecipe.SearchFilters{}, true, false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"hidden\" id=\"result-list\"></div></section>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div class=\"hidden\" id=\"result-list\"></div></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -103,7 +107,7 @@ func highlightSection(liked bool) templ.Component {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<section class=\"w-full flex flex-col items-center justify-center my-8 py-4\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<section class=\"w-full flex flex-col items-center justify-center my-8 py-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -111,7 +115,7 @@ func highlightSection(liked bool) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"leading-relaxed p-4 my-8\">Our 'Recipe of the Week' is the cream of the crop! We handpick it by looking at what recipes our community loves most. This isn't just about how many people view a recipe; it's also about how many times it's been made, liked, reviewed, and its average rating, all combined to find the true fan favorite of the week. It's our way of highlighting the best recipes that truly resonate with our users!</p><div class=\"flex items-center justify-center w-full\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p class=\"leading-relaxed p-4 my-8\">Our 'Recipe of the Week' is the cream of the crop! We handpick it by looking at what recipes our community loves most. This isn't just about how many people view a recipe; it's also about how many times it's been made, liked, reviewed, and its average rating, all combined to find the true fan favorite of the week. It's our way of highlighting the best recipes that truly resonate with our users!</p><div class=\"flex items-center justify-center w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -119,7 +123,7 @@ func highlightSection(liked bool) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></section>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -148,7 +152,7 @@ func listsSection() templ.Component {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<section class=\"w-full flex flex-col items-center justify-center my-8 py-4\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<section class=\"w-full flex flex-col items-center justify-center my-8 py-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -156,7 +160,7 @@ func listsSection() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"w-full\"><h3 class=\"text-lg mt-8 mx-4\">Recently viewed</h3><div class=\"flex overflow-x-auto gap-x-4 mx-4 my-4\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"w-full\"><h3 class=\"text-lg mt-8 mx-4\">Recently viewed</h3><div class=\"flex overflow-x-auto gap-x-4 mx-4 my-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -184,7 +188,7 @@ func listsSection() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><h3 class=\"text-lg mt-8 mx-4\">Make again</h3><div class=\"flex overflow-x-auto gap-x-4 mx-4 my-4\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div><h3 class=\"text-lg mt-8 mx-4\">Make again</h3><div class=\"flex overflow-x-auto gap-x-4 mx-4 my-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -212,7 +216,7 @@ func listsSection() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></div></section>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -241,7 +245,7 @@ func ctaSection() templ.Component {
|
||||
templ_7745c5c3_Var5 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<section class=\"w-full flex flex-col items-center justify-center mt-16 py-8 md:py-12 bg-gradient-to-br from-blue-100 to-purple-100 text-center\"><h2 class=\"text-2xl md:text-3xl font-extrabold text-gray-800 mb-6 px-4\">Unleash Your Inner Chef!</h2><p class=\"text-md md:text-lg text-gray-700 max-w-2xl mb-10 px-4 leading-relaxed\">Have a unique recipe idea? Want to share your culinary masterpiece with the world? It's time to bring your creations to life!</p><a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<section class=\"w-full flex flex-col items-center justify-center mt-16 py-8 md:py-12 bg-gradient-to-br from-blue-100 to-purple-100 text-center\"><h2 class=\"text-2xl md:text-3xl font-extrabold text-gray-800 mb-6 px-4\">Unleash Your Inner Chef!</h2><p class=\"text-md md:text-lg text-gray-700 max-w-2xl mb-10 px-4 leading-relaxed\">Have a unique recipe idea? Want to share your culinary masterpiece with the world? It's time to bring your creations to life!</p><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -250,7 +254,7 @@ func ctaSection() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" class=\"flex items-center justify-center\n bg-gradient-to-r from-blue-400 to-blue-600 text-white\n px-12 py-5 rounded-full shadow-sm hover:shadow-md\n transition-all duration-300 ease-in-out shadow-blue-700\n text-lg md:text-2xl font-bold uppercase tracking-wide\">Create Your Recipe!</a></section>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" class=\"flex items-center justify-center\n bg-gradient-to-r from-blue-400 to-blue-600 text-white\n px-12 py-5 rounded-full shadow-sm hover:shadow-md\n transition-all duration-300 ease-in-out shadow-blue-700\n text-lg md:text-2xl font-bold uppercase tracking-wide\">Create Your Recipe!</a></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -283,7 +287,7 @@ func HomePage() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"w-full h-fit flex justify-center\"><div class=\"mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"w-full h-fit flex justify-center\"><div class=\"mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -307,7 +311,7 @@ func HomePage() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@ -3,14 +3,13 @@ package templates
|
||||
import "github.com/haydenhargreaves/Potion/internal/templates/components"
|
||||
|
||||
templ ListPage() {
|
||||
@components.Navbar("list")
|
||||
<div class="w-full h-screen flex justify-center">
|
||||
@components.Navbar("list")
|
||||
<div class="w-full h-screen flex justify-center">
|
||||
<div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white">
|
||||
<div class="flex flex-col items-center justify-center h-full gap-y-2">
|
||||
<h1 class="text-4xl text-gray-800 font-semibold">Page Under Construction </h1>
|
||||
<h1 class="text-4xl text-gray-800 font-semibold text-center">Page Under Construction </h1>
|
||||
<p class="text-gray-700">Sit tight, this page is coming soon!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ func ListPage() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full h-screen flex justify-center\"><div class=\"mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white\"><div class=\"flex flex-col items-center justify-center h-full gap-y-2\"><h1 class=\"text-4xl text-gray-800 font-semibold\">Page Under Construction </h1><p class=\"text-gray-700\">Sit tight, this page is coming soon!</p></div></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full h-screen flex justify-center\"><div class=\"mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white\"><div class=\"flex flex-col items-center justify-center h-full gap-y-2\"><h1 class=\"text-4xl text-gray-800 font-semibold text-center\">Page Under Construction </h1><p class=\"text-gray-700\">Sit tight, this page is coming soon!</p></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import "strings"
|
||||
import domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||
import domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||
import domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
||||
|
||||
func displayDifficulty(diff int) string {
|
||||
switch diff {
|
||||
@ -43,9 +44,8 @@ templ userDetailsSection(user domainUser.User, recipeCount int) {
|
||||
} else {
|
||||
<img
|
||||
class="w-24 md:w-32 border-2 border-blue-500 rounded-full shadow-blue-500 shadow select-none"
|
||||
src={ fmt.Sprintf("https://ui-avatars.com/api/?name=%s+%s&size=150", strings.Split(user.Name, " ")[0], strings.Split(user.Name, " ")[1])}
|
||||
src={ fmt.Sprintf("https://ui-avatars.com/api/?name=%s+%s&size=150", strings.Split(user.Name, " " )[0], strings.Split(user.Name, " " )[1]) }
|
||||
/>
|
||||
|
||||
}
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<div class="">
|
||||
@ -66,7 +66,7 @@ templ recipesSection(recipes []domainRecipe.Recipe) {
|
||||
<h2 class="text-2xl font-semibold text-gray-800">My Recipes</h2>
|
||||
<ul class="w-full my-2">
|
||||
if len(recipes) <= 4 {
|
||||
for _, recipe :=range recipes {
|
||||
for _, recipe := range recipes {
|
||||
@recipeListItem(recipe)
|
||||
}
|
||||
} else {
|
||||
@ -92,18 +92,14 @@ templ favoritesSection(recipes []domainRecipe.Recipe) {
|
||||
</section>
|
||||
}
|
||||
|
||||
templ activitySection() {
|
||||
templ activitySection(engagement []domainEngagement.Engagement) {
|
||||
<section class="p-8">
|
||||
<h2 class="text-2xl font-semibold text-gray-800">Recent Activity</h2>
|
||||
<p class="text-sm my-2">Activity section is under construction!</p>
|
||||
<!--
|
||||
<ul class="w-full my-2">
|
||||
@activityListItem()
|
||||
@activityListItem()
|
||||
@activityListItem()
|
||||
@activityListItem()
|
||||
@activityListItem()
|
||||
@activityListItem()
|
||||
for _, eng := range engagement {
|
||||
@activityListItem(eng)
|
||||
}
|
||||
<a href="/" class="bg-red-500">
|
||||
<li
|
||||
class="w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 hover:text-blue-600 duration-150 text-center"
|
||||
@ -112,16 +108,18 @@ templ activitySection() {
|
||||
</li>
|
||||
</a>
|
||||
</ul>
|
||||
-->
|
||||
</section>
|
||||
}
|
||||
|
||||
templ recipeListItem(recipe domainRecipe.Recipe) {
|
||||
<li class="w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 duration-150">
|
||||
<p class="text-base md:text-lg">
|
||||
<a href={ templ.SafeURL(fmt.Sprintf(domain.WEB_RECIPE, recipe.Id)) } class="hover:text-blue-600 duration-100">
|
||||
<p
|
||||
hx-post={ fmt.Sprintf(domain.API_ENGAGEMENT_VIEW, recipe.Id) }
|
||||
hx-trigger="click"
|
||||
hx-swap="none"
|
||||
class="text-base md:text-lg hover:text-blue-600 duration-100 cursor-pointer"
|
||||
>
|
||||
{ recipe.Title }
|
||||
</a>
|
||||
</p>
|
||||
<p class="hidden md:block text-sm text-gray-700 my-1.5">
|
||||
Difficulty: <span class="font-semibold">{ displayDifficulty(recipe.Difficulty) }</span>
|
||||
@ -145,15 +143,15 @@ templ recipeListItem(recipe domainRecipe.Recipe) {
|
||||
</li>
|
||||
}
|
||||
|
||||
templ activityListItem() {
|
||||
templ activityListItem(engagement domainEngagement.Engagement) {
|
||||
<li
|
||||
class="w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 duration-150 flex justify-between items-center"
|
||||
>
|
||||
<p class="text-sm md:text-base text-gray-800">
|
||||
Rated "Spicy Chicken Wings"
|
||||
{ engagement.Message }
|
||||
</p>
|
||||
<p class="text-xs md:text-sm text-gray-600 w-fit shrink-0">
|
||||
2 days ago
|
||||
{ engagement.Created.Format("01/02/2006") }
|
||||
</p>
|
||||
</li>
|
||||
}
|
||||
@ -169,14 +167,14 @@ templ logoutSection() {
|
||||
</section>
|
||||
}
|
||||
|
||||
templ ProfilePage(user domainUser.User, recipes []domainRecipe.Recipe) {
|
||||
templ ProfilePage(user domainUser.User, recipes []domainRecipe.Recipe, engagement []domainEngagement.Engagement) {
|
||||
@components.Navbar(" profile")
|
||||
<div class="w-full h-fit flex justify-center">
|
||||
<div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 border-l border-r border-gray-300 bg-white flex flex-col">
|
||||
@userDetailsSection(user, len(recipes))
|
||||
@recipesSection(recipes)
|
||||
@favoritesSection(recipes)
|
||||
@activitySection()
|
||||
@activitySection(engagement)
|
||||
@logoutSection()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -14,6 +14,7 @@ import "strings"
|
||||
import domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||
import domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||
import domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
||||
|
||||
func displayDifficulty(diff int) string {
|
||||
switch diff {
|
||||
@ -73,7 +74,7 @@ func userDetailsSection(user domainUser.User, recipeCount int) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(user.ImageUrl)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 41, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 42, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -91,7 +92,7 @@ func userDetailsSection(user domainUser.User, recipeCount int) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("https://ui-avatars.com/api/?name=%s+%s&size=150", strings.Split(user.Name, " ")[0], strings.Split(user.Name, " ")[1]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 46, Col: 140}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 47, Col: 143}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -226,7 +227,7 @@ func favoritesSection(recipes []domainRecipe.Recipe) templ.Component {
|
||||
})
|
||||
}
|
||||
|
||||
func activitySection() templ.Component {
|
||||
func activitySection(engagement []domainEngagement.Engagement) 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 {
|
||||
@ -247,7 +248,17 @@ func activitySection() templ.Component {
|
||||
templ_7745c5c3_Var9 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<section class=\"p-8\"><h2 class=\"text-2xl font-semibold text-gray-800\">Recent Activity</h2><p class=\"text-sm my-2\">Activity section is under construction!</p><!--\n\t\t<ul class=\"w-full my-2\">\n\t\t\t@activityListItem()\n\t\t\t@activityListItem()\n\t\t\t@activityListItem()\n\t\t\t@activityListItem()\n\t\t\t@activityListItem()\n\t\t\t@activityListItem()\n\t\t\t<a href=\"/\" class=\"bg-red-500\">\n\t\t\t\t<li\n\t\t\t\t\tclass=\"w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 hover:text-blue-600 duration-150 text-center\"\n\t\t\t\t>\n\t\t\t\t\tSee all...\n\t\t\t\t</li>\n\t\t\t</a>\n\t\t</ul>\n --></section>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<section class=\"p-8\"><h2 class=\"text-2xl font-semibold text-gray-800\">Recent Activity</h2><p class=\"text-sm my-2\">Activity section is under construction!</p><ul class=\"w-full my-2\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, eng := range engagement {
|
||||
templ_7745c5c3_Err = activityListItem(eng).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<a href=\"/\" class=\"bg-red-500\"><li class=\"w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 hover:text-blue-600 duration-150 text-center\">See all...</li></a></ul></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -276,130 +287,134 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component {
|
||||
templ_7745c5c3_Var10 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<li class=\"w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 duration-150\"><p class=\"text-base md:text-lg\"><a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<li class=\"w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 duration-150\"><p hx-post=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 templ.SafeURL = templ.SafeURL(fmt.Sprintf(domain.WEB_RECIPE, recipe.Id))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var11)))
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(domain.API_ENGAGEMENT_VIEW, recipe.Id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 117, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" class=\"hover:text-blue-600 duration-100\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"text-base md:text-lg hover:text-blue-600 duration-100 cursor-pointer\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 123, Col: 18}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 122, Col: 18}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</a></p><p class=\"hidden md:block text-sm text-gray-700 my-1.5\">Difficulty: <span class=\"font-semibold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</p><p class=\"hidden md:block text-sm text-gray-700 my-1.5\">Difficulty: <span class=\"font-semibold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(displayDifficulty(recipe.Difficulty))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 127, Col: 81}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 125, Col: 81}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span> | Duration: <span class=\"font-semibold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span> | Duration: <span class=\"font-semibold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Duration.Total)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 128, Col: 66}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 126, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " min</span> | Category: <span class=\"font-semibold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " min</span> | Category: <span class=\"font-semibold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 129, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 127, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</span></p><p class=\"md:hidden text-xs md:text-sm text-gray-700 my-1\">Difficulty: <span class=\"font-semibold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span></p><p class=\"md:hidden text-xs md:text-sm text-gray-700 my-1\">Difficulty: <span class=\"font-semibold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(displayDifficulty(recipe.Difficulty))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 132, Col: 81}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 130, Col: 81}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span></p><p class=\"md:hidden text-xs md:text-sm text-gray-700 my-1\">Duration: <span class=\"font-semibold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span></p><p class=\"md:hidden text-xs md:text-sm text-gray-700 my-1\">Duration: <span class=\"font-semibold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Duration.Total)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 135, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 133, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " min</span></p><p class=\"md:hidden text-xs md:text-sm text-gray-700 my-1\">Category: <span class=\"font-semibold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " min</span></p><p class=\"md:hidden text-xs md:text-sm text-gray-700 my-1\">Category: <span class=\"font-semibold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 138, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 136, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</span></p>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</span></p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(recipe.Tags) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p class=\"text-xs italic text-gray-500\">Tags: ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<p class=\"text-xs italic text-gray-500\">Tags: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(displayTags(recipe.Tags))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 142, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 140, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</li>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -407,7 +422,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component {
|
||||
})
|
||||
}
|
||||
|
||||
func activityListItem() templ.Component {
|
||||
func activityListItem(engagement domainEngagement.Engagement) 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 {
|
||||
@ -428,7 +443,33 @@ func activityListItem() templ.Component {
|
||||
templ_7745c5c3_Var20 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<li class=\"w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 duration-150 flex justify-between items-center\"><p class=\"text-sm md:text-base text-gray-800\">Rated \"Spicy Chicken Wings\"</p><p class=\"text-xs md:text-sm text-gray-600 w-fit shrink-0\">2 days ago</p></li>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<li class=\"w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 duration-150 flex justify-between items-center\"><p class=\"text-sm md:text-base text-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(engagement.Message)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 151, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</p><p class=\"text-xs md:text-sm text-gray-600 w-fit shrink-0\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(engagement.Created.Format("01/02/2006"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 154, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</p></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -452,21 +493,21 @@ func logoutSection() templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var21 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var21 == nil {
|
||||
templ_7745c5c3_Var21 = templ.NopComponent
|
||||
templ_7745c5c3_Var23 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var23 == nil {
|
||||
templ_7745c5c3_Var23 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<section class=\"w-full flex flex-col justify-center items-center py-8 border-t border-gray-300 mt-auto\"><a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<section class=\"w-full flex flex-col justify-center items-center py-8 border-t border-gray-300 mt-auto\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 templ.SafeURL = domain.API_AUTH_LOGOUT
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var22)))
|
||||
var templ_7745c5c3_Var24 templ.SafeURL = domain.API_AUTH_LOGOUT
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var24)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" class=\"text-center border border-red-500 text-red-500 w-9/10 md:w-1/3 py-2 rounded-lg hover:cursor-pointer hover:bg-red-100 duration-300\">Logout</a></section>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" class=\"text-center border border-red-500 text-red-500 w-9/10 md:w-1/3 py-2 rounded-lg hover:cursor-pointer hover:bg-red-100 duration-300\">Logout</a></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -474,7 +515,7 @@ func logoutSection() templ.Component {
|
||||
})
|
||||
}
|
||||
|
||||
func ProfilePage(user domainUser.User, recipes []domainRecipe.Recipe) templ.Component {
|
||||
func ProfilePage(user domainUser.User, recipes []domainRecipe.Recipe, engagement []domainEngagement.Engagement) 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 {
|
||||
@ -490,16 +531,16 @@ func ProfilePage(user domainUser.User, recipes []domainRecipe.Recipe) templ.Comp
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var23 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var23 == nil {
|
||||
templ_7745c5c3_Var23 = templ.NopComponent
|
||||
templ_7745c5c3_Var25 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var25 == nil {
|
||||
templ_7745c5c3_Var25 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = components.Navbar(" profile").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"w-full h-fit flex justify-center\"><div class=\"mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 border-l border-r border-gray-300 bg-white flex flex-col\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div class=\"w-full h-fit flex justify-center\"><div class=\"mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 border-l border-r border-gray-300 bg-white flex flex-col\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -515,7 +556,7 @@ func ProfilePage(user domainUser.User, recipes []domainRecipe.Recipe) templ.Comp
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = activitySection().Render(ctx, templ_7745c5c3_Buffer)
|
||||
templ_7745c5c3_Err = activitySection(engagement).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -523,7 +564,7 @@ func ProfilePage(user domainUser.User, recipes []domainRecipe.Recipe) templ.Comp
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||
domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||
"github.com/haydenhargreaves/Potion/internal/templates/components"
|
||||
"time"
|
||||
@ -63,28 +65,6 @@ templ starIcon(filled bool) {
|
||||
}
|
||||
}
|
||||
|
||||
templ RecipePage(recipe domain.Recipe, user domainUser.User) {
|
||||
@components.Navbar("")
|
||||
<div class="w-full flex justify-center">
|
||||
<div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white">
|
||||
<img class="bg-gray-100 w-full h-96 mx-auto mb-8" src="" alt=""/>
|
||||
<div class="px-4 py-8 md:px-8">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-gray-800">{ recipe.Title }</h1>
|
||||
<p class="text-sm mt-2 mb-1 text-gray-700">Author: { user.Name }</p>
|
||||
<p class="text-sm mb-2 text-gray-700">Category: { recipe.Category }</p>
|
||||
</div>
|
||||
@metadataSection(recipe)
|
||||
<div class="px-4 py-8 md:px-8">
|
||||
<h3 class="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3>
|
||||
<p class="text-gray-700">{ recipe.Description }</p>
|
||||
</div>
|
||||
@ingredientList(recipe.Ingredients)
|
||||
@instructionList(recipe.Instructions)
|
||||
@tagList(recipe.Tags, recipe.Created, recipe.Modified)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ metadataSection(recipe domain.Recipe) {
|
||||
<div
|
||||
class="border border-blue-300 bg-blue-50 text-gray-700 mx-4 md:mx-8 rounded-lg flex flex-col
|
||||
@ -131,8 +111,8 @@ templ ingredientList(ingredients []domain.RecipeIngredient) {
|
||||
<h2 class="text-2xl text-gray-800 font-semibold mb-2">Ingredients</h2>
|
||||
<hr class="text-gray-300"/>
|
||||
<ul class="text-lg my-4 text-gray-700">
|
||||
for i, ingredient := range ingredients {
|
||||
@ingredientListItem(ingredient.Name, ingredient.Quantity, i%2 == 1)
|
||||
for _, ingredient := range ingredients {
|
||||
@ingredientListItem(ingredient.Name, ingredient.Quantity)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@ -169,13 +149,9 @@ templ tagList(tags []domain.Tag, created time.Time, modified *time.Time) {
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ingredientListItem(name, quantity string, odd bool) {
|
||||
templ ingredientListItem(name, quantity string) {
|
||||
<li
|
||||
if odd {
|
||||
class="p-2 hover:bg-gray-100 transition-all duration-300 rounded-sm flex items-center justify-start bg-[#f8f8f8]"
|
||||
} else {
|
||||
class="p-2 hover:bg-gray-100 transition-all duration-300 rounded-sm flex items-center justify-start"
|
||||
}
|
||||
class="p-2 hover:bg-gray-100 transition-all duration-300 rounded-sm flex items-center justify-start odd:bg-[#f8f8f8]"
|
||||
>
|
||||
<span class="mr-4">
|
||||
<svg class="h-4 text-gray-400" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@ -193,17 +169,11 @@ templ ingredientListItem(name, quantity string, odd bool) {
|
||||
}
|
||||
|
||||
templ instructionListItem(num int, content string) {
|
||||
<li
|
||||
if num % 2==0 {
|
||||
class="p-4 flex items-start gap-x-4 bg-[#f8f8f8]"
|
||||
} else {
|
||||
class="p-4 flex items-start gap-x-4"
|
||||
}
|
||||
>
|
||||
<div class="size-10 bg-blue-50 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<h3 class="text-blue-600 font-semibold">{ num }</h3>
|
||||
<li class="p-4 flex items-start gap-x-4 odd:bg-[#f8f8f8]">
|
||||
<div class="size-8 md:size-10 bg-blue-50 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<h3 class="text-base md:text-xl text-blue-600 font-semibold">{ num }</h3>
|
||||
</div>
|
||||
<p class="">{ content }</p>
|
||||
<p class="text-base">{ content }</p>
|
||||
</li>
|
||||
}
|
||||
|
||||
@ -212,3 +182,263 @@ templ tagListItem(content string) {
|
||||
{ content }
|
||||
</li>
|
||||
}
|
||||
|
||||
templ favoriteButton(favorited bool, id int, loggedIn bool) {
|
||||
if favorited {
|
||||
<button
|
||||
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id) }
|
||||
hx-trigger="click"
|
||||
hx-swap="none"
|
||||
if loggedIn {
|
||||
hx-on:click="favoriteButtonHandler();"
|
||||
}
|
||||
class="flex items-center justify-center gap-x-1 rounded-lg border border-blue-300 bg-blue-50 text-gray-800 px-6 py-3 flex-grow hover:bg-blue-100 hover:border-blue-500 duration-300"
|
||||
id="favorite-button"
|
||||
>
|
||||
<svg class="h-6 text-red-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2 9.1371C2 14 6.01943 16.5914 8.96173 18.9109C10 19.7294 11 20.5 12 20.5C13 20.5 14 19.7294 15.0383 18.9109C17.9806 16.5914 22 14 22 9.1371C22 4.27416 16.4998 0.825464 12 5.50063C7.50016 0.825464 2 4.27416 2 9.1371Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
Unfavorite
|
||||
</button>
|
||||
} else {
|
||||
<button
|
||||
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id) }
|
||||
hx-trigger="click"
|
||||
hx-swap="none"
|
||||
if loggedIn {
|
||||
hx-on:click="favoriteButtonHandler();"
|
||||
}
|
||||
class="flex items-center justify-center gap-x-1 rounded-lg border border-gray-300 text-gray-800 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300"
|
||||
id="favorite-button"
|
||||
>
|
||||
<svg class="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 6.00019C10.2006 3.90317 7.19377 3.2551 4.93923 5.17534C2.68468 7.09558 2.36727 10.3061 4.13778 12.5772C5.60984 14.4654 10.0648 18.4479 11.5249 19.7369C11.6882 19.8811 11.7699 19.9532 11.8652 19.9815C11.9483 20.0062 12.0393 20.0062 12.1225 19.9815C12.2178 19.9532 12.2994 19.8811 12.4628 19.7369C13.9229 18.4479 18.3778 14.4654 19.8499 12.5772C21.6204 10.3061 21.3417 7.07538 19.0484 5.17534C16.7551 3.2753 13.7994 3.90317 12 6.00019Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
Favorite
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
templ madeButton(id int, loggedIn bool) {
|
||||
<button
|
||||
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_MAKE, id) }
|
||||
hx-trigger="click"
|
||||
hx-swap="none"
|
||||
id="make-button"
|
||||
if loggedIn {
|
||||
hx-on:click="makeButtonHandler();"
|
||||
}
|
||||
class="flex items-center justify-center gap-x-1 rounded-lg border border-gray-300 text-gray-800 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300"
|
||||
>
|
||||
<svg
|
||||
class="h-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 -3.84 122.88 122.88"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
style="enable-background:new 0 0 122.88 115.21"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M29.03,100.46l20.79-25.21l9.51,12.13L41,110.69C33.98,119.61,20.99,110.21,29.03,100.46L29.03,100.46z M53.31,43.05 c1.98-6.46,1.07-11.98-6.37-20.18L28.76,1c-2.58-3.03-8.66,1.42-6.12,5.09L37.18,24c2.75,3.34-2.36,7.76-5.2,4.32L16.94,9.8 c-2.8-3.21-8.59,1.03-5.66,4.7c4.24,5.1,10.8,13.43,15.04,18.53c2.94,2.99-1.53,7.42-4.43,3.69L6.96,18.32 c-2.19-2.38-5.77-0.9-6.72,1.88c-1.02,2.97,1.49,5.14,3.2,7.34L20.1,49.06c5.17,5.99,10.95,9.54,17.67,7.53 c1.03-0.31,2.29-0.94,3.64-1.77l44.76,57.78c2.41,3.11,7.06,3.44,10.08,0.93l0.69-0.57c3.4-2.83,3.95-8,1.04-11.34L50.58,47.16 C51.96,45.62,52.97,44.16,53.31,43.05L53.31,43.05z M65.98,55.65l7.37-8.94C63.87,23.21,99-8.11,116.03,6.29 C136.72,23.8,105.97,66,84.36,55.57l-8.73,11.09L65.98,55.65L65.98,55.65z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
Made This!
|
||||
</button>
|
||||
}
|
||||
|
||||
templ shareButton(id int) {
|
||||
<button
|
||||
id="share-button"
|
||||
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_SHARE, id) }
|
||||
hx-trigger="click"
|
||||
hx-swap="none"
|
||||
hx-on:click="shareButtonHandler();"
|
||||
class="flex items-center justify-center gap-x-1 rounded-lg border border-gray-300 text-gray-800 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300"
|
||||
>
|
||||
<svg class="h-7" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M13.803 5.33333C13.803 3.49238 15.3022 2 17.1515 2C19.0008 2 20.5 3.49238 20.5 5.33333C20.5 7.17428 19.0008 8.66667 17.1515 8.66667C16.2177 8.66667 15.3738 8.28596 14.7671 7.67347L10.1317 10.8295C10.1745 11.0425 10.197 11.2625 10.197 11.4872C10.197 11.9322 10.109 12.3576 9.94959 12.7464L15.0323 16.0858C15.6092 15.6161 16.3473 15.3333 17.1515 15.3333C19.0008 15.3333 20.5 16.8257 20.5 18.6667C20.5 20.5076 19.0008 22 17.1515 22C15.3022 22 13.803 20.5076 13.803 18.6667C13.803 18.1845 13.9062 17.7255 14.0917 17.3111L9.05007 13.9987C8.46196 14.5098 7.6916 14.8205 6.84848 14.8205C4.99917 14.8205 3.5 13.3281 3.5 11.4872C3.5 9.64623 4.99917 8.15385 6.84848 8.15385C7.9119 8.15385 8.85853 8.64725 9.47145 9.41518L13.9639 6.35642C13.8594 6.03359 13.803 5.6896 13.803 5.33333Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
Share
|
||||
</button>
|
||||
}
|
||||
|
||||
templ buttonSection(favorited bool, id int, loggedIn bool) {
|
||||
<section class="w-full flex flex-col md:flex-row gap-x-4 gap-y-2 py-8 px-4 md:px-8">
|
||||
@favoriteButton(favorited, id, loggedIn)
|
||||
@madeButton(id, loggedIn)
|
||||
@shareButton(id)
|
||||
</section>
|
||||
}
|
||||
|
||||
templ RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool) {
|
||||
@components.Navbar("")
|
||||
<div class="w-full flex justify-center">
|
||||
<div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white">
|
||||
<img class="bg-gray-100 w-full h-96 mx-auto mb-8" src="" alt=""/>
|
||||
<div class="px-4 py-8 md:px-8">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-gray-800">{ recipe.Title }</h1>
|
||||
<p class="text-sm mt-2 mb-1 text-gray-700">Author: { user.Name }</p>
|
||||
<p class="text-sm mb-2 text-gray-700">Category: { recipe.Category }</p>
|
||||
</div>
|
||||
@metadataSection(recipe)
|
||||
@buttonSection(recipe.Favorite, recipe.Id, loggedIn)
|
||||
<div class="px-4 py-8 md:px-8">
|
||||
<h3 class="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3>
|
||||
<p class="text-gray-700">{ recipe.Description }</p>
|
||||
</div>
|
||||
@ingredientList(recipe.Ingredients)
|
||||
@instructionList(recipe.Instructions)
|
||||
@tagList(recipe.Tags, recipe.Created, recipe.Modified)
|
||||
</div>
|
||||
</div>
|
||||
@scripts(recipe.Id)
|
||||
}
|
||||
|
||||
templ scripts(id int) {
|
||||
<script>
|
||||
function shareButtonHandler() {
|
||||
const button = document.getElementById("share-button");
|
||||
const before = button.outerHTML;
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
// TODO: Fix this to use the real domain somehow
|
||||
const url = "http://localhost:7331/v1/web/recipe/{{ id }}"
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
button.outerHTML = `
|
||||
<button id="share-button"
|
||||
class="flex items-center justify-center gap-x-1 rounded-lg border border-green-500 text-green-500 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300">
|
||||
<svg class="h-7" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M13.803 5.33333C13.803 3.49238 15.3022 2 17.1515 2C19.0008 2 20.5 3.49238 20.5 5.33333C20.5 7.17428 19.0008 8.66667 17.1515 8.66667C16.2177 8.66667 15.3738 8.28596 14.7671 7.67347L10.1317 10.8295C10.1745 11.0425 10.197 11.2625 10.197 11.4872C10.197 11.9322 10.109 12.3576 9.94959 12.7464L15.0323 16.0858C15.6092 15.6161 16.3473 15.3333 17.1515 15.3333C19.0008 15.3333 20.5 16.8257 20.5 18.6667C20.5 20.5076 19.0008 22 17.1515 22C15.3022 22 13.803 20.5076 13.803 18.6667C13.803 18.1845 13.9062 17.7255 14.0917 17.3111L9.05007 13.9987C8.46196 14.5098 7.6916 14.8205 6.84848 14.8205C4.99917 14.8205 3.5 13.3281 3.5 11.4872C3.5 9.64623 4.99917 8.15385 6.84848 8.15385C7.9119 8.15385 8.85853 8.64725 9.47145 9.41518L13.9639 6.35642C13.8594 6.03359 13.803 5.6896 13.803 5.33333Z"
|
||||
fill="currentColor"></path>
|
||||
</svg>
|
||||
Link Copied!
|
||||
</button>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
const newButton = document.getElementById("share-button");
|
||||
newButton.outerHTML = before;
|
||||
}, 2000);
|
||||
});
|
||||
} else {
|
||||
console.warn("Clipboard API not available.");
|
||||
|
||||
button.outerHTML = `
|
||||
<button id="share-button"
|
||||
class="flex items-center justify-center gap-x-1 rounded-lg border border-red-500 text-red-500 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300">
|
||||
<svg class="h-7" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M13.803 5.33333C13.803 3.49238 15.3022 2 17.1515 2C19.0008 2 20.5 3.49238 20.5 5.33333C20.5 7.17428 19.0008 8.66667 17.1515 8.66667C16.2177 8.66667 15.3738 8.28596 14.7671 7.67347L10.1317 10.8295C10.1745 11.0425 10.197 11.2625 10.197 11.4872C10.197 11.9322 10.109 12.3576 9.94959 12.7464L15.0323 16.0858C15.6092 15.6161 16.3473 15.3333 17.1515 15.3333C19.0008 15.3333 20.5 16.8257 20.5 18.6667C20.5 20.5076 19.0008 22 17.1515 22C15.3022 22 13.803 20.5076 13.803 18.6667C13.803 18.1845 13.9062 17.7255 14.0917 17.3111L9.05007 13.9987C8.46196 14.5098 7.6916 14.8205 6.84848 14.8205C4.99917 14.8205 3.5 13.3281 3.5 11.4872C3.5 9.64623 4.99917 8.15385 6.84848 8.15385C7.9119 8.15385 8.85853 8.64725 9.47145 9.41518L13.9639 6.35642C13.8594 6.03359 13.803 5.6896 13.803 5.33333Z"
|
||||
fill="currentColor"></path>
|
||||
</svg>
|
||||
Failed!
|
||||
</button>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
const newButton = document.getElementById("share-button");
|
||||
newButton.outerHTML = before;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function makeButtonHandler() {
|
||||
const button = document.getElementById("make-button");
|
||||
|
||||
button.outerHTML = `
|
||||
<button
|
||||
id="make-button"
|
||||
class="flex items-center justify-center gap-x-1 rounded-lg border border-blue-500 text-blue-500 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300"
|
||||
>
|
||||
<svg
|
||||
class="h-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 -3.84 122.88 122.88"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
style="enable-background:new 0 0 122.88 115.21"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M29.03,100.46l20.79-25.21l9.51,12.13L41,110.69C33.98,119.61,20.99,110.21,29.03,100.46L29.03,100.46z M53.31,43.05 c1.98-6.46,1.07-11.98-6.37-20.18L28.76,1c-2.58-3.03-8.66,1.42-6.12,5.09L37.18,24c2.75,3.34-2.36,7.76-5.2,4.32L16.94,9.8 c-2.8-3.21-8.59,1.03-5.66,4.7c4.24,5.1,10.8,13.43,15.04,18.53c2.94,2.99-1.53,7.42-4.43,3.69L6.96,18.32 c-2.19-2.38-5.77-0.9-6.72,1.88c-1.02,2.97,1.49,5.14,3.2,7.34L20.1,49.06c5.17,5.99,10.95,9.54,17.67,7.53 c1.03-0.31,2.29-0.94,3.64-1.77l44.76,57.78c2.41,3.11,7.06,3.44,10.08,0.93l0.69-0.57c3.4-2.83,3.95-8,1.04-11.34L50.58,47.16 C51.96,45.62,52.97,44.16,53.31,43.05L53.31,43.05z M65.98,55.65l7.37-8.94C63.87,23.21,99-8.11,116.03,6.29 C136.72,23.8,105.97,66,84.36,55.57l-8.73,11.09L65.98,55.65L65.98,55.65z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
Made This!
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function favoriteButtonHandler() {
|
||||
const button = document.getElementById("favorite-button");
|
||||
|
||||
console.log(button.classList);
|
||||
console.log(button.classList.contains("border-blue-300"));
|
||||
|
||||
const toggleClasses = [
|
||||
"border-gray-300", "hover:bg-gray-50", "hover:border-blue-300",
|
||||
"border-blue-300", "bg-blue-50", "hover:bg-blue-100", "hover:border-blue-500"
|
||||
];
|
||||
|
||||
for (const cls of toggleClasses) {
|
||||
console.log("toggling class " + cls);
|
||||
button.classList.toggle(cls);
|
||||
}
|
||||
|
||||
if (!button.classList.contains("border-blue-300")) {
|
||||
button.innerHTML = `
|
||||
<svg class="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 6.00019C10.2006 3.90317 7.19377 3.2551 4.93923 5.17534C2.68468 7.09558 2.36727 10.3061 4.13778 12.5772C5.60984 14.4654 10.0648 18.4479 11.5249 19.7369C11.6882 19.8811 11.7699 19.9532 11.8652 19.9815C11.9483 20.0062 12.0393 20.0062 12.1225 19.9815C12.2178 19.9532 12.2994 19.8811 12.4628 19.7369C13.9229 18.4479 18.3778 14.4654 19.8499 12.5772C21.6204 10.3061 21.3417 7.07538 19.0484 5.17534C16.7551 3.2753 13.7994 3.90317 12 6.00019Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
Favorite
|
||||
`;
|
||||
|
||||
} else {
|
||||
button.innerHTML = `
|
||||
<svg class="h-6 text-red-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2 9.1371C2 14 6.01943 16.5914 8.96173 18.9109C10 19.7294 11 20.5 12 20.5C13 20.5 14 19.7294 15.0383 18.9109C17.9806 16.5914 22 14 22 9.1371C22 4.27416 16.4998 0.825464 12 5.50063C7.50016 0.825464 2 4.27416 2 9.1371Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
Unfavorite
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -25,8 +25,8 @@ templ SearchPage(filters domainRecipe.SearchFilters, searchOnLoad bool) {
|
||||
|
||||
templ ResultList(recipes []domain.Recipe) {
|
||||
<div id="result-list" class="flex flex-col w-full p-4 items-center">
|
||||
for i, recipe := range recipes {
|
||||
@searchResult(recipe, i%2 == 1)
|
||||
for _, recipe := range recipes {
|
||||
@searchResult(recipe)
|
||||
}
|
||||
if len(recipes) == 0 || recipes == nil {
|
||||
<p class="text-gray-700 text-sm py-4">No results</p>
|
||||
@ -36,19 +36,17 @@ templ ResultList(recipes []domain.Recipe) {
|
||||
</div>
|
||||
}
|
||||
|
||||
templ searchResult(recipe domain.Recipe, odd bool) {
|
||||
<a
|
||||
href={ templ.SafeURL(fmt.Sprintf(domainServer.WEB_RECIPE, recipe.Id)) }
|
||||
if odd {
|
||||
class="w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row bg-[#f8f8f8]"
|
||||
} else {
|
||||
class="w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row"
|
||||
}
|
||||
templ searchResult(recipe domain.Recipe) {
|
||||
<div
|
||||
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_VIEW, recipe.Id) }
|
||||
hx-trigger="click"
|
||||
hx-swap="none"
|
||||
class="w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row even:bg-[#f8f8f8] cursor-pointer"
|
||||
>
|
||||
<img class="bg-gray-50 size-56 md:size-40 rounded-md border-0" src=""/>
|
||||
<div class="text-gray-700 p-4 flex flex-col items-center md:items-start">
|
||||
<h3 class="text-xl font-semibold text-black pb-1">
|
||||
{ recipe.Title } <span class="text-sm font-normal">{ recipe.Category }</span>
|
||||
{ recipe.Title } <span class="text-sm font-normal hidden md:inline">{ recipe.Category }</span>
|
||||
</h3>
|
||||
<div class="text-sm flex gap-x-3 gap-y-1 items-center flex-wrap">
|
||||
<span class="flex gap-x-1 align-center">
|
||||
@ -70,7 +68,7 @@ templ searchResult(recipe domain.Recipe, odd bool) {
|
||||
</div>
|
||||
<p class="text-sm py-2 text-center md:text-left">{ recipe.Description }</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ servingIconSm() {
|
||||
|
||||
@ -94,8 +94,8 @@ func ResultList(recipes []domain.Recipe) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for i, recipe := range recipes {
|
||||
templ_7745c5c3_Err = searchResult(recipe, i%2 == 1).Render(ctx, templ_7745c5c3_Buffer)
|
||||
for _, recipe := range recipes {
|
||||
templ_7745c5c3_Err = searchResult(recipe).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -119,7 +119,7 @@ func ResultList(recipes []domain.Recipe) templ.Component {
|
||||
})
|
||||
}
|
||||
|
||||
func searchResult(recipe domain.Recipe, odd bool) templ.Component {
|
||||
func searchResult(recipe domain.Recipe) 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 {
|
||||
@ -140,57 +140,46 @@ func searchResult(recipe domain.Recipe, odd bool) templ.Component {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div hx-post=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 templ.SafeURL = templ.SafeURL(fmt.Sprintf(domainServer.WEB_RECIPE, recipe.Id))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(domainServer.API_ENGAGEMENT_VIEW, recipe.Id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 41, Col: 70}
|
||||
}
|
||||
_, 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, 9, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if odd {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " class=\"w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row bg-[#f8f8f8]\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " class=\"w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "><img class=\"bg-gray-50 size-56 md:size-40 rounded-md border-0\" src=\"\"><div class=\"text-gray-700 p-4 flex flex-col items-center md:items-start\"><h3 class=\"text-xl font-semibold text-black pb-1\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row even:bg-[#f8f8f8] cursor-pointer\"><img class=\"bg-gray-50 size-56 md:size-40 rounded-md border-0\" src=\"\"><div class=\"text-gray-700 p-4 flex flex-col items-center md:items-start\"><h3 class=\"text-xl font-semibold text-black pb-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 51, Col: 18}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 49, Col: 18}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " <span class=\"text-sm font-normal\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " <span class=\"text-sm font-normal hidden md:inline\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 51, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 49, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></h3><div class=\"text-sm flex gap-x-3 gap-y-1 items-center flex-wrap\"><span class=\"flex gap-x-1 align-center\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></h3><div class=\"text-sm flex gap-x-3 gap-y-1 items-center flex-wrap\"><span class=\"flex gap-x-1 align-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -201,13 +190,13 @@ func searchResult(recipe domain.Recipe, odd bool) templ.Component {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Duration.Total)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 56, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 54, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " min</span> <span class=\"flex gap-x-1 align-center\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " min</span> <span class=\"flex gap-x-1 align-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -223,7 +212,7 @@ func searchResult(recipe domain.Recipe, odd bool) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span> <span class=\"flex gap-x-1 align-center\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span> <span class=\"flex gap-x-1 align-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -231,33 +220,33 @@ func searchResult(recipe domain.Recipe, odd bool) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Serves ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Serves ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Serves)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 68, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 66, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span></div><p class=\"text-sm py-2 text-center md:text-left\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></div><p class=\"text-sm py-2 text-center md:text-left\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 71, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 69, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</p></div></a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -286,7 +275,7 @@ func servingIconSm() templ.Component {
|
||||
templ_7745c5c3_Var10 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<svg class=\"h-5 text-blue-600\" fill=\"currentColor\" version=\"1.1\" id=\"Icons\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 32 32\" xml:space=\"preserve\"><g><circle cx=\"12\" cy=\"16\" r=\"5\"></circle> <path d=\"M12,6C6.5,6,2,10.5,2,16s4.5,10,10,10s10-4.5,10-10S17.5,6,12,6z M12,23c-3.9,0-7-3.1-7-7s3.1-7,7-7s7,3.1,7,7\n\t\tS15.9,23,12,23z\"></path> <path d=\"M30,10.5V5c0-0.6-0.4-1-1-1s-1,0.4-1,1v5.5c0,0.2,0,0.4,0,0.5h-1V5c0-0.6-0.4-1-1-1s-1,0.4-1,1v6h-1c0-0.2,0-0.4,0-0.5V5\n\t\tc0-0.6-0.4-1-1-1s-1,0.4-1,1v5.5c0,1.9,0.5,3.4,1.4,4.3c0.7,0.8,1,1.8,0.9,2.7l-1,7.3c-0.1,0.8,0.1,1.6,0.6,2.2S25.2,28,26,28\n\t\ts1.5-0.3,2.1-0.9s0.8-1.4,0.6-2.2l-1-7.3c-0.1-1,0.2-2,0.9-2.8C29.5,13.8,30,12.3,30,10.5z\"></path></g></svg>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<svg class=\"h-5 text-blue-600\" fill=\"currentColor\" version=\"1.1\" id=\"Icons\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 32 32\" xml:space=\"preserve\"><g><circle cx=\"12\" cy=\"16\" r=\"5\"></circle> <path d=\"M12,6C6.5,6,2,10.5,2,16s4.5,10,10,10s10-4.5,10-10S17.5,6,12,6z M12,23c-3.9,0-7-3.1-7-7s3.1-7,7-7s7,3.1,7,7\n\t\tS15.9,23,12,23z\"></path> <path d=\"M30,10.5V5c0-0.6-0.4-1-1-1s-1,0.4-1,1v5.5c0,0.2,0,0.4,0,0.5h-1V5c0-0.6-0.4-1-1-1s-1,0.4-1,1v6h-1c0-0.2,0-0.4,0-0.5V5\n\t\tc0-0.6-0.4-1-1-1s-1,0.4-1,1v5.5c0,1.9,0.5,3.4,1.4,4.3c0.7,0.8,1,1.8,0.9,2.7l-1,7.3c-0.1,0.8,0.1,1.6,0.6,2.2S25.2,28,26,28\n\t\ts1.5-0.3,2.1-0.9s0.8-1.4,0.6-2.2l-1-7.3c-0.1-1,0.2-2,0.9-2.8C29.5,13.8,30,12.3,30,10.5z\"></path></g></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -315,7 +304,7 @@ func timeIconSm() templ.Component {
|
||||
templ_7745c5c3_Var11 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<svg class=\"h-5 text-blue-600\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12 7V12L14.5 13.5M21 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_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<svg class=\"h-5 text-blue-600\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12 7V12L14.5 13.5M21 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
|
||||
}
|
||||
@ -345,12 +334,12 @@ func starIconSm(filled bool) templ.Component {
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if filled {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<svg class=\"h-4 text-blue-600\" fill=\"currentColor\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M23.632 9.201a.628.628 0 0 1-.22.678l-5.726 4.96 1.727 7.394a.606.606 0 0 1-.935.676l-6.503-3.953-6.503 3.953a.713.713 0 0 1-.374.112.57.57 0 0 1-.34-.109.629.629 0 0 1-.222-.679l1.729-7.393L.539 9.879A.607.607 0 0 1 .897 8.78l7.536-.635 2.965-7.083a.62.62 0 0 1 1.155.001l2.965 7.082 7.536.635a.63.63 0 0 1 .578.42z\"></path> <path fill=\"none\" d=\"M0 0h24v24H0z\"></path></svg>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<svg class=\"h-4 text-blue-600\" fill=\"currentColor\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M23.632 9.201a.628.628 0 0 1-.22.678l-5.726 4.96 1.727 7.394a.606.606 0 0 1-.935.676l-6.503-3.953-6.503 3.953a.713.713 0 0 1-.374.112.57.57 0 0 1-.34-.109.629.629 0 0 1-.222-.679l1.729-7.393L.539 9.879A.607.607 0 0 1 .897 8.78l7.536-.635 2.965-7.083a.62.62 0 0 1 1.155.001l2.965 7.082 7.536.635a.63.63 0 0 1 .578.42z\"></path> <path fill=\"none\" d=\"M0 0h24v24H0z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<svg class=\"h-4 text-gray-500\" fill=\"currentColor\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M23.054 8.781l-7.536-.635-2.965-7.082a.619.619 0 0 0-1.155 0L8.433 8.145.896 8.78a.607.607 0 0 0-.357 1.1l5.726 4.96-1.729 7.395a.63.63 0 0 0 .223.679.573.573 0 0 0 .339.108.717.717 0 0 0 .374-.111l6.503-3.954 6.503 3.953a.606.606 0 0 0 .935-.677l-1.727-7.392 5.725-4.96a.607.607 0 0 0-.357-1.099zm-6.48 5.698l1.662 7.113-6.261-3.806-6.262 3.807 1.663-7.114-5.513-4.776 7.257-.611 2.855-6.817 2.855 6.817 7.257.611z\"></path> <path fill=\"none\" d=\"M0 0h24v24H0z\"></path></svg>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<svg class=\"h-4 text-gray-500\" fill=\"currentColor\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M23.054 8.781l-7.536-.635-2.965-7.082a.619.619 0 0 0-1.155 0L8.433 8.145.896 8.78a.607.607 0 0 0-.357 1.1l5.726 4.96-1.729 7.395a.63.63 0 0 0 .223.679.573.573 0 0 0 .339.108.717.717 0 0 0 .374-.111l6.503-3.954 6.503 3.953a.606.606 0 0 0 .935-.677l-1.727-7.392 5.725-4.96a.607.607 0 0 0-.357-1.099zm-6.48 5.698l1.662 7.113-6.261-3.806-6.262 3.807 1.663-7.114-5.513-4.776 7.257-.611 2.855-6.817 2.855 6.817 7.257.611z\"></path> <path fill=\"none\" d=\"M0 0h24v24H0z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
monospace;
|
||||
--color-red-100: oklch(93.6% 0.032 17.717);
|
||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||
--color-green-300: oklch(87.1% 0.15 154.449);
|
||||
--color-green-500: oklch(72.3% 0.219 149.579);
|
||||
--color-blue-50: oklch(97% 0.014 254.604);
|
||||
--color-blue-100: oklch(93.2% 0.032 255.585);
|
||||
--color-blue-200: oklch(88.2% 0.059 254.128);
|
||||
@ -28,7 +30,6 @@
|
||||
--color-gray-600: oklch(44.6% 0.03 256.802);
|
||||
--color-gray-700: oklch(37.3% 0.034 259.733);
|
||||
--color-gray-800: oklch(27.8% 0.033 256.848);
|
||||
--color-gray-900: oklch(21% 0.034 264.665);
|
||||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--spacing: 0.25rem;
|
||||
@ -238,9 +239,6 @@
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
.top-1 {
|
||||
top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.top-1\/2 {
|
||||
top: calc(1/2 * 100%);
|
||||
}
|
||||
@ -250,9 +248,6 @@
|
||||
.left-0 {
|
||||
left: calc(var(--spacing) * 0);
|
||||
}
|
||||
.left-1 {
|
||||
left: calc(var(--spacing) * 1);
|
||||
}
|
||||
.left-1\/2 {
|
||||
left: calc(1/2 * 100%);
|
||||
}
|
||||
@ -368,6 +363,10 @@
|
||||
width: calc(var(--spacing) * 5);
|
||||
height: calc(var(--spacing) * 5);
|
||||
}
|
||||
.size-8 {
|
||||
width: calc(var(--spacing) * 8);
|
||||
height: calc(var(--spacing) * 8);
|
||||
}
|
||||
.size-10 {
|
||||
width: calc(var(--spacing) * 10);
|
||||
height: calc(var(--spacing) * 10);
|
||||
@ -420,18 +419,12 @@
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.w-1 {
|
||||
width: calc(var(--spacing) * 1);
|
||||
}
|
||||
.w-1\/3 {
|
||||
width: calc(1/3 * 100%);
|
||||
}
|
||||
.w-1\/4 {
|
||||
width: calc(1/4 * 100%);
|
||||
}
|
||||
.w-3 {
|
||||
width: calc(var(--spacing) * 3);
|
||||
}
|
||||
.w-3\/4 {
|
||||
width: calc(3/4 * 100%);
|
||||
}
|
||||
@ -444,9 +437,6 @@
|
||||
.w-5 {
|
||||
width: calc(var(--spacing) * 5);
|
||||
}
|
||||
.w-9 {
|
||||
width: calc(var(--spacing) * 9);
|
||||
}
|
||||
.w-9\/10 {
|
||||
width: calc(9/10 * 100%);
|
||||
}
|
||||
@ -465,9 +455,6 @@
|
||||
.max-w-2xl {
|
||||
max-width: var(--container-2xl);
|
||||
}
|
||||
.flex-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@ -477,21 +464,10 @@
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.-translate-x-1 {
|
||||
--tw-translate-x: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-x-1\/2 {
|
||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1 {
|
||||
--tw-translate-y: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1\/2 {
|
||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
@ -504,15 +480,9 @@
|
||||
--tw-scale-y: 50%;
|
||||
scale: var(--tw-scale-x) var(--tw-scale-y);
|
||||
}
|
||||
.transform {
|
||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||
}
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.resize {
|
||||
resize: both;
|
||||
}
|
||||
.resize-none {
|
||||
resize: none;
|
||||
}
|
||||
@ -651,15 +621,15 @@
|
||||
.border-gray-300 {
|
||||
border-color: var(--color-gray-300);
|
||||
}
|
||||
.border-green-500 {
|
||||
border-color: var(--color-green-500);
|
||||
}
|
||||
.border-red-500 {
|
||||
border-color: var(--color-red-500);
|
||||
}
|
||||
.border-white {
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
.bg-\[\#f8f8f8\] {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
.bg-black {
|
||||
background-color: var(--color-black);
|
||||
}
|
||||
@ -701,9 +671,6 @@
|
||||
--tw-gradient-position: to right in oklab;
|
||||
background-image: linear-gradient(var(--tw-gradient-stops));
|
||||
}
|
||||
.bg-none {
|
||||
background-image: none;
|
||||
}
|
||||
.from-blue-100 {
|
||||
--tw-gradient-from: var(--color-blue-100);
|
||||
--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));
|
||||
@ -752,6 +719,9 @@
|
||||
.px-5 {
|
||||
padding-inline: calc(var(--spacing) * 5);
|
||||
}
|
||||
.px-6 {
|
||||
padding-inline: calc(var(--spacing) * 6);
|
||||
}
|
||||
.px-8 {
|
||||
padding-inline: calc(var(--spacing) * 8);
|
||||
}
|
||||
@ -900,6 +870,9 @@
|
||||
.text-gray-800 {
|
||||
color: var(--color-gray-800);
|
||||
}
|
||||
.text-green-500 {
|
||||
color: var(--color-green-500);
|
||||
}
|
||||
.text-red-500 {
|
||||
color: var(--color-red-500);
|
||||
}
|
||||
@ -1066,6 +1039,16 @@
|
||||
color: var(--color-blue-700);
|
||||
}
|
||||
}
|
||||
.odd\:bg-\[\#f8f8f8\] {
|
||||
&:nth-child(odd) {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
}
|
||||
.even\:bg-\[\#f8f8f8\] {
|
||||
&:nth-child(even) {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
}
|
||||
.even\:bg-gray-50 {
|
||||
&:nth-child(even) {
|
||||
background-color: var(--color-gray-50);
|
||||
@ -1093,6 +1076,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:border-blue-300 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
border-color: var(--color-blue-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:border-blue-400 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@ -1100,6 +1090,20 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:border-blue-500 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
border-color: var(--color-blue-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-blue-100 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-blue-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-blue-200 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@ -1274,6 +1278,17 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.md\:inline {
|
||||
@media (width >= 48rem) {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
.md\:size-10 {
|
||||
@media (width >= 48rem) {
|
||||
width: calc(var(--spacing) * 10);
|
||||
height: calc(var(--spacing) * 10);
|
||||
}
|
||||
}
|
||||
.md\:size-12 {
|
||||
@media (width >= 48rem) {
|
||||
width: calc(var(--spacing) * 12);
|
||||
@ -1323,6 +1338,11 @@
|
||||
width: calc(2/5 * 100%);
|
||||
}
|
||||
}
|
||||
.md\:w-3\/4 {
|
||||
@media (width >= 48rem) {
|
||||
width: calc(3/4 * 100%);
|
||||
}
|
||||
}
|
||||
.md\:w-32 {
|
||||
@media (width >= 48rem) {
|
||||
width: calc(var(--spacing) * 32);
|
||||
@ -1442,6 +1462,12 @@
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
}
|
||||
}
|
||||
.md\:text-xl {
|
||||
@media (width >= 48rem) {
|
||||
font-size: var(--text-xl);
|
||||
line-height: var(--tw-leading, var(--text-xl--line-height));
|
||||
}
|
||||
}
|
||||
.lg\:flex {
|
||||
@media (width >= 64rem) {
|
||||
display: flex;
|
||||
@ -1483,26 +1509,6 @@
|
||||
inherits: false;
|
||||
initial-value: 1;
|
||||
}
|
||||
@property --tw-rotate-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-rotate-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-rotate-z {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-skew-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-skew-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
@ -1712,11 +1718,6 @@
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-scale-z: 1;
|
||||
--tw-rotate-x: initial;
|
||||
--tw-rotate-y: initial;
|
||||
--tw-rotate-z: initial;
|
||||
--tw-skew-x: initial;
|
||||
--tw-skew-y: initial;
|
||||
--tw-border-style: solid;
|
||||
--tw-gradient-position: initial;
|
||||
--tw-gradient-from: #0000;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user