From 25ac0fd5270523c3c5022831dda52fd643f2434f Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Sun, 30 Nov 2025 21:53:07 -0700 Subject: [PATCH] (FEAT): Searching is working! So much progress! Yay!! Whats missing is the global storage of the filters. That is the final touch for searching. --- internal/app/server/recipe_handler_v2.go | 36 +++++- internal/app/server/server.go | 3 +- internal/domain/recipe/recipe.go | 11 +- web/src/App.tsx | 2 + web/src/components/buttons/DropdownButton.tsx | 6 +- web/src/components/buttons/FilterButton.tsx | 12 +- web/src/components/icons/TimeIconSmall.tsx | 2 +- web/src/components/inputs/RecipeSearchBar.tsx | 78 +++++++++-- .../inputs/RecipeSearchFilterDropdown.tsx | 122 +++++++----------- web/src/components/items/FavoriteResult.tsx | 56 -------- .../components/items/RecipeSearchResult.tsx | 73 +++++++++++ web/src/pages/Favorites.tsx | 102 +-------------- web/src/pages/Home.tsx | 5 +- web/src/pages/Search.tsx | 22 ++++ web/src/services/RecipeService.ts | 17 ++- web/src/types/api/recipe.ts | 6 + web/src/types/filters.ts | 4 + web/src/types/search.ts | 3 + 18 files changed, 299 insertions(+), 261 deletions(-) delete mode 100644 web/src/components/items/FavoriteResult.tsx create mode 100644 web/src/components/items/RecipeSearchResult.tsx create mode 100644 web/src/pages/Search.tsx create mode 100644 web/src/types/filters.ts diff --git a/internal/app/server/recipe_handler_v2.go b/internal/app/server/recipe_handler_v2.go index 46f90c1..f7b8148 100644 --- a/internal/app/server/recipe_handler_v2.go +++ b/internal/app/server/recipe_handler_v2.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/gin-gonic/gin" + domain "github.com/haydenhargreaves/Potion/internal/domain/recipe" ) // GetRecipeOfTheWeekHandler fetchs the current recipe of the week and returns it. @@ -32,7 +33,7 @@ func (s *Server) GetRecipeOfTheWeekHandlerV2(ctx *gin.Context) { }) } -func (s *Server) GetRecipeV2(ctx *gin.Context) { +func (s *Server) GetRecipeHandlerV2(ctx *gin.Context) { id := ctx.Param("id") parsedId, err := strconv.Atoi(id) if err != nil { @@ -59,3 +60,36 @@ func (s *Server) GetRecipeV2(ctx *gin.Context) { "recipe": recipe, }) } + +func (s *Server) SearchRecipeHandlerV2(ctx *gin.Context) { + var filters domain.SearchFilters + + // Parse filters + if err := ctx.ShouldBindJSON(&filters); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to parse filters. %s", err.Error()), + }) + return + } + + // This is optional, so we can do this + userId := getUserId(ctx) + + // Did I really have two APIs...? + // TODO: Fix service at some point, no need to accept the favorites (bool) param + recipes, err := s.deps.RecipeService.SearchRecipes(filters, userId, filters.Favorites) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "status": http.StatusBadRequest, + "message": fmt.Sprintf("[ERROR] Failed to get searched recipes. %s", err.Error()), + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "status": http.StatusOK, + "message": "[OK] Successfully retrieved recipes based on provided filters.", + "recipes": recipes, + }) +} diff --git a/internal/app/server/server.go b/internal/app/server/server.go index b141f6e..67ac926 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -201,8 +201,9 @@ func (s *Server) Setup() *Server { // ---- VERSION 2 ROUTES ---- // router_api_v2 := router_v2.Group(domain.API) - router_api_v2.GET("/recipe/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeV2) + router_api_v2.GET("/recipe/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeHandlerV2) router_api_v2.GET("/recipe/of-the-week", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeOfTheWeekHandlerV2) + router_api_v2.POST("/recipe/search", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.SearchRecipeHandlerV2) router_api_v2.GET("/auth/login", s.GetGoogleAuthUrlHandlerV2) router_api_v2.GET("/auth/callback", s.GoogleCallbackHandlerV2) diff --git a/internal/domain/recipe/recipe.go b/internal/domain/recipe/recipe.go index 56419fa..d73585b 100644 --- a/internal/domain/recipe/recipe.go +++ b/internal/domain/recipe/recipe.go @@ -78,11 +78,12 @@ type Recipe struct { // 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 + Search string `json:"Search"` + MealType int `json:"MealType"` + Time int `json:"Time"` + Difficulty int `json:"Difficulty"` + ServingSize int `json:"ServingSize"` + Favorites bool `json:"Favorites"` } // Tag is a model which represents a single tag in the Tags table. A tag is mapped to a recipe diff --git a/web/src/App.tsx b/web/src/App.tsx index ba64894..a4ef3b0 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -12,6 +12,7 @@ import LoginPage from './pages/Login'; import { use, type ReactNode } from 'react'; import { AuthContext } from './context/AuthContext'; import RecipePage from './pages/Recipe'; +import SearchPage from './pages/Search'; function ProtectedRoute({ children }: { children: ReactNode }) { const { isLoggedIn } = use(AuthContext) @@ -39,6 +40,7 @@ function App() { }> } /> } /> + } /> } /> } /> } /> diff --git a/web/src/components/buttons/DropdownButton.tsx b/web/src/components/buttons/DropdownButton.tsx index 266678a..f1de8b3 100644 --- a/web/src/components/buttons/DropdownButton.tsx +++ b/web/src/components/buttons/DropdownButton.tsx @@ -4,13 +4,13 @@ interface DropdownButtonProps { name: string; value: string; selected: boolean; + changeHandler: (e: React.ChangeEvent) => void; } -export default function DropdownButton({ content, name, value, selected }: DropdownButtonProps) { - +export default function DropdownButton({ content, name, value, selected, changeHandler }: DropdownButtonProps) { return (