diff --git a/doc/TechnicalSpecification.md b/doc/TechnicalSpecification.md index 4f03d6d..f9a28b4 100644 --- a/doc/TechnicalSpecification.md +++ b/doc/TechnicalSpecification.md @@ -347,6 +347,12 @@ found in **OTHER** section. - [ ] Read (Required, Default: F) boolean - [ ] Created (Required) date/time stamp +- [x] RecipeOfTheWeek: Represents the recipe of the week. + - [x] ID (PK) Serial + - [x] RecipeId (FK: Recipe.Id, Required) Serial + - [x] Score (Required) float (Computed score) + - [x] Created (Required) date/time stamp (serves as the validity) + '**': Not sure implementation diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index 61dfe22..0f3629e 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -48,9 +48,29 @@ func HomePage(ctx *gin.Context) { return } - page = templates.HomePage(true, viewedRecipes, madeRecipes) + // Get the recipe of the week + recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(&userId) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": fmt.Sprintf("Error getting made recipes. %s\n", err.Error()), + }) + return + } + + page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek) } else { - page = templates.HomePage(false, nil, nil) + // Get the recipe of the week + recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(nil) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "status": http.StatusInternalServerError, + "message": fmt.Sprintf("Error getting made recipes. %s\n", err.Error()), + }) + return + } + + page = templates.HomePage(false, nil, nil, recipeOfTheWeek) } title := "Potion - Home" diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index e9d48d6..063db48 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -174,6 +174,9 @@ func (s *RecipeService) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) return s.recipeRepository.GetUserFavoriteRecipes(id) } +// GetUserViewedRecipes returns a list of the most recent x (limit) recipes viewed by a user, from +// the provided userId. This will return a list of size 'limit'. Any errors will be bubbled up to +// the caller. func (s *RecipeService) GetUserViewedRecipes(userId, limit int) ([]domain.Recipe, error) { engagement, err := s.engagementRepository.GetUserEngagementFiltered(userId, limit, domainEngagement.EngagementViewed) if err != nil { @@ -188,6 +191,9 @@ func (s *RecipeService) GetUserViewedRecipes(userId, limit int) ([]domain.Recipe return s.recipeRepository.GetRecipes(ids, &userId) } +// GetUserMadeRecipes returns a list of the most recent x (limit) recipes made by a user, from the +// provided userId. This will return a list of size 'limit'. Any errors will be bubbled up to the +// caller. func (s *RecipeService) GetUserMadeRecipes(userId, limit int) ([]domain.Recipe, error) { engagement, err := s.engagementRepository.GetUserEngagementFiltered(userId, limit, domainEngagement.EngagementMade) if err != nil { @@ -201,3 +207,9 @@ func (s *RecipeService) GetUserMadeRecipes(userId, limit int) ([]domain.Recipe, return s.recipeRepository.GetRecipes(ids, &userId) } + +// GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value, +// the recipe will be nil. Any errors will be bubbled to the caller. +func (s *RecipeService) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) { + return s.recipeRepository.GetRecipeOfTheWeek(userId) +} diff --git a/internal/domain/recipe/repository.go b/internal/domain/recipe/repository.go index 761aa2e..8574035 100644 --- a/internal/domain/recipe/repository.go +++ b/internal/domain/recipe/repository.go @@ -10,4 +10,5 @@ type RecipeRepository interface { GetUserFavoriteRecipes(id int) ([]Recipe, error) GetRecipeTags(recipe *Recipe) error GetRecipeFavorite(recipe *Recipe, userId int) error + GetRecipeOfTheWeek(userId *int) (*Recipe, error) } diff --git a/internal/domain/recipe/service.go b/internal/domain/recipe/service.go index 373d417..91bf12f 100644 --- a/internal/domain/recipe/service.go +++ b/internal/domain/recipe/service.go @@ -1,6 +1,8 @@ package domain -import "github.com/gin-gonic/gin" +import ( + "github.com/gin-gonic/gin" +) type RecipeService interface { CreateRecipe(ctx *gin.Context) (*Recipe, error) @@ -10,4 +12,5 @@ type RecipeService interface { GetUserFavoriteRecipes(id int) ([]Recipe, error) GetUserViewedRecipes(userId, limit int) ([]Recipe, error) GetUserMadeRecipes(userId, limit int) ([]Recipe, error) + GetRecipeOfTheWeek(userId *int) (*Recipe, error) } diff --git a/internal/infrastructure/database/migrations/009_create_recipe_of_the_week_table.sql b/internal/infrastructure/database/migrations/009_create_recipe_of_the_week_table.sql new file mode 100644 index 0000000..0f8277a --- /dev/null +++ b/internal/infrastructure/database/migrations/009_create_recipe_of_the_week_table.sql @@ -0,0 +1,14 @@ +-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com) +-- Desc: Create the recipe of the week table. +-- Date: 07/26/2025 + +BEGIN; + +CREATE TABLE IF NOT EXISTS RecipeOfTheWeek ( + Id SERIAL PRIMARY KEY NOT NULL, + RecipeId INTEGER NOT NULL REFERENCES recipes(id), + Score NUMERIC(10, 4) NOT NULL, + Created TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMIT; diff --git a/internal/infrastructure/database/migrations/010_create_recipe_of_the_week_procedure.sql b/internal/infrastructure/database/migrations/010_create_recipe_of_the_week_procedure.sql new file mode 100644 index 0000000..0ddcb6f --- /dev/null +++ b/internal/infrastructure/database/migrations/010_create_recipe_of_the_week_procedure.sql @@ -0,0 +1,41 @@ +-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com) +-- Desc: Create the recipe of the week stored procedure. +-- Date: 07/26/2025 + +CREATE OR REPLACE PROCEDURE calculate_recipe_of_the_week_procedure() +LANGUAGE plpgsql +AS $$ +BEGIN + -- Insert the highest-scoring recipe from the last 7 days into daily_top_recipes + INSERT INTO RecipeOfTheWeek (RecipeId, Score, Created) + SELECT + e.Entity AS RecipeId, + ( + -- Weights can be configured here + SUM(CASE WHEN e.Type = 'viewed' THEN 1 ELSE 0 END) * 0.20 + + SUM(CASE WHEN e.Type = 'made' THEN 1 ELSE 0 END) * 0.40 + + SUM(CASE WHEN e.Type = 'liked' THEN 1 ELSE 0 END) * 0.30 + + SUM(CASE WHEN e.Type = 'shared' THEN 1 ELSE 0 END) * 0.10 + ) AS Score, + NOW() + FROM + Engagements e + WHERE + e.Created >= NOW() - INTERVAL '7 days' + AND e.Entity IS NOT NULL + GROUP BY e.Entity + ORDER BY Score DESC + LIMIT 1; + + RAISE NOTICE 'Successfully calculated and stored the top recipe for the day.'; + + + EXCEPTION + WHEN NO_DATA_FOUND THEN + -- Handle cases where no engagements are found in the last 7 days + RAISE NOTICE 'No engagement data found for the last 7 days to calculate a top recipe.'; + WHEN OTHERS THEN + -- Catch any other potential errors and re-raise them after logging + RAISE EXCEPTION 'An error occurred during top recipe calculation: %', SQLERRM; +END; +$$; diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go index f043102..4b444f6 100644 --- a/internal/infrastructure/database/repository/recipe_repository.go +++ b/internal/infrastructure/database/repository/recipe_repository.go @@ -676,6 +676,9 @@ func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) { return recipes, nil } +// GetUserRecipes gets a list of a users favorited recipes. This function does not ensure the user is +// authenticated or exists. If nothing is found, a blank slice will be returned. The resulting list +// is sorted by the created dates, newest first. Any errors will be bubbled to the caller. func (r *RecipeRepository) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) { tx, err := r.db.Begin() if err != nil { @@ -766,8 +769,13 @@ func (r *RecipeRepository) GetUserFavoriteRecipes(id int) ([]domain.Recipe, erro // GetRecipeTags 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 Tags array with the recipe's tags from the database. -// The recipe is modified in place and is not returned. Any errors will be bubbled to the caller. +// The recipe is modified in place and is not returned. If the recipe is nil, the function will +// return nothing (skipping). Any errors will be bubbled to the caller. func (r *RecipeRepository) GetRecipeTags(recipe *domain.Recipe) error { + if recipe == nil { + return nil + } + tx, err := r.db.Begin() if err != nil { tx.Rollback() @@ -808,8 +816,13 @@ func (r *RecipeRepository) GetRecipeTags(recipe *domain.Recipe) error { // 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. +// userId. The recipe is modified in place and is not returned. If the recipe is nil, the function +// will return nothing (skipping). Any errors will be bubbled to the caller. func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int) error { + if recipe == nil { + return nil + } + tx, err := r.db.Begin() if err != nil { tx.Rollback() @@ -832,3 +845,90 @@ func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int) return nil } + +// GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value, +// the recipe will be nil. This function simply collects the most recent entry in the recipeoftheweek +// table and return it. Any errors will be bubbled to the caller. +func (r *RecipeRepository) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) { + tx, err := r.db.Begin() + if err != nil { + tx.Rollback() + return nil, err + } + + query := ` + SELECT + r.id, r.title, r.description, r.instructions, r.serves, r.difficulty, r.duration, r.category, + r.ingredients, r.userid, r.modified, r.created + FROM recipes r + JOIN recipeoftheweek rw ON rw.recipeid = r.id + ORDER BY created DESC + LIMIT 1; + ` + + var durationBytes []byte + var ingredientBytes []byte + + var recipe domain.Recipe + if err := tx.QueryRow(query).Scan( + &recipe.Id, + &recipe.Title, + &recipe.Description, + pq.Array(&recipe.Instructions), + &recipe.Serves, + &recipe.Difficulty, + &durationBytes, + &recipe.Category, + &ingredientBytes, + &recipe.UserId, + &recipe.Modified, + &recipe.Created, + ); err != nil { + return nil, fmt.Errorf("Failed to location recipe in database: %s", err.Error()) + } + + // Parse duration + if len(durationBytes) > 0 { + var duration domain.RecipeDuration + if err := json.Unmarshal(durationBytes, &duration); err != nil { + return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error()) + } + + recipe.Duration = duration + } else { + recipe.Duration = domain.RecipeDuration{} + } + + // Parse ingredient + if len(ingredientBytes) > 0 { + var ingredients []domain.RecipeIngredient + if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil { + return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error()) + } + + recipe.Ingredients = ingredients + } else { + recipe.Ingredients = []domain.RecipeIngredient{} + } + + // Add tags + 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() + return nil, err + } + + return &recipe, nil +} diff --git a/internal/templates/components/cards.templ b/internal/templates/components/cards.templ index 58a8f20..ced3d84 100644 --- a/internal/templates/components/cards.templ +++ b/internal/templates/components/cards.templ @@ -5,20 +5,18 @@ import "github.com/haydenhargreaves/Potion/internal/domain/recipe" import domainServer "github.com/haydenhargreaves/Potion/internal/domain/server" templ likeButton() { - + + + } templ RecipeCardSmall(recipe domain.Recipe) {
- -
+ +

