From d2835c636c02d946872e02eb0e1ef469b6adb12e Mon Sep 17 00:00:00 2001
From: Hayden Hargreaves
Date: Sun, 13 Jul 2025 21:34:54 -0700
Subject: [PATCH] (DB/FEAT): Began the implementation of the user engagement!
The database requirements have been added, as well as the service/repo
architecture. A few small functions have been created, but the system is
not complete by any means. More work is required to mark this task
complete.
---
doc/TechnicalSpecification.md | 27 +-
internal/app/handlers/page_handler.go | 24 +-
internal/app/server/server.go | 9 +-
internal/app/service/engagement_service.go | 60 ++++
internal/domain/engagement/engagement.go | 28 ++
internal/domain/engagement/repository.go | 7 +
internal/domain/engagement/service.go | 7 +
internal/domain/server/server.go | 10 +-
.../migrations/006_create_engagment_enum.sql | 16 ++
.../007_create_engagement_table.sql | 16 ++
.../repository/engagement_repository.go | 153 ++++++++++
internal/templates/pages/profile.templ | 269 ++++++++----------
internal/templates/pages/profile_templ.go | 117 +++++---
13 files changed, 536 insertions(+), 207 deletions(-)
create mode 100644 internal/app/service/engagement_service.go
create mode 100644 internal/domain/engagement/engagement.go
create mode 100644 internal/domain/engagement/repository.go
create mode 100644 internal/domain/engagement/service.go
create mode 100644 internal/infrastructure/database/migrations/006_create_engagment_enum.sql
create mode 100644 internal/infrastructure/database/migrations/007_create_engagement_table.sql
create mode 100644 internal/infrastructure/database/repository/engagement_repository.go
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) {
-
-
+
+
if user.ImageUrl != "" {
-

+

} else {
-

-
+

}
-
-
-
{ user.Name }
-
{ user.Email }
-
-
-
{ recipeCount } recipes
-
0 favorites
-
-
-
-
+
+
+
{ user.Name }
+
{ user.Email }
+
+
+
{ recipeCount } recipes
+
0 favorites
+
+
+
+
}
templ recipesSection(recipes []domainRecipe.Recipe) {
-
- My Recipes
-
- if len(recipes) <= 4 {
- for _, recipe :=range recipes {
- @recipeListItem(recipe)
- }
- } else {
- for _, recipe := range recipes[:4] {
- @recipeListItem(recipe)
- }
- }
-
- -
- See all...
-
-
-
-
+
+ My Recipes
+
+ if len(recipes) <= 4 { for _, recipe :=range recipes { @recipeListItem(recipe) } } else { for _, recipe :=range
+ recipes[:4] { @recipeListItem(recipe) } }
+ -
+ See all...
+
+
+
+
}
templ favoritesSection(recipes []domainRecipe.Recipe) {
-
- My Favorites
- Favorites section is under construction!
-
+
+ My Favorites
+ Favorites section is under construction!
+
}
-templ activitySection() {
-
- Recent Activity
- Activity section is under construction!
-
-
+templ activitySection(engagement []domainEngagement.Engagement) {
+
+ Recent Activity
+ Activity section is under construction!
+
+
}
templ recipeListItem(recipe domainRecipe.Recipe) {
-
-
-
- { recipe.Title }
-
-
-
- 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) }
-
- }
-
+
+
+
+ { recipe.Title }
+
+
+
+ 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) }
+
+ }
+
}
-templ activityListItem() {
-
-
- Rated "Spicy Chicken Wings"
-
-
- 2 days ago
-
-
+templ activityListItem(engagement domainEngagement.Engagement) {
+
+
+ { engagement.Message }
+
+
+ { engagement.Created.Format("01/02/2006") }
+
+
}
templ logoutSection() {
-
+
}
-templ ProfilePage(user domainUser.User, recipes []domainRecipe.Recipe) {
- @components.Navbar(" profile")
-
-
- @userDetailsSection(user, len(recipes))
- @recipesSection(recipes)
- @favoritesSection(recipes)
- @activitySection()
- @logoutSection()
-
-
+templ ProfilePage(user domainUser.User, recipes []domainRecipe.Recipe, engagement []domainEngagement.Engagement) {
+@components.Navbar(" profile")
+
+
+ @userDetailsSection(user, len(recipes))
+ @recipesSection(recipes)
+ @favoritesSection(recipes)
+ @activitySection(engagement)
+ @logoutSection()
+
+
}
diff --git a/internal/templates/pages/profile_templ.go b/internal/templates/pages/profile_templ.go
index b035f02..c6310b6 100644
--- a/internal/templates/pages/profile_templ.go
+++ b/internal/templates/pages/profile_templ.go
@@ -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: 141}
}
_, 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, "Recent Activity
Activity section is under construction!
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Recent Activity
Activity section is under construction!
")
+ 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, "- See all...
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -276,7 +287,7 @@ 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, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" class=\"hover:text-blue-600 duration-100\">")
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: 118, 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, "
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, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -407,7 +418,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 +439,33 @@ func activityListItem() templ.Component {
templ_7745c5c3_Var20 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Rated \"Spicy Chicken Wings\"
2 days ago
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "")
+ 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, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -452,21 +489,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, "")
+ 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")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -474,7 +511,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 +527,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, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -515,7 +552,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 +560,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, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}