From 4614b4ef37c5c35a7cb224b2320a54d3ee9e671c Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Fri, 25 Jul 2025 16:53:31 -0700 Subject: [PATCH 1/5] (UI): Implemented the UI for the recipe of the week. --- internal/templates/components/cards.templ | 43 ++++---- internal/templates/components/cards_templ.go | 107 +++++++++++++++++-- internal/templates/pages/home.templ | 24 +++-- internal/templates/pages/home_templ.go | 74 ++++++++----- web/static/css/tailwind.css | 37 ++++++- 5 files changed, 217 insertions(+), 68 deletions(-) diff --git a/internal/templates/components/cards.templ b/internal/templates/components/cards.templ index 58a8f20..7378274 100644 --- a/internal/templates/components/cards.templ +++ b/internal/templates/components/cards.templ @@ -56,35 +56,40 @@ templ ContentCardSmall(content, target string) { } // TODO: Implement this using a recipe type parameter! -templ RecipeCardLarge(liked bool) { -
- -
-

Avocado Toast

+templ RecipeCardLarge(recipe *domain.Recipe) { + if recipe != nil { +
+ +
+

+ { recipe.Title } +

- Hayden Hargreaves + Serves { recipe.Serves }

-

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

+ { recipe.Description } +

+ +
+

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

- if liked { + 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..4a817ad 100644 --- a/internal/templates/components/cards_templ.go +++ b/internal/templates/components/cards_templ.go @@ -197,7 +197,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 +218,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: 65, Col: 22} + } + _, 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: 68, Col: 26} + } + _, 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: 71, Col: 26} + } + _, 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: 76, Col: 22} + } + _, 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: 76, Col: 50} + } + _, 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/home.templ b/internal/templates/pages/home.templ index 657caf9..87f80d4 100644 --- a/internal/templates/pages/home.templ +++ b/internal/templates/pages/home.templ @@ -47,7 +47,7 @@ templ highlightSection(liked bool) { resonate with our users!

- @components.RecipeCardLarge(false) + @components.RecipeCardLarge(nil)
} @@ -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 {
diff --git a/internal/templates/pages/home_templ.go b/internal/templates/pages/home_templ.go index e66f94c..d49bc7e 100644 --- a/internal/templates/pages/home_templ.go +++ b/internal/templates/pages/home_templ.go @@ -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(nil).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 } @@ -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 } @@ -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/web/static/css/tailwind.css b/web/static/css/tailwind.css index a1d4d52..2c0d983 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -8,8 +8,8 @@ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; --color-red-100: oklch(93.6% 0.032 17.717); + --color-red-200: oklch(88.5% 0.062 18.334); --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); @@ -379,6 +379,14 @@ width: calc(var(--spacing) * 56); height: calc(var(--spacing) * 56); } + .size-64 { + width: calc(var(--spacing) * 64); + height: calc(var(--spacing) * 64); + } + .size-72 { + width: calc(var(--spacing) * 72); + height: calc(var(--spacing) * 72); + } .size-80 { width: calc(var(--spacing) * 80); height: calc(var(--spacing) * 80); @@ -446,6 +454,15 @@ .w-52 { width: calc(var(--spacing) * 52); } + .w-64 { + width: calc(var(--spacing) * 64); + } + .w-72 { + width: calc(var(--spacing) * 72); + } + .w-80 { + width: calc(var(--spacing) * 80); + } .w-fit { width: fit-content; } @@ -455,6 +472,9 @@ .max-w-2xl { max-width: var(--container-2xl); } + .min-w-full { + min-width: 100%; + } .flex-shrink-0 { flex-shrink: 0; } @@ -624,6 +644,9 @@ .border-green-500 { border-color: var(--color-green-500); } + .border-red-200 { + border-color: var(--color-red-200); + } .border-red-500 { border-color: var(--color-red-500); } @@ -831,6 +854,9 @@ --tw-tracking: var(--tracking-wide); letter-spacing: var(--tracking-wide); } + .text-wrap { + text-wrap: wrap; + } .text-ellipsis { text-overflow: ellipsis; } @@ -873,9 +899,6 @@ .text-red-500 { color: var(--color-red-500); } - .text-red-800 { - color: var(--color-red-800); - } .text-white { color: var(--color-white); } @@ -1313,6 +1336,12 @@ height: calc(var(--spacing) * 64); } } + .md\:size-80 { + @media (width >= 48rem) { + width: calc(var(--spacing) * 80); + height: calc(var(--spacing) * 80); + } + } .md\:h-24 { @media (width >= 48rem) { height: calc(var(--spacing) * 24); From 68b97cea21125004a4398999d75aa56a7b39e89e Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Sat, 26 Jul 2025 20:10:48 -0700 Subject: [PATCH 2/5] (FEAT/DB): Implemented the DB migrations for RotW. Created a table and a stored procedure to generate the values in the table! This is my first implementation of an SP, which is very cool! --- doc/TechnicalSpecification.md | 6 +++ .../009_create_recipe_of_the_week_table.sql | 14 +++++++ ...10_create_recipe_of_the_week_procedure.sql | 41 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 internal/infrastructure/database/migrations/009_create_recipe_of_the_week_table.sql create mode 100644 internal/infrastructure/database/migrations/010_create_recipe_of_the_week_procedure.sql 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/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; +$$; From 53943dd1839f4751aab4208968e722fc757ca3f3 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Sat, 26 Jul 2025 22:36:48 -0700 Subject: [PATCH 3/5] (FEAT): Implemented recipe of the week! And image placeholder. Still having the stupid ass nil dereferences, I think I might need to migrate to using success returns instead of pointers. Because they're fucked. And even more so now. --- internal/app/handlers/page_handler.go | 26 +++++- internal/app/service/recipe_service.go | 10 +++ internal/domain/recipe/repository.go | 3 + internal/domain/recipe/service.go | 7 +- .../database/repository/recipe_repository.go | 85 ++++++++++++++++++ internal/templates/components/cards.templ | 82 ++++++++--------- internal/templates/components/cards_templ.go | 31 ++++--- internal/templates/pages/favorites.templ | 2 +- internal/templates/pages/favorites_templ.go | 2 +- internal/templates/pages/home.templ | 8 +- internal/templates/pages/home_templ.go | 8 +- internal/templates/pages/recipe.templ | 2 +- internal/templates/pages/recipe_templ.go | 2 +- internal/templates/pages/search.templ | 2 +- internal/templates/pages/search_templ.go | 2 +- web/static/css/tailwind.css | 50 +---------- web/static/img/recipe_placeholder.png | Bin 0 -> 1072 bytes web/static/img/recipe_placeholder_wide.jpg | Bin 0 -> 7930 bytes 18 files changed, 199 insertions(+), 123 deletions(-) create mode 100644 web/static/img/recipe_placeholder.png create mode 100644 web/static/img/recipe_placeholder_wide.jpg diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index 61dfe22..ae28e7b 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strconv" + "time" "github.com/a-h/templ" "github.com/gin-gonic/gin" @@ -28,6 +29,7 @@ func HomePage(ctx *gin.Context) { loggedIn := domain.IsLoggedIn(ctx) + var page templ.Component if loggedIn { userId := ctx.MustGet("userId").(int) @@ -48,9 +50,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, time.Now()) + 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, time.Now()) + 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..1d3f231 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,7 @@ func (s *RecipeService) GetUserMadeRecipes(userId, limit int) ([]domain.Recipe, return s.recipeRepository.GetRecipes(ids, &userId) } + +func (s *RecipeService) GetRecipeOfTheWeek(userId *int, date time.Time) (*domain.Recipe, error) { + return s.recipeRepository.GetRecipeOfTheWeek(userId, date) +} diff --git a/internal/domain/recipe/repository.go b/internal/domain/recipe/repository.go index 761aa2e..55db598 100644 --- a/internal/domain/recipe/repository.go +++ b/internal/domain/recipe/repository.go @@ -1,5 +1,7 @@ package domain +import "time" + type RecipeRepository interface { CreateRecipe(recipe *Recipe) error GetRecipe(id int, userId *int) (*Recipe, error) @@ -10,4 +12,5 @@ type RecipeRepository interface { GetUserFavoriteRecipes(id int) ([]Recipe, error) GetRecipeTags(recipe *Recipe) error GetRecipeFavorite(recipe *Recipe, userId int) error + GetRecipeOfTheWeek(userId *int, date time.Time) (*Recipe, error) } diff --git a/internal/domain/recipe/service.go b/internal/domain/recipe/service.go index 373d417..33b83ef 100644 --- a/internal/domain/recipe/service.go +++ b/internal/domain/recipe/service.go @@ -1,6 +1,10 @@ package domain -import "github.com/gin-gonic/gin" +import ( + "time" + + "github.com/gin-gonic/gin" +) type RecipeService interface { CreateRecipe(ctx *gin.Context) (*Recipe, error) @@ -10,4 +14,5 @@ type RecipeService interface { GetUserFavoriteRecipes(id int) ([]Recipe, error) GetUserViewedRecipes(userId, limit int) ([]Recipe, error) GetUserMadeRecipes(userId, limit int) ([]Recipe, error) + GetRecipeOfTheWeek(userId *int, date time.Time) (*Recipe, error) } diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go index f043102..960abee 100644 --- a/internal/infrastructure/database/repository/recipe_repository.go +++ b/internal/infrastructure/database/repository/recipe_repository.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strings" + "time" domain "github.com/haydenhargreaves/Potion/internal/domain/recipe" "github.com/lib/pq" @@ -832,3 +833,87 @@ func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int) return nil } + +func (r *RecipeRepository) GetRecipeOfTheWeek(userId *int, date time.Time) (*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 7378274..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,41 +53,39 @@ templ ContentCardSmall(content, target string) {
} -// TODO: Implement this using a recipe type parameter! templ RecipeCardLarge(recipe *domain.Recipe) { - if recipe != nil { -
- -
-

- { recipe.Title } -

-

- Serves { recipe.Serves } -

-

- { recipe.Description } -

- -
-

- { recipe.Category } - { recipe.Duration.Total } mins + if recipe != nil { +

+ +
+

+ { recipe.Title } +

+

+ Serves { recipe.Serves }

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

+ { recipe.Description } +

+
+

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

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

Coming soon!

- } + } else { +

Coming soon!

+ } } diff --git a/internal/templates/components/cards_templ.go b/internal/templates/components/cards_templ.go index 4a817ad..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,7 +196,6 @@ func ContentCardSmall(content, target string) templ.Component { }) } -// TODO: Implement this using a recipe type parameter! 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 @@ -219,14 +218,14 @@ func RecipeCardLarge(recipe *domain.Recipe) templ.Component { } ctx = templ.ClearChildren(ctx) if recipe != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

