(FEAT): Finished fixing up the search page!

This commit is contained in:
Hayden Hargreaves 2025-12-28 17:39:28 -07:00
parent 14b41d204f
commit 54c557bec5
6 changed files with 26 additions and 83 deletions

View File

@ -193,7 +193,12 @@ func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
// //
// The favorites parameter is used to only return filters favorited by the userId provided. // The favorites parameter is used to only return filters favorited by the userId provided.
func (s *RecipeService) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) { func (s *RecipeService) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) {
return s.recipeRepository.SearchRecipes(filters, userId, favorites) ids, err := s.recipeRepository.SearchRecipes(filters, userId, favorites)
if err != nil {
return []domain.Recipe{}, err
}
return s.recipeRepository.GetRecipes(ids, userId)
} }
// GetUserRecipes returns a list of the recipes that the user has created. The user's // GetUserRecipes returns a list of the recipes that the user has created. The user's

View File

@ -4,7 +4,7 @@ type RecipeRepository interface {
CreateRecipe(recipe *Recipe) error CreateRecipe(recipe *Recipe) error
GetRecipe(id int, userId *int) (*Recipe, error) GetRecipe(id int, userId *int) (*Recipe, error)
GetRecipes(ids []int, userId *int) ([]Recipe, error) GetRecipes(ids []int, userId *int) ([]Recipe, error)
SearchRecipes(filters SearchFilters, userId *int, favorites bool) ([]Recipe, error) SearchRecipes(filters SearchFilters, userId *int, favorites bool) ([]int, error)
CreateRecipeTags(recipe Recipe, tags []string) error CreateRecipeTags(recipe Recipe, tags []string) error
GetUserRecipes(id int) ([]Recipe, error) GetUserRecipes(id int) ([]Recipe, error)
GetUserFavoriteRecipes(id int) ([]Recipe, error) GetUserFavoriteRecipes(id int) ([]Recipe, error)

View File

@ -221,7 +221,10 @@ func isBitActive(bits, pos int) bool {
// TODO: Pagination is required, to provide infinite scroll. // TODO: Pagination is required, to provide infinite scroll.
// //
// TODO: This does not work in the current build, the DB does not return valid values. // TODO: This does not work in the current build, the DB does not return valid values.
func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) { //
// 12/28/25: This function has changed, now longer returns the recipes, but their IDs for fetching
// elsewhere.
func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]int, error) {
// Compute meals type filters (there are 7 bits) // Compute meals type filters (there are 7 bits)
var mealConditions []string var mealConditions []string
for i := range 7 { for i := range 7 {
@ -305,17 +308,6 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i
// Define columns to select. More fields can be added if the full text search is required // Define columns to select. More fields can be added if the full text search is required
columns := []string{ columns := []string{
"r.id", "r.id",
"r.title",
"r.description",
"r.instructions",
"r.serves",
"r.difficulty",
"r.duration",
"r.category",
"r.ingredients",
"r.userid",
"r.modified",
"r.created",
} }
// TODO: Need to add these to the query // TODO: Need to add these to the query
@ -392,76 +384,21 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i
// Execute the query // Execute the query
rows, err := r.db.Query(query) rows, err := r.db.Query(query)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query recipes: %w", err) return []int{}, fmt.Errorf("failed to query recipes: %w", err)
} }
defer rows.Close() defer rows.Close()
var recipes []domain.Recipe var ids []int
for rows.Next() { for rows.Next() {
// Parsed values location var id int
var recipe domain.Recipe if err := rows.Scan(&id); err != nil {
var durationBytes []byte return []int{}, fmt.Errorf("failed to extract ID: %s\n", err.Error())
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 ids = append(ids, id)
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{}
}
// Add tags
if err := r.GetRecipeTags(&recipe); err != nil {
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
}
// Add recipe if not a favorite search
if !favorites && userId != nil {
if err := r.GetRecipeFavorite(&recipe, *userId); err != nil {
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
}
}
if favorites {
recipe.Favorite = true
}
recipes = append(recipes, recipe)
} }
return recipes, nil return ids, nil
} }
// CreateRecipeTags accepts a list of tags (names) and a recipe (already created by the DB) and // CreateRecipeTags accepts a list of tags (names) and a recipe (already created by the DB) and

View File

