From 79d62b140ea729aa398f0f9f97252e6157cf7ff3 Mon Sep 17 00:00:00 2001 From: araemer Date: Fri, 27 Feb 2026 20:37:07 +0100 Subject: [PATCH] Adapt to new recipe filter and fix create recipe --- frontend/src/api/dtos/CompactRecipeDto.ts | 11 +++++ .../api/dtos/CompactRecipeFilterRequest.ts | 13 ++++++ .../api/dtos/CompactRecipeFilterResponse.ts | 8 ++++ .../endpoints/CompactRecipeRestResource.ts | 40 ++++++++++------- .../src/components/recipes/RecipeEditPage.tsx | 1 + .../src/components/recipes/RecipeListPage.tsx | 43 ++++++++++--------- frontend/src/mappers/RecipeMapper.ts | 2 +- 7 files changed, 82 insertions(+), 36 deletions(-) create mode 100644 frontend/src/api/dtos/CompactRecipeDto.ts create mode 100644 frontend/src/api/dtos/CompactRecipeFilterRequest.ts create mode 100644 frontend/src/api/dtos/CompactRecipeFilterResponse.ts diff --git a/frontend/src/api/dtos/CompactRecipeDto.ts b/frontend/src/api/dtos/CompactRecipeDto.ts new file mode 100644 index 0000000..db9ad38 --- /dev/null +++ b/frontend/src/api/dtos/CompactRecipeDto.ts @@ -0,0 +1,11 @@ + +import { AbstractDto } from "./AbstractDto.js"; +/** + * DTO describing the essential header data of a recipe + * Used to populate lists + */ + +export class CompactRecipeDto extends AbstractDto { + title!: string; + // @todo add resource and rating here once implemented! +} \ No newline at end of file diff --git a/frontend/src/api/dtos/CompactRecipeFilterRequest.ts b/frontend/src/api/dtos/CompactRecipeFilterRequest.ts new file mode 100644 index 0000000..0d162c7 --- /dev/null +++ b/frontend/src/api/dtos/CompactRecipeFilterRequest.ts @@ -0,0 +1,13 @@ +/** + * Request wrapper for searching recipes based on a filter + */ +export class CompactRecipeFilterRequest { + /** + * Search string applied to the recipe title + */ + searchString?: string; + /** + * List of tags that must be applied to the recipe + */ + tagIdList?: string[]; +} \ No newline at end of file diff --git a/frontend/src/api/dtos/CompactRecipeFilterResponse.ts b/frontend/src/api/dtos/CompactRecipeFilterResponse.ts new file mode 100644 index 0000000..408ba93 --- /dev/null +++ b/frontend/src/api/dtos/CompactRecipeFilterResponse.ts @@ -0,0 +1,8 @@ +import {CompactRecipeDto} from "./CompactRecipeDto.js"; + +/** + * Filter response containing a list of all recipes matching the search + */ +export class CompactRecipeFilterResponse { + compactRecipeList! : CompactRecipeDto[]; +} \ No newline at end of file diff --git a/frontend/src/api/endpoints/CompactRecipeRestResource.ts b/frontend/src/api/endpoints/CompactRecipeRestResource.ts index dd374e1..22a69c8 100644 --- a/frontend/src/api/endpoints/CompactRecipeRestResource.ts +++ b/frontend/src/api/endpoints/CompactRecipeRestResource.ts @@ -1,22 +1,32 @@ -import type {RecipeModel} from "../../models/RecipeModel" import {apiClient} from "../apiClient.ts"; - +import type {CompactRecipeDto} from "../dtos/CompactRecipeDto.ts"; +import type {CompactRecipeFilterRequest} from "../dtos/CompactRecipeFilterRequest.ts"; +import type {CompactRecipeFilterResponse} from "../dtos/CompactRecipeFilterResponse.ts"; /** - * URL for handling recipes header data + * URL for handling recipe header data */ -const RECIPE_URL = "/compact-recipe" +const COMPACT_RECIPE_URL = "/compact-recipe" /** - * Load list of all recipes - * @param searchString Search string for filtering recipeList - * @returns Array of recipe + * Loads recipe header data for all recipes matching the given filter criteria. + * If neither a search string nor tag IDs are provided, all recipes are returned. + * + * @param searchString Optional title search string + * @param tagIdList Optional list of tag IDs the recipe must have all of + * @returns Filtered list of compact recipe DTOs */ -export async function fetchRecipeList(searchString: string): Promise { - let url: string = RECIPE_URL; // add an s to the base URL as we want to load a list - // if there's a search string add it as query parameter - if (searchString && searchString !== "") { - url += "?search=" + searchString; - } - return apiClient.get(url); -} +export async function fetchRecipeList( + searchString?: string, + tagIdList?: string[] +): Promise { + const request: CompactRecipeFilterRequest = { + searchString: searchString && searchString.length > 0 ? searchString : undefined, + tagIdList: tagIdList && tagIdList.length > 0 ? tagIdList : undefined, + }; + const response = await apiClient.post( + `${COMPACT_RECIPE_URL}/list-by-filter`, + request + ); + return response.compactRecipeList; +} \ No newline at end of file diff --git a/frontend/src/components/recipes/RecipeEditPage.tsx b/frontend/src/components/recipes/RecipeEditPage.tsx index f6fa145..dd92ed6 100644 --- a/frontend/src/components/recipes/RecipeEditPage.tsx +++ b/frontend/src/components/recipes/RecipeEditPage.tsx @@ -30,6 +30,7 @@ export default function RecipeEditPage() { title: "", ingredientGroupList: [], instructionStepList: [], + tagList: [], servings: { amount: 1, unit: "" diff --git a/frontend/src/components/recipes/RecipeListPage.tsx b/frontend/src/components/recipes/RecipeListPage.tsx index 786bc2d..dc4359c 100644 --- a/frontend/src/components/recipes/RecipeListPage.tsx +++ b/frontend/src/components/recipes/RecipeListPage.tsx @@ -1,39 +1,41 @@ import {useEffect, useState} from "react" import RecipeListItem from "./RecipeListItem" -import type {RecipeModel} from "../../models/RecipeModel" +import type {CompactRecipeDto} from "../../api/dtos/CompactRecipeDto.ts" import {fetchRecipeList} from "../../api/endpoints/CompactRecipeRestResource.ts" import {useNavigate} from "react-router-dom" import {getRecipeAddUrl, getRecipeDetailUrl, getRecipeListUrl} from "../../routes" import RecipeListToolbar from "./RecipeListToolbar" -import StickyHeader from "../basics/StickyHeader.tsx"; -import PageContainer from "../basics/PageContainer.tsx"; -import PageContentLayout from "../basics/PageContentLayout.tsx"; +import StickyHeader from "../basics/StickyHeader.tsx" +import PageContainer from "../basics/PageContainer.tsx" +import PageContentLayout from "../basics/PageContentLayout.tsx" + +/** Debounce delay in ms — list only reloads once the user stops typing */ +const SEARCH_DEBOUNCE_MS = 200 /** * Displays a list of recipes in a sidebar layout. * Each recipe link fills the available width. */ export default function RecipeListPage() { - const navigate = useNavigate() - const [recipeList, setRecipeList] = useState(null) + const [recipeList, setRecipeList] = useState(null) const [searchString, setSearchString] = useState("") - // 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 + const [tagIdList, setTagIdList] = useState([]) + + // Reload list whenever search string or tag filter changes, debounced to + // avoid firing on every keystroke useEffect(() => { - console.log("loading recipe list with searchString", searchString) - const loadRecipeList = async () => { + const timeout = setTimeout(async () => { try { - // Fetch recipe list - const data = await fetchRecipeList(searchString) - // @todo add and use compact recipe mapper + const data = await fetchRecipeList(searchString, tagIdList) setRecipeList(data) } catch (err) { console.error(err) } - } - loadRecipeList() - }, [searchString]) + }, SEARCH_DEBOUNCE_MS) + + return () => clearTimeout(timeout) + }, [searchString, tagIdList]) const handleAdd = () => { navigate(getRecipeAddUrl()) @@ -42,8 +44,9 @@ export default function RecipeListPage() { if (!recipeList) { return
Loading!
} + return ( - /*Container spanning entire screen used to center content horizontally */ + /* Container spanning entire screen used to center content horizontally */ {/* Container defining the maximum width of the content */} @@ -56,7 +59,7 @@ export default function RecipeListPage() { numberOfRecipes={recipeList.length} /> - {/*Content - List of recipe cards */} + {/* Content - List of recipe cards */}
@@ -64,7 +67,7 @@ export default function RecipeListPage() { ))}
@@ -72,4 +75,4 @@ export default function RecipeListPage() { ) -} +} \ No newline at end of file diff --git a/frontend/src/mappers/RecipeMapper.ts b/frontend/src/mappers/RecipeMapper.ts index e11422d..f55dc57 100644 --- a/frontend/src/mappers/RecipeMapper.ts +++ b/frontend/src/mappers/RecipeMapper.ts @@ -77,7 +77,7 @@ export function mapRecipeModelToDto(model: RecipeModel): RecipeDto { const tagDtos = model.tagList.map(mapTagModelToDto) return { - id: model.id, + id: model.id ? model.id : undefined, title: model.title, amount: model.servings.amount, amountDescription: model.servings.unit,