From 7ad710f880d476f34af32aea0eed99a58c9adf57 Mon Sep 17 00:00:00 2001
From: Hayden Hargreaves
Date: Sun, 6 Jul 2025 22:40:15 -0700
Subject: [PATCH 1/7] (FEAT): Search is returning recipes, next just need a UI
and wire job.
Furthermore, not sure how we are going to handle the searching. Maybe a
full-text search index? For now, it has been ignored, but the filters
seem to be working properly.
---
doc/TechnicalSpecification.md | 2 +-
internal/app/handlers/recipe_handler.go | 10 +
internal/app/server/server.go | 2 +-
internal/app/service/recipe_service.go | 44 +++++
internal/domain/recipe/recipe.go | 35 +++-
internal/domain/recipe/repository.go | 1 +
internal/domain/recipe/service.go | 1 +
internal/domain/server/routes.go | 2 +
.../database/repository/recipe_repository.go | 174 ++++++++++++++++++
internal/templates/components/dropdowns.templ | 61 +++---
.../templates/components/dropdowns_templ.go | 94 ++++++----
internal/templates/pages/create.templ | 3 +-
internal/templates/pages/create_templ.go | 16 +-
internal/templates/pages/home.templ | 18 +-
internal/templates/pages/home_templ.go | 71 ++++---
web/static/css/tailwind.css | 86 +++++++++
16 files changed, 520 insertions(+), 100 deletions(-)
diff --git a/doc/TechnicalSpecification.md b/doc/TechnicalSpecification.md
index 5e15783..c07f639 100644
--- a/doc/TechnicalSpecification.md
+++ b/doc/TechnicalSpecification.md
@@ -194,7 +194,7 @@ creation process will take place here
- [x] Recipe Creation Wizard
- [x] Create a new recipe object in the database
- [x] Recipe should be attached to a user (logged in)
- - [ ] User should be directed to the view recipe page on a successful creation
+ - [x] User should be directed to the view recipe page on a successful creation
diff --git a/internal/app/handlers/recipe_handler.go b/internal/app/handlers/recipe_handler.go
index 6756423..7854a98 100644
--- a/internal/app/handlers/recipe_handler.go
+++ b/internal/app/handlers/recipe_handler.go
@@ -34,3 +34,13 @@ func CreateRecipe(ctx *gin.Context) {
ctx.Header("HX-Redirect", url)
ctx.Status(http.StatusCreated)
}
+
+func SearchRecipes(ctx *gin.Context) {
+ deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
+
+ recipes, err := deps.RecipeService.SearchRecipes(ctx)
+ if err != nil {
+ ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
+ }
+ ctx.JSON(http.StatusOK, gin.H{"recipes": recipes})
+}
diff --git a/internal/app/server/server.go b/internal/app/server/server.go
index a234450..b972deb 100644
--- a/internal/app/server/server.go
+++ b/internal/app/server/server.go
@@ -176,8 +176,8 @@ func (s *Server) Setup() *Server {
router_api.GET("/auth/logout", handlers.Logout)
// Recipe endpoints
- // TODO: This should be post. Temp!
router_api.POST("/recipe", handlers.CreateRecipe)
+ router_api.POST("/recipe/search", handlers.SearchRecipes)
return s
}
diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go
index e1f9fe6..4057218 100644
--- a/internal/app/service/recipe_service.go
+++ b/internal/app/service/recipe_service.go
@@ -129,3 +129,47 @@ func (s *RecipeService) GetRecipe(id int) (*domain.Recipe, error) {
return recipe, err
}
+
+// toBits converts an array of stringified numbers into a single summed value
+func toBits(arr []string) (bits int) {
+ for _, x := range arr {
+ num, _ := strconv.Atoi(x)
+ bits += num
+ }
+ return
+}
+
+// isBitActive returns true when the bit at pos (0 indexed) is true.
+func isBitActive(bits, pos int) bool {
+ return (bits>>pos)&1 == 1
+}
+
+func (s *RecipeService) SearchRecipes(ctx *gin.Context) ([]domain.Recipe, error) {
+ // NOTE: How are the filters handled?
+ // Each input is given a bit value (e.g., 00001 for 1) and will be passed
+ // back to this handler as an array. The values are then added together
+ // and will result in a integer which represents bit values. These bits
+ // can then be passed to the repository and are then parsed to determine
+ // which filters should be applied.
+ // Parsing these is simple, for each filter option, use the bitwise and (&)
+ // operator with the value we expect for the filter. When 1, we can ensure
+ // the filter is provided.
+ // A function above (isBitActive) provides an example of testing of testing
+ // the filter parsing.
+
+ search := ctx.PostForm("search") // string, search query for titles
+ meal := toBits(ctx.PostFormArray("meal"))
+ time := toBits(ctx.PostFormArray("time"))
+ difficulty := toBits(ctx.PostFormArray("difficulty"))
+ serving := toBits(ctx.PostFormArray("serving"))
+
+ filters := domain.SearchFilters{
+ Search: search,
+ MealType: meal,
+ Time: time,
+ Difficulty: difficulty,
+ ServingSize: serving,
+ }
+
+ return s.recipeRepository.SearchRecipes(filters)
+}
diff --git a/internal/domain/recipe/recipe.go b/internal/domain/recipe/recipe.go
index f05b36e..1eb61fc 100644
--- a/internal/domain/recipe/recipe.go
+++ b/internal/domain/recipe/recipe.go
@@ -18,12 +18,34 @@ const (
MealBreakfast RecipeMeal = "breakfast"
MealLunch RecipeMeal = "lunch"
MealDinner RecipeMeal = "dinner"
- MealDessert RecipeMeal = "dessert"
+ MealDesert RecipeMeal = "dessert"
MealSnack RecipeMeal = "snack"
MealSide RecipeMeal = "side"
MealOther RecipeMeal = "other"
)
+// ParseMeal converts an integer value into a meal type (string). Values are 0-indexed.
+func ParseMeal(meal int) RecipeMeal {
+ switch meal {
+ case 0:
+ return MealBreakfast
+ case 1:
+ return MealLunch
+ case 2:
+ return MealDinner
+ case 3:
+ return MealDesert
+ case 4:
+ return MealSnack
+ case 5:
+ return MealSide
+ case 6:
+ return MealOther
+ default:
+ return MealOther
+ }
+}
+
// RecipeIngredient is a single ingredients in a recipe. These have JSON tags which allow them
// to be marshaled into a JSON array and stored in the database (JSONB).
type RecipeIngredient struct {
@@ -47,3 +69,14 @@ type Recipe struct {
Modified *time.Time // Pointer to allow null
Created time.Time
}
+
+// SearchFilters is a model which represents the required filters to complete a recipe search.
+// The integer values should be provided as bits and used to parse out individual flags. More
+// details can be found in the SearchRecipes service function.
+type SearchFilters struct {
+ Search string
+ MealType int
+ Time int
+ Difficulty int
+ ServingSize int
+}
diff --git a/internal/domain/recipe/repository.go b/internal/domain/recipe/repository.go
index d8eda0e..f89c026 100644
--- a/internal/domain/recipe/repository.go
+++ b/internal/domain/recipe/repository.go
@@ -3,4 +3,5 @@ package domain
type RecipeRepository interface {
CreateRecipe(recipe *Recipe) error
GetRecipe(id int) (*Recipe, error)
+ SearchRecipes(filters SearchFilters) ([]Recipe, error)
}
diff --git a/internal/domain/recipe/service.go b/internal/domain/recipe/service.go
index dc2409b..558f63f 100644
--- a/internal/domain/recipe/service.go
+++ b/internal/domain/recipe/service.go
@@ -5,4 +5,5 @@ import "github.com/gin-gonic/gin"
type RecipeService interface {
CreateRecipe(ctx *gin.Context) (*Recipe, error)
GetRecipe(id int) (*Recipe, error)
+ SearchRecipes(ctx *gin.Context) ([]Recipe, error)
}
diff --git a/internal/domain/server/routes.go b/internal/domain/server/routes.go
index 47ac188..41d9aa7 100644
--- a/internal/domain/server/routes.go
+++ b/internal/domain/server/routes.go
@@ -19,3 +19,5 @@ const WEB_RECIPE = VERSION + WEB + "/recipe/%d"
const API_AUTH_LOGIN = VERSION + API + "/auth/login"
const API_AUTH_CALLBACK = VERSION + API + "/auth/callback"
const API_AUTH_LOGOUT = VERSION + API + "/auth/logout"
+const API_CREATE_RECIPE = VERSION + API + "/recipe"
+const API_SEARCH_RECIPES = VERSION + API + "/recipe/search"
diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go
index cfbf04d..6727448 100644
--- a/internal/infrastructure/database/repository/recipe_repository.go
+++ b/internal/infrastructure/database/repository/recipe_repository.go
@@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
+ "strings"
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
"github.com/lib/pq"
@@ -134,6 +135,8 @@ func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) {
}
recipe.Duration = duration
+ } else {
+ recipe.Duration = domain.RecipeDuration{}
}
// Parse ingredient
@@ -144,7 +147,178 @@ func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) {
}
recipe.Ingredients = ingredients
+ } else {
+ recipe.Ingredients = []domain.RecipeIngredient{}
}
return &recipe, nil
}
+
+// isBitActive returns true when the bit at pos (0 indexed) is true.
+func isBitActive(bits, pos int) bool {
+ return (bits>>pos)&1 == 1
+}
+
+func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain.Recipe, error) {
+ tx, err := r.db.Begin()
+ if err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+
+ // Generate the query
+ query := "SELECT * FROM recipes"
+
+ // Compute meals type filters (there are 7 bits)
+ var mealConditions []string
+ for i := range 7 {
+ if isBitActive(filters.MealType, i) {
+ mealConditions = append(mealConditions, fmt.Sprintf("category = '%s'", domain.ParseMeal(i)))
+ }
+ }
+
+ // Compute time filters (there are 5 bits)
+ var timeConditions []string
+ for i := range 5 {
+ var cond string
+ if isBitActive(filters.Time, i) {
+ switch i {
+ case 0:
+ cond = "(duration->>'total')::int < 15"
+ case 1:
+ cond = "(duration->>'total')::int BETWEEN 15 AND 30"
+ case 2:
+ cond = "(duration->>'total')::int BETWEEN 30 AND 60"
+ case 3:
+ cond = "(duration->>'total')::int BETWEEN 60 AND 120"
+ case 4:
+ cond = "(duration->>'total')::int > 120"
+ }
+ timeConditions = append(timeConditions, cond)
+ }
+ }
+
+ // Compute difficulty filters (there are 5 bits)
+ var difficultyConditions []string
+ for i := range 5 {
+ if isBitActive(filters.Difficulty, i) {
+ cond := fmt.Sprintf("difficulty = '%d'", i+1)
+ difficultyConditions = append(difficultyConditions, cond)
+ }
+ }
+
+ // Compute serving size filters (there are 5 bits)
+ var servingConditions []string
+ for i := range 5 {
+ var cond string
+ if isBitActive(filters.ServingSize, i) {
+ switch i {
+ case 0:
+ cond = "serves BETWEEN 1 AND 2"
+ case 1:
+ cond = "serves BETWEEN 2 AND 4"
+ case 2:
+ cond = "serves BETWEEN 4 AND 6"
+ case 3:
+ cond = "serves BETWEEN 6 AND 8"
+ case 4:
+ cond = "serves > 8"
+ }
+ servingConditions = append(servingConditions, cond)
+ }
+ }
+
+ // TODO: Title search somehow...
+
+ // Merge condition strings
+ mealString := fmt.Sprintf("(%s)", strings.Join(mealConditions, " OR "))
+ timeString := fmt.Sprintf("(%s)", strings.Join(timeConditions, " OR "))
+ difficultyString := fmt.Sprintf("(%s)", strings.Join(difficultyConditions, " OR "))
+ servingString := fmt.Sprintf("(%s)", strings.Join(servingConditions, " OR "))
+
+ // Combine condition strings
+ var conditions []string
+ if len(mealConditions) > 0 {
+ conditions = append(conditions, mealString)
+ }
+ if len(timeConditions) > 0 {
+ conditions = append(conditions, timeString)
+ }
+ if len(difficultyConditions) > 0 {
+ conditions = append(conditions, difficultyString)
+ }
+ if len(servingConditions) > 0 {
+ conditions = append(conditions, servingString)
+ }
+
+ // Convert and append conditions
+ if len(conditions) > 0 {
+ conditionsString := fmt.Sprintf("WHERE %s", strings.Join(conditions, " AND "))
+ query = fmt.Sprintf("%s %s;", query, conditionsString)
+ } else {
+ query += ";"
+ }
+
+ // Execute the query
+ rows, err := tx.Query(query)
+ if err != nil {
+ return nil, fmt.Errorf("failed to query recipes: %w", err)
+ }
+ defer rows.Close()
+
+ var recipes []domain.Recipe
+ for rows.Next() {
+ // Parsed values location
+ var recipe domain.Recipe
+ var durationBytes []byte
+ var ingredientBytes []byte
+
+ if err := rows.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 scan recipe row: %w", err)
+ }
+
+ // Parse duration from bytes
+ if len(durationBytes) > 0 {
+ var duration domain.RecipeDuration
+ if err := json.Unmarshal(durationBytes, &duration); err != nil {
+ return nil, fmt.Errorf("failed to parse duration for recipe ID %d: %w", recipe.Id, err)
+ }
+ recipe.Duration = duration
+ } else {
+ recipe.Duration = domain.RecipeDuration{}
+ }
+
+ // Parse ingredients from bytes
+ if len(ingredientBytes) > 0 {
+ var ingredients []domain.RecipeIngredient
+ if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
+ return nil, fmt.Errorf("failed to parse ingredients for recipe ID %d: %w", recipe.Id, err)
+ }
+ recipe.Ingredients = ingredients
+ } else {
+ recipe.Ingredients = []domain.RecipeIngredient{}
+ }
+
+ recipes = append(recipes, recipe)
+ }
+
+ if err := tx.Commit(); err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+
+ return recipes, nil
+}
diff --git a/internal/templates/components/dropdowns.templ b/internal/templates/components/dropdowns.templ
index 6439394..226efc8 100644
--- a/internal/templates/components/dropdowns.templ
+++ b/internal/templates/components/dropdowns.templ
@@ -1,12 +1,15 @@
package components
-templ dropdownButton(name string) {
-
- { name }
-
+templ dropdownButton(content, name, value string) {
+
+
+
+ { content }
+
+
}
templ FilterDropdown() {
@@ -30,13 +33,13 @@ templ FilterDropdown() {
Meal
- @dropdownButton("Breakfast")
- @dropdownButton("Lunch")
- @dropdownButton("Dinner")
- @dropdownButton("Desert")
- @dropdownButton("Snack")
- @dropdownButton("Side")
- @dropdownButton("Other")
+ @dropdownButton("Breakfast", "meal", "1")
+ @dropdownButton("Lunch", "meal", "2")
+ @dropdownButton("Dinner", "meal", "4")
+ @dropdownButton("Desert", "meal", "8")
+ @dropdownButton("Snack", "meal", "16")
+ @dropdownButton("Side", "meal", "32")
+ @dropdownButton("Other", "meal", "64")
@@ -44,11 +47,11 @@ templ FilterDropdown() {
Cook Time
- @dropdownButton("< 15 min")
- @dropdownButton("15 to 30 min")
- @dropdownButton("30 to 60 min")
- @dropdownButton("60 to 120 min")
- @dropdownButton("+120 min")
+ @dropdownButton("< 15 min", "time", "1")
+ @dropdownButton("15 to 30 min", "time", "2")
+ @dropdownButton("30 to 60 min", "time", "4")
+ @dropdownButton("60 to 120 min", "time", "8")
+ @dropdownButton("+120 min", "time", "16")
@@ -56,11 +59,11 @@ templ FilterDropdown() {
Difficulty
- @dropdownButton("Beginner")
- @dropdownButton("Easy")
- @dropdownButton("Intermediate")
- @dropdownButton("Challegening")
- @dropdownButton("Extreme")
+ @dropdownButton("Beginner", "difficulty", "1")
+ @dropdownButton("Easy", "difficulty", "2")
+ @dropdownButton("Intermediate", "difficulty", "4")
+ @dropdownButton("Challenging", "difficulty", "8")
+ @dropdownButton("Extreme", "difficulty", "16")
@@ -68,11 +71,11 @@ templ FilterDropdown() {
Serving Size
- @dropdownButton("1 to 2")
- @dropdownButton("1 to 4")
- @dropdownButton("4 to 6")
- @dropdownButton("6 to 8")
- @dropdownButton("8+")
+ @dropdownButton("1 to 2", "serving", "1")
+ @dropdownButton("2 to 4", "serving", "2")
+ @dropdownButton("4 to 6", "serving", "4")
+ @dropdownButton("6 to 8", "serving", "8")
+ @dropdownButton("8+", "serving", "16")
diff --git a/internal/templates/components/dropdowns_templ.go b/internal/templates/components/dropdowns_templ.go
index dde6499..e6605c4 100644
--- a/internal/templates/components/dropdowns_templ.go
+++ b/internal/templates/components/dropdowns_templ.go
@@ -8,7 +8,7 @@ package components
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
-func dropdownButton(name string) templ.Component {
+func dropdownButton(content, name, value string) 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 {
@@ -29,20 +29,46 @@ func dropdownButton(name string) 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, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" value=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var3 string
+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(value)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/dropdowns.templ`, Line: 5, Col: 52}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"sr-only peer\"> ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(content)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/dropdowns.templ`, Line: 10, Col: 12}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -66,116 +92,116 @@ func FilterDropdown() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var3 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var3 == nil {
- templ_7745c5c3_Var3 = templ.NopComponent
+ templ_7745c5c3_Var5 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var5 == nil {
+ templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/internal/templates/pages/create.templ b/internal/templates/pages/create.templ
index 25e874c..a83d479 100644
--- a/internal/templates/pages/create.templ
+++ b/internal/templates/pages/create.templ
@@ -1,6 +1,7 @@
package templates
import "github.com/haydenhargreaves/Potion/internal/templates/components"
+import "github.com/haydenhargreaves/Potion/internal/domain/server"
templ CreatePage() {
@components.Navbar("create")
@@ -24,7 +25,7 @@ templ Page() {
share your masterpiece!
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Welcome to the Recipe Creation Wizard! Simply fill in the details about your culinary creation, including the recipe's name, a description, and other specifics like its category, duration, and difficulty. Don't forget to dynamically add all your ingredients and instructions using the dedicated buttons, and feel free to upload an appealing image. All required fields are marked with an * . Once everything looks perfect, just hit the \"Create Recipe\" button to share your masterpiece!
Description * Please enter a description. Between 1-1000 characters.
Category * Select a category Breakfast Lunch Dinner Dessert Snack Side Other Please select a category.
Difficulty * Select a difficulty Beginner Easy Intermediate Challenging Extreme Please select a difficulty.
Ingredients * Add Ingredient Instructions * Please enter at least one step.
Add Instruction Step Recipe Image
Create Recipe ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/internal/templates/pages/home.templ b/internal/templates/pages/home.templ
index b214922..1b4d6d7 100644
--- a/internal/templates/pages/home.templ
+++ b/internal/templates/pages/home.templ
@@ -26,15 +26,18 @@ templ introSection() {
}
templ searchBar() {
+
@@ -54,6 +58,7 @@ templ searchBar() {
templ filterButton() {
@components.BannerText("Craving Something Specific?")
-
+
@searchBar()
@components.FilterDropdown()
-
+
}
diff --git a/internal/templates/pages/home_templ.go b/internal/templates/pages/home_templ.go
index 5e3d5de..221b42b 100644
--- a/internal/templates/pages/home_templ.go
+++ b/internal/templates/pages/home_templ.go
@@ -61,7 +61,7 @@ func searchBar() 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
}
@@ -98,7 +98,7 @@ func filterButton() templ.Component {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -135,7 +135,20 @@ func searchSection() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -147,7 +160,7 @@ func searchSection() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -171,12 +184,12 @@ func highlightSection(liked bool) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var5 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var5 == nil {
- templ_7745c5c3_Var5 = templ.NopComponent
+ templ_7745c5c3_Var6 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var6 == nil {
+ templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -184,7 +197,7 @@ func highlightSection(liked bool) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Our 'Recipe of the Week' is the cream of the crop! We handpick it by looking at what recipes our community loves most. This isn't just about how many people view a recipe; it's also about how many times it's been made, liked, reviewed, and its average rating, all combined to find the true fan favorite of the week. It's our way of highlighting the best recipes that truly resonate with our users!
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
Our 'Recipe of the Week' is the cream of the crop! We handpick it by looking at what recipes our community loves most. This isn't just about how many people view a recipe; it's also about how many times it's been made, liked, reviewed, and its average rating, all combined to find the true fan favorite of the week. It's our way of highlighting the best recipes that truly resonate with our users!
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -192,7 +205,7 @@ func highlightSection(liked bool) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -216,12 +229,12 @@ func listsSection() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var6 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var6 == nil {
- templ_7745c5c3_Var6 = templ.NopComponent
+ templ_7745c5c3_Var7 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var7 == nil {
+ templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -229,7 +242,7 @@ func listsSection() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Recently viewed ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
Recently viewed ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -257,7 +270,7 @@ func listsSection() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
Make again ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
Make again ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -285,7 +298,7 @@ func listsSection() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -309,21 +322,21 @@ func ctaSection() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var7 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var7 == nil {
- templ_7745c5c3_Var7 = templ.NopComponent
+ templ_7745c5c3_Var8 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var8 == nil {
+ templ_7745c5c3_Var8 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" 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
}
@@ -347,16 +360,16 @@ func HomePage() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var9 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var9 == nil {
- templ_7745c5c3_Var9 = templ.NopComponent
+ templ_7745c5c3_Var10 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var10 == nil {
+ templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = components.Navbar("home").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -380,7 +393,7 @@ func HomePage() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css
index 2f9e417..8d6b704 100644
--- a/web/static/css/tailwind.css
+++ b/web/static/css/tailwind.css
@@ -212,6 +212,17 @@
}
}
@layer utilities {
+ .sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+ }
.absolute {
position: absolute;
}
@@ -221,6 +232,9 @@
.static {
position: static;
}
+ .top-1 {
+ top: calc(var(--spacing) * 1);
+ }
.top-1\/2 {
top: calc(1/2 * 100%);
}
@@ -230,6 +244,9 @@
.left-0 {
left: calc(var(--spacing) * 0);
}
+ .left-1 {
+ left: calc(var(--spacing) * 1);
+ }
.left-1\/2 {
left: calc(1/2 * 100%);
}
@@ -323,6 +340,9 @@
.hidden {
display: none;
}
+ .inline-block {
+ display: inline-block;
+ }
.inline-flex {
display: inline-flex;
}
@@ -375,12 +395,18 @@
.h-screen {
height: 100vh;
}
+ .w-1 {
+ width: calc(var(--spacing) * 1);
+ }
.w-1\/3 {
width: calc(1/3 * 100%);
}
.w-1\/4 {
width: calc(1/4 * 100%);
}
+ .w-3 {
+ width: calc(var(--spacing) * 3);
+ }
.w-3\/4 {
width: calc(3/4 * 100%);
}
@@ -393,6 +419,9 @@
.w-5 {
width: calc(var(--spacing) * 5);
}
+ .w-9 {
+ width: calc(var(--spacing) * 9);
+ }
.w-9\/10 {
width: calc(9/10 * 100%);
}
@@ -414,16 +443,30 @@
.max-w-xl {
max-width: var(--container-xl);
}
+ .flex-shrink {
+ flex-shrink: 1;
+ }
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
+ .border-collapse {
+ border-collapse: collapse;
+ }
+ .-translate-x-1 {
+ --tw-translate-x: calc(var(--spacing) * -1);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
.-translate-x-1\/2 {
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
+ .-translate-y-1 {
+ --tw-translate-y: calc(var(--spacing) * -1);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
.-translate-y-1\/2 {
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -431,6 +474,9 @@
.cursor-pointer {
cursor: pointer;
}
+ .resize {
+ resize: both;
+ }
.resize-none {
resize: none;
}
@@ -766,6 +812,9 @@
.uppercase {
text-transform: uppercase;
}
+ .underline {
+ text-decoration-line: underline;
+ }
.shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -844,6 +893,21 @@
.\[-webkit-line-clamp\:4\] {
-webkit-line-clamp: 4;
}
+ .peer-checked\:border-blue-600 {
+ &:is(:where(.peer):checked ~ *) {
+ border-color: var(--color-blue-600);
+ }
+ }
+ .peer-checked\:bg-blue-600 {
+ &:is(:where(.peer):checked ~ *) {
+ background-color: var(--color-blue-600);
+ }
+ }
+ .peer-checked\:text-white {
+ &:is(:where(.peer):checked ~ *) {
+ color: var(--color-white);
+ }
+ }
.peer-invalid\:block {
&:is(:where(.peer):invalid ~ *) {
display: block;
@@ -920,6 +984,13 @@
}
}
}
+ .hover\:border-gray-400 {
+ &:hover {
+ @media (hover: hover) {
+ border-color: var(--color-gray-400);
+ }
+ }
+ }
.hover\:bg-blue-200 {
&:hover {
@media (hover: hover) {
@@ -978,6 +1049,15 @@
}
}
}
+ .checked\:hover\:bg-blue-700 {
+ &:checked {
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-blue-700);
+ }
+ }
+ }
+ }
.focus\:bg-blue-200 {
&:focus {
background-color: var(--color-blue-200);
@@ -999,6 +1079,12 @@
--tw-ring-color: var(--color-blue-500);
}
}
+ .focus\:ring-offset-2 {
+ &:focus {
+ --tw-ring-offset-width: 2px;
+ --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ }
+ }
.focus\:outline-hidden {
&:focus {
--tw-outline-style: none;
From c0fc47c569b3917cfd25736d985e077f421b2436 Mon Sep 17 00:00:00 2001
From: Hayden Hargreaves
Date: Mon, 7 Jul 2025 21:36:13 -0700
Subject: [PATCH 2/7] (UI): UI implementation of the search page. Somewhat
wired up.
Missing some context storage and better passing of data to allow
between the home page to the search page. Need a way to store the search
results in state so they can be retrieved when the page is reloaded,
etc.
---
internal/app/handlers/page_handler.go | 23 +-
internal/app/handlers/recipe_handler.go | 12 +-
internal/app/server/server.go | 1 +
.../database/repository/recipe_repository.go | 2 +
internal/templates/components/dropdowns.templ | 124 +++---
.../templates/components/dropdowns_templ.go | 12 +-
.../templates/components/search_bar.templ | 74 ++++
.../templates/components/search_bar_templ.go | 100 +++++
internal/templates/pages/home.templ | 73 +---
internal/templates/pages/home_templ.go | 145 ++-----
internal/templates/pages/search.templ | 129 +++++++
internal/templates/pages/search_templ.go | 361 ++++++++++++++++++
internal/templates/partials/row.templ | 0
internal/templates/partials/row_templ.go | 10 -
web/static/css/tailwind.css | 116 +++---
15 files changed, 855 insertions(+), 327 deletions(-)
create mode 100644 internal/templates/components/search_bar.templ
create mode 100644 internal/templates/components/search_bar_templ.go
create mode 100644 internal/templates/pages/search.templ
create mode 100644 internal/templates/pages/search_templ.go
delete mode 100644 internal/templates/partials/row.templ
delete mode 100644 internal/templates/partials/row_templ.go
diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go
index 1f87c36..82d3d62 100644
--- a/internal/app/handlers/page_handler.go
+++ b/internal/app/handlers/page_handler.go
@@ -15,21 +15,21 @@ func LoginPage(ctx *gin.Context) {
title := "Potion - Login"
page := pages.LoginPage()
- ctx.HTML(200, "", layouts.AppLayout(title, page))
+ ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
}
func HomePage(ctx *gin.Context) {
title := "Potion - Home"
page := pages.HomePage()
- ctx.HTML(200, "", layouts.AppLayout(title, page))
+ ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
}
func FavoritesPage(ctx *gin.Context) {
title := "Potion - Favorites"
page := pages.FavoritesPage()
- ctx.HTML(200, "", layouts.AppLayout(title, page))
+ ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
}
func CreatePage(ctx *gin.Context) {
@@ -42,7 +42,7 @@ func CreatePage(ctx *gin.Context) {
title := "Potion - Create"
page := pages.CreatePage()
- ctx.HTML(200, "", layouts.AppLayout(title, page))
+ ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
}
func ProfilePage(ctx *gin.Context) {
@@ -59,14 +59,14 @@ func ProfilePage(ctx *gin.Context) {
title := "Potion - Profile"
page := pages.ProfilePage(user)
- ctx.HTML(200, "", layouts.AppLayout(title, page))
+ ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
}
func ListPage(ctx *gin.Context) {
title := "Potion - Shopping List"
page := pages.ListPage()
- ctx.HTML(200, "", layouts.AppLayout(title, page))
+ ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
}
// TODO: Figure out how to handle errors, think we just need a simple display.
@@ -102,5 +102,14 @@ func RecipePage(ctx *gin.Context) {
title := "Potion - View Recipe"
page := pages.RecipePage(*recipe, *user)
- ctx.HTML(200, "", layouts.AppLayout(title, page))
+ ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
+}
+
+func SearchPage(ctx *gin.Context) {
+ title := "Potion - Recipe Search"
+ page := pages.SearchPage()
+
+ fmt.Println("I OPENED A PAGE!")
+
+ ctx.HTML(http.StatusOK, "", layouts.AppLayout(title, page))
}
diff --git a/internal/app/handlers/recipe_handler.go b/internal/app/handlers/recipe_handler.go
index 7854a98..2490927 100644
--- a/internal/app/handlers/recipe_handler.go
+++ b/internal/app/handlers/recipe_handler.go
@@ -6,14 +6,9 @@ import (
"github.com/gin-gonic/gin"
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
+ templates "github.com/haydenhargreaves/Potion/internal/templates/pages"
)
-const CREATE_SUCCESS_HTML = `
-
- Success! Your new masterpiece was created!
-
-`
-
const CREATE_ERROR_HTML = `
Uh oh! Something went wrong when creating your recipe. Please try again. %s
@@ -42,5 +37,8 @@ func SearchRecipes(ctx *gin.Context) {
if err != nil {
ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
}
- ctx.JSON(http.StatusOK, gin.H{"recipes": recipes})
+
+ // Render content as the response
+ ctx.Status(200)
+ templates.ResultList(recipes).Render(ctx.Request.Context(), ctx.Writer)
}
diff --git a/internal/app/server/server.go b/internal/app/server/server.go
index b972deb..5ec329f 100644
--- a/internal/app/server/server.go
+++ b/internal/app/server/server.go
@@ -165,6 +165,7 @@ func (s *Server) Setup() *Server {
router_web.GET("/profile", handlers.ProfilePage)
router_web.GET("/list", handlers.ListPage)
router_web.GET("/recipe/:id", handlers.RecipePage)
+ router_web.GET("/search", handlers.SearchPage)
// WEB state endpoints
router_state.POST("/tags", handlers.NewTag)
diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go
index 6727448..973e6c2 100644
--- a/internal/infrastructure/database/repository/recipe_repository.go
+++ b/internal/infrastructure/database/repository/recipe_repository.go
@@ -259,6 +259,8 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain
query += ";"
}
+ fmt.Println(query)
+
// Execute the query
rows, err := tx.Query(query)
if err != nil {
diff --git a/internal/templates/components/dropdowns.templ b/internal/templates/components/dropdowns.templ
index 226efc8..43f858d 100644
--- a/internal/templates/components/dropdowns.templ
+++ b/internal/templates/components/dropdowns.templ
@@ -1,19 +1,17 @@
package components
templ dropdownButton(content, name, value string) {
-
-
-
- { content }
-
-
+
+
+
+ { content }
+
+
}
templ FilterDropdown() {
-
-
-}
+
+ }
diff --git a/internal/templates/components/dropdowns_templ.go b/internal/templates/components/dropdowns_templ.go
index e6605c4..4a1373d 100644
--- a/internal/templates/components/dropdowns_templ.go
+++ b/internal/templates/components/dropdowns_templ.go
@@ -29,7 +29,7 @@ func dropdownButton(content, name, value string) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " \n function toggleDropdown() {\n const menu = document.getElementById(\"filter-dropdown-menu\");\n const button = document.getElementById(\"filter-dropdown-button\");\n\n if (menu.classList.contains(\"block\")) {\n menu.classList.remove(\"block\");\n menu.classList.add(\"hidden\");\n } else {\n menu.classList.remove(\"hidden\");\n menu.classList.add(\"block\");\n }\n }\nApply Filters
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/internal/templates/components/search_bar.templ b/internal/templates/components/search_bar.templ
index 93f5354..436ecfb 100644
--- a/internal/templates/components/search_bar.templ
+++ b/internal/templates/components/search_bar.templ
@@ -3,7 +3,7 @@ package components
import domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
-templ SearchBar(filters domainRecipe.SearchFilters) {
+templ SearchBar(filters domainRecipe.SearchFilters, redirect bool) {
+
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -74,7 +87,7 @@ func SearchBar(filters domainRecipe.SearchFilters) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -98,12 +111,12 @@ func filterButton() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var4 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var4 == nil {
- templ_7745c5c3_Var4 = templ.NopComponent
+ templ_7745c5c3_Var5 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var5 == nil {
+ templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/internal/templates/pages/create.templ b/internal/templates/pages/create.templ
index a83d479..ee26f4f 100644
--- a/internal/templates/pages/create.templ
+++ b/internal/templates/pages/create.templ
@@ -78,7 +78,7 @@ templ Page() {
Description * Please enter a description. Between 1-1000 characters.
Category * Select a category Breakfast Lunch Dinner Dessert Snack Side Other Please select a category.
Difficulty * Select a difficulty Beginner Easy Intermediate Challenging Extreme Please select a difficulty.
Ingredients * Add Ingredient Instructions * Please enter at least one step.
Add Instruction Step Recipe Image
Create Recipe ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-swap=\"outerHTML\" hx-target=\"#response\" hx-trigger=\"submit\" hx-encoding=\"multipart/form-data\">
Description * Please enter a description. Between 1-1000 characters.
Category * Select a category Breakfast Lunch Dinner Dessert Snack Side Other Please select a category.
Difficulty * Select a difficulty Beginner Easy Intermediate Challenging Extreme Please select a difficulty.
Ingredients * Add Ingredient Instructions * Please enter at least one step.
Add Instruction Step Recipe Image
Create Recipe ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/internal/templates/pages/home.templ b/internal/templates/pages/home.templ
index a24aa16..c9edfe0 100644
--- a/internal/templates/pages/home.templ
+++ b/internal/templates/pages/home.templ
@@ -29,7 +29,8 @@ templ introSection() {
templ searchSection() {
@components.BannerText("Craving Something Specific?")
- @components.SearchBar(domainRecipe.SearchFilters{})
+ @components.SearchBar(domainRecipe.SearchFilters{}, true)
+
}
diff --git a/internal/templates/pages/home_templ.go b/internal/templates/pages/home_templ.go
index c9bbc9b..bc18bba 100644
--- a/internal/templates/pages/home_templ.go
+++ b/internal/templates/pages/home_templ.go
@@ -70,11 +70,11 @@ func searchSection() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = components.SearchBar(domainRecipe.SearchFilters{}).Render(ctx, templ_7745c5c3_Buffer)
+ templ_7745c5c3_Err = components.SearchBar(domainRecipe.SearchFilters{}, true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/internal/templates/pages/search.templ b/internal/templates/pages/search.templ
index 247fc0e..0374340 100644
--- a/internal/templates/pages/search.templ
+++ b/internal/templates/pages/search.templ
@@ -16,7 +16,7 @@ templ SearchPage(filters domainRecipe.SearchFilters) {
bg-white flex flex-col items-center"
>
@components.BannerText("Recipe Search")
- @components.SearchBar(filters)
+ @components.SearchBar(filters, false)
@ResultList(nil)
diff --git a/internal/templates/pages/search_templ.go b/internal/templates/pages/search_templ.go
index 7126411..40658a4 100644
--- a/internal/templates/pages/search_templ.go
+++ b/internal/templates/pages/search_templ.go
@@ -49,7 +49,7 @@ func SearchPage(filters domainRecipe.SearchFilters) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = components.SearchBar(filters).Render(ctx, templ_7745c5c3_Buffer)
+ templ_7745c5c3_Err = components.SearchBar(filters, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css
index bb35278..c01b4bc 100644
--- a/web/static/css/tailwind.css
+++ b/web/static/css/tailwind.css
@@ -7,7 +7,6 @@
'Noto Color Emoji';
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
- --color-red-50: oklch(97.1% 0.013 17.38);
--color-red-100: oklch(93.6% 0.032 17.717);
--color-red-500: oklch(63.7% 0.237 25.331);
--color-green-100: oklch(96.2% 0.044 156.743);
@@ -34,7 +33,6 @@
--color-black: #000;
--color-white: #fff;
--spacing: 0.25rem;
- --container-xl: 36rem;
--container-2xl: 42rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
@@ -236,9 +234,6 @@
.static {
position: static;
}
- .top-1 {
- top: calc(var(--spacing) * 1);
- }
.top-1\/2 {
top: calc(1/2 * 100%);
}
@@ -248,9 +243,6 @@
.left-0 {
left: calc(var(--spacing) * 0);
}
- .left-1 {
- left: calc(var(--spacing) * 1);
- }
.left-1\/2 {
left: calc(1/2 * 100%);
}
@@ -272,6 +264,9 @@
.mx-auto {
margin-inline: auto;
}
+ .my-0 {
+ margin-block: calc(var(--spacing) * 0);
+ }
.my-1 {
margin-block: calc(var(--spacing) * 1);
}
@@ -361,18 +356,10 @@
width: calc(var(--spacing) * 10);
height: calc(var(--spacing) * 10);
}
- .size-28 {
- width: calc(var(--spacing) * 28);
- height: calc(var(--spacing) * 28);
- }
.size-32 {
width: calc(var(--spacing) * 32);
height: calc(var(--spacing) * 32);
}
- .size-40 {
- width: calc(var(--spacing) * 40);
- height: calc(var(--spacing) * 40);
- }
.size-56 {
width: calc(var(--spacing) * 56);
height: calc(var(--spacing) * 56);
@@ -414,18 +401,12 @@
.min-h-screen {
min-height: 100vh;
}
- .w-1 {
- width: calc(var(--spacing) * 1);
- }
.w-1\/3 {
width: calc(1/3 * 100%);
}
.w-1\/4 {
width: calc(1/4 * 100%);
}
- .w-3 {
- width: calc(var(--spacing) * 3);
- }
.w-3\/4 {
width: calc(3/4 * 100%);
}
@@ -438,9 +419,6 @@
.w-5 {
width: calc(var(--spacing) * 5);
}
- .w-9 {
- width: calc(var(--spacing) * 9);
- }
.w-9\/10 {
width: calc(9/10 * 100%);
}
@@ -459,30 +437,16 @@
.max-w-2xl {
max-width: var(--container-2xl);
}
- .flex-shrink {
- flex-shrink: 1;
- }
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
- .border-collapse {
- border-collapse: collapse;
- }
- .-translate-x-1 {
- --tw-translate-x: calc(var(--spacing) * -1);
- translate: var(--tw-translate-x) var(--tw-translate-y);
- }
.-translate-x-1\/2 {
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
- .-translate-y-1 {
- --tw-translate-y: calc(var(--spacing) * -1);
- translate: var(--tw-translate-x) var(--tw-translate-y);
- }
.-translate-y-1\/2 {
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -490,9 +454,6 @@
.cursor-pointer {
cursor: pointer;
}
- .resize {
- resize: both;
- }
.resize-none {
resize: none;
}
@@ -523,6 +484,9 @@
.justify-center {
justify-content: center;
}
+ .justify-end {
+ justify-content: flex-end;
+ }
.justify-start {
justify-content: flex-start;
}
@@ -550,15 +514,9 @@
.gap-y-1 {
row-gap: calc(var(--spacing) * 1);
}
- .gap-y-2 {
- row-gap: calc(var(--spacing) * 2);
- }
.gap-y-3 {
row-gap: calc(var(--spacing) * 3);
}
- .gap-y-4 {
- row-gap: calc(var(--spacing) * 4);
- }
.overflow-hidden {
overflow: hidden;
}
@@ -646,6 +604,9 @@
.bg-blue-500 {
background-color: var(--color-blue-500);
}
+ .bg-blue-600 {
+ background-color: var(--color-blue-600);
+ }
.bg-gray-50 {
background-color: var(--color-gray-50);
}
@@ -655,9 +616,6 @@
.bg-gray-200 {
background-color: var(--color-gray-200);
}
- .bg-green-100 {
- background-color: var(--color-green-100);
- }
.bg-red-100 {
background-color: var(--color-red-100);
}
@@ -747,6 +705,9 @@
.py-8 {
padding-block: calc(var(--spacing) * 8);
}
+ .pt-2 {
+ padding-top: calc(var(--spacing) * 2);
+ }
.pr-4 {
padding-right: calc(var(--spacing) * 4);
}
@@ -851,9 +812,6 @@
.text-gray-800 {
color: var(--color-gray-800);
}
- .text-green-600 {
- color: var(--color-green-600);
- }
.text-red-500 {
color: var(--color-red-500);
}
@@ -863,9 +821,6 @@
.uppercase {
text-transform: uppercase;
}
- .underline {
- text-decoration-line: underline;
- }
.shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -1042,6 +997,13 @@
}
}
}
+ .hover\:bg-blue-700 {
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-blue-700);
+ }
+ }
+ }
.hover\:bg-gray-50 {
&:hover {
@media (hover: hover) {
@@ -1170,6 +1132,11 @@
margin-block: calc(var(--spacing) * 0);
}
}
+ .md\:my-2 {
+ @media (width >= 48rem) {
+ margin-block: calc(var(--spacing) * 2);
+ }
+ }
.md\:flex {
@media (width >= 48rem) {
display: flex;
@@ -1275,11 +1242,21 @@
padding-block: calc(var(--spacing) * 0);
}
}
+ .md\:py-2 {
+ @media (width >= 48rem) {
+ padding-block: calc(var(--spacing) * 2);
+ }
+ }
.md\:py-12 {
@media (width >= 48rem) {
padding-block: calc(var(--spacing) * 12);
}
}
+ .md\:pt-2 {
+ @media (width >= 48rem) {
+ padding-top: calc(var(--spacing) * 2);
+ }
+ }
.md\:pt-14 {
@media (width >= 48rem) {
padding-top: calc(var(--spacing) * 14);
@@ -1308,6 +1285,12 @@
line-height: var(--tw-leading, var(--text-4xl--line-height));
}
}
+ .md\:text-base {
+ @media (width >= 48rem) {
+ font-size: var(--text-base);
+ line-height: var(--tw-leading, var(--text-base--line-height));
+ }
+ }
.md\:text-lg {
@media (width >= 48rem) {
font-size: var(--text-lg);
From 70536147b7a3d4b722a88341ebaf30c0e8f4d348 Mon Sep 17 00:00:00 2001
From: Hayden Hargreaves
Date: Thu, 10 Jul 2025 17:51:23 -0700
Subject: [PATCH 5/7] (UI/FEAT): The search page will now execute the search
when loaded.
However, this only occurs when the page is loaded WITH filters. If
filters are not found in the cookies, such as when a blank page is
loaded, then a search will only execute on submit.
---
internal/app/handlers/page_handler.go | 6 ++--
.../templates/components/search_bar.templ | 10 ++++--
.../templates/components/search_bar_templ.go | 33 ++++++++++++++-----
internal/templates/pages/home.templ | 2 +-
internal/templates/pages/home_templ.go | 2 +-
internal/templates/pages/search.templ | 4 +--
internal/templates/pages/search_templ.go | 4 +--
7 files changed, 40 insertions(+), 21 deletions(-)
diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go
index a01402b..3dd8975 100644
--- a/internal/app/handlers/page_handler.go
+++ b/internal/app/handlers/page_handler.go
@@ -113,14 +113,14 @@ func SearchPage(ctx *gin.Context) {
// Get filters from cookies
if bytes, err := ctx.Cookie("search-filters"); err != nil {
fmt.Printf("ERROR: Failed to get search-filter cookie. %s\n", err.Error())
- page = pages.SearchPage(domainRecipe.SearchFilters{})
+ page = pages.SearchPage(domainRecipe.SearchFilters{}, false)
} else {
var filters domainRecipe.SearchFilters
if err := json.Unmarshal([]byte(bytes), &filters); err != nil {
fmt.Printf("ERROR: Failed to unmarshal search-filter cookie. %s\n", err.Error())
- page = pages.SearchPage(domainRecipe.SearchFilters{})
+ page = pages.SearchPage(domainRecipe.SearchFilters{}, false)
} else {
- page = pages.SearchPage(filters)
+ page = pages.SearchPage(filters, true)
}
}
diff --git a/internal/templates/components/search_bar.templ b/internal/templates/components/search_bar.templ
index 436ecfb..3d4071f 100644
--- a/internal/templates/components/search_bar.templ
+++ b/internal/templates/components/search_bar.templ
@@ -3,18 +3,22 @@ package components
import domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
-templ SearchBar(filters domainRecipe.SearchFilters, redirect bool) {
+templ SearchBar(filters domainRecipe.SearchFilters, redirect bool, searchOnLoad bool) {
-
+
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -87,7 +102,7 @@ func SearchBar(filters domainRecipe.SearchFilters, redirect bool) templ.Componen
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -116,7 +131,7 @@ func filterButton() templ.Component {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/internal/templates/pages/home.templ b/internal/templates/pages/home.templ
index c9edfe0..05a0e70 100644
--- a/internal/templates/pages/home.templ
+++ b/internal/templates/pages/home.templ
@@ -29,7 +29,7 @@ templ introSection() {
templ searchSection() {
@components.BannerText("Craving Something Specific?")
- @components.SearchBar(domainRecipe.SearchFilters{}, true)
+ @components.SearchBar(domainRecipe.SearchFilters{}, true, false)
}
diff --git a/internal/templates/pages/home_templ.go b/internal/templates/pages/home_templ.go
index bc18bba..2490417 100644
--- a/internal/templates/pages/home_templ.go
+++ b/internal/templates/pages/home_templ.go
@@ -70,7 +70,7 @@ func searchSection() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = components.SearchBar(domainRecipe.SearchFilters{}, true).Render(ctx, templ_7745c5c3_Buffer)
+ templ_7745c5c3_Err = components.SearchBar(domainRecipe.SearchFilters{}, true, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/internal/templates/pages/search.templ b/internal/templates/pages/search.templ
index 0374340..b40f5fd 100644
--- a/internal/templates/pages/search.templ
+++ b/internal/templates/pages/search.templ
@@ -8,7 +8,7 @@ import (
"github.com/haydenhargreaves/Potion/internal/templates/components"
)
-templ SearchPage(filters domainRecipe.SearchFilters) {
+templ SearchPage(filters domainRecipe.SearchFilters, searchOnLoad bool) {
@components.Navbar("")
@components.BannerText("Recipe Search")
- @components.SearchBar(filters, false)
+ @components.SearchBar(filters, false, searchOnLoad)
@ResultList(nil)
diff --git a/internal/templates/pages/search_templ.go b/internal/templates/pages/search_templ.go
index 40658a4..fcaf4e0 100644
--- a/internal/templates/pages/search_templ.go
+++ b/internal/templates/pages/search_templ.go
@@ -16,7 +16,7 @@ import (
"github.com/haydenhargreaves/Potion/internal/templates/components"
)
-func SearchPage(filters domainRecipe.SearchFilters) templ.Component {
+func SearchPage(filters domainRecipe.SearchFilters, searchOnLoad bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -49,7 +49,7 @@ func SearchPage(filters domainRecipe.SearchFilters) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = components.SearchBar(filters, false).Render(ctx, templ_7745c5c3_Buffer)
+ templ_7745c5c3_Err = components.SearchBar(filters, false, searchOnLoad).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
From 3c5109c7d0ee752cd69a82fcf6dfb6982cb81e6a Mon Sep 17 00:00:00 2001
From: Hayden Hargreaves
Date: Thu, 10 Jul 2025 19:54:21 -0700
Subject: [PATCH 6/7] (DB/FEAT): Implemented full text search vector for
database searching!
This is a HUGE upgrade and can mark the searching nearly complete! Other
than the scrolling and some other smaller UI things. The search appears
to be working.
---
.../migrations/004_create_fts_index.sql | 17 +++++
.../database/repository/recipe_repository.go | 74 +++++++++++++++----
2 files changed, 76 insertions(+), 15 deletions(-)
create mode 100644 internal/infrastructure/database/migrations/004_create_fts_index.sql
diff --git a/internal/infrastructure/database/migrations/004_create_fts_index.sql b/internal/infrastructure/database/migrations/004_create_fts_index.sql
new file mode 100644
index 0000000..e58b359
--- /dev/null
+++ b/internal/infrastructure/database/migrations/004_create_fts_index.sql
@@ -0,0 +1,17 @@
+-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com)
+-- Desc: Create a full text index on the recipes table.
+-- Date: 07/10/2025
+
+BEGIN;
+
+-- Update recipes table with the search vector column
+ALTER TABLE Recipes
+ADD search_vector tsvector GENERATED ALWAYS AS (
+ setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
+ setweight(to_tsvector('english', coalesce(description, '')), 'B')
+) STORED;
+
+-- Create the search index
+CREATE INDEX idx_recipe_search_vector ON recipes USING GIN (search_vector);
+
+COMMIT;
diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go
index 3372d2c..bdbf297 100644
--- a/internal/infrastructure/database/repository/recipe_repository.go
+++ b/internal/infrastructure/database/repository/recipe_repository.go
@@ -99,7 +99,13 @@ func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) {
return nil, err
}
- query := "SELECT * FROM recipes WHERE id = $1"
+ query := `
+ SELECT
+ id, title, description, instructions, serves, difficulty, duration, category, ingredients,
+ userid, modified, created
+ FROM recipes
+ WHERE id = $1
+ `
var durationBytes []byte
var ingredientBytes []byte
@@ -160,10 +166,10 @@ func isBitActive(bits, pos int) bool {
}
// SearchRecipes will search the recipe table using the provided filters and return an unbound list
-// of recipes. The filters are fairly complex, they are stored as bit masks. A more details
-// description can be found in the recipe service implementation. Any errors will be bubbled to the
-// caller.
-//
+// of recipes. The filters are fairly complex, they are stored as bit masks. A more details
+// description can be found in the recipe service implementation. Any errors will be bubbled to the
+// caller.
+//
// TODO: Pagination is required, to provide infinite scroll.
func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain.Recipe, error) {
tx, err := r.db.Begin()
@@ -172,9 +178,6 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain
return nil, err
}
- // Generate the query
- query := "SELECT * FROM recipes"
-
// Compute meals type filters (there are 7 bits)
var mealConditions []string
for i := range 7 {
@@ -257,15 +260,56 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain
conditions = append(conditions, servingString)
}
- // Convert and append conditions
- if len(conditions) > 0 {
- conditionsString := fmt.Sprintf("WHERE %s", strings.Join(conditions, " AND "))
- query = fmt.Sprintf("%s %s;", query, conditionsString)
- } else {
- query += ";"
+ // Define columns to select. More fields can be added if the full text search is required
+ columns := []string{
+ "id",
+ "title",
+ "description",
+ "instructions",
+ "serves",
+ "difficulty",
+ "duration",
+ "category",
+ "ingredients",
+ "userid",
+ "modified",
+ "created",
}
- fmt.Println(query)
+ // Create search vector query
+ var orderBy string = ""
+ if filters.Search != "" {
+ vector_query := strings.ReplaceAll(filters.Search, " ", " | ")
+
+ conditions = append(
+ conditions,
+ fmt.Sprintf("search_vector @@ to_tsquery('english', '%s')", vector_query),
+ )
+
+ template := `
+ ORDER BY
+ ts_rank(search_vector, to_tsquery('english', '%s')) DESC,
+ ts_rank_cd(search_vector, to_tsquery('english', '%s')) DESC
+ `
+ orderBy = fmt.Sprintf(template, vector_query, vector_query)
+ }
+
+ // Generate the query
+ query := fmt.Sprintf("SELECT %s FROM recipes", strings.Join(columns, ","))
+
+ // Convert and append conditions if provided
+ if len(conditions) > 0 {
+ conditionsString := fmt.Sprintf("WHERE %s", strings.Join(conditions, " AND "))
+ query = fmt.Sprintf("%s %s", query, conditionsString)
+ }
+
+ // Append sorting order if exists
+ if len(orderBy) > 0 {
+ query = fmt.Sprintf("%s %s", query, orderBy)
+ }
+
+ // Finish it off with a colon!
+ query += ";"
// Execute the query
rows, err := tx.Query(query)
From e38f4e8b51567e18b2e16fb905645a8767259d84 Mon Sep 17 00:00:00 2001
From: Hayden Hargreaves
Date: Thu, 10 Jul 2025 20:01:01 -0700
Subject: [PATCH 7/7] (DOC): Updated technical specifications.
---
doc/TechnicalSpecification.md | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/doc/TechnicalSpecification.md b/doc/TechnicalSpecification.md
index c07f639..367385a 100644
--- a/doc/TechnicalSpecification.md
+++ b/doc/TechnicalSpecification.md
@@ -78,19 +78,19 @@ well as view lists of recently made recipes, recently viewed and trending recipe
##### API Requirements
-- [ ] Message & Pills Banner
- - [ ] Search all recipes of a specific meal type
+- [ ] ~Message & Pills Banner~
+ - [ ] ~Search all recipes of a specific meal type~
-- [ ] Search bar
- - [ ] Text search on titles based on search query
- - [ ] Text search on tags based on search query
- - [ ] Text search on **meal** based on search query
+- [x] Search bar
+ - [x] Text search on titles based on search query
+ - [x] Text search on tags based on search query
+ - [x] Text search on **meal** based on search query
-- [ ] Filter drop down
- - [ ] Update search to only contain meals from selected filter
- - [ ] Update search to only contain means that meet the time requirement of the selected filter
- - [ ] Update search to only contain meals that meet the difficulty level of the selected filter
- - [ ] Update search to only contain meals that meet the serving size of the selected filter
+- [x] Filter drop down
+ - [x] Update search to only contain meals from selected filter
+ - [x] Update search to only contain means that meet the time requirement of the selected filter
+ - [x] Update search to only contain meals that meet the difficulty level of the selected filter
+ - [x] Update search to only contain meals that meet the serving size of the selected filter
- [ ] Recipe of The Week
- [ ] Fetch the most performing recipe of the last 7 days and all of the required meta data