@ -40,10 +40,10 @@ export default function RecipeMetaData({ recipe }: RecipeMetaDataProps) {
> >
<div className="flex gap-x-1 my-2"> <div className="flex gap-x-1 my-2">
{Array.from({ length: recipe?.Difficulty ?? 0 }).map((_, i) => ( {Array.from({ length: recipe?.Difficulty ?? 0 }).map((_, i) => (
<StarIcon key={`${recipe?.Id}-filled-${i}`} filled={true} /> <StarIcon key={`${recipe?.Id}-filled-${i}`} size={6} filled={true} />
))} ))}
{Array.from({ length: 5 - (recipe?.Difficulty ?? 0) }).map((_, i) => ( {Array.from({ length: 5 - (recipe?.Difficulty ?? 0) }).map((_, i) => (
<StarIcon key={`${recipe?.Id}-unfilled-${i}`} filled={false} /> <StarIcon key={`${recipe?.Id}-unfilled-${i}`} size={6} filled={false} />
))} ))}
</div> </div>
<p>{displayDifficulty(recipe?.Difficulty ?? 0)}</p> <p>{displayDifficulty(recipe?.Difficulty ?? 0)}</p>

View File

@ -1,19 +1,20 @@
interface StarIconProps { interface StarIconProps {
filled: boolean; filled: boolean;
size: number;
}; };
export default function StarIcon({ filled }: StarIconProps) { export default function StarIcon({ filled, size = 6 }: StarIconProps) {
return <> return <>
{filled ? ( {filled ? (
<svg className="h-6 text-blue-600" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg className={`h-${size} text-blue-600`} fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path <path
d="M23.632 9.201a.628.628 0 0 1-.22.678l-5.726 4.96 1.727 7.394a.606.606 0 0 1-.935.676l-6.503-3.953-6.503 3.953a.713.713 0 0 1-.374.112.57.57 0 0 1-.34-.109.629.629 0 0 1-.222-.679l1.729-7.393L.539 9.879A.607.607 0 0 1 .897 8.78l7.536-.635 2.965-7.083a.62.62 0 0 1 1.155.001l2.965 7.082 7.536.635a.63.63 0 0 1 .578.42z" d="M23.632 9.201a.628.628 0 0 1-.22.678l-5.726 4.96 1.727 7.394a.606.606 0 0 1-.935.676l-6.503-3.953-6.503 3.953a.713.713 0 0 1-.374.112.57.57 0 0 1-.34-.109.629.629 0 0 1-.222-.679l1.729-7.393L.539 9.879A.607.607 0 0 1 .897 8.78l7.536-.635 2.965-7.083a.62.62 0 0 1 1.155.001l2.965 7.082 7.536.635a.63.63 0 0 1 .578.42z"
></path> ></path>
<path fill="none" d="M0 0h24v24H0z"></path> <path fill="none" d="M0 0h24v24H0z"></path>
</svg> </svg>
) : ( ) : (
<svg className="h-6 text-gray-500" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg className={`h-${size} text-gray-500`} fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path <path
d="M23.054 8.781l-7.536-.635-2.965-7.082a.619.619 0 0 0-1.155 0L8.433 8.145.896 8.78a.607.607 0 0 0-.357 1.1l5.726 4.96-1.729 7.395a.63.63 0 0 0 .223.679.573.573 0 0 0 .339.108.717.717 0 0 0 .374-.111l6.503-3.954 6.503 3.953a.606.606 0 0 0 .935-.677l-1.727-7.392 5.725-4.96a.607.607 0 0 0-.357-1.099zm-6.48 5.698l1.662 7.113-6.261-3.806-6.262 3.807 1.663-7.114-5.513-4.776 7.257-.611 2.855-6.817 2.855 6.817 7.257.611z" d="M23.054 8.781l-7.536-.635-2.965-7.082a.619.619 0 0 0-1.155 0L8.433 8.145.896 8.78a.607.607 0 0 0-.357 1.1l5.726 4.96-1.729 7.395a.63.63 0 0 0 .223.679.573.573 0 0 0 .339.108.717.717 0 0 0 .374-.111l6.503-3.954 6.503 3.953a.606.606 0 0 0 .935-.677l-1.727-7.392 5.725-4.96a.607.607 0 0 0-.357-1.099zm-6.48 5.698l1.662 7.113-6.261-3.806-6.262 3.807 1.663-7.114-5.513-4.776 7.257-.611 2.855-6.817 2.855 6.817 7.257.611z"
></path> ></path>

View File

@ -41,10 +41,10 @@ export default function RecipeSearchResult({ recipe }: RecipeSearchResultProps)
</span> </span>
<span className="flex gap-x-1 align-center"> <span className="flex gap-x-1 align-center">
{Array.from({ length: recipe.Difficulty }).map((_, i) => ( {Array.from({ length: recipe.Difficulty }).map((_, i) => (
<StarIcon key={`${recipe.Id}-filled-${i}`} filled={true} /> <StarIcon key={`${recipe.Id}-filled-${i}`} size={4} filled={true} />
))} ))}
{Array.from({ length: 5 - (recipe.Difficulty) }).map((_, i) => ( {Array.from({ length: 5 - (recipe.Difficulty) }).map((_, i) => (
<StarIcon key={`${recipe.Id}-unfilled-${i}`} filled={false} /> <StarIcon key={`${recipe.Id}-unfilled-${i}`} size={4} filled={false} />
))} ))}
</span> </span>
<span className="flex gap-x-1 align-center"> <span className="flex gap-x-1 align-center">