") + 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: 65, Col: 22} + 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 { @@ -239,7 +238,7 @@ func RecipeCardLarge(recipe *domain.Recipe) templ.Component { 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: 68, Col: 26} + 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 { @@ -252,7 +251,7 @@ func RecipeCardLarge(recipe *domain.Recipe) templ.Component { 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: 71, Col: 26} + 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 { @@ -265,7 +264,7 @@ func RecipeCardLarge(recipe *domain.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/components/cards.templ`, Line: 76, Col: 22} + 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 { @@ -278,7 +277,7 @@ func RecipeCardLarge(recipe *domain.Recipe) templ.Component { 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: 76, Col: 50} + 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 { @@ -301,7 +300,7 @@ func RecipeCardLarge(recipe *domain.Recipe) templ.Component { var templ_7745c5c3_Var17 string templ_7745c5c3_Var17, 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: 83, Col: 70} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/cards.templ`, Line: 79, Col: 71} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != 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 87f80d4..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(nil) + @components.RecipeCardLarge(recipeOfTheWeek)
} @@ -122,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 d49bc7e..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(nil).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 } @@ -304,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 { @@ -341,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 } 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 2c0d983..080b507 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -8,7 +8,6 @@ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; --color-red-100: oklch(93.6% 0.032 17.717); - --color-red-200: oklch(88.5% 0.062 18.334); --color-red-500: oklch(63.7% 0.237 25.331); --color-green-500: oklch(72.3% 0.219 149.579); --color-blue-50: oklch(97% 0.014 254.604); @@ -335,9 +334,6 @@ .mb-16 { margin-bottom: calc(var(--spacing) * 16); } - .\[display\:-webkit-box\] { - display: -webkit-box; - } .block { display: block; } @@ -379,14 +375,6 @@ width: calc(var(--spacing) * 56); height: calc(var(--spacing) * 56); } - .size-64 { - width: calc(var(--spacing) * 64); - height: calc(var(--spacing) * 64); - } - .size-72 { - width: calc(var(--spacing) * 72); - height: calc(var(--spacing) * 72); - } .size-80 { width: calc(var(--spacing) * 80); height: calc(var(--spacing) * 80); @@ -424,6 +412,9 @@ .h-screen { height: 100vh; } + .min-h-72 { + min-height: calc(var(--spacing) * 72); + } .min-h-screen { min-height: 100vh; } @@ -454,12 +445,6 @@ .w-52 { width: calc(var(--spacing) * 52); } - .w-64 { - width: calc(var(--spacing) * 64); - } - .w-72 { - width: calc(var(--spacing) * 72); - } .w-80 { width: calc(var(--spacing) * 80); } @@ -472,9 +457,6 @@ .max-w-2xl { max-width: var(--container-2xl); } - .min-w-full { - min-width: 100%; - } .flex-shrink-0 { flex-shrink: 0; } @@ -644,9 +626,6 @@ .border-green-500 { border-color: var(--color-green-500); } - .border-red-200 { - border-color: var(--color-red-200); - } .border-red-500 { border-color: var(--color-red-500); } @@ -994,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); @@ -1330,18 +1303,6 @@ height: calc(var(--spacing) * 48); } } - .md\:size-64 { - @media (width >= 48rem) { - width: calc(var(--spacing) * 64); - height: calc(var(--spacing) * 64); - } - } - .md\:size-80 { - @media (width >= 48rem) { - width: calc(var(--spacing) * 80); - height: calc(var(--spacing) * 80); - } - } .md\:h-24 { @media (width >= 48rem) { height: calc(var(--spacing) * 24); @@ -1362,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 0000000000000000000000000000000000000000..c8664d907a793d90bb47a2859139e80e55b5d63a GIT binary patch literal 1072 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 Date: Sat, 26 Jul 2025 22:46:45 -0700 Subject: [PATCH 4/5] (FIX): Removed date requirement from rotf --- internal/app/handlers/page_handler.go | 6 ++---- internal/app/service/recipe_service.go | 6 ++++-- internal/domain/recipe/repository.go | 4 +--- internal/domain/recipe/service.go | 4 +--- .../infrastructure/database/repository/recipe_repository.go | 3 +-- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index ae28e7b..0f3629e 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "strconv" - "time" "github.com/a-h/templ" "github.com/gin-gonic/gin" @@ -29,7 +28,6 @@ func HomePage(ctx *gin.Context) { loggedIn := domain.IsLoggedIn(ctx) - var page templ.Component if loggedIn { userId := ctx.MustGet("userId").(int) @@ -51,7 +49,7 @@ func HomePage(ctx *gin.Context) { } // Get the recipe of the week - recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(&userId, time.Now()) + recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(&userId) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "status": http.StatusInternalServerError, @@ -63,7 +61,7 @@ func HomePage(ctx *gin.Context) { page = templates.HomePage(true, viewedRecipes, madeRecipes, recipeOfTheWeek) } else { // Get the recipe of the week - recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(nil, time.Now()) + recipeOfTheWeek, err := deps.RecipeService.GetRecipeOfTheWeek(nil) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "status": http.StatusInternalServerError, diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index 1d3f231..6a34016 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -208,6 +208,8 @@ func (s *RecipeService) GetUserMadeRecipes(userId, limit int) ([]domain.Recipe, return s.recipeRepository.GetRecipes(ids, &userId) } -func (s *RecipeService) GetRecipeOfTheWeek(userId *int, date time.Time) (*domain.Recipe, error) { - return s.recipeRepository.GetRecipeOfTheWeek(userId, date) +// GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value, +// the recipe will be nil. The +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 55db598..8574035 100644 --- a/internal/domain/recipe/repository.go +++ b/internal/domain/recipe/repository.go @@ -1,7 +1,5 @@ package domain -import "time" - type RecipeRepository interface { CreateRecipe(recipe *Recipe) error GetRecipe(id int, userId *int) (*Recipe, error) @@ -12,5 +10,5 @@ type RecipeRepository interface { GetUserFavoriteRecipes(id int) ([]Recipe, error) GetRecipeTags(recipe *Recipe) error GetRecipeFavorite(recipe *Recipe, userId int) error - GetRecipeOfTheWeek(userId *int, date time.Time) (*Recipe, error) + GetRecipeOfTheWeek(userId *int) (*Recipe, error) } diff --git a/internal/domain/recipe/service.go b/internal/domain/recipe/service.go index 33b83ef..91bf12f 100644 --- a/internal/domain/recipe/service.go +++ b/internal/domain/recipe/service.go @@ -1,8 +1,6 @@ package domain import ( - "time" - "github.com/gin-gonic/gin" ) @@ -14,5 +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, date time.Time) (*Recipe, error) + GetRecipeOfTheWeek(userId *int) (*Recipe, error) } diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go index 960abee..6d9fd9e 100644 --- a/internal/infrastructure/database/repository/recipe_repository.go +++ b/internal/infrastructure/database/repository/recipe_repository.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "strings" - "time" domain "github.com/haydenhargreaves/Potion/internal/domain/recipe" "github.com/lib/pq" @@ -834,7 +833,7 @@ func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int) return nil } -func (r *RecipeRepository) GetRecipeOfTheWeek(userId *int, date time.Time) (*domain.Recipe, error) { +func (r *RecipeRepository) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) { tx, err := r.db.Begin() if err != nil { tx.Rollback() From ae2b2791ce3552930843ad054e82886275d47763 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Sat, 26 Jul 2025 23:02:34 -0700 Subject: [PATCH 5/5] (FIX): Fixed one of small deref errors. The recipe get tags method was failing when a nil recipe is provided. Not sure when that would happen, but without fixing it, we can just return nil when a blank recipe is returned. --- internal/app/service/recipe_service.go | 2 +- .../database/repository/recipe_repository.go | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index 6a34016..063db48 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -209,7 +209,7 @@ func (s *RecipeService) GetUserMadeRecipes(userId, limit int) ([]domain.Recipe, } // GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value, -// the recipe will be nil. The +// 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/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go index 6d9fd9e..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() @@ -833,6 +846,9 @@ 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 {