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:
Hayden Hargreaves 2025-07-15 21:53:16 -07:00
commit d191902448
32 changed files with 1930 additions and 637 deletions

View File

@ -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

View 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)
}
}

View File

@ -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))
}

View File

@ -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

View 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)
}

View File

@ -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.")

View 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
}

View 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)
}

View 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)
}

View File

@ -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.

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
}

View File

@ -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
}

View File

@ -7,7 +7,7 @@ templ FavoritesPage() {
<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>

View File

@ -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
}

View File

@ -10,10 +10,8 @@ templ introSection() {
<video class="" autoplay loop muted playsinline>
<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>
@ -29,7 +27,9 @@ templ introSection() {
templ searchSection() {
<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>
}
@ -78,8 +78,7 @@ templ listsSection() {
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"
>
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,14 +86,11 @@ 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>

View File

@ -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
}

View File

@ -7,10 +7,9 @@ templ ListPage() {
<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>
}

View File

@ -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
}

View File

@ -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 {
@ -45,7 +46,6 @@ templ userDetailsSection(user domainUser.User, recipeCount int) {
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]) }
/>
}
<div class="flex flex-col gap-y-4">
<div class="">
@ -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>

View File

@ -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
}

View File

@ -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

View File

@ -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() {

View File

@ -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
}

View File

@ -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;