diff --git a/doc/TechnicalSpecification.md b/doc/TechnicalSpecification.md
index dce8da4..8af89c5 100644
--- a/doc/TechnicalSpecification.md
+++ b/doc/TechnicalSpecification.md
@@ -291,12 +291,13 @@ 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***
@@ -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
diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go
index d375b78..a11959f 100644
--- a/internal/app/handlers/page_handler.go
+++ b/internal/app/handlers/page_handler.go
@@ -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))
}
@@ -111,6 +120,17 @@ func RecipePage(ctx *gin.Context) {
return
}
+ // Add engagement
+ if user != nil {
+ if _, err = deps.EngagementService.UserViewRecipe(user.Id, recipe.Id); err != nil {
+ fmt.Printf("ERROR: %s\n", err.Error())
+ ctx.JSON(400, err.Error())
+ return
+ }
+ }
+ // TODO: Need handling for anon viewing of the recipe
+ // I also do not really like that this runs on refresh, might need some better handling
+
title := "Potion - View Recipe"
page := pages.RecipePage(*recipe, *user)
diff --git a/internal/app/server/server.go b/internal/app/server/server.go
index 406169b..33865f9 100644
--- a/internal/app/server/server.go
+++ b/internal/app/server/server.go
@@ -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,
+ UserService: userService,
+ AuthService: authService,
+ RecipeService: recipeService,
+ EngagementService: engagementService,
}
// Apply middleware
diff --git a/internal/app/service/engagement_service.go b/internal/app/service/engagement_service.go
new file mode 100644
index 0000000..1ff43fb
--- /dev/null
+++ b/internal/app/service/engagement_service.go
@@ -0,0 +1,60 @@
+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,
+ }
+}
+
+// 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)
+ if err != nil {
+ return domain.Engagement{}, err
+ }
+
+ message := fmt.Sprintf("Viewed \"%s\"", recipe.Title)
+
+ return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementViewed)
+}
+
+// UserLikeRecipe 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) UserLikeRecipe(userId, recipeId int) (domain.Engagement, error) {
+ recipe, err := s.recipeRepository.GetRecipe(recipeId)
+ if err != nil {
+ return domain.Engagement{}, err
+ }
+
+ message := fmt.Sprintf("Liked \"%s\"", recipe.Title)
+
+ return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementLiked)
+}
+
+// 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)
+}
diff --git a/internal/domain/engagement/engagement.go b/internal/domain/engagement/engagement.go
new file mode 100644
index 0000000..92ca859
--- /dev/null
+++ b/internal/domain/engagement/engagement.go
@@ -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
+}
diff --git a/internal/domain/engagement/repository.go b/internal/domain/engagement/repository.go
new file mode 100644
index 0000000..204682a
--- /dev/null
+++ b/internal/domain/engagement/repository.go
@@ -0,0 +1,7 @@
+package domain
+
+type EngagementRepository interface {
+ AddUserEngagement(userId int, message string, engagementType EngagementType) (Engagement, error)
+ AddUserEntityEngagement(userId, entityId int, message string, engagementType EngagementType) (Engagement, error)
+ GetUserEngagement(userId, limit int) ([]Engagement, error)
+}
diff --git a/internal/domain/engagement/service.go b/internal/domain/engagement/service.go
new file mode 100644
index 0000000..a4884b4
--- /dev/null
+++ b/internal/domain/engagement/service.go
@@ -0,0 +1,7 @@
+package domain
+
+type EngagementService interface {
+ UserViewRecipe(userId, recipeId int) (Engagement, error)
+ UserLikeRecipe(userId, recipeId int) (Engagement, error)
+ GetUserEngagement(userId, limit int) ([]Engagement, error)
+}
diff --git a/internal/domain/server/server.go b/internal/domain/server/server.go
index 71f9fa5..a8861aa 100644
--- a/internal/domain/server/server.go
+++ b/internal/domain/server/server.go
@@ -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"
)
@@ -11,12 +12,13 @@ import (
// InjectedDependencies is a collection of dependencies that are injected into the application. They
// are stored in the context and can be accessed by handlers via the context.
type InjectedDependencies struct {
- UserService domainUser.UserService
- AuthService domainAuth.AuthService
- RecipeService domainRecipe.RecipeService
+ 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
+// JwtClaims is the data stored in the JSON web token. All that is needed is the users ID and their
// Google email provided.
type JwtClaims struct {
UserId int `json:"id"`
diff --git a/internal/infrastructure/database/migrations/006_create_engagment_enum.sql b/internal/infrastructure/database/migrations/006_create_engagment_enum.sql
new file mode 100644
index 0000000..8d64d23
--- /dev/null
+++ b/internal/infrastructure/database/migrations/006_create_engagment_enum.sql
@@ -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;
diff --git a/internal/infrastructure/database/migrations/007_create_engagement_table.sql b/internal/infrastructure/database/migrations/007_create_engagement_table.sql
new file mode 100644
index 0000000..a8831ff
--- /dev/null
+++ b/internal/infrastructure/database/migrations/007_create_engagement_table.sql
@@ -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;
diff --git a/internal/infrastructure/database/repository/engagement_repository.go b/internal/infrastructure/database/repository/engagement_repository.go
new file mode 100644
index 0000000..4d9faf8
--- /dev/null
+++ b/internal/infrastructure/database/repository/engagement_repository.go
@@ -0,0 +1,153 @@
+package repository
+
+import (
+ "database/sql"
+ "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
+ if err := tx.QueryRow(query, engagementType, message, userId, time.Now()).Scan(
+ &engagement.Id,
+ &engagement.Type,
+ &engagement.Message,
+ &engagement.Entity,
+ &engagement.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
+ }
+
+ return engagement, nil
+}
+
+// AddUserEngagement 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.
+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
+ if err := tx.QueryRow(query, engagementType, message, entityId, userId, time.Now()).Scan(
+ &engagement.Id,
+ &engagement.Type,
+ &engagement.Message,
+ &engagement.Entity,
+ &engagement.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
+ }
+
+ 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
+ if err := rows.Scan(
+ &engagement.Id,
+ &engagement.Type,
+ &engagement.Message,
+ &engagement.Entity,
+ &engagement.UserId,
+ &engagement.Created,
+ ); err != nil {
+ tx.Rollback()
+ return []domain.Engagement{}, fmt.Errorf("Failed to scan user engagement. %s", err.Error())
+ }
+
+ engagements = append(engagements, engagement)
+ }
+
+ if err := tx.Commit(); err != nil {
+ tx.Rollback()
+ return []domain.Engagement{}, err
+ }
+
+ return engagements, err
+}
diff --git a/internal/templates/pages/profile.templ b/internal/templates/pages/profile.templ
index 00e0a76..1ed940c 100644
--- a/internal/templates/pages/profile.templ
+++ b/internal/templates/pages/profile.templ
@@ -6,178 +6,157 @@ 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 {
- case 1:
- return "Beginner"
- case 2:
- return "Easy"
- case 3:
- return "Intermediate"
- case 4:
- return "Challenging"
- case 5:
- return "Extreme"
- default:
- return ""
- }
+switch diff {
+case 1:
+return "Beginner"
+case 2:
+return "Easy"
+case 3:
+return "Intermediate"
+case 4:
+return "Challenging"
+case 5:
+return "Extreme"
+default:
+return ""
+}
}
func displayTags(tags []domainRecipe.Tag) string {
- names := make([]string, 0, len(tags))
- for _, tag := range tags {
- names = append(names, tag.Name)
- }
- return strings.Join(names, ", ")
+names := make([]string, 0, len(tags))
+for _, tag := range tags {
+names = append(names, tag.Name)
+}
+return strings.Join(names, ", ")
}
templ userDetailsSection(user domainUser.User, recipeCount int) {
- { user.Email } { recipeCount } recipes 0 favorites { user.Email } { recipeCount } recipes 0 favorites Favorites section is under construction! Favorites section is under construction! Activity section is under construction! Activity section is under construction!
+
} else {
-
-
+
}
-
{ user.Name }
- { user.Name }
+ My Recipes
-
- if len(recipes) <= 4 {
- for _, recipe :=range recipes {
- @recipeListItem(recipe)
- }
- } else {
- for _, recipe := range recipes[:4] {
- @recipeListItem(recipe)
- }
- }
-
-
- My Recipes
+
+ if len(recipes) <= 4 { for _, recipe :=range recipes { @recipeListItem(recipe) } } else { for _, recipe :=range
+ recipes[:4] { @recipeListItem(recipe) } }
+
+My Favorites
- My Favorites
+ Recent Activity
- Recent Activity
+
+ for _, eng := range engagement {
+ @activityListItem(eng)
+ }
+
+
+
- Difficulty: { displayDifficulty(recipe.Difficulty) } - | Duration: { recipe.Duration.Total } min - | Category: { recipe.Category } -
-- Difficulty: { displayDifficulty(recipe.Difficulty) } -
-- Duration: { recipe.Duration.Total } min -
-- Category: { recipe.Category } -
- if len(recipe.Tags) > 0 { -- Tags: { displayTags(recipe.Tags) } -
- } -+ Difficulty: { displayDifficulty(recipe.Difficulty) } + | Duration: { recipe.Duration.Total } min + | Category: { recipe.Category } +
++ Difficulty: { displayDifficulty(recipe.Difficulty) } +
++ Duration: { recipe.Duration.Total } min +
++ Category: { recipe.Category } +
+ if len(recipe.Tags) > 0 { ++ Tags: { displayTags(recipe.Tags) } +
+ } +- Rated "Spicy Chicken Wings" -
-- 2 days ago -
-+ { engagement.Message } +
++ { engagement.Created.Format("01/02/2006") } +
+Activity section is under construction!
Activity section is under construction!
Difficulty: ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
Difficulty: ") 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: 122, 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, " | Duration: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " | Duration: ") 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: 123, 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 | Category: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " min | Category: ") 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: 124, 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, "
Difficulty: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
Difficulty: ") 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: 127, 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, "
Duration: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
Duration: ") 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: 130, 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
Category: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " min
Category: ") 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: 133, 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, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(recipe.Tags) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Tags: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
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: 137, 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, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Rated \"Spicy Chicken Wings\"
2 days ago
") + 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: 148, Col: 24} + } + _, 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, "
") + 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: 151, Col: 47} + } + _, 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, "