{ recipe.Title }

@@ -55,36 +53,39 @@ templ ContentCardSmall(content, target string) {
} -// TODO: Implement this using a recipe type parameter! -templ RecipeCardLarge(liked bool) { -
- -
-

Avocado Toast

-

- Hayden Hargreaves -

-

- Avocado toast is a delicious and simple breakfast, snack or light meal! Learn how to - make the BEST avocado toast with this recipe, plus fun variations. - Avocado toast is a delicious and simple breakfast, snack or light meal! Learn how to - make the BEST avocado toast with this recipe, plus fun variations. - Avocado toast is a delicious and simple breakfast, snack or light meal! Learn how to - make the BEST avocado toast with this recipe, plus fun variations. -

-
-

- Breakfast - 15 min +templ RecipeCardLarge(recipe *domain.Recipe) { + if recipe != nil { +

+ +
+

+ { recipe.Title } +

+

+ Serves { recipe.Serves }

- if liked { - @likeButton() - } +

+ { recipe.Description } +

+
+

+ { recipe.Category } - { recipe.Duration.Total } mins +

+ if recipe.Favorite { + @likeButton() + } +
+
-
-
+ } else { +

Coming soon!

+ } } diff --git a/internal/templates/components/cards_templ.go b/internal/templates/components/cards_templ.go index f4c2b96..65fb91b 100644 --- a/internal/templates/components/cards_templ.go +++ b/internal/templates/components/cards_templ.go @@ -33,7 +33,7 @@ func likeButton() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -62,14 +62,14 @@ func RecipeCardSmall(recipe domain.Recipe) templ.Component { templ_7745c5c3_Var2 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 23, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 21, Col: 18} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -82,7 +82,7 @@ func RecipeCardSmall(recipe domain.Recipe) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Serves) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 26, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 24, Col: 26} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -95,7 +95,7 @@ func RecipeCardSmall(recipe domain.Recipe) templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 30, Col: 22} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 28, Col: 22} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -108,7 +108,7 @@ func RecipeCardSmall(recipe domain.Recipe) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Duration.Total) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 30, Col: 50} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 28, Col: 50} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -131,7 +131,7 @@ func RecipeCardSmall(recipe domain.Recipe) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(domainServer.API_ENGAGEMENT_VIEW, recipe.Id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 37, Col: 70} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 35, Col: 70} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -182,7 +182,7 @@ func ContentCardSmall(content, target string) templ.Component { var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(content) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 52, Col: 32} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 50, Col: 32} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -196,8 +196,7 @@ func ContentCardSmall(content, target string) templ.Component { }) } -// TODO: Implement this using a recipe type parameter! -func RecipeCardLarge(liked bool) templ.Component { +func RecipeCardLarge(recipe *domain.Recipe) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -218,19 +217,104 @@ func RecipeCardLarge(liked bool) templ.Component { templ_7745c5c3_Var11 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

Avocado Toast

Hayden Hargreaves

Avocado toast is a delicious and simple breakfast, snack or light meal! Learn how to make the BEST avocado toast with this recipe, plus fun variations. Avocado toast is a delicious and simple breakfast, snack or light meal! Learn how to make the BEST avocado toast with this recipe, plus fun variations. Avocado toast is a delicious and simple breakfast, snack or light meal! Learn how to make the BEST avocado toast with this recipe, plus fun variations.

Breakfast - 15 min

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if liked { - templ_7745c5c3_Err = likeButton().Render(ctx, templ_7745c5c3_Buffer) + if recipe != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

") + 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/components/cards.templ`, Line: 62, Col: 19} + } + _, 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, 13, "

Serves ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Serves) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 65, Col: 27} + } + _, 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, 14, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 68, Col: 25} + } + _, 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, 15, "

") + 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/components/cards.templ`, Line: 72, Col: 23} + } + _, 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, 16, " - ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Duration.Total) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 72, Col: 51} + } + _, 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, 17, " mins

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if recipe.Favorite { + templ_7745c5c3_Err = likeButton().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

Coming soon!

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err } return nil }) diff --git a/internal/templates/pages/favorites.templ b/internal/templates/pages/favorites.templ index 7ddc99e..0f8f808 100644 --- a/internal/templates/pages/favorites.templ +++ b/internal/templates/pages/favorites.templ @@ -25,7 +25,7 @@ templ favoriteResult(recipe domain.Recipe) { hx-swap="none" class="w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row even:bg-[#f8f8f8] cursor-pointer" > - +
diff --git a/internal/templates/pages/favorites_templ.go b/internal/templates/pages/favorites_templ.go index 790cfd8..a16e478 100644 --- a/internal/templates/pages/favorites_templ.go +++ b/internal/templates/pages/favorites_templ.go @@ -97,7 +97,7 @@ func favoriteResult(recipe domain.Recipe) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row even:bg-[#f8f8f8] cursor-pointer\">

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row even:bg-[#f8f8f8] cursor-pointer\">

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/templates/pages/home.templ b/internal/templates/pages/home.templ index 657caf9..8c9b49b 100644 --- a/internal/templates/pages/home.templ +++ b/internal/templates/pages/home.templ @@ -36,7 +36,7 @@ templ searchSection() { } -templ highlightSection(liked bool) { +templ highlightSection(recipeOfTheWeek *domainRecipe.Recipe) {
@components.BannerText("Recipe of the Week!")

@@ -47,7 +47,7 @@ templ highlightSection(liked bool) { resonate with our users!

- @components.RecipeCardLarge(false) + @components.RecipeCardLarge(recipeOfTheWeek)
} @@ -59,10 +59,14 @@ templ listsSection(loggedIn bool, viewed, made []domainRecipe.Recipe) {

Recently viewed

if loggedIn {
- for _, recipe := range viewed { - @components.RecipeCardSmall(recipe) + if len(viewed) > 0 { + for _, recipe := range viewed { + @components.RecipeCardSmall(recipe) + } + @components.ContentCardSmall("View full history...", "/v1/web/history") + } else { +

You have not viewed any recipes. There is nothing to show.

} - @components.ContentCardSmall("View full history...", "/v1/web/history")
} else {
@@ -74,10 +78,14 @@ templ listsSection(loggedIn bool, viewed, made []domainRecipe.Recipe) {

Make again

if loggedIn {
- for _, recipe := range made { - @components.RecipeCardSmall(recipe) - } - @components.ContentCardSmall("View full history...", "/v1/web/history") + if len(made) > 0 { + for _, recipe := range made { + @components.RecipeCardSmall(recipe) + } + @components.ContentCardSmall("View full history...", "/v1/web/history") + } else { +

You have not made any recipes. There is nothing to show.

+ }
} else {
@@ -114,13 +122,13 @@ templ ctaSection() { } -templ HomePage(loggedIn bool, viewed, made []domainRecipe.Recipe) { +templ HomePage(loggedIn bool, viewed, made []domainRecipe.Recipe, recipeOfTheWeek *domainRecipe.Recipe) { @components.Navbar("home")
@introSection() @searchSection() - @highlightSection(false) + @highlightSection(recipeOfTheWeek) @listsSection(loggedIn, viewed, made) @ctaSection()
diff --git a/internal/templates/pages/home_templ.go b/internal/templates/pages/home_templ.go index e66f94c..4dca9af 100644 --- a/internal/templates/pages/home_templ.go +++ b/internal/templates/pages/home_templ.go @@ -86,7 +86,7 @@ func searchSection() templ.Component { }) } -func highlightSection(liked bool) templ.Component { +func highlightSection(recipeOfTheWeek *domainRecipe.Recipe) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -119,7 +119,7 @@ func highlightSection(liked bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = components.RecipeCardLarge(false).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = components.RecipeCardLarge(recipeOfTheWeek).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -169,22 +169,33 @@ func listsSection(loggedIn bool, viewed, made []domainRecipe.Recipe) templ.Compo if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - for _, recipe := range viewed { - templ_7745c5c3_Err = components.RecipeCardSmall(recipe).Render(ctx, templ_7745c5c3_Buffer) + if len(viewed) > 0 { + for _, recipe := range viewed { + templ_7745c5c3_Err = components.RecipeCardSmall(recipe).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.ContentCardSmall("View full history...", "/v1/web/history").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

You have not viewed any recipes. There is nothing to show.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = components.ContentCardSmall("View full history...", "/v1/web/history").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">

Log in to view metrics.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

Make again

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

Make again

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if loggedIn { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - for _, recipe := range made { - templ_7745c5c3_Err = components.RecipeCardSmall(recipe).Render(ctx, templ_7745c5c3_Buffer) + if len(made) > 0 { + for _, recipe := range made { + templ_7745c5c3_Err = components.RecipeCardSmall(recipe).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.ContentCardSmall("View full history...", "/v1/web/history").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

You have not made any recipes. There is nothing to show.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = components.ContentCardSmall("View full history...", "/v1/web/history").Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">

Log in to view metrics.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -265,7 +287,7 @@ func ctaSection() templ.Component { templ_7745c5c3_Var7 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

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, 25, "\" 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 } @@ -282,7 +304,7 @@ func ctaSection() templ.Component { }) } -func HomePage(loggedIn bool, viewed, made []domainRecipe.Recipe) templ.Component { +func HomePage(loggedIn bool, viewed, made []domainRecipe.Recipe, recipeOfTheWeek *domainRecipe.Recipe) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -307,7 +329,7 @@ func HomePage(loggedIn bool, viewed, made []domainRecipe.Recipe) templ.Component 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, 26, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -319,7 +341,7 @@ func HomePage(loggedIn bool, viewed, made []domainRecipe.Recipe) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = highlightSection(false).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = highlightSection(recipeOfTheWeek).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -331,7 +353,7 @@ func HomePage(loggedIn bool, viewed, made []domainRecipe.Recipe) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/templates/pages/recipe.templ b/internal/templates/pages/recipe.templ index 44b6b8c..69d6c02 100644 --- a/internal/templates/pages/recipe.templ +++ b/internal/templates/pages/recipe.templ @@ -295,7 +295,7 @@ templ RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool, doma @components.Navbar("")
- +

{ recipe.Title }

Author: { user.Name }

diff --git a/internal/templates/pages/recipe_templ.go b/internal/templates/pages/recipe_templ.go index b68c36a..10be9fc 100644 --- a/internal/templates/pages/recipe_templ.go +++ b/internal/templates/pages/recipe_templ.go @@ -803,7 +803,7 @@ func RecipePage(recipe domain.Recipe, user domainUser.User, loggedIn bool, domai if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
\"\"

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

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/templates/pages/search.templ b/internal/templates/pages/search.templ index e5acc11..faab3d1 100644 --- a/internal/templates/pages/search.templ +++ b/internal/templates/pages/search.templ @@ -43,7 +43,7 @@ templ searchResult(recipe domain.Recipe) { hx-swap="none" class="w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row even:bg-[#f8f8f8] cursor-pointer" > - +
diff --git a/internal/templates/pages/search_templ.go b/internal/templates/pages/search_templ.go index c3bec1a..7280f63 100644 --- a/internal/templates/pages/search_templ.go +++ b/internal/templates/pages/search_templ.go @@ -153,7 +153,7 @@ func searchResult(recipe domain.Recipe) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row even:bg-[#f8f8f8] cursor-pointer\">

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row even:bg-[#f8f8f8] cursor-pointer\">

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index a1d4d52..080b507 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-800: oklch(44.4% 0.177 26.899); --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); @@ -335,9 +334,6 @@ .mb-16 { margin-bottom: calc(var(--spacing) * 16); } - .\[display\:-webkit-box\] { - display: -webkit-box; - } .block { display: block; } @@ -416,6 +412,9 @@ .h-screen { height: 100vh; } + .min-h-72 { + min-height: calc(var(--spacing) * 72); + } .min-h-screen { min-height: 100vh; } @@ -446,6 +445,9 @@ .w-52 { width: calc(var(--spacing) * 52); } + .w-80 { + width: calc(var(--spacing) * 80); + } .w-fit { width: fit-content; } @@ -831,6 +833,9 @@ --tw-tracking: var(--tracking-wide); letter-spacing: var(--tracking-wide); } + .text-wrap { + text-wrap: wrap; + } .text-ellipsis { text-overflow: ellipsis; } @@ -873,9 +878,6 @@ .text-red-500 { color: var(--color-red-500); } - .text-red-800 { - color: var(--color-red-800); - } .text-white { color: var(--color-white); } @@ -971,12 +973,6 @@ -webkit-user-select: none; user-select: none; } - .\[-webkit-box-orient\:vertical\] { - -webkit-box-orient: vertical; - } - .\[-webkit-line-clamp\:4\] { - -webkit-line-clamp: 4; - } .peer-checked\:border-blue-600 { &:is(:where(.peer):checked ~ *) { border-color: var(--color-blue-600); @@ -1307,12 +1303,6 @@ height: calc(var(--spacing) * 48); } } - .md\:size-64 { - @media (width >= 48rem) { - width: calc(var(--spacing) * 64); - height: calc(var(--spacing) * 64); - } - } .md\:h-24 { @media (width >= 48rem) { height: calc(var(--spacing) * 24); @@ -1333,11 +1323,6 @@ width: calc(1/4 * 100%); } } - .md\:w-2\/5 { - @media (width >= 48rem) { - width: calc(2/5 * 100%); - } - } .md\:w-3\/4 { @media (width >= 48rem) { width: calc(3/4 * 100%); diff --git a/web/static/img/recipe_placeholder.png b/web/static/img/recipe_placeholder.png new file mode 100644 index 0000000..c8664d9 Binary files /dev/null and b/web/static/img/recipe_placeholder.png differ diff --git a/web/static/img/recipe_placeholder_wide.jpg b/web/static/img/recipe_placeholder_wide.jpg new file mode 100644 index 0000000..07d0520 Binary files /dev/null and b/web/static/img/recipe_placeholder_wide.jpg differ