Compare commits

..

2 commits

Author SHA1 Message Date
Anika Raemer
5bbd01480f create SvgIcon component 2025-09-20 17:30:04 +02:00
Anika Raemer
73b805546f move the recipe list's toolbar to a component of its own 2025-09-20 17:00:27 +02:00
5 changed files with 87 additions and 50 deletions

View file

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import SvgIcon, { Icon } from "./SvgIcon"
/** /**
* Custom search field component including a clear search functionality * Custom search field component including a clear search functionality
*/ */
@ -37,40 +37,18 @@ export default function SearchField({onSearchStringChanged} : SearchFieldProps){
Clears search string on click Clears search string on click
*/} */}
<button <button
className="absolute right-0 inset-y-0 flex items-center" className="absolute right-0 inset-y-0 flex items-center -ml-1 mr-3"
onClick = { () => changeSearchString("") } onClick = { () => changeSearchString("") }
> >
<svg <SvgIcon
xmlns="http://www.w3.org/2000/svg" icon = {Icon.X}
className="-ml-1 mr-3 h-6 w-6 text-gray-400 hover:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/> />
</svg>
</button> </button>
{/* Left icon: Looking glass */} {/* Left icon: Looking glass */}
<div className="absolute left-0 inset-y-0 flex items-center"> <div className="absolute left-0 inset-y-0 flex items-center ml-3">
<svg <SvgIcon
xmlns="http://www.w3.org/2000/svg" icon = {Icon.LookingGlass}
className="ml-3 h-6 w-6 text-gray-400 hover:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/> />
</svg>
</div> </div>
</div> </div>
) )

View file

@ -0,0 +1,38 @@
/**
* SVG Icon component
*/
/**
* Enum-like const object+type definition to define icons.
* The string corresponds to the path definition of the icon
*/
export const Icon = {
LookingGlass: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z",
X: "M6 18L18 6M6 6l12 12"
} as const;
export type Icon = typeof Icon[keyof typeof Icon];
type SvgIconProps = {
pathDefinition : string
icon : Icon
}
export default function SvgIcon({icon} : SvgIconProps){
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-gray-400 hover:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d={icon}
/>
</svg>
)
}

View file

@ -94,7 +94,7 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
placeholder="1" placeholder="1"
value={draft.servings.amount} value={draft.servings.amount}
onChange={e => { onChange={e => {
let tempServings = draft.servings const tempServings = draft.servings
tempServings.amount = Number(e.target.value) tempServings.amount = Number(e.target.value)
setDraft({...draft, servings: tempServings}) setDraft({...draft, servings: tempServings})
}} }}
@ -104,7 +104,7 @@ export default function RecipeEditor({ recipe, onSave, onCancel }: RecipeEditorP
placeholder="Persons" placeholder="Persons"
value={draft.servings.unit} value={draft.servings.unit}
onChange={e => { onChange={e => {
let tempServings = draft.servings const tempServings = draft.servings
tempServings.unit = e.target.value tempServings.unit = e.target.value
setDraft({...draft, servings: tempServings}) setDraft({...draft, servings: tempServings})
}} }}

View file

@ -5,6 +5,7 @@ import { fetchRecipeList } from "../../api/recipePoint"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { getRecipeAddUrl, getRecipeDetailUrl } from "../../routes" import { getRecipeAddUrl, getRecipeDetailUrl } from "../../routes"
import SearchField from "../basics/SearchField" import SearchField from "../basics/SearchField"
import RecipeListToolbar from "./RecipeListToolbar"
/** /**
* Displays a list of recipes in a sidebar layout. * Displays a list of recipes in a sidebar layout.
@ -44,25 +45,11 @@ export default function RecipeListPage() {
{/* Header - remains in position when scrolling */} {/* Header - remains in position when scrolling */}
<div className="sticky bg-gray-100 top-0 left-0 right-0 pb-4 border-b-2 border-gray-300"> <div className="sticky bg-gray-100 top-0 left-0 right-0 pb-4 border-b-2 border-gray-300">
<h1 className="content-title text-blue-900">Recipes</h1> <h1 className="content-title text-blue-900">Recipes</h1>
<div className="flex flex-wrap items-center gap-2 w-full"> <RecipeListToolbar
{/* Label: left-aligned on medium+ screens, full-width on small screens */} onAddClicked={handleAdd}
<div className="order-2 md:order-1 w-full md:w-auto md:flex-1"> onSearchStringChanged={setSearchString}
<label className="label">{recipeList.length} Recipes</label> numberOfRecipes={recipeList.length}
</div> />
{/* Search + Add button container: right-aligned on medium+ screens */}
<div className="order-1 md:order-2 flex flex-1 md:flex-none justify-end gap-2 min-w-[160px]">
<div className="flex-1 md:flex-none md:max-w-[500px]">
<SearchField onSearchStringChanged={setSearchString} />
</div>
<button
className="primary-button flex-shrink-0"
onClick={handleAdd}
>
Add recipe
</button>
</div>
</div>
</div> </div>
{/*Content - List of recipe cards */} {/*Content - List of recipe cards */}
<div className="w-full pt-4"> <div className="w-full pt-4">

View file

@ -0,0 +1,34 @@
import SearchField from "../basics/SearchField"
/**
* Toolbar for RecipeListPage containing searchfield, add recipe button and number of recipes
*/
type RecepeListToolbarProps = {
onSearchStringChanged: (searchString : string) => void
onAddClicked: () => void
numberOfRecipes : number
}
export default function RecipeListToolbar({onSearchStringChanged, onAddClicked, numberOfRecipes} : RecepeListToolbarProps){
return (
<div className="flex flex-wrap items-center gap-2 w-full">
{/* Label: left-aligned on medium+ screens, full-width on small screens */}
<div className="order-2 md:order-1 w-full md:w-auto md:flex-1">
<label className="label">{numberOfRecipes} Recipes</label>
</div>
{/* Search + Add button container: right-aligned on medium+ screens */}
<div className="order-1 md:order-2 flex flex-1 md:flex-none justify-end gap-2 min-w-[160px]">
<div className="flex-1 md:flex-none md:max-w-[500px]">
<SearchField onSearchStringChanged={onSearchStringChanged} />
</div>
<button
className="primary-button flex-shrink-0"
onClick={onAddClicked}
>
Add recipe
</button>
</div>
</div>
)
}