From d2835c636c02d946872e02eb0e1ef469b6adb12e Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Sun, 13 Jul 2025 21:34:54 -0700 Subject: [PATCH 01/10] (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 } From 4a94b1a08c4338463933042f66eee7b06af5692a Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Sun, 13 Jul 2025 21:37:51 -0700 Subject: [PATCH 02/10] (UI/FIX): Fixed the retarded formatting. Templ's formatter sucks. --- internal/templates/pages/profile.templ | 263 ++++++++++++---------- internal/templates/pages/profile_templ.go | 31 +-- 2 files changed, 156 insertions(+), 138 deletions(-) diff --git a/internal/templates/pages/profile.templ b/internal/templates/pages/profile.templ index 1ed940c..1dcf8a2 100644 --- a/internal/templates/pages/profile.templ +++ b/internal/templates/pages/profile.templ @@ -9,154 +9,171 @@ 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

    -
    -
    -
    -
    +
    +
    + if user.ImageUrl != "" { + + } else { + + } +
    +
    +

    { 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(engagement []domainEngagement.Engagement) { -
    -

    Recent Activity

    -

    Activity section is under construction!

    - -
    +
    +

    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(engagement domainEngagement.Engagement) { -
  • -

    - { engagement.Message } -

    -

    - { engagement.Created.Format("01/02/2006") } -

    -
  • +
  • +

    + { engagement.Message } +

    +

    + { engagement.Created.Format("01/02/2006") } +

    +
  • } templ logoutSection() { -
    - - Logout - -
    +
    + + Logout + +
    } 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() -
    -
    + @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 c6310b6..6e31303 100644 --- a/internal/templates/pages/profile_templ.go +++ b/internal/templates/pages/profile_templ.go @@ -90,9 +90,10 @@ func userDetailsSection(user domainUser.User, recipeCount int) templ.Component { return templ_7745c5c3_Err } 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])) + 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: 47, Col: 141} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 48, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -110,7 +111,7 @@ func userDetailsSection(user domainUser.User, recipeCount int) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 52, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 53, Col: 62} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -123,7 +124,7 @@ func userDetailsSection(user domainUser.User, recipeCount int) templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 53, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 54, Col: 47} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -136,7 +137,7 @@ func userDetailsSection(user domainUser.User, recipeCount int) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(recipeCount) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 56, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 57, Col: 72} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -303,7 +304,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 118, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 119, Col: 18} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -316,7 +317,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 122, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 123, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -329,7 +330,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 123, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 124, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -342,7 +343,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 124, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 125, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -355,7 +356,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 127, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 128, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -368,7 +369,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 130, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 131, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { @@ -381,7 +382,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 133, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 134, Col: 58} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { @@ -399,7 +400,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 137, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 138, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { @@ -446,7 +447,7 @@ func activityListItem(engagement domainEngagement.Engagement) templ.Component { 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 149, Col: 23} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { @@ -459,7 +460,7 @@ func activityListItem(engagement domainEngagement.Engagement) templ.Component { 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 152, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { From acbb07cb34f829bc72f9a5e8a82d5f745602c236 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Mon, 14 Jul 2025 18:07:58 -0700 Subject: [PATCH 03/10] (UI): Small UI tweaks. --- internal/templates/pages/favorites.templ | 2 +- internal/templates/pages/favorites_templ.go | 2 +- internal/templates/pages/home.templ | 6 ++- internal/templates/pages/home_templ.go | 28 ++++++++------ internal/templates/pages/list.templ | 2 +- internal/templates/pages/list_templ.go | 2 +- internal/templates/pages/search.templ | 2 +- internal/templates/pages/search_templ.go | 4 +- web/static/css/tailwind.css | 42 +++++++++++++++++++-- 9 files changed, 65 insertions(+), 25 deletions(-) diff --git a/internal/templates/pages/favorites.templ b/internal/templates/pages/favorites.templ index 70b1267..8d0e238 100644 --- a/internal/templates/pages/favorites.templ +++ b/internal/templates/pages/favorites.templ @@ -7,7 +7,7 @@ templ FavoritesPage() {
    -

    Page Under Construction

    +

    Page Under Construction

    Sit tight, this page is coming soon!

    diff --git a/internal/templates/pages/favorites_templ.go b/internal/templates/pages/favorites_templ.go index 325310c..a09f819 100644 --- a/internal/templates/pages/favorites_templ.go +++ b/internal/templates/pages/favorites_templ.go @@ -35,7 +35,7 @@ func FavoritesPage() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

    Page Under Construction

    Sit tight, this page is coming soon!

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

    Page Under Construction

    Sit tight, this page is coming soon!

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/templates/pages/home.templ b/internal/templates/pages/home.templ index 05a0e70..3cc3a4b 100644 --- a/internal/templates/pages/home.templ +++ b/internal/templates/pages/home.templ @@ -29,8 +29,10 @@ templ introSection() { templ searchSection() {
    @components.BannerText("Craving Something Specific?") - @components.SearchBar(domainRecipe.SearchFilters{}, true, false) - +
    + @components.SearchBar(domainRecipe.SearchFilters{}, true, false) +
    +
    } diff --git a/internal/templates/pages/home_templ.go b/internal/templates/pages/home_templ.go index 2490417..4e3c435 100644 --- a/internal/templates/pages/home_templ.go +++ b/internal/templates/pages/home_templ.go @@ -70,11 +70,15 @@ func searchSection() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } templ_7745c5c3_Err = components.SearchBar(domainRecipe.SearchFilters{}, true, false).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -103,7 +107,7 @@ func highlightSection(liked bool) templ.Component { templ_7745c5c3_Var3 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -111,7 +115,7 @@ func highlightSection(liked bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

    Our 'Recipe of the Week' is the cream of the crop! We handpick it by looking at what recipes our community loves most. This isn't just about how many people view a recipe; it's also about how many times it's been made, liked, reviewed, and its average rating, all combined to find the true fan favorite of the week. It's our way of highlighting the best recipes that truly resonate with our users!

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

    Our 'Recipe of the Week' is the cream of the crop! We handpick it by looking at what recipes our community loves most. This isn't just about how many people view a recipe; it's also about how many times it's been made, liked, reviewed, and its average rating, all combined to find the true fan favorite of the week. It's our way of highlighting the best recipes that truly resonate with our users!

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -119,7 +123,7 @@ func highlightSection(liked bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -148,7 +152,7 @@ func listsSection() templ.Component { templ_7745c5c3_Var4 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -156,7 +160,7 @@ func listsSection() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

    Recently viewed

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

    Recently viewed

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -184,7 +188,7 @@ func listsSection() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

    Make again

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

    Make again

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -212,7 +216,7 @@ func listsSection() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -241,7 +245,7 @@ func ctaSection() templ.Component { templ_7745c5c3_Var5 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

    Unleash Your Inner Chef!

    Have a unique recipe idea? Want to share your culinary masterpiece with the world? It's time to bring your creations to life!

    Unleash Your Inner Chef!

    Have a unique recipe idea? Want to share your culinary masterpiece with the world? It's time to bring your creations to life!

    Create Your Recipe!
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" class=\"flex items-center justify-center\n bg-gradient-to-r from-blue-400 to-blue-600 text-white\n px-12 py-5 rounded-full shadow-sm hover:shadow-md\n transition-all duration-300 ease-in-out shadow-blue-700\n text-lg md:text-2xl font-bold uppercase tracking-wide\">Create Your Recipe!") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -283,7 +287,7 @@ func HomePage() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -307,7 +311,7 @@ func HomePage() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/templates/pages/list.templ b/internal/templates/pages/list.templ index 9e8acfd..1fcfc39 100644 --- a/internal/templates/pages/list.templ +++ b/internal/templates/pages/list.templ @@ -7,7 +7,7 @@ templ ListPage() {
    -

    Page Under Construction

    +

    Page Under Construction

    Sit tight, this page is coming soon!

    diff --git a/internal/templates/pages/list_templ.go b/internal/templates/pages/list_templ.go index 5f800d5..003bea5 100644 --- a/internal/templates/pages/list_templ.go +++ b/internal/templates/pages/list_templ.go @@ -35,7 +35,7 @@ func ListPage() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

    Page Under Construction

    Sit tight, this page is coming soon!

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

    Page Under Construction

    Sit tight, this page is coming soon!

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/templates/pages/search.templ b/internal/templates/pages/search.templ index b40f5fd..aab9696 100644 --- a/internal/templates/pages/search.templ +++ b/internal/templates/pages/search.templ @@ -48,7 +48,7 @@ templ searchResult(recipe domain.Recipe, odd bool) {

    - { recipe.Title } { recipe.Category } + { recipe.Title }

    diff --git a/internal/templates/pages/search_templ.go b/internal/templates/pages/search_templ.go index fcaf4e0..e2c4bdd 100644 --- a/internal/templates/pages/search_templ.go +++ b/internal/templates/pages/search_templ.go @@ -177,14 +177,14 @@ func searchResult(recipe domain.Recipe, odd bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -286,7 +271,7 @@ func servingIconSm() templ.Component { templ_7745c5c3_Var10 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -315,7 +300,7 @@ func timeIconSm() templ.Component { templ_7745c5c3_Var11 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -345,12 +330,12 @@ func starIconSm(filled bool) templ.Component { } ctx = templ.ClearChildren(ctx) if filled { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index b738e44..fc8d34e 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -9,6 +9,9 @@ monospace; --color-red-100: oklch(93.6% 0.032 17.717); --color-red-500: oklch(63.7% 0.237 25.331); + --color-red-600: oklch(57.7% 0.245 27.325); + --color-green-300: oklch(87.1% 0.15 154.449); + --color-green-500: oklch(72.3% 0.219 149.579); --color-blue-50: oklch(97% 0.014 254.604); --color-blue-100: oklch(93.2% 0.032 255.585); --color-blue-200: oklch(88.2% 0.059 254.128); @@ -367,6 +370,10 @@ width: calc(var(--spacing) * 5); height: calc(var(--spacing) * 5); } + .size-8 { + width: calc(var(--spacing) * 8); + height: calc(var(--spacing) * 8); + } .size-10 { width: calc(var(--spacing) * 10); height: calc(var(--spacing) * 10); @@ -422,9 +429,6 @@ .w-1 { width: calc(var(--spacing) * 1); } - .w-1\/2 { - width: calc(1/2 * 100%); - } .w-1\/3 { width: calc(1/3 * 100%); } @@ -464,21 +468,9 @@ .w-full { width: 100%; } - .max-w-1 { - max-width: calc(var(--spacing) * 1); - } - .max-w-1\/2 { - max-width: calc(1/2 * 100%); - } .max-w-2xl { max-width: var(--container-2xl); } - .max-w-4 { - max-width: calc(var(--spacing) * 4); - } - .max-w-4\/5 { - max-width: calc(4/5 * 100%); - } .flex-shrink { flex-shrink: 1; } @@ -491,9 +483,6 @@ .flex-grow { flex-grow: 1; } - .flex-grow-0 { - flex-grow: 0; - } .border-collapse { border-collapse: collapse; } @@ -668,6 +657,12 @@ .border-gray-300 { border-color: var(--color-gray-300); } + .border-green-300 { + border-color: var(--color-green-300); + } + .border-green-500 { + border-color: var(--color-green-500); + } .border-red-500 { border-color: var(--color-red-500); } @@ -766,6 +761,9 @@ .px-5 { padding-inline: calc(var(--spacing) * 5); } + .px-6 { + padding-inline: calc(var(--spacing) * 6); + } .px-8 { padding-inline: calc(var(--spacing) * 8); } @@ -914,6 +912,9 @@ .text-gray-800 { color: var(--color-gray-800); } + .text-green-500 { + color: var(--color-green-500); + } .text-red-500 { color: var(--color-red-500); } @@ -1080,6 +1081,16 @@ color: var(--color-blue-700); } } + .odd\:bg-\[\#f8f8f8\] { + &:nth-child(odd) { + background-color: #f8f8f8; + } + } + .even\:bg-\[\#f8f8f8\] { + &:nth-child(even) { + background-color: #f8f8f8; + } + } .even\:bg-gray-50 { &:nth-child(even) { background-color: var(--color-gray-50); @@ -1107,6 +1118,13 @@ } } } + .hover\:border-blue-300 { + &:hover { + @media (hover: hover) { + border-color: var(--color-blue-300); + } + } + } .hover\:border-blue-400 { &:hover { @media (hover: hover) { @@ -1233,11 +1251,6 @@ animation: var(--animate-bounce); } } - .sm\:hidden { - @media (width >= 40rem) { - display: none; - } - } .sm\:w-3\/4 { @media (width >= 40rem) { width: calc(3/4 * 100%); @@ -1298,9 +1311,10 @@ display: inline; } } - .md\:inline-block { + .md\:size-10 { @media (width >= 48rem) { - display: inline-block; + width: calc(var(--spacing) * 10); + height: calc(var(--spacing) * 10); } } .md\:size-12 { @@ -1476,6 +1490,12 @@ line-height: var(--tw-leading, var(--text-sm--line-height)); } } + .md\:text-xl { + @media (width >= 48rem) { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + } .lg\:flex { @media (width >= 64rem) { display: flex; From bebeb254923368073a33b680b20965450abd47e9 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Mon, 14 Jul 2025 21:30:45 -0700 Subject: [PATCH 05/10] (DB/FEAT): Implemented toggle favorite in the backend. The frontend is half wired up, just need to update the button. I also want to update the recipe methods to return the favorite status. This will follow very similar to the way I updated the tags. Another method which can be called to attach the favorite state. --- doc/TechnicalSpecification.md | 10 ++-- internal/app/handlers/engagement_handler.go | 4 +- internal/app/server/server.go | 2 +- internal/app/service/engagement_service.go | 23 +++++-- internal/domain/engagement/repository.go | 1 + internal/domain/engagement/service.go | 2 +- internal/domain/server/routes.go | 2 +- .../migrations/008_create_favorites_table.sql | 14 +++++ .../repository/engagement_repository.go | 60 +++++++++++++++++++ internal/templates/pages/recipe.templ | 2 +- internal/templates/pages/recipe_templ.go | 6 +- 11 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 internal/infrastructure/database/migrations/008_create_favorites_table.sql diff --git a/doc/TechnicalSpecification.md b/doc/TechnicalSpecification.md index 8af89c5..4f03d6d 100644 --- a/doc/TechnicalSpecification.md +++ b/doc/TechnicalSpecification.md @@ -299,11 +299,11 @@ found in **OTHER** section. - [x] UserId (FK: User.Id) Serial, optional for not logged in users - [x] Created (Required) date/time stamp -- [ ] Likes: **Many-to-many** table to represent a list of recipes liked by a user. - - [ ] ID (PK) *Composite key*** - - [ ] UserId (FK: User.Id, Required) Serial - - [ ] RecipeId (FK: Recipe.Id, Required) Serial - - [ ] Created (Required) date/time stamp +- [x] Favorites: **Many-to-many** table to represent a list of recipes favorites by a user. + - [x] ID (PK) *Composite key*** + - [x] UserId (FK: User.Id, Required) Serial + - [x] RecipeId (FK: Recipe.Id, Required) Serial + - [x] Created (Required) date/time stamp - [x] Tags: Represents a single tag that can be had by many recipes. - [x] ID (PK) Serial diff --git a/internal/app/handlers/engagement_handler.go b/internal/app/handlers/engagement_handler.go index 36bcddc..d57bc6b 100644 --- a/internal/app/handlers/engagement_handler.go +++ b/internal/app/handlers/engagement_handler.go @@ -33,7 +33,7 @@ func EngagementViewRecipe(ctx *gin.Context) { } } -func EngagementLikeRecipe(ctx *gin.Context) { +func EngagementFavoriteRecipe(ctx *gin.Context) { deps := ctx.MustGet("deps").(*domain.InjectedDependencies) if !domain.IsLoggedIn(ctx) { @@ -46,7 +46,7 @@ func EngagementLikeRecipe(ctx *gin.Context) { recipeId, _ := strconv.Atoi(id) userId := ctx.MustGet("userId").(int) - if _, err := deps.EngagementService.UserLikeRecipe(userId, recipeId); err != nil { + if _, err := deps.EngagementService.UserFavoriteRecipe(userId, recipeId); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "status": http.StatusInternalServerError, "message": err.Error(), diff --git a/internal/app/server/server.go b/internal/app/server/server.go index fee14fd..226aa43 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -188,7 +188,7 @@ func (s *Server) Setup() *Server { // Engagement endpoints router_api.POST("/engagement/view/:id", handlers.EngagementViewRecipe) - router_api.POST("/engagement/like/:id", handlers.EngagementLikeRecipe) + router_api.POST("/engagement/favorite/:id", handlers.EngagementFavoriteRecipe) router_api.POST("/engagement/make/:id", handlers.EngagementMakeRecipe) // Catch un-routed URLS diff --git a/internal/app/service/engagement_service.go b/internal/app/service/engagement_service.go index 27f0971..6aa2c8e 100644 --- a/internal/app/service/engagement_service.go +++ b/internal/app/service/engagement_service.go @@ -39,22 +39,35 @@ func (s *EngagementService) UserViewRecipe(userId, recipeId int) (domain.Engagem 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. +// UserFavoriteRecipe requires a user ID and a recipe ID to create an engagement record in the database. // A message will be generated using the recipe data and then used to add a like engagement to the // database. -func (s *EngagementService) UserLikeRecipe(userId, recipeId int) (domain.Engagement, error) { +func (s *EngagementService) UserFavoriteRecipe(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) + // Update the favorites DB + liked, err := s.engagementRepository.UserFavoriteRecipeToggle(userId, recipeId) + if err != nil { + return domain.Engagement{}, err + } + + // Determine if this like is a saving or unsaving + var message string + if liked { + message = fmt.Sprintf("Saved \"%s\"", recipe.Title) + } else { + message = fmt.Sprintf("Unsaved \"%s\"", recipe.Title) + } return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementLiked) + } -// 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 +// UserMakeRecipe requires a user ID and a recipe ID to create an engagement record in the database. +// A message will be generated using the recipe data and then used to add a make engagement to the // database. func (s *EngagementService) UserMakeRecipe(userId, recipeId int) (domain.Engagement, error) { recipe, err := s.recipeRepository.GetRecipe(recipeId) diff --git a/internal/domain/engagement/repository.go b/internal/domain/engagement/repository.go index 204682a..906a08e 100644 --- a/internal/domain/engagement/repository.go +++ b/internal/domain/engagement/repository.go @@ -4,4 +4,5 @@ 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) + UserFavoriteRecipeToggle(userId, recipeId int) (bool, error) } diff --git a/internal/domain/engagement/service.go b/internal/domain/engagement/service.go index 2643e91..815ac47 100644 --- a/internal/domain/engagement/service.go +++ b/internal/domain/engagement/service.go @@ -2,7 +2,7 @@ package domain type EngagementService interface { UserViewRecipe(userId, recipeId int) (Engagement, error) - UserLikeRecipe(userId, recipeId int) (Engagement, error) + UserFavoriteRecipe(userId, recipeId int) (Engagement, error) UserMakeRecipe(userId, recipeId int) (Engagement, error) GetUserEngagement(userId, limit int) ([]Engagement, error) } diff --git a/internal/domain/server/routes.go b/internal/domain/server/routes.go index 221e1fa..a6f2fab 100644 --- a/internal/domain/server/routes.go +++ b/internal/domain/server/routes.go @@ -26,7 +26,7 @@ const API_CREATE_RECIPE = VERSION + API + "/recipe" const API_SEARCH_RECIPES = VERSION + API + "/recipe/search" const API_ENGAGEMENT_VIEW = VERSION + API + "/engagement/view/%d" -const API_ENGAGEMENT_LIKE = VERSION + API + "/engagement/like/%d" +const API_ENGAGEMENT_FAVORITE = VERSION + API + "/engagement/favorite/%d" const API_ENGAGEMENT_MAKE = VERSION + API + "/engagement/make/%d" // State prefixed routes diff --git a/internal/infrastructure/database/migrations/008_create_favorites_table.sql b/internal/infrastructure/database/migrations/008_create_favorites_table.sql new file mode 100644 index 0000000..8997128 --- /dev/null +++ b/internal/infrastructure/database/migrations/008_create_favorites_table.sql @@ -0,0 +1,14 @@ +-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com) +-- Desc: Create the favorites table. +-- Date: 07/14/2025 + +BEGIN; + +CREATE TABLE IF NOT EXISTS Favorites ( + Id SERIAL PRIMARY KEY NOT NULL, + UserId INTEGER NOT NULL REFERENCES users(id), + RecipeId INTEGER NOT NULL REFERENCES recipes(id), + Created TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMIT; diff --git a/internal/infrastructure/database/repository/engagement_repository.go b/internal/infrastructure/database/repository/engagement_repository.go index 50ab3a4..e869262 100644 --- a/internal/infrastructure/database/repository/engagement_repository.go +++ b/internal/infrastructure/database/repository/engagement_repository.go @@ -2,6 +2,7 @@ package repository import ( "database/sql" + "errors" "fmt" "time" @@ -153,3 +154,62 @@ func (r *EngagementRepository) GetUserEngagement(userId, limit int) ([]domain.En return engagements, err } + +// UserFavoriteRecipeToggle toggles the status of a users favorite of a recipe. If the user has already +// favorited the provided recipe, the database entry will be delete, hence removing the favorite. Otherwise, +// an entry will be created. The NEW status of the users favorite will be returned as the boolean. Any +// errors will be bubbled to the caller. +func (r *EngagementRepository) UserFavoriteRecipeToggle(userId, recipeId int) (bool, error) { + tx, err := r.db.Begin() + if err != nil { + tx.Rollback() + return false, err + } + + query := ` + SELECT id + FROM favorites + WHERE userid = $1 AND recipeid = $2 + ` + + var id int + + err = tx.QueryRow(query, userId, recipeId).Scan(&id) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + tx.Rollback() + return false, fmt.Errorf("Failed to get recipe favorite. %s", err.Error()) + } + } + + // Means we should create + var success bool + if id == 0 { + createQuery := "INSERT INTO favorites (userid, recipeid, created) VALUES ($1, $2, $3);" + + if result, err := tx.Exec(createQuery, userId, recipeId, time.Now()); err != nil { + tx.Rollback() + return false, fmt.Errorf("Failed to create recipe favorite. %s", err.Error()) + } else { + rows, _ := result.RowsAffected() + success = rows == 1 + } + } else { + deleteQuery := "DELETE FROM favorites WHERE id = $1;" + + if result, err := tx.Exec(deleteQuery, id); err != nil { + tx.Rollback() + return false, fmt.Errorf("Failed to remove recipe favorite. %s", err.Error()) + } else { + rows, _ := result.RowsAffected() + success = rows == 1 + } + } + + if err := tx.Commit(); err != nil { + tx.Rollback() + return false, err + } + + return success, nil +} diff --git a/internal/templates/pages/recipe.templ b/internal/templates/pages/recipe.templ index 37e51f5..1d8e17f 100644 --- a/internal/templates/pages/recipe.templ +++ b/internal/templates/pages/recipe.templ @@ -228,7 +228,7 @@ templ madeButton(id int) { hx-trigger="click" hx-swap="none" id="make-button" - hx-on:click="makeButtonHandler();" + hx-on:click="makeButtonHandler();" class="flex items-center justify-center gap-x-1 rounded-lg border border-gray-300 text-gray-800 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300" > Date: Tue, 15 Jul 2025 19:17:41 -0700 Subject: [PATCH 06/10] (FEAT): Updated recipe repo to include recipe favorite status. This means we need to pass the user id into the various methods that call it. But, since it is a pointer, we can use nil if we don't have a user to check with (this is noted in the service). --- internal/app/handlers/page_handler.go | 11 ++- internal/app/service/engagement_service.go | 10 +-- internal/app/service/recipe_service.go | 4 +- internal/domain/recipe/recipe.go | 4 +- internal/domain/recipe/repository.go | 3 +- internal/domain/recipe/service.go | 2 +- .../database/repository/recipe_repository.go | 55 ++++++++++++- internal/templates/pages/recipe.templ | 8 +- internal/templates/pages/recipe_templ.go | 10 +-- web/static/css/tailwind.css | 81 ++++--------------- 10 files changed, 96 insertions(+), 92 deletions(-) diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index a11959f..b0e9f16 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -104,15 +104,22 @@ func RecipePage(ctx *gin.Context) { return } + // Get signed in user, if they exist + var userId *int = nil + if domainServer.IsLoggedIn(ctx) { + storeId := ctx.MustGet("userId").(int) + userId = &storeId + } + // Get recipe - recipe, err := deps.RecipeService.GetRecipe(parsed) + recipe, err := deps.RecipeService.GetRecipe(parsed, userId) if err != nil { fmt.Printf("ERROR: %s\n", err.Error()) ctx.JSON(400, err.Error()) return } - // Get user + // Get user (owner) user, err := deps.UserService.GetUser(recipe.UserId) if err != nil { fmt.Printf("ERROR: %s\n", err.Error()) diff --git a/internal/app/service/engagement_service.go b/internal/app/service/engagement_service.go index 6aa2c8e..362b9ad 100644 --- a/internal/app/service/engagement_service.go +++ b/internal/app/service/engagement_service.go @@ -29,7 +29,7 @@ func NewEngagementService(engagementRepository domain.EngagementRepository, reci // 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) + recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId) if err != nil { return domain.Engagement{}, err } @@ -43,17 +43,17 @@ func (s *EngagementService) UserViewRecipe(userId, recipeId int) (domain.Engagem // A message will be generated using the recipe data and then used to add a like engagement to the // database. func (s *EngagementService) UserFavoriteRecipe(userId, recipeId int) (domain.Engagement, error) { - recipe, err := s.recipeRepository.GetRecipe(recipeId) + recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId) if err != nil { return domain.Engagement{}, err } - // Update the favorites DB + // Update the favorites DB liked, err := s.engagementRepository.UserFavoriteRecipeToggle(userId, recipeId) if err != nil { return domain.Engagement{}, err } - + // Determine if this like is a saving or unsaving var message string if liked { @@ -70,7 +70,7 @@ func (s *EngagementService) UserFavoriteRecipe(userId, recipeId int) (domain.Eng // A message will be generated using the recipe data and then used to add a make engagement to the // database. func (s *EngagementService) UserMakeRecipe(userId, recipeId int) (domain.Engagement, error) { - recipe, err := s.recipeRepository.GetRecipe(recipeId) + recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId) if err != nil { return domain.Engagement{}, err } diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index 870f053..87cdae8 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -123,8 +123,8 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) { // GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore, // if the recipe is nil, an error will be returned, so the caller does not need to check for a nil // recipe (e.g., if the error is nil the recipe exists) -func (s *RecipeService) GetRecipe(id int) (*domain.Recipe, error) { - recipe, err := s.recipeRepository.GetRecipe(id) +func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) { + recipe, err := s.recipeRepository.GetRecipe(id, userId) if recipe == nil { return nil, fmt.Errorf("Failed to get recipe from database. Nil result.") diff --git a/internal/domain/recipe/recipe.go b/internal/domain/recipe/recipe.go index 013a091..bad14a2 100644 --- a/internal/domain/recipe/recipe.go +++ b/internal/domain/recipe/recipe.go @@ -55,7 +55,8 @@ type RecipeIngredient struct { // Recipe is the database model of a recipe. There is no need to map to a different model so // this will remain in the domain. The Tags field should be loaded from the external Tags table, -// but is still attached to this domain object. +// but is still attached to this domain object. The Favorite field should also be loaded from +// the external favorites table, these are user specific. type Recipe struct { Id int Title string @@ -70,6 +71,7 @@ type Recipe struct { Modified *time.Time // Pointer to allow null Created time.Time Tags []Tag + Favorite bool // Per requesting user } // SearchFilters is a model which represents the required filters to complete a recipe search. diff --git a/internal/domain/recipe/repository.go b/internal/domain/recipe/repository.go index a5c57d9..d1a3a6e 100644 --- a/internal/domain/recipe/repository.go +++ b/internal/domain/recipe/repository.go @@ -2,9 +2,10 @@ package domain type RecipeRepository interface { CreateRecipe(recipe *Recipe) error - GetRecipe(id int) (*Recipe, error) + GetRecipe(id int, userId *int) (*Recipe, error) SearchRecipes(filters SearchFilters) ([]Recipe, error) CreateRecipeTags(recipe Recipe, tags []string) error GetUserRecipes(id int) ([]Recipe, error) GetRecipeTags(recipe *Recipe) error + GetRecipeFavorite(recipe *Recipe, userId int) error } diff --git a/internal/domain/recipe/service.go b/internal/domain/recipe/service.go index b3466f4..33d919a 100644 --- a/internal/domain/recipe/service.go +++ b/internal/domain/recipe/service.go @@ -4,7 +4,7 @@ import "github.com/gin-gonic/gin" type RecipeService interface { CreateRecipe(ctx *gin.Context) (*Recipe, error) - GetRecipe(id int) (*Recipe, error) + GetRecipe(id int, userId *int) (*Recipe, error) SearchRecipes(filters SearchFilters) ([]Recipe, error) GetUserRecipes(id int) ([]Recipe, error) } diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go index e92739a..da5c6f7 100644 --- a/internal/infrastructure/database/repository/recipe_repository.go +++ b/internal/infrastructure/database/repository/recipe_repository.go @@ -92,7 +92,7 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error { // GetRecipe gets a recipe from the database via its ID. The operation is wrapped in a transaction // for added safety. The repository will not check for a nil result, instead the service will. Callers // are responsible for protecting against double nil results. Any errors will be bubbled to the caller. -func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) { +func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) { tx, err := r.db.Begin() if err != nil { tx.Rollback() @@ -153,7 +153,18 @@ func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) { } // Add tags - r.GetRecipeTags(&recipe) + if err := r.GetRecipeTags(&recipe); err != nil { + fmt.Printf("ERROR getting recipe tags. %s\n", err.Error()) + } + + // Get favorite status, if user id is provided + if userId != nil { + if err := r.GetRecipeFavorite(&recipe, *userId); err != nil { + fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error()) + } + } else { + recipe.Favorite = false + } if err := tx.Commit(); err != nil { tx.Rollback() @@ -368,7 +379,9 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain } // Add tags - r.GetRecipeTags(&recipe) + if err := r.GetRecipeTags(&recipe); err != nil { + fmt.Printf("ERROR getting recipe tags. %s\n", err.Error()) + } recipes = append(recipes, recipe) } @@ -517,7 +530,14 @@ func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) { } // Add tags - r.GetRecipeTags(&recipe) + if err := r.GetRecipeTags(&recipe); err != nil { + fmt.Printf("ERROR getting recipe tags. %s\n", err.Error()) + } + + // Get favorite status + if err := r.GetRecipeFavorite(&recipe, id); err != nil { + fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error()) + } recipes = append(recipes, recipe) } @@ -571,3 +591,30 @@ func (r *RecipeRepository) GetRecipeTags(recipe *domain.Recipe) error { return nil } + +// GetRecipeFavorite requires a recipe to be filled with at least an ID. This function will use the +// ID defined in the provided recipe to fill the favorite status of the recipe, based on the provided +// userId. The recipe is modified in place and is not returned. Any errors will be bubbled to the caller. +func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int) error { + tx, err := r.db.Begin() + if err != nil { + tx.Rollback() + return err + } + + query := ` + SELECT COUNT(*) + FROM favorites + WHERE recipeid = $1 AND userid = $2; + ` + + var count int + if err := tx.QueryRow(query, recipe.Id, userId).Scan(&count); err != nil { + tx.Rollback() + return fmt.Errorf("Failed to get recipe favorite. %s", err.Error()) + } + + recipe.Favorite = count > 0 + + return nil +} diff --git a/internal/templates/pages/recipe.templ b/internal/templates/pages/recipe.templ index 1d8e17f..202ac5d 100644 --- a/internal/templates/pages/recipe.templ +++ b/internal/templates/pages/recipe.templ @@ -186,10 +186,10 @@ templ tagListItem(content string) { templ favoriteButton(favorited bool, id int) { if favorited {
    @metadataSection(recipe) - @buttonSection(false, recipe.Id) + @buttonSection(recipe.Favorite, recipe.Id)

    About this recipe

    { recipe.Description }

    diff --git a/internal/templates/pages/recipe_templ.go b/internal/templates/pages/recipe_templ.go index 1b250d8..0e2b1b1 100644 --- a/internal/templates/pages/recipe_templ.go +++ b/internal/templates/pages/recipe_templ.go @@ -1,4 +1,4 @@ - // Code generated by templ - DO NOT EDIT. +// Code generated by templ - DO NOT EDIT. // templ: version: v0.3.865 package templates @@ -586,13 +586,13 @@ func favoriteButton(favorited bool, id int) templ.Component { var templ_7745c5c3_Var22 string templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 189, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 189, Col: 66} } _, 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, 39, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"flex items-center justify-center gap-x-1 rounded-lg border border-gray-300 text-gray-800 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300\"> Unfavorite") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"flex items-center justify-center gap-x-1 rounded-lg border border-blue-300 bg-blue-50 text-gray-800 px-6 py-3 flex-grow hover:bg-blue-100 hover:border-blue-500 duration-300\"> Unfavorite") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -604,7 +604,7 @@ func favoriteButton(favorited bool, id int) templ.Component { var templ_7745c5c3_Var23 string templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 204, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 204, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { @@ -807,7 +807,7 @@ func RecipePage(recipe domain.Recipe, user domainUser.User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = buttonSection(false, recipe.Id).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = buttonSection(recipe.Favorite, recipe.Id).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index fc8d34e..14b37e1 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -9,7 +9,6 @@ monospace; --color-red-100: oklch(93.6% 0.032 17.717); --color-red-500: oklch(63.7% 0.237 25.331); - --color-red-600: oklch(57.7% 0.245 27.325); --color-green-300: oklch(87.1% 0.15 154.449); --color-green-500: oklch(72.3% 0.219 149.579); --color-blue-50: oklch(97% 0.014 254.604); @@ -240,9 +239,6 @@ .static { position: static; } - .top-1 { - top: calc(var(--spacing) * 1); - } .top-1\/2 { top: calc(1/2 * 100%); } @@ -252,9 +248,6 @@ .left-0 { left: calc(var(--spacing) * 0); } - .left-1 { - left: calc(var(--spacing) * 1); - } .left-1\/2 { left: calc(1/2 * 100%); } @@ -426,18 +419,12 @@ .min-h-screen { min-height: 100vh; } - .w-1 { - width: calc(var(--spacing) * 1); - } .w-1\/3 { width: calc(1/3 * 100%); } .w-1\/4 { width: calc(1/4 * 100%); } - .w-3 { - width: calc(var(--spacing) * 3); - } .w-3\/4 { width: calc(3/4 * 100%); } @@ -450,9 +437,6 @@ .w-5 { width: calc(var(--spacing) * 5); } - .w-9 { - width: calc(var(--spacing) * 9); - } .w-9\/10 { width: calc(9/10 * 100%); } @@ -471,9 +455,6 @@ .max-w-2xl { max-width: var(--container-2xl); } - .flex-shrink { - flex-shrink: 1; - } .flex-shrink-0 { flex-shrink: 0; } @@ -483,21 +464,10 @@ .flex-grow { flex-grow: 1; } - .border-collapse { - border-collapse: collapse; - } - .-translate-x-1 { - --tw-translate-x: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .-translate-x-1\/2 { --tw-translate-x: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); } - .-translate-y-1 { - --tw-translate-y: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -510,15 +480,9 @@ --tw-scale-y: 50%; scale: var(--tw-scale-x) var(--tw-scale-y); } - .transform { - transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); - } .cursor-pointer { cursor: pointer; } - .resize { - resize: both; - } .resize-none { resize: none; } @@ -657,9 +621,6 @@ .border-gray-300 { border-color: var(--color-gray-300); } - .border-green-300 { - border-color: var(--color-green-300); - } .border-green-500 { border-color: var(--color-green-500); } @@ -669,9 +630,6 @@ .border-white { border-color: var(--color-white); } - .bg-\[\#f8f8f8\] { - background-color: #f8f8f8; - } .bg-black { background-color: var(--color-black); } @@ -1132,6 +1090,20 @@ } } } + .hover\:border-blue-500 { + &:hover { + @media (hover: hover) { + border-color: var(--color-blue-500); + } + } + } + .hover\:bg-blue-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-blue-100); + } + } + } .hover\:bg-blue-200 { &:hover { @media (hover: hover) { @@ -1537,26 +1509,6 @@ inherits: false; initial-value: 1; } -@property --tw-rotate-x { - syntax: "*"; - inherits: false; -} -@property --tw-rotate-y { - syntax: "*"; - inherits: false; -} -@property --tw-rotate-z { - syntax: "*"; - inherits: false; -} -@property --tw-skew-x { - syntax: "*"; - inherits: false; -} -@property --tw-skew-y { - syntax: "*"; - inherits: false; -} @property --tw-border-style { syntax: "*"; inherits: false; @@ -1766,11 +1718,6 @@ --tw-scale-x: 1; --tw-scale-y: 1; --tw-scale-z: 1; - --tw-rotate-x: initial; - --tw-rotate-y: initial; - --tw-rotate-z: initial; - --tw-skew-x: initial; - --tw-skew-y: initial; --tw-border-style: solid; --tw-gradient-position: initial; --tw-gradient-from: #0000; From 7e355d5edadae1016a6f45fde6406479030883be Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Tue, 15 Jul 2025 19:44:19 -0700 Subject: [PATCH 07/10] (UI/FIX): Fixed the favorite button rendering and updating! I don't like the way that templ requires JS, but it is what it is. I just don't want to return HTML from the server. --- internal/app/handlers/page_handler.go | 5 +- internal/app/service/recipe_service.go | 4 + internal/templates/pages/recipe.templ | 77 ++++++++++++++++--- internal/templates/pages/recipe_templ.go | 98 ++++++++++++++++-------- 4 files changed, 137 insertions(+), 47 deletions(-) diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index b0e9f16..a5c4ec0 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -106,7 +106,8 @@ func RecipePage(ctx *gin.Context) { // Get signed in user, if they exist var userId *int = nil - if domainServer.IsLoggedIn(ctx) { + var loggedIn = domainServer.IsLoggedIn(ctx) + if loggedIn { storeId := ctx.MustGet("userId").(int) userId = &storeId } @@ -139,7 +140,7 @@ func RecipePage(ctx *gin.Context) { // 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) + page := pages.RecipePage(*recipe, *user, loggedIn) ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page)) } diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index 87cdae8..0f5a68b 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -123,6 +123,10 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) { // GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore, // if the recipe is nil, an error will be returned, so the caller does not need to check for a nil // recipe (e.g., if the error is nil the recipe exists) +// +// A userId should be provided to allow the favorite status to be updated. Without a userId (nil), +// the favorite status will return false, not because its not a favorite, but because it cannot find +// out! func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) { recipe, err := s.recipeRepository.GetRecipe(id, userId) diff --git a/internal/templates/pages/recipe.templ b/internal/templates/pages/recipe.templ index 202ac5d..f853b55 100644 --- a/internal/templates/pages/recipe.templ +++ b/internal/templates/pages/recipe.templ @@ -151,7 +151,7 @@ templ tagList(tags []domain.Tag, created time.Time, modified *time.Time) { templ ingredientListItem(name, quantity string) {
  • @@ -173,7 +173,7 @@ templ instructionListItem(num int, content string) {

    { num }

    -

    { content }

    +

    { content }

  • } @@ -183,13 +183,17 @@ templ tagListItem(content string) { } -templ favoriteButton(favorited bool, id int) { +templ favoriteButton(favorited bool, id int, loggedIn bool) { if favorited { - `; } + function favoriteButtonHandler() { + const button = document.getElementById("favorite-button"); + + console.log(button.classList); + console.log(button.classList.contains("border-blue-300")); + + const toggleClasses = [ + "border-gray-300", "hover:bg-gray-50", "hover:border-blue-300", + "border-blue-300", "bg-blue-50", "hover:bg-blue-100", "hover:border-blue-500" + ]; + + for (const cls of toggleClasses) { + console.log("toggling class " + cls); + button.classList.toggle(cls); + } + + if (!button.classList.contains("border-blue-300")) { + button.innerHTML = ` + + + + Favorite + `; + + } else { + button.innerHTML = ` + + + + Unfavorite + `; + } + + } + } diff --git a/internal/templates/pages/recipe_templ.go b/internal/templates/pages/recipe_templ.go index 0e2b1b1..e26ba3f 100644 --- a/internal/templates/pages/recipe_templ.go +++ b/internal/templates/pages/recipe_templ.go @@ -426,7 +426,7 @@ func ingredientListItem(name, quantity string) templ.Component { templ_7745c5c3_Var13 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
  • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -494,14 +494,14 @@ func instructionListItem(num int, content string) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(content) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 176, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 176, Col: 32} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { @@ -557,7 +557,7 @@ func tagListItem(content string) templ.Component { }) } -func favoriteButton(favorited bool, id int) templ.Component { +func favoriteButton(favorited bool, id int, loggedIn bool) 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 { @@ -592,25 +592,45 @@ func favoriteButton(favorited bool, id int) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"flex items-center justify-center gap-x-1 rounded-lg border border-blue-300 bg-blue-50 text-gray-800 px-6 py-3 flex-grow hover:bg-blue-100 hover:border-blue-500 duration-300\"> Unfavorite") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" hx-trigger=\"click\" hx-swap=\"none\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if loggedIn { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " hx-on:click=\"favoriteButtonHandler();\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, " class=\"flex items-center justify-center gap-x-1 rounded-lg border border-blue-300 bg-blue-50 text-gray-800 px-6 py-3 flex-grow hover:bg-blue-100 hover:border-blue-500 duration-300\" id=\"favorite-button\"> Unfavorite") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" hx-trigger=\"click\" hx-swap=\"none\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if loggedIn { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " hx-on:click=\"favoriteButtonHandler();\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, " class=\"flex items-center justify-center gap-x-1 rounded-lg border border-gray-300 text-gray-800 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300\" id=\"favorite-button\"> Favorite") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -619,7 +639,7 @@ func favoriteButton(favorited bool, id int) templ.Component { }) } -func madeButton(id int) templ.Component { +func madeButton(id int, loggedIn bool) 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 { @@ -640,20 +660,30 @@ func madeButton(id int) templ.Component { templ_7745c5c3_Var24 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" hx-trigger=\"click\" hx-swap=\"none\" id=\"make-button\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if loggedIn { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " hx-on:click=\"makeButtonHandler();\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " class=\"flex items-center justify-center gap-x-1 rounded-lg border border-gray-300 text-gray-800 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300\"> Made This!") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -682,7 +712,7 @@ func shareButton() templ.Component { templ_7745c5c3_Var26 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -690,7 +720,7 @@ func shareButton() templ.Component { }) } -func buttonSection(favorited bool, id int) templ.Component { +func buttonSection(favorited bool, id int, loggedIn bool) 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 { @@ -711,15 +741,15 @@ func buttonSection(favorited bool, id int) templ.Component { templ_7745c5c3_Var27 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = favoriteButton(favorited, id).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = favoriteButton(favorited, id, loggedIn).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = madeButton(id).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = madeButton(id, loggedIn).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -727,7 +757,7 @@ func buttonSection(favorited bool, id int) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -735,7 +765,7 @@ func buttonSection(favorited bool, id int) templ.Component { }) } -func RecipePage(recipe domain.Recipe, user domainUser.User) templ.Component { +func RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool) 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 { @@ -760,46 +790,46 @@ func RecipePage(recipe domain.Recipe, user domainUser.User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
    \"\"

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
    \"\"

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var29 string templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 287, Col: 75} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 297, Col: 75} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "

    Author: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

    Author: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 288, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 298, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "

    Category: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "

    Category: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var31 string templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 289, Col: 69} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 299, Col: 69} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -807,24 +837,24 @@ func RecipePage(recipe domain.Recipe, user domainUser.User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = buttonSection(recipe.Favorite, recipe.Id).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = buttonSection(recipe.Favorite, recipe.Id, loggedIn).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "

    About this recipe

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "

    About this recipe

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var32 string templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 295, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 305, Col: 49} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -840,7 +870,7 @@ func RecipePage(recipe domain.Recipe, user domainUser.User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -873,19 +903,19 @@ func scripts(id int) templ.Component { templ_7745c5c3_Var33 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\"\n navigator.clipboard.writeText(url).then(() => {\n button.outerHTML = `\n \n `;\n\n setTimeout(() => {\n const newButton = document.getElementById(\"share-button\");\n newButton.outerHTML = before;\n }, 2000);\n });\n } else {\n console.warn(\"Clipboard API not available.\");\n\n button.outerHTML = `\n \n `;\n\n setTimeout(() => {\n const newButton = document.getElementById(\"share-button\");\n newButton.outerHTML = before;\n }, 2000);\n }\n }\n\n function makeButtonHandler() {\n const button = document.getElementById(\"make-button\");\n\n button.outerHTML = `\n \n \n \n \n \n \n Made This!\n \n `;\n }\n\n function favoriteButtonHandler() {\n const button = document.getElementById(\"favorite-button\");\n\n console.log(button.classList);\n console.log(button.classList.contains(\"border-blue-300\"));\n\n const toggleClasses = [\n \"border-gray-300\", \"hover:bg-gray-50\", \"hover:border-blue-300\",\n \"border-blue-300\", \"bg-blue-50\", \"hover:bg-blue-100\", \"hover:border-blue-500\"\n ];\n\n for (const cls of toggleClasses) {\n console.log(\"toggling class \" + cls);\n button.classList.toggle(cls);\n }\n\n if (!button.classList.contains(\"border-blue-300\")) {\n button.innerHTML = `\n \n \n \n Favorite\n `;\n\n } else {\n button.innerHTML = `\n \n \n \n Unfavorite\n `;\n }\n\n }\n\n") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } From e4c1a575be9133ec4b4dc71d36f0c034b6dbf180 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Tue, 15 Jul 2025 20:14:32 -0700 Subject: [PATCH 08/10] (FEAT): Implemented anon engagements. This required some fixing of old repo methods, since the nullable user id was a bit hard to parse. But it should be working now. --- internal/app/handlers/page_handler.go | 14 +- internal/app/service/engagement_service.go | 14 ++ internal/domain/engagement/repository.go | 2 + internal/domain/engagement/service.go | 1 + .../repository/engagement_repository.go | 152 +++++++++++++++++- 5 files changed, 173 insertions(+), 10 deletions(-) diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index a5c4ec0..7c5c48d 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -129,15 +129,21 @@ func RecipePage(ctx *gin.Context) { } // Add engagement - if user != nil { - if _, err = deps.EngagementService.UserViewRecipe(user.Id, recipe.Id); err != nil { + if loggedIn { + fmt.Println("CALLING USER VIEW") + if _, err = deps.EngagementService.UserViewRecipe(*userId, recipe.Id); err != nil { + fmt.Printf("ERROR: %s\n", err.Error()) + ctx.JSON(400, err.Error()) + return + } + } else { + fmt.Println("CALLING VIEW") + if _, err = deps.EngagementService.ViewRecipe(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, loggedIn) diff --git a/internal/app/service/engagement_service.go b/internal/app/service/engagement_service.go index 362b9ad..9e7bf92 100644 --- a/internal/app/service/engagement_service.go +++ b/internal/app/service/engagement_service.go @@ -25,6 +25,20 @@ func NewEngagementService(engagementRepository domain.EngagementRepository, reci } } +// ViewRecipe requires a user ID and a recipe ID to create an engagement record in the database. +// A message will be generated using the recipe data and then used to add a view engagement to the +// database. +func (s *EngagementService) ViewRecipe(recipeId int) (domain.Engagement, error) { + recipe, err := s.recipeRepository.GetRecipe(recipeId, nil) + if err != nil { + return domain.Engagement{}, err + } + + message := fmt.Sprintf("Viewed \"%s\"", recipe.Title) + + return s.engagementRepository.AddEntityEngagement(recipeId, message, domain.EngagementViewed) +} + // 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. diff --git a/internal/domain/engagement/repository.go b/internal/domain/engagement/repository.go index 906a08e..294b794 100644 --- a/internal/domain/engagement/repository.go +++ b/internal/domain/engagement/repository.go @@ -3,6 +3,8 @@ package domain type EngagementRepository interface { AddUserEngagement(userId int, message string, engagementType EngagementType) (Engagement, error) AddUserEntityEngagement(userId, entityId int, message string, engagementType EngagementType) (Engagement, error) + AddEngagement(message string, engagementType EngagementType) (Engagement, error) + AddEntityEngagement(entityId int, message string, engagementType EngagementType) (Engagement, error) GetUserEngagement(userId, limit int) ([]Engagement, error) UserFavoriteRecipeToggle(userId, recipeId int) (bool, error) } diff --git a/internal/domain/engagement/service.go b/internal/domain/engagement/service.go index 815ac47..a4dd0fc 100644 --- a/internal/domain/engagement/service.go +++ b/internal/domain/engagement/service.go @@ -1,6 +1,7 @@ package domain type EngagementService interface { + ViewRecipe(recipeId int) (Engagement, error) UserViewRecipe(userId, recipeId int) (Engagement, error) UserFavoriteRecipe(userId, recipeId int) (Engagement, error) UserMakeRecipe(userId, recipeId int) (Engagement, error) diff --git a/internal/infrastructure/database/repository/engagement_repository.go b/internal/infrastructure/database/repository/engagement_repository.go index e869262..16cdc9b 100644 --- a/internal/infrastructure/database/repository/engagement_repository.go +++ b/internal/infrastructure/database/repository/engagement_repository.go @@ -44,12 +44,13 @@ func (r *EngagementRepository) AddUserEngagement(userId int, message string, eng ` var engagement domain.Engagement + var engUserId sql.NullInt32 if err := tx.QueryRow(query, engagementType, message, userId, time.Now()).Scan( &engagement.Id, &engagement.Type, &engagement.Message, &engagement.Entity, - &engagement.UserId, + &engUserId, &engagement.Created, ); err != nil { tx.Rollback() @@ -61,10 +62,15 @@ func (r *EngagementRepository) AddUserEngagement(userId int, message string, eng return domain.Engagement{}, err } + // Is user is valid + if engUserId.Valid { + engagement.UserId = int(engUserId.Int32) + } + return engagement, nil } -// AddUserEngagement creates an engagement record in the database with the user ID provided. This +// AddUserEntityEngagement creates an engagement record in the database with the user ID provided. This // function requires an entity ID as it should be used when there is a reference to external an // entity. The message should be provided, but a blank string ("") is acceptable. The engagement // type parameter determines the labeling of the engagement in the database. Any errors will be @@ -87,12 +93,13 @@ func (r *EngagementRepository) AddUserEntityEngagement(userId, entityId int, mes ` var engagement domain.Engagement + var engUserId sql.NullInt32 if err := tx.QueryRow(query, engagementType, message, entityId, userId, time.Now()).Scan( &engagement.Id, &engagement.Type, &engagement.Message, &engagement.Entity, - &engagement.UserId, + &engUserId, &engagement.Created, ); err != nil { tx.Rollback() @@ -104,6 +111,133 @@ func (r *EngagementRepository) AddUserEntityEngagement(userId, entityId int, mes return domain.Engagement{}, err } + // Is user is valid + if engUserId.Valid { + engagement.UserId = int(engUserId.Int32) + } + + return engagement, nil +} + +// AddEngagement creates an engagement record in the database without any user. This function does +// not accept an entity ID as it should be used when there is no need to reference an entity or user. +// The message should be provided, but a blank string ("") is acceptable. The engagement type +// parameter determines the labeling of the engagement in the database. Any errors will be bubbled +// to the caller. +// +// List of allowed engagements: viewed, shared +func (r *EngagementRepository) AddEngagement(message string, engagementType domain.EngagementType) (domain.Engagement, error) { + // Prevent invalid engagement types + switch engagementType { + case domain.EngagementViewed: + case domain.EngagementShared: + break + case domain.EngagementMade: + case domain.EngagementLiked: + case domain.EngagementReviewed: + case domain.EngagementRated: + return domain.Engagement{}, fmt.Errorf("Attempting to use disallowed anonymous engagement type.") + } + + tx, err := r.db.Begin() + if err != nil { + tx.Rollback() + return domain.Engagement{}, err + } + + query := ` + INSERT INTO Engagements ( + type, message, entity, userid, created + ) VALUES ( + $1, $2, NULL, NULL, $3 + ) RETURNING *; + ` + + var engagement domain.Engagement + var userId sql.NullInt32 + if err := tx.QueryRow(query, engagementType, message, time.Now()).Scan( + &engagement.Id, + &engagement.Type, + &engagement.Message, + &engagement.Entity, + &userId, + &engagement.Created, + ); err != nil { + tx.Rollback() + return domain.Engagement{}, fmt.Errorf("Failed to insert engagement into database. %s", err.Error()) + } + + if err := tx.Commit(); err != nil { + tx.Rollback() + return domain.Engagement{}, err + } + + // Is user is valid + if userId.Valid { + engagement.UserId = int(userId.Int32) + } + + return engagement, nil +} + +// AddEntityEngagement creates an engagement record in the database without any user. This function +// requires an entity ID as it should be used when there is a reference to external an entity. +// The message should be provided, but a blank string ("") is acceptable. The engagement type +// parameter determines the labeling of the engagement in the database. Any errors will be +// bubbled to the caller. +// +// List of allowed engagements: viewed, shared +func (r *EngagementRepository) AddEntityEngagement(entityId int, message string, engagementType domain.EngagementType) (domain.Engagement, error) { + // Prevent invalid engagement types + switch engagementType { + case domain.EngagementViewed: + case domain.EngagementShared: + break + case domain.EngagementMade: + case domain.EngagementLiked: + case domain.EngagementReviewed: + case domain.EngagementRated: + return domain.Engagement{}, fmt.Errorf("Attempting to use disallowed anonymous engagement type.") + } + + tx, err := r.db.Begin() + if err != nil { + tx.Rollback() + return domain.Engagement{}, err + } + + query := ` + INSERT INTO Engagements ( + type, message, entity, userid, created + ) VALUES ( + $1, $2, $3, NULL, $4 + ) RETURNING *; + ` + + var engagement domain.Engagement + var userId sql.NullInt32 + if err := tx.QueryRow(query, engagementType, message, entityId, time.Now()).Scan( + &engagement.Id, + &engagement.Type, + &engagement.Message, + &engagement.Entity, + &userId, + &engagement.Created, + ); err != nil { + tx.Rollback() + return domain.Engagement{}, fmt.Errorf("Failed to insert engagement into database. %s", err.Error()) + } + + if err := tx.Commit(); err != nil { + tx.Rollback() + return domain.Engagement{}, err + } + + // Is user is valid + if userId.Valid { + engagement.UserId = int(userId.Int32) + } + return engagement, nil } @@ -132,18 +266,24 @@ func (r *EngagementRepository) GetUserEngagement(userId, limit int) ([]domain.En var engagements []domain.Engagement for rows.Next() { var engagement domain.Engagement + var engUserId sql.NullInt32 if err := rows.Scan( &engagement.Id, &engagement.Type, &engagement.Message, &engagement.Entity, - &engagement.UserId, + &engUserId, &engagement.Created, ); err != nil { tx.Rollback() return []domain.Engagement{}, fmt.Errorf("Failed to scan user engagement. %s", err.Error()) } + // Add user if valid + if engUserId.Valid { + engagement.UserId = int(engUserId.Int32) + } + engagements = append(engagements, engagement) } @@ -157,7 +297,7 @@ func (r *EngagementRepository) GetUserEngagement(userId, limit int) ([]domain.En // UserFavoriteRecipeToggle toggles the status of a users favorite of a recipe. If the user has already // favorited the provided recipe, the database entry will be delete, hence removing the favorite. Otherwise, -// an entry will be created. The NEW status of the users favorite will be returned as the boolean. Any +// an entry will be created. The NEW status of the users favorite will be returned as the boolean. Any // errors will be bubbled to the caller. func (r *EngagementRepository) UserFavoriteRecipeToggle(userId, recipeId int) (bool, error) { tx, err := r.db.Begin() @@ -173,7 +313,7 @@ func (r *EngagementRepository) UserFavoriteRecipeToggle(userId, recipeId int) (b ` var id int - + err = tx.QueryRow(query, userId, recipeId).Scan(&id) if err != nil { if !errors.Is(err, sql.ErrNoRows) { From 2a33edc8f6ca5eacab05fb8cd2a4dfc9b136ac5b Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Tue, 15 Jul 2025 21:19:47 -0700 Subject: [PATCH 09/10] (FEAT): Implemented API for the share engagement. This includes user and no user routes! Now wired to the frontend, however, it will still create an engagement even if it fails... --- internal/app/handlers/engagement_handler.go | 49 +++++++-- internal/app/handlers/page_handler.go | 2 - internal/app/server/server.go | 1 + internal/app/service/engagement_service.go | 28 ++++++ internal/domain/engagement/service.go | 2 + internal/domain/server/routes.go | 1 + internal/templates/pages/recipe.templ | 31 +++--- internal/templates/pages/recipe_templ.go | 105 +++++++++++--------- 8 files changed, 148 insertions(+), 71 deletions(-) diff --git a/internal/app/handlers/engagement_handler.go b/internal/app/handlers/engagement_handler.go index d57bc6b..dcca6d5 100644 --- a/internal/app/handlers/engagement_handler.go +++ b/internal/app/handlers/engagement_handler.go @@ -8,24 +8,55 @@ import ( domain "github.com/haydenhargreaves/Potion/internal/domain/server" ) - - func EngagementViewRecipe(ctx *gin.Context) { deps := ctx.MustGet("deps").(*domain.InjectedDependencies) - id := ctx.Param("id") + recipeId, _ := strconv.Atoi(ctx.Param("id")) if !domain.IsLoggedIn(ctx) { - // TODO: Anon view - ctx.Status(http.StatusNoContent) + if _, err := deps.EngagementService.ViewRecipe(recipeId); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Status(http.StatusNoContent) + } return } - recipeId, _ := strconv.Atoi(id) userId := ctx.MustGet("userId").(int) if _, err := deps.EngagementService.UserViewRecipe(userId, recipeId); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Status(http.StatusNoContent) + } +} + +func EngagementShareRecipe(ctx *gin.Context) { + deps := ctx.MustGet("deps").(*domain.InjectedDependencies) + recipeId, _ := strconv.Atoi(ctx.Param("id")) + + if !domain.IsLoggedIn(ctx) { + if _, err := deps.EngagementService.ShareRecipe(recipeId); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": err.Error(), + }) + } else { + ctx.Status(http.StatusNoContent) + } + return + } + + userId := ctx.MustGet("userId").(int) + + if _, err := deps.EngagementService.UserShareRecipe(userId, recipeId); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, "message": err.Error(), }) } else { @@ -48,7 +79,7 @@ func EngagementFavoriteRecipe(ctx *gin.Context) { if _, err := deps.EngagementService.UserFavoriteRecipe(userId, recipeId); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, + "status": http.StatusInternalServerError, "message": err.Error(), }) } else { @@ -71,7 +102,7 @@ func EngagementMakeRecipe(ctx *gin.Context) { if _, err := deps.EngagementService.UserMakeRecipe(userId, recipeId); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ - "status": http.StatusInternalServerError, + "status": http.StatusInternalServerError, "message": err.Error(), }) } else { diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index 7c5c48d..3b36eb4 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -130,14 +130,12 @@ func RecipePage(ctx *gin.Context) { // Add engagement if loggedIn { - fmt.Println("CALLING USER VIEW") if _, err = deps.EngagementService.UserViewRecipe(*userId, recipe.Id); err != nil { fmt.Printf("ERROR: %s\n", err.Error()) ctx.JSON(400, err.Error()) return } } else { - fmt.Println("CALLING VIEW") if _, err = deps.EngagementService.ViewRecipe(recipe.Id); err != nil { fmt.Printf("ERROR: %s\n", err.Error()) ctx.JSON(400, err.Error()) diff --git a/internal/app/server/server.go b/internal/app/server/server.go index 226aa43..091e464 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -188,6 +188,7 @@ func (s *Server) Setup() *Server { // Engagement endpoints router_api.POST("/engagement/view/:id", handlers.EngagementViewRecipe) + router_api.POST("/engagement/share/:id", handlers.EngagementShareRecipe) router_api.POST("/engagement/favorite/:id", handlers.EngagementFavoriteRecipe) router_api.POST("/engagement/make/:id", handlers.EngagementMakeRecipe) diff --git a/internal/app/service/engagement_service.go b/internal/app/service/engagement_service.go index 9e7bf92..54ec6a9 100644 --- a/internal/app/service/engagement_service.go +++ b/internal/app/service/engagement_service.go @@ -39,6 +39,20 @@ func (s *EngagementService) ViewRecipe(recipeId int) (domain.Engagement, error) return s.engagementRepository.AddEntityEngagement(recipeId, message, domain.EngagementViewed) } +// ShareRecipe requires a user ID and a recipe ID to create an engagement record in the database. +// A message will be generated using the recipe data and then used to add a view engagement to the +// database. +func (s *EngagementService) ShareRecipe(recipeId int) (domain.Engagement, error) { + recipe, err := s.recipeRepository.GetRecipe(recipeId, nil) + if err != nil { + return domain.Engagement{}, err + } + + message := fmt.Sprintf("Shared \"%s\"", recipe.Title) + + return s.engagementRepository.AddEntityEngagement(recipeId, message, domain.EngagementShared) +} + // UserViewRecipe requires a user ID and a recipe ID to create an engagement record in the database. // A message will be generated using the recipe data and then used to add a view engagement to the // database. @@ -94,6 +108,20 @@ func (s *EngagementService) UserMakeRecipe(userId, recipeId int) (domain.Engagem return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementMade) } +// UserShareRecipe requires a user ID and a recipe ID to create an engagement record in the database. +// A message will be generated using the recipe data and then used to add a make engagement to the +// database. +func (s *EngagementService) UserShareRecipe(userId, recipeId int) (domain.Engagement, error) { + recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId) + if err != nil { + return domain.Engagement{}, err + } + + message := fmt.Sprintf("Shared \"%s\"", recipe.Title) + + return s.engagementRepository.AddUserEntityEngagement(userId, recipeId, message, domain.EngagementShared) +} + // GetUserEngagement returns a list of the users most recent engagement entries. The number of records // is determined by the limit passed into this function. The results are sorted, newest-to-oldest. func (s *EngagementService) GetUserEngagement(userId, limit int) ([]domain.Engagement, error) { diff --git a/internal/domain/engagement/service.go b/internal/domain/engagement/service.go index a4dd0fc..4c3a0ff 100644 --- a/internal/domain/engagement/service.go +++ b/internal/domain/engagement/service.go @@ -2,8 +2,10 @@ package domain type EngagementService interface { ViewRecipe(recipeId int) (Engagement, error) + ShareRecipe(recipeId int) (Engagement, error) UserViewRecipe(userId, recipeId int) (Engagement, error) UserFavoriteRecipe(userId, recipeId int) (Engagement, error) UserMakeRecipe(userId, recipeId int) (Engagement, error) + UserShareRecipe(userId, recipeId int) (Engagement, error) GetUserEngagement(userId, limit int) ([]Engagement, error) } diff --git a/internal/domain/server/routes.go b/internal/domain/server/routes.go index a6f2fab..b6823da 100644 --- a/internal/domain/server/routes.go +++ b/internal/domain/server/routes.go @@ -26,6 +26,7 @@ const API_CREATE_RECIPE = VERSION + API + "/recipe" const API_SEARCH_RECIPES = VERSION + API + "/recipe/search" const API_ENGAGEMENT_VIEW = VERSION + API + "/engagement/view/%d" +const API_ENGAGEMENT_SHARE = VERSION + API + "/engagement/share/%d" const API_ENGAGEMENT_FAVORITE = VERSION + API + "/engagement/favorite/%d" const API_ENGAGEMENT_MAKE = VERSION + API + "/engagement/make/%d" diff --git a/internal/templates/pages/recipe.templ b/internal/templates/pages/recipe.templ index f853b55..3ce0723 100644 --- a/internal/templates/pages/recipe.templ +++ b/internal/templates/pages/recipe.templ @@ -189,11 +189,11 @@ templ favoriteButton(favorited bool, id int, loggedIn bool) { hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id) } hx-trigger="click" hx-swap="none" - if loggedIn { - hx-on:click="favoriteButtonHandler();" - } + if loggedIn { + hx-on:click="favoriteButtonHandler();" + } class="flex items-center justify-center gap-x-1 rounded-lg border border-blue-300 bg-blue-50 text-gray-800 px-6 py-3 flex-grow hover:bg-blue-100 hover:border-blue-500 duration-300" - id="favorite-button" + id="favorite-button" > } -templ shareButton() { +templ shareButton(id int) {
    \"\"

    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 297, Col: 75} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

    Author: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

    \"\"

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 298, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 300, Col: 75} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "

    Category: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "

    Author: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category) + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 299, Col: 69} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 301, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "

    Category: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 302, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -841,20 +854,20 @@ func RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool) templ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "

    About this recipe

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "

    About this recipe

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var32 string - templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Description) + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 305, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 308, Col: 49} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -870,7 +883,7 @@ func RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool) templ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -898,24 +911,24 @@ func scripts(id int) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var33 := templ.GetChildren(ctx) - if templ_7745c5c3_Var33 == nil { - templ_7745c5c3_Var33 = templ.NopComponent + templ_7745c5c3_Var34 := templ.GetChildren(ctx) + if templ_7745c5c3_Var34 == nil { + templ_7745c5c3_Var34 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\"\n navigator.clipboard.writeText(url).then(() => {\n button.outerHTML = `\n \n `;\n\n setTimeout(() => {\n const newButton = document.getElementById(\"share-button\");\n newButton.outerHTML = before;\n }, 2000);\n });\n } else {\n console.warn(\"Clipboard API not available.\");\n\n button.outerHTML = `\n \n `;\n\n setTimeout(() => {\n const newButton = document.getElementById(\"share-button\");\n newButton.outerHTML = before;\n }, 2000);\n }\n }\n\n function makeButtonHandler() {\n const button = document.getElementById(\"make-button\");\n\n button.outerHTML = `\n \n \n \n \n \n \n Made This!\n \n `;\n }\n\n function favoriteButtonHandler() {\n const button = document.getElementById(\"favorite-button\");\n\n console.log(button.classList);\n console.log(button.classList.contains(\"border-blue-300\"));\n\n const toggleClasses = [\n \"border-gray-300\", \"hover:bg-gray-50\", \"hover:border-blue-300\",\n \"border-blue-300\", \"bg-blue-50\", \"hover:bg-blue-100\", \"hover:border-blue-500\"\n ];\n\n for (const cls of toggleClasses) {\n console.log(\"toggling class \" + cls);\n button.classList.toggle(cls);\n }\n\n if (!button.classList.contains(\"border-blue-300\")) {\n button.innerHTML = `\n \n \n \n Favorite\n `;\n\n } else {\n button.innerHTML = `\n \n \n \n Unfavorite\n `;\n }\n\n }\n\n") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } From 15cb03589c8b40506cbe9bb7bccafabd79783813 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Tue, 15 Jul 2025 21:44:20 -0700 Subject: [PATCH 10/10] (UI/FIX): Fixed the view engagement to only run on clicks. But that means we have to redirect from the handler. I didn't want to, but I guess that makes it easier when more pages direct to the recipe page. --- internal/app/handlers/engagement_handler.go | 7 ++- internal/app/handlers/page_handler.go | 27 +++++------ internal/app/service/recipe_service.go | 4 +- .../database/repository/recipe_repository.go | 2 - internal/templates/pages/profile.templ | 14 +++--- internal/templates/pages/profile_templ.go | 45 ++++++++++--------- internal/templates/pages/recipe.templ | 1 + internal/templates/pages/recipe_templ.go | 4 +- internal/templates/pages/search.templ | 10 +++-- internal/templates/pages/search_templ.go | 24 +++++----- 10 files changed, 76 insertions(+), 62 deletions(-) diff --git a/internal/app/handlers/engagement_handler.go b/internal/app/handlers/engagement_handler.go index dcca6d5..1f80b7b 100644 --- a/internal/app/handlers/engagement_handler.go +++ b/internal/app/handlers/engagement_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "net/http" "strconv" @@ -19,7 +20,8 @@ func EngagementViewRecipe(ctx *gin.Context) { "message": err.Error(), }) } else { - ctx.Status(http.StatusNoContent) + ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId)) + ctx.Status(http.StatusOK) } return } @@ -32,7 +34,8 @@ func EngagementViewRecipe(ctx *gin.Context) { "message": err.Error(), }) } else { - ctx.Status(http.StatusNoContent) + ctx.Header("HX-Redirect", fmt.Sprintf(domain.WEB_RECIPE, recipeId)) + ctx.Status(http.StatusOK) } } diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index 3b36eb4..cadcee1 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -129,19 +129,20 @@ func RecipePage(ctx *gin.Context) { } // Add engagement - if loggedIn { - if _, err = deps.EngagementService.UserViewRecipe(*userId, recipe.Id); err != nil { - fmt.Printf("ERROR: %s\n", err.Error()) - ctx.JSON(400, err.Error()) - return - } - } else { - if _, err = deps.EngagementService.ViewRecipe(recipe.Id); err != nil { - fmt.Printf("ERROR: %s\n", err.Error()) - ctx.JSON(400, err.Error()) - return - } - } + // BUG: Don't want to do this here + // if loggedIn { + // if _, err = deps.EngagementService.UserViewRecipe(*userId, recipe.Id); err != nil { + // fmt.Printf("ERROR: %s\n", err.Error()) + // ctx.JSON(400, err.Error()) + // return + // } + // } else { + // if _, err = deps.EngagementService.ViewRecipe(recipe.Id); err != nil { + // fmt.Printf("ERROR: %s\n", err.Error()) + // ctx.JSON(400, err.Error()) + // return + // } + // } title := "Potion - View Recipe" page := pages.RecipePage(*recipe, *user, loggedIn) diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index 0f5a68b..be4ba73 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -33,7 +33,7 @@ func NewRecipeService(recipeRepository domain.RecipeRepository) domain.RecipeSer // occur. // // TODO: Implement validation in the API. -// TODO: Implement image creation and tag creation. +// TODO: Implement image creation. func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) { // Ensure user is logged in if !domainServer.IsLoggedIn(ctx) { @@ -110,7 +110,7 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) { if image != nil { } - // TODO: Create the tags in the database + // Create the tags if len(tags) > 0 { if err := s.recipeRepository.CreateRecipeTags(recipe, tags); err != nil { return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error()) diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go index da5c6f7..a0a578d 100644 --- a/internal/infrastructure/database/repository/recipe_repository.go +++ b/internal/infrastructure/database/repository/recipe_repository.go @@ -251,8 +251,6 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain } } - // TODO: Title search somehow... - // Merge condition strings mealString := fmt.Sprintf("(%s)", strings.Join(mealConditions, " OR ")) timeString := fmt.Sprintf("(%s)", strings.Join(timeConditions, " OR ")) diff --git a/internal/templates/pages/profile.templ b/internal/templates/pages/profile.templ index 1dcf8a2..c13d33e 100644 --- a/internal/templates/pages/profile.templ +++ b/internal/templates/pages/profile.templ @@ -44,8 +44,7 @@ templ userDetailsSection(user domainUser.User, recipeCount int) { } else { }
    @@ -67,7 +66,7 @@ templ recipesSection(recipes []domainRecipe.Recipe) {

    My Recipes

      if len(recipes) <= 4 { - for _, recipe :=range recipes { + for _, recipe := range recipes { @recipeListItem(recipe) } } else { @@ -114,10 +113,13 @@ templ activitySection(engagement []domainEngagement.Engagement) { templ recipeListItem(recipe domainRecipe.Recipe) {
    • -

      - +

      { recipe.Title } -

    • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"text-base md:text-lg hover:text-blue-600 duration-100 cursor-pointer\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 119, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 122, Col: 18} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

      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: 123, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 125, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -330,7 +333,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 124, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 126, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -343,7 +346,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 125, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 127, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -356,7 +359,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 128, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 130, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -369,7 +372,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 131, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 133, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { @@ -382,7 +385,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 134, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 136, Col: 58} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { @@ -400,7 +403,7 @@ func recipeListItem(recipe domainRecipe.Recipe) templ.Component { 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: 138, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 140, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { @@ -447,7 +450,7 @@ func activityListItem(engagement domainEngagement.Engagement) templ.Component { 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: 149, Col: 23} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 151, Col: 23} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { @@ -460,7 +463,7 @@ func activityListItem(engagement domainEngagement.Engagement) templ.Component { 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: 152, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/profile.templ`, Line: 154, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { diff --git a/internal/templates/pages/recipe.templ b/internal/templates/pages/recipe.templ index 3ce0723..c3ea191 100644 --- a/internal/templates/pages/recipe.templ +++ b/internal/templates/pages/recipe.templ @@ -322,6 +322,7 @@ templ scripts(id int) { const before = button.outerHTML; if (navigator.clipboard && navigator.clipboard.writeText) { + // TODO: Fix this to use the real domain somehow const url = "http://localhost:7331/v1/web/recipe/{{ id }}" navigator.clipboard.writeText(url).then(() => { button.outerHTML = ` diff --git a/internal/templates/pages/recipe_templ.go b/internal/templates/pages/recipe_templ.go index 3f81041..616ed96 100644 --- a/internal/templates/pages/recipe_templ.go +++ b/internal/templates/pages/recipe_templ.go @@ -916,13 +916,13 @@ func scripts(id int) templ.Component { templ_7745c5c3_Var34 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "