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

- -
+
+

My Recipes

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

    - if len(recipe.Tags) > 0 { -

    - Tags: { displayTags(recipe.Tags) } -

    - } -
  • +
  • +

    + + { recipe.Title } + +

    + +

    + 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() { -
    - - Logout - -
    +
    + + Logout + +
    } -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 } @@ -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, "
    Logout
    ") + 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 }