123 lines
3.9 KiB
TypeScript
123 lines
3.9 KiB
TypeScript
import { use, useEffect, useState, type ChangeEvent, type Dispatch, type FormEvent, type SetStateAction } from "react";
|
|
import type { SearchFilters } from "../../types/search";
|
|
import FilterButton from "../buttons/FilterButton";
|
|
import RecipeSearchFilterDropdown from "./RecipeSearchFilterDropdown";
|
|
import { SearchRecipes } from "../../services/RecipeService";
|
|
import { isApiError } from "../../types/api/error";
|
|
import type { Recipe } from "../../types/recipe";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { FilterContext } from "../../context/FilterContext";
|
|
|
|
interface RecipeSearchBarProps {
|
|
// filters: SearchFilters;
|
|
// setFilters: React.Dispatch<React.SetStateAction<SearchFilters>>;
|
|
redirect: boolean;
|
|
searchOnLoad: boolean;
|
|
favorites: boolean;
|
|
setRecipes: Dispatch<SetStateAction<Recipe[]>> | null;
|
|
|
|
// Loading is optional
|
|
loading?: boolean;
|
|
setLoading?: Dispatch<SetStateAction<boolean>>;
|
|
};
|
|
|
|
export default function RecipeSearchBar({ redirect, searchOnLoad, favorites, setRecipes, loading, setLoading }: RecipeSearchBarProps) {
|
|
const navigate = useNavigate();
|
|
const { filters, setFilters } = use(FilterContext);
|
|
|
|
const [displayDropdown, setDisplayDropdown] = useState<boolean>(false);
|
|
|
|
// SERVER FUNCTIONS
|
|
const fetchSearchResults = async () => {
|
|
if (redirect) {
|
|
await navigate("/v2/web/search");
|
|
return;
|
|
}
|
|
|
|
// Should not allow many queries, thought we should allow redirect through loading
|
|
if (loading) return;
|
|
if (setLoading) setLoading(true);
|
|
|
|
try {
|
|
const result = await SearchRecipes(filters);
|
|
if (isApiError(result)) {
|
|
console.error(result.message);
|
|
return;
|
|
}
|
|
|
|
if (setRecipes)
|
|
setRecipes(result);
|
|
} finally {
|
|
if (setLoading) setLoading(false);
|
|
}
|
|
}
|
|
|
|
// HANDLERS
|
|
const toggleDropdownHandler = () => setDisplayDropdown(!displayDropdown);
|
|
|
|
// TODO: Store filters in a global state somewhere!
|
|
const searchHandler = async (e: FormEvent<HTMLFormElement>): Promise<void> => {
|
|
e.preventDefault();
|
|
await fetchSearchResults();
|
|
};
|
|
|
|
const queryInputHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
|
const new_filters: SearchFilters = {
|
|
...filters,
|
|
Search: e.target.value,
|
|
};
|
|
setFilters(new_filters);
|
|
}
|
|
|
|
// EFFECTS
|
|
// TODO: Learn how to use 'useCallback' here to prevent endless loading and fix warning
|
|
useEffect(() => {
|
|
if (searchOnLoad)
|
|
void fetchSearchResults();
|
|
}, [searchOnLoad]);
|
|
|
|
useEffect(() => {
|
|
setFilters({
|
|
...filters,
|
|
Favorites: favorites
|
|
});
|
|
}, [favorites]);
|
|
|
|
|
|
return (
|
|
<form className="w-full px-4 my-8" onSubmit={(e) => void searchHandler(e)}>
|
|
<div className="flex w-full gap-x-2">
|
|
<div className="relative w-full">
|
|
<input type="hidden" name="redirect" value={JSON.stringify(redirect)} />
|
|
<input
|
|
type="search"
|
|
name="search"
|
|
placeholder="Search recipes, ingredients..."
|
|
value={filters ? filters.Search : ""}
|
|
onChange={queryInputHandler}
|
|
className="w-full pr-4 pl-10 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
<button className="absolute left-3 top-1/2 -translate-y-1/2">
|
|
<svg
|
|
className="h-5 w-5 text-gray-400"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<FilterButton click={toggleDropdownHandler} />
|
|
</div>
|
|
<RecipeSearchFilterDropdown filters={filters} setFilters={setFilters} display={displayDropdown} />
|
|
</form>
|
|
);
|
|
}
|