add search

This commit is contained in:
Anika Raemer 2025-09-13 14:30:34 +02:00
parent f1a9b6d444
commit df406b636e
3 changed files with 51 additions and 17 deletions

View file

@ -12,39 +12,60 @@ app.use(express.json());
let recipeList = recipes; let recipeList = recipes;
// Routes // Routes
/**
* Load recipe list
* allows for filtering the list based on the recipes title using the query paramter search
*/
app.get("/recipe", (req, res) => { app.get("/recipe", (req, res) => {
console.log("GET /recipe") // extract search string from query parameters, convert to lower case for case insensitive search
res.json(recipeList); let searchString : string = req.query.search ? req.query.search.toString().toLowerCase() : "";
console.log("searchString", searchString)
let resultList : Recipe[] = []
if(searchString && searchString !== ""){
// if theres a search string filter recipe list based on title
// convert title to lower case in order to allow for case insensitive filtering
resultList = recipeList.filter(recipe => recipe.title.toLowerCase().includes(searchString));
} else {
// if there is no search string simply return the entire list
resultList = recipeList;
}
res.json(resultList)
}); });
/**
* Load a single recipe by its id
*/
app.get("/recipe/:id", (req, res) => { app.get("/recipe/:id", (req, res) => {
let recipeId : string = req.params.id; let recipeId : string = req.params.id;
console.log("GET /recipe/", recipeId); const recipe = recipeList.find(r => r.id === recipeId);
const recipe = recipeList.find(r => r.id === req.params.id);
console.log(recipe ? "SUCCESS" : "404")
recipe ? res.json(recipe) : res.status(404).send("Recipe not found"); recipe ? res.json(recipe) : res.status(404).send("Recipe not found");
}); });
/**
* Create a new recipe
*/
app.post("/recipe", (req, res) => { app.post("/recipe", (req, res) => {
console.log("POST /recipe")
const newRecipe: Recipe = { id: uuidv4(), ...req.body }; const newRecipe: Recipe = { id: uuidv4(), ...req.body };
recipeList.push(newRecipe); recipeList.push(newRecipe);
res.status(201).json(newRecipe); res.status(201).json(newRecipe);
}); });
/**
* Save an existing recipe
*/
app.put("/recipe/:id", (req, res) => { app.put("/recipe/:id", (req, res) => {
let recipeId : string = req.params.id; let recipeId : string = req.params.id;
console.log("PUT /recipe/", recipeId) const index = recipeList.findIndex(r => r.id === recipeId);
const index = recipes.findIndex(r => r.id === recipeId);
if (index === -1) { if (index === -1) {
console.log("404")
return res.status(404).send("Recipe not found"); return res.status(404).send("Recipe not found");
} }
console.log("SUCCESS");
recipeList[index] = { ...recipeList[index], ...req.body }; recipeList[index] = { ...recipeList[index], ...req.body };
res.json(recipeList[index]); res.json(recipeList[index]);
}); });
/**
* Delete a recipe by id
*/
app.delete("/recipe/:id", (req, res) => { app.delete("/recipe/:id", (req, res) => {
recipeList = recipeList.filter(r => r.id !== req.params.id); recipeList = recipeList.filter(r => r.id !== req.params.id);
res.status(204).send(); res.status(204).send();

View file

@ -24,10 +24,17 @@ export async function fetchRecipe(id: string): Promise<Recipe> {
/** /**
* Load list of all recipes * Load list of all recipes
* @param searchString Search string for filtering recipeList
* @returns Array of recipe * @returns Array of recipe
*/ */
export async function fetchRecipeList(): Promise<Recipe[]> { export async function fetchRecipeList(searchString : string): Promise<Recipe[]> {
const res = await fetch(`${RECIPE_URL}/`) let url : string = RECIPE_URL;
// if there's a search string add it as query parameter
if(searchString && searchString !== ""){
url +="?search=" + searchString;
}
console.log("calling url", url)
const res = await fetch(url)
if (!res.ok) { if (!res.ok) {
throw new Error(`Failed to fetch recipe list`) throw new Error(`Failed to fetch recipe list`)
} }

View file

@ -3,7 +3,7 @@ import RecipeListItem from "./RecipeListItem"
import type { Recipe } from "../../types/recipe" import type { Recipe } from "../../types/recipe"
import { fetchRecipeList } from "../../api/recipePoint" import { fetchRecipeList } from "../../api/recipePoint"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { getRecipeAddUrl, getRecipeAddUrlDefinition, getRecipeDetailUrl } from "../../routes" import { getRecipeAddUrl, getRecipeDetailUrl } from "../../routes"
/** /**
* Displays a list of recipes in a sidebar layout. * Displays a list of recipes in a sidebar layout.
@ -13,24 +13,27 @@ export default function RecipeListPage() {
const navigate = useNavigate() const navigate = useNavigate()
const [recipeList, setRecipeList] = useState<Recipe[]|null>(null) const [recipeList, setRecipeList] = useState<Recipe[]|null>(null)
// load recipes once on render const [searchString, setSearchString] = useState<string>("")
// load recipes once on render and whenever search string changes
// @todo add delay. Only reload list if the search string hasn't changed for ~200 ms
useEffect(() => { useEffect(() => {
console.log("loading recipe list with searchString", searchString)
const loadRecipeList = async () => { const loadRecipeList = async () => {
try { try {
// Fetch recipe list // Fetch recipe list
console.log("loading recipe list") const data = await fetchRecipeList(searchString)
const data = await fetchRecipeList()
setRecipeList(data) setRecipeList(data)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
} }
loadRecipeList() loadRecipeList()
}, []) }, [,searchString])
const handleAdd = () => { const handleAdd = () => {
navigate(getRecipeAddUrl()) navigate(getRecipeAddUrl())
} }
if(!recipeList) { return <div>Unable to load recipes!</div>} if(!recipeList) { return <div>Unable to load recipes!</div>}
return ( return (
/*Contaier spanning entire screen used to center content horizontally */ /*Contaier spanning entire screen used to center content horizontally */
@ -44,6 +47,9 @@ export default function RecipeListPage() {
<label className="text-gray-500 w-2/3">{recipeList.length} Recipes</label> <label className="text-gray-500 w-2/3">{recipeList.length} Recipes</label>
<input className="input-field" <input className="input-field"
placeholder="Search" placeholder="Search"
onChange={e => {
setSearchString(e.target.value)
}}
></input> ></input>
<button className="primary-button" <button className="primary-button"
onClick={handleAdd} onClick={handleAdd}