Adapt to new recipe filter and fix create recipe
This commit is contained in:
parent
709cb23f3d
commit
79d62b140e
7 changed files with 82 additions and 36 deletions
11
frontend/src/api/dtos/CompactRecipeDto.ts
Normal file
11
frontend/src/api/dtos/CompactRecipeDto.ts
Normal file
|
|
@ -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!
|
||||||
|
}
|
||||||
13
frontend/src/api/dtos/CompactRecipeFilterRequest.ts
Normal file
13
frontend/src/api/dtos/CompactRecipeFilterRequest.ts
Normal file
|
|
@ -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[];
|
||||||
|
}
|
||||||
8
frontend/src/api/dtos/CompactRecipeFilterResponse.ts
Normal file
8
frontend/src/api/dtos/CompactRecipeFilterResponse.ts
Normal file
|
|
@ -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[];
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,32 @@
|
||||||
import type {RecipeModel} from "../../models/RecipeModel"
|
|
||||||
import {apiClient} from "../apiClient.ts";
|
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
|
* Loads recipe header data for all recipes matching the given filter criteria.
|
||||||
* @param searchString Search string for filtering recipeList
|
* If neither a search string nor tag IDs are provided, all recipes are returned.
|
||||||
* @returns Array of recipe
|
*
|
||||||
|
* @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<RecipeModel[]> {
|
export async function fetchRecipeList(
|
||||||
let url: string = RECIPE_URL; // add an s to the base URL as we want to load a list
|
searchString?: string,
|
||||||
// if there's a search string add it as query parameter
|
tagIdList?: string[]
|
||||||
if (searchString && searchString !== "") {
|
): Promise<CompactRecipeDto[]> {
|
||||||
url += "?search=" + searchString;
|
const request: CompactRecipeFilterRequest = {
|
||||||
}
|
searchString: searchString && searchString.length > 0 ? searchString : undefined,
|
||||||
return apiClient.get(url);
|
tagIdList: tagIdList && tagIdList.length > 0 ? tagIdList : undefined,
|
||||||
|
};
|
||||||
|
const response = await apiClient.post<CompactRecipeFilterResponse>(
|
||||||
|
`${COMPACT_RECIPE_URL}/list-by-filter`,
|
||||||
|
request
|
||||||
|
);
|
||||||
|
return response.compactRecipeList;
|
||||||
}
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ export default function RecipeEditPage() {
|
||||||
title: "",
|
title: "",
|
||||||
ingredientGroupList: [],
|
ingredientGroupList: [],
|
||||||
instructionStepList: [],
|
instructionStepList: [],
|
||||||
|
tagList: [],
|
||||||
servings: {
|
servings: {
|
||||||
amount: 1,
|
amount: 1,
|
||||||
unit: ""
|
unit: ""
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,41 @@
|
||||||
import {useEffect, useState} from "react"
|
import {useEffect, useState} from "react"
|
||||||
import RecipeListItem from "./RecipeListItem"
|
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 {fetchRecipeList} from "../../api/endpoints/CompactRecipeRestResource.ts"
|
||||||
import {useNavigate} from "react-router-dom"
|
import {useNavigate} from "react-router-dom"
|
||||||
import {getRecipeAddUrl, getRecipeDetailUrl, getRecipeListUrl} from "../../routes"
|
import {getRecipeAddUrl, getRecipeDetailUrl, getRecipeListUrl} from "../../routes"
|
||||||
import RecipeListToolbar from "./RecipeListToolbar"
|
import RecipeListToolbar from "./RecipeListToolbar"
|
||||||
import StickyHeader from "../basics/StickyHeader.tsx";
|
import StickyHeader from "../basics/StickyHeader.tsx"
|
||||||
import PageContainer from "../basics/PageContainer.tsx";
|
import PageContainer from "../basics/PageContainer.tsx"
|
||||||
import PageContentLayout from "../basics/PageContentLayout.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.
|
* Displays a list of recipes in a sidebar layout.
|
||||||
* Each recipe link fills the available width.
|
* Each recipe link fills the available width.
|
||||||
*/
|
*/
|
||||||
export default function RecipeListPage() {
|
export default function RecipeListPage() {
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [recipeList, setRecipeList] = useState<RecipeModel[] | null>(null)
|
const [recipeList, setRecipeList] = useState<CompactRecipeDto[] | null>(null)
|
||||||
const [searchString, setSearchString] = useState<string>("")
|
const [searchString, setSearchString] = useState<string>("")
|
||||||
// load recipes once on render and whenever search string changes
|
const [tagIdList, setTagIdList] = useState<string[]>([])
|
||||||
// @todo add delay. Only reload list if the search string hasn't changed for ~200 ms
|
|
||||||
|
// Reload list whenever search string or tag filter changes, debounced to
|
||||||
|
// avoid firing on every keystroke
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("loading recipe list with searchString", searchString)
|
const timeout = setTimeout(async () => {
|
||||||
const loadRecipeList = async () => {
|
|
||||||
try {
|
try {
|
||||||
// Fetch recipe list
|
const data = await fetchRecipeList(searchString, tagIdList)
|
||||||
const data = await fetchRecipeList(searchString)
|
|
||||||
// @todo add and use compact recipe mapper
|
|
||||||
setRecipeList(data)
|
setRecipeList(data)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
}
|
}, SEARCH_DEBOUNCE_MS)
|
||||||
loadRecipeList()
|
|
||||||
}, [searchString])
|
return () => clearTimeout(timeout)
|
||||||
|
}, [searchString, tagIdList])
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
navigate(getRecipeAddUrl())
|
navigate(getRecipeAddUrl())
|
||||||
|
|
@ -42,8 +44,9 @@ export default function RecipeListPage() {
|
||||||
if (!recipeList) {
|
if (!recipeList) {
|
||||||
return <div>Loading!</div>
|
return <div>Loading!</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
/*Container spanning entire screen used to center content horizontally */
|
/* Container spanning entire screen used to center content horizontally */
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
{/* Container defining the maximum width of the content */}
|
{/* Container defining the maximum width of the content */}
|
||||||
<PageContentLayout>
|
<PageContentLayout>
|
||||||
|
|
@ -56,7 +59,7 @@ export default function RecipeListPage() {
|
||||||
numberOfRecipes={recipeList.length}
|
numberOfRecipes={recipeList.length}
|
||||||
/>
|
/>
|
||||||
</StickyHeader>
|
</StickyHeader>
|
||||||
{/*Content - List of recipe cards */}
|
{/* Content - List of recipe cards */}
|
||||||
<div className="w-full pt-4">
|
<div className="w-full pt-4">
|
||||||
<div
|
<div
|
||||||
className="grid gap-6 grid-cols-[repeat(auto-fit,minmax(220px,auto))] max-w-6xl mx-auto justify-center">
|
className="grid gap-6 grid-cols-[repeat(auto-fit,minmax(220px,auto))] max-w-6xl mx-auto justify-center">
|
||||||
|
|
@ -64,7 +67,7 @@ export default function RecipeListPage() {
|
||||||
<RecipeListItem
|
<RecipeListItem
|
||||||
key={recipe.id}
|
key={recipe.id}
|
||||||
title={recipe.title}
|
title={recipe.title}
|
||||||
targetPath={recipe.id !== undefined ? getRecipeDetailUrl(recipe.id) : getRecipeListUrl()} // @todo proper error handling
|
targetPath={recipe.id !== undefined ? getRecipeDetailUrl(recipe.id) : getRecipeListUrl()}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export function mapRecipeModelToDto(model: RecipeModel): RecipeDto {
|
||||||
const tagDtos = model.tagList.map(mapTagModelToDto)
|
const tagDtos = model.tagList.map(mapTagModelToDto)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: model.id,
|
id: model.id ? model.id : undefined,
|
||||||
title: model.title,
|
title: model.title,
|
||||||
amount: model.servings.amount,
|
amount: model.servings.amount,
|
||||||
amountDescription: model.servings.unit,
|
amountDescription: model.servings.unit,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue