diff --git a/doc/TechnicalSpecification.md b/doc/TechnicalSpecification.md index 5e15783..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 @@ -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/auth_handler.go b/internal/app/handlers/auth_handler.go index 6b28357..2da9eff 100644 --- a/internal/app/handlers/auth_handler.go +++ b/internal/app/handlers/auth_handler.go @@ -42,7 +42,7 @@ func GoogleCallback(ctx *gin.Context) { jwt, int(time.Now().Add(7*24*time.Hour).Sub(time.Now()).Seconds()), "/", - "localhost", + "", // TODO: Real live domain false, // TODO: True in prod true, ) @@ -60,6 +60,6 @@ func GoogleCallback(ctx *gin.Context) { // This route will direct the user back to the home page. func Logout(ctx *gin.Context) { // TODO: Use same values as the GoogleCallback function - ctx.SetCookie("jwt_token", "", -1, "/", "localhost", false, true) + ctx.SetCookie("jwt_token", "", -1, "/", "", false, true) // TODO: Update settings ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) } diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index 1f87c36..3dd8975 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -1,12 +1,15 @@ package handlers import ( + "encoding/json" "fmt" "net/http" "strconv" + "github.com/a-h/templ" "github.com/gin-gonic/gin" - domain "github.com/haydenhargreaves/Potion/internal/domain/server" + domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" + domainServer "github.com/haydenhargreaves/Potion/internal/domain/server" layouts "github.com/haydenhargreaves/Potion/internal/templates/layouts" pages "github.com/haydenhargreaves/Potion/internal/templates/pages" ) @@ -15,64 +18,64 @@ 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) { // If not logged in, direct to the login page - if !domain.IsLoggedIn(ctx) { - ctx.Redirect(http.StatusSeeOther, domain.WEB_LOGIN) + if !domainServer.IsLoggedIn(ctx) { + ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) return } 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) { // If not logged in, direct to the login page - if !domain.IsLoggedIn(ctx) { - ctx.Redirect(http.StatusSeeOther, domain.WEB_LOGIN) + if !domainServer.IsLoggedIn(ctx) { + ctx.Redirect(http.StatusSeeOther, domainServer.WEB_LOGIN) return } // Else, get the user data - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) + deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) user := deps.UserService.GetAuthenicatedUser(ctx) 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. func RecipePage(ctx *gin.Context) { // Call recipe service to get via ID - deps := ctx.MustGet("deps").(*domain.InjectedDependencies) + deps := ctx.MustGet("deps").(*domainServer.InjectedDependencies) id := ctx.Param("id") // Parse ID @@ -102,5 +105,26 @@ 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) { + var page templ.Component + // 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{}, 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{}, false) + } else { + page = pages.SearchPage(filters, true) + } + } + + title := "Potion - Recipe Search" + + 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 6756423..a256307 100644 --- a/internal/app/handlers/recipe_handler.go +++ b/internal/app/handlers/recipe_handler.go @@ -1,19 +1,18 @@ package handlers import ( + "encoding/json" "fmt" "net/http" + "strconv" + "time" "github.com/gin-gonic/gin" + domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" 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 @@ -34,3 +33,56 @@ func CreateRecipe(ctx *gin.Context) { ctx.Header("HX-Redirect", url) ctx.Status(http.StatusCreated) } + +// 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 +} + +// TODO: I don't love doing all of this here, but it seems to be the only way to get it to work... +func SearchRecipes(ctx *gin.Context) { + deps := ctx.MustGet("deps").(*domain.InjectedDependencies) + + + // create filters + filters := domainRecipe.SearchFilters{ + Search: ctx.PostForm("search"), // string, search query for titles + MealType: toBits(ctx.PostFormArray("meal")), + Time: toBits(ctx.PostFormArray("time")), + Difficulty: toBits(ctx.PostFormArray("difficulty")), + ServingSize: toBits(ctx.PostFormArray("serving")), + } + + // Set the filters into the cookies, so they can be reloaded + if bytes, err := json.Marshal(filters); err == nil { + ctx.SetCookie( + "search-filters", + string(bytes), + int(time.Now().Add(24 * time.Hour).Sub(time.Now()).Seconds()), + "/", + "", // TODO: Need an actual domain + false, // TODO: True in prod + true, + ) + } + + redirect := ctx.PostForm("redirect") + if redirect == "true" { + ctx.Header("HX-Redirect", domain.WEB_SEARCH) + ctx.Status(http.StatusOK) + return + } + + recipes, err := deps.RecipeService.SearchRecipes(filters) + if err != nil { + ctx.JSON(http.StatusOK, gin.H{"error": err.Error()}) + } + + // Render content as the response + ctx.Status(200) + templates.ResultList(recipes).Render(ctx.Request.Context(), ctx.Writer) +} diff --git a/internal/app/handlers/state_handler.go b/internal/app/handlers/state_handler.go index 274f864..91c2de3 100644 --- a/internal/app/handlers/state_handler.go +++ b/internal/app/handlers/state_handler.go @@ -6,11 +6,12 @@ import ( "strings" "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/server" ) const TAG_HTML = `

  • >pos)&1 == 1 +} + +// 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. +// +// TODO: Pagination is required, to provide infinite scroll. +func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain.Recipe, error) { + tx, err := r.db.Begin() + if err != nil { + tx.Rollback() + return nil, err + } + + // 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) + } + + // 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", + } + + // 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) + 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..2940e38 100644 --- a/internal/templates/components/dropdowns.templ +++ b/internal/templates/components/dropdowns.templ @@ -1,16 +1,27 @@ package components -templ dropdownButton(name string) { - +import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" + +// isBitActive returns true when the bit at pos (0 indexed) is true. +func isBitActive(bits, pos int) bool { +return (bits>>pos)&1 == 1 } -templ FilterDropdown() { - - -} + + } diff --git a/internal/templates/components/dropdowns_templ.go b/internal/templates/components/dropdowns_templ.go index dde6499..f3aa7af 100644 --- a/internal/templates/components/dropdowns_templ.go +++ b/internal/templates/components/dropdowns_templ.go @@ -8,7 +8,14 @@ package components import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -func dropdownButton(name string) templ.Component { +import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" + +// isBitActive returns true when the bit at pos (0 indexed) is true. +func isBitActive(bits, pos int) bool { + return (bits>>pos)&1 == 1 +} + +func dropdownButton(content, name, value string, selected 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 { @@ -29,20 +36,56 @@ func dropdownButton(name string) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") 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 new file mode 100644 index 0000000..3d4071f --- /dev/null +++ b/internal/templates/components/search_bar.templ @@ -0,0 +1,80 @@ +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, searchOnLoad bool) { +
    +
    +
    + + + +
    + @filterButton() +
    + @FilterDropdown(filters) +
    +} + +templ filterButton() { + +} diff --git a/internal/templates/components/search_bar_templ.go b/internal/templates/components/search_bar_templ.go new file mode 100644 index 0000000..fcd00da --- /dev/null +++ b/internal/templates/components/search_bar_templ.go @@ -0,0 +1,142 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.865 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import domainServer "github.com/haydenhargreaves/Potion/internal/domain/server" +import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" + +func SearchBar(filters domainRecipe.SearchFilters, redirect bool, 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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = filterButton().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = FilterDropdown(filters).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func filterButton() 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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + 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, 10, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/templates/pages/create.templ b/internal/templates/pages/create.templ index 25e874c..ee26f4f 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!

    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!

    Please enter a title. Between 1-128 characters.

    Please enter a description. Between 1-1000 characters.

      Please enter a time (minutes).

      Please enter a time (minutes).

      Please enter a serving size.

      Please select a category.

      Please select a difficulty.

      Please enter at least one ingredient.

      Please provide a quantity.

      Please enter at least one step.

      ") + 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!

      Please enter a title. Between 1-128 characters.

      Please enter a description. Between 1-1000 characters.

        Please enter a time (minutes).

        Please enter a time (minutes).

        Please enter a serving size.

        Please select a category.

        Please select a difficulty.

        Please enter at least one ingredient.

        Please provide a quantity.

        Please enter at least one step.

        ") 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..05a0e70 100644 --- a/internal/templates/pages/home.templ +++ b/internal/templates/pages/home.templ @@ -2,6 +2,7 @@ package templates import "github.com/haydenhargreaves/Potion/internal/templates/components" import "github.com/haydenhargreaves/Potion/internal/domain/server" +import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" templ introSection() {
        @@ -25,69 +26,11 @@ templ introSection() {
        } -templ searchBar() { -
        -
        - - - - -
        - @filterButton() -
        -} - -templ filterButton() { - -} - templ searchSection() {
        @components.BannerText("Craving Something Specific?") -
        - @searchBar() - @components.FilterDropdown() -
        + @components.SearchBar(domainRecipe.SearchFilters{}, true, false) +
        } diff --git a/internal/templates/pages/home_templ.go b/internal/templates/pages/home_templ.go index 5e3d5de..2490417 100644 --- a/internal/templates/pages/home_templ.go +++ b/internal/templates/pages/home_templ.go @@ -10,6 +10,7 @@ import templruntime "github.com/a-h/templ/runtime" import "github.com/haydenhargreaves/Potion/internal/templates/components" import "github.com/haydenhargreaves/Potion/internal/domain/server" +import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" func introSection() templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { @@ -40,7 +41,7 @@ func introSection() templ.Component { }) } -func searchBar() templ.Component { +func searchSection() 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 { @@ -61,73 +62,7 @@ func searchBar() templ.Component { templ_7745c5c3_Var2 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
        ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = filterButton().Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
        ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func filterButton() 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 { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var3 := templ.GetChildren(ctx) - if templ_7745c5c3_Var3 == nil { - templ_7745c5c3_Var3 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func searchSection() 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 { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var4 := templ.GetChildren(ctx) - if templ_7745c5c3_Var4 == nil { - templ_7745c5c3_Var4 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
        ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -135,19 +70,11 @@ 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 = components.SearchBar(domainRecipe.SearchFilters{}, true, false).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = searchBar().Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = components.FilterDropdown().Render(ctx, templ_7745c5c3_Buffer) - 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, 3, "
        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -171,12 +98,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_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
        ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -184,7 +111,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, 5, "

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

        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -192,7 +119,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, 6, "
        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -216,12 +143,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_Var4 := templ.GetChildren(ctx) + if templ_7745c5c3_Var4 == nil { + templ_7745c5c3_Var4 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
        ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -229,7 +156,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, 8, "

        Recently viewed

        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -257,7 +184,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, 9, "

        Make again

        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -285,7 +212,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, 10, "
        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -309,21 +236,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_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, 15, "

        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, 12, "\" 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 +274,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_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = 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, 13, "
        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -380,7 +307,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, 14, "
        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/templates/pages/search.templ b/internal/templates/pages/search.templ new file mode 100644 index 0000000..b40f5fd --- /dev/null +++ b/internal/templates/pages/search.templ @@ -0,0 +1,130 @@ +package templates + +import ( + "fmt" + "github.com/haydenhargreaves/Potion/internal/domain/recipe" + domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" + domainServer "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" +) + +templ SearchPage(filters domainRecipe.SearchFilters, searchOnLoad bool) { + @components.Navbar("") +
        +
        + @components.BannerText("Recipe Search") + @components.SearchBar(filters, false, searchOnLoad) +
        + @ResultList(nil) +
        +
        +} + +templ ResultList(recipes []domain.Recipe) { +
        + for i, recipe := range recipes { + @searchResult(recipe, i%2 == 1) + } + if len(recipes) == 0 || recipes == nil { +

        No results

        + } else { +

        End of results

        + } +
        +} + +templ searchResult(recipe domain.Recipe, odd bool) { + + +
        +

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

        +
        + + @timeIconSm() + { recipe.Duration.Total } min + + + for _ = range(recipe.Difficulty) { + @starIconSm(true) + } + for _ = range(5 - recipe.Difficulty) { + @starIconSm(false) + } + + + @servingIconSm() + Serves { recipe.Serves } + +
        +

        { recipe.Description }

        +
        +
        +} + +templ servingIconSm() { + + + + + + + +} + +templ timeIconSm() { + + + +} + +templ starIconSm(filled bool) { + if filled { + + + + + } else { + + + + + } +} diff --git a/internal/templates/pages/search_templ.go b/internal/templates/pages/search_templ.go new file mode 100644 index 0000000..fcaf4e0 --- /dev/null +++ b/internal/templates/pages/search_templ.go @@ -0,0 +1,362 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.865 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" + "github.com/haydenhargreaves/Potion/internal/domain/recipe" + domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" + domainServer "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/templates/components" +) + +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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = components.Navbar("").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
        ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.BannerText("Recipe Search").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.SearchBar(filters, false, searchOnLoad).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
        ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ResultList(nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
        ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func ResultList(recipes []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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
        ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for i, recipe := range recipes { + templ_7745c5c3_Err = searchResult(recipe, i%2 == 1).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(recipes) == 0 || recipes == nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

        No results

        ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

        End of results

        ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
        ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func searchResult(recipe domain.Recipe, odd 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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

        ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 51, Col: 18} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + 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 + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Category) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 51, Col: 72} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + 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 + } + templ_7745c5c3_Err = timeIconSm().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Duration.Total) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 56, Col: 28} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " min ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _ = range recipe.Difficulty { + templ_7745c5c3_Err = starIconSm(true).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for _ = range 5 - recipe.Difficulty { + templ_7745c5c3_Err = starIconSm(false).Render(ctx, templ_7745c5c3_Buffer) + 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 + } + templ_7745c5c3_Err = servingIconSm().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Serves ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Serves) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 68, Col: 27} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + 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 + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/search.templ`, Line: 71, Col: 72} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

        ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func servingIconSm() 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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func timeIconSm() 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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func starIconSm(filled 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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if filled { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/templates/partials/row.templ b/internal/templates/partials/row.templ deleted file mode 100644 index e69de29..0000000 diff --git a/internal/templates/partials/row_templ.go b/internal/templates/partials/row_templ.go deleted file mode 100644 index 1d1995d..0000000 --- a/internal/templates/partials/row_templ.go +++ /dev/null @@ -1,10 +0,0 @@ -// Code generated by templ - DO NOT EDIT. - -// templ: version: v0.3.865 - -//lint:file-ignore SA4006 This context is only used if a nested component is present. - -import "github.com/a-h/templ" -import templruntime "github.com/a-h/templ/runtime" - -var _ = templruntime.GeneratedTemplate \ No newline at end of file diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index 2f9e417..c01b4bc 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -30,9 +30,9 @@ --color-gray-600: oklch(44.6% 0.03 256.802); --color-gray-700: oklch(37.3% 0.034 259.733); --color-gray-800: oklch(27.8% 0.033 256.848); + --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); @@ -50,6 +50,7 @@ --text-3xl--line-height: calc(2.25 / 1.875); --text-4xl: 2.25rem; --text-4xl--line-height: calc(2.5 / 2.25); + --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; @@ -57,6 +58,7 @@ --tracking-wide: 0.025em; --leading-relaxed: 1.625; --radius-sm: 0.25rem; + --radius-md: 0.375rem; --radius-lg: 0.5rem; --radius-xl: 0.75rem; --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); @@ -212,6 +214,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; } @@ -251,6 +264,9 @@ .mx-auto { margin-inline: auto; } + .my-0 { + margin-block: calc(var(--spacing) * 0); + } .my-1 { margin-block: calc(var(--spacing) * 1); } @@ -323,6 +339,9 @@ .hidden { display: none; } + .inline-block { + display: inline-block; + } .inline-flex { display: inline-flex; } @@ -341,6 +360,10 @@ width: calc(var(--spacing) * 32); height: calc(var(--spacing) * 32); } + .size-56 { + width: calc(var(--spacing) * 56); + height: calc(var(--spacing) * 56); + } .size-80 { width: calc(var(--spacing) * 80); height: calc(var(--spacing) * 80); @@ -375,6 +398,9 @@ .h-screen { height: 100vh; } + .min-h-screen { + min-height: 100vh; + } .w-1\/3 { width: calc(1/3 * 100%); } @@ -411,9 +437,6 @@ .max-w-2xl { max-width: var(--container-2xl); } - .max-w-xl { - max-width: var(--container-xl); - } .flex-shrink-0 { flex-shrink: 0; } @@ -461,6 +484,9 @@ .justify-center { justify-content: center; } + .justify-end { + justify-content: flex-end; + } .justify-start { justify-content: flex-start; } @@ -476,12 +502,21 @@ .gap-x-2 { column-gap: calc(var(--spacing) * 2); } + .gap-x-3 { + column-gap: calc(var(--spacing) * 3); + } .gap-x-4 { column-gap: calc(var(--spacing) * 4); } .gap-x-8 { column-gap: calc(var(--spacing) * 8); } + .gap-y-1 { + row-gap: calc(var(--spacing) * 1); + } + .gap-y-3 { + row-gap: calc(var(--spacing) * 3); + } .overflow-hidden { overflow: hidden; } @@ -494,6 +529,9 @@ .rounded-lg { border-radius: var(--radius-lg); } + .rounded-md { + border-radius: var(--radius-md); + } .rounded-sm { border-radius: var(--radius-sm); } @@ -504,6 +542,10 @@ border-style: var(--tw-border-style); border-width: 1px; } + .border-0 { + border-style: var(--tw-border-style); + border-width: 0px; + } .border-2 { border-style: var(--tw-border-style); border-width: 2px; @@ -562,15 +604,18 @@ .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); + } .bg-gray-100 { background-color: var(--color-gray-100); } .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); } @@ -660,9 +705,15 @@ .py-8 { padding-block: calc(var(--spacing) * 8); } + .pt-2 { + padding-top: calc(var(--spacing) * 2); + } .pr-4 { padding-right: calc(var(--spacing) * 4); } + .pb-1 { + padding-bottom: calc(var(--spacing) * 1); + } .pl-10 { padding-left: calc(var(--spacing) * 10); } @@ -713,6 +764,10 @@ --tw-font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium); } + .font-normal { + --tw-font-weight: var(--font-weight-normal); + font-weight: var(--font-weight-normal); + } .font-semibold { --tw-font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold); @@ -727,6 +782,9 @@ .whitespace-nowrap { white-space: nowrap; } + .text-black { + color: var(--color-black); + } .text-blue-500 { color: var(--color-blue-500); } @@ -754,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); } @@ -844,6 +899,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; @@ -927,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) { @@ -978,11 +1055,6 @@ } } } - .focus\:bg-blue-200 { - &:focus { - background-color: var(--color-blue-200); - } - } .focus\:bg-gray-50 { &:focus { background-color: var(--color-gray-50); @@ -1009,17 +1081,6 @@ } } } - .focus\:outline-2 { - &:focus { - outline-style: var(--tw-outline-style); - outline-width: 2px; - } - } - .focus\:outline-blue-500 { - &:focus { - outline-color: var(--color-blue-500); - } - } .focus\:outline-none { &:focus { --tw-outline-style: none; @@ -1071,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; @@ -1087,6 +1153,12 @@ height: calc(var(--spacing) * 32); } } + .md\:size-40 { + @media (width >= 48rem) { + width: calc(var(--spacing) * 40); + height: calc(var(--spacing) * 40); + } + } .md\:size-64 { @media (width >= 48rem) { width: calc(var(--spacing) * 64); @@ -1108,11 +1180,6 @@ width: calc(1/4 * 100%); } } - .md\:w-2\/3 { - @media (width >= 48rem) { - width: calc(2/3 * 100%); - } - } .md\:w-2\/5 { @media (width >= 48rem) { width: calc(2/5 * 100%); @@ -1138,6 +1205,11 @@ flex-direction: row; } } + .md\:items-start { + @media (width >= 48rem) { + align-items: flex-start; + } + } .md\:border-x { @media (width >= 48rem) { border-inline-style: var(--tw-border-style); @@ -1170,16 +1242,31 @@ 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); } } + .md\:text-left { + @media (width >= 48rem) { + text-align: left; + } + } .md\:text-2xl { @media (width >= 48rem) { font-size: var(--text-2xl); @@ -1198,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);