Files
food-tracer/client/src/components/RecipeIngredientsEditor.tsx
T
Ondřej Anděl 8dac67bdd1 feat: import jen některých dnů
feat: násobení jídel/cviků
feat: refaktor napojování šablon
2026-09-30 19:10:54 +02:00

138 lines
6.2 KiB
TypeScript

import { Button, Form } from "react-bootstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { RecipeIngredient } from "../../../types";
import { formatCalories, formatPriceInput, getIngredientCalories, parseIntInput, parsePriceInput } from "../Utils";
/** Rozpracovaná surovina — hodnoty jsou texty, protože se editují v polích. */
export type DraftIngredient = {
name: string,
weight: string,
caloriesPer100g: string,
calories: string,
price: string,
};
export const emptyIngredient = (): DraftIngredient => ({
name: '',
weight: '',
caloriesPer100g: '',
calories: '',
price: '',
});
/** Převede uloženou surovinu na rozpracovanou pro editaci. */
export const toDraftIngredient = (ingredient: RecipeIngredient): DraftIngredient => ({
name: ingredient.name,
weight: ingredient.weight != null ? String(ingredient.weight) : '',
caloriesPer100g: ingredient.caloriesPer100g != null ? String(ingredient.caloriesPer100g) : '',
calories: ingredient.calories != null ? String(ingredient.calories) : '',
price: formatPriceInput(ingredient.price),
});
/** Převede rozpracovanou surovinu na hodnoty pro výpočet a uložení. */
export const toIngredient = (draft: DraftIngredient): RecipeIngredient => ({
name: draft.name.trim(),
weight: parseIntInput(draft.weight),
caloriesPer100g: parseIntInput(draft.caloriesPer100g),
calories: parseIntInput(draft.calories),
price: parsePriceInput(draft.price),
});
type Props = {
ingredients: DraftIngredient[],
onChange: (ingredients: DraftIngredient[]) => void,
};
/**
* Editor surovin receptu.
*
* Energie suroviny se dopočte z gramáže a hodnoty na 100 g; ručně jde zadat jen
* tam, kde dopočítat nejde (třeba „lžíce oleje" bez vážení).
*/
const RecipeIngredientsEditor = ({ ingredients, onChange }: Readonly<Props>) => {
const updateAt = (index: number, changes: Partial<DraftIngredient>) =>
onChange(ingredients.map((item, i) => (i === index ? { ...item, ...changes } : item)));
return (
<>
{ingredients.map((ingredient, index) => {
const derived = getIngredientCalories({
weight: parseIntInput(ingredient.weight),
caloriesPer100g: parseIntInput(ingredient.caloriesPer100g),
});
return (
// Suroviny nemají vlastní id, v rámci rozpracovaného receptu je index jediný klíč
<div className="workout-item-row recipe-ingredient-row" key={index}>
<Form.Control
value={ingredient.name}
placeholder="Surovina"
aria-label="Název suroviny"
onChange={event => updateAt(index, { name: event.target.value })}
/>
<div className="input-group recipe-ingredient-weight">
<Form.Control
inputMode="numeric"
value={ingredient.weight}
placeholder="200"
aria-label="Množství v gramech"
onChange={event => updateAt(index, { weight: event.target.value.replace(/\D/g, '') })}
/>
<span className="input-group-text">g</span>
</div>
<Form.Control
className="recipe-ingredient-rate"
inputMode="numeric"
value={ingredient.caloriesPer100g}
placeholder="kcal/100 g"
aria-label="Energie na 100 g"
onChange={event => updateAt(index, { caloriesPer100g: event.target.value.replace(/\D/g, '') })}
/>
{derived != null ? (
<div className="recipe-ingredient-calories">{formatCalories(derived)}</div>
) : (
<Form.Control
className="recipe-ingredient-calories"
inputMode="numeric"
value={ingredient.calories}
placeholder="kcal"
title="Energie použitého množství, když ji nejde dopočítat z gramáže"
onChange={event => updateAt(index, { calories: event.target.value.replace(/\D/g, '') })}
/>
)}
<div className="input-group recipe-ingredient-price">
<Form.Control
inputMode="decimal"
value={ingredient.price}
placeholder="cena"
aria-label="Cena použitého množství"
onChange={event => updateAt(index, { price: event.target.value })}
/>
<span className="input-group-text">Kč</span>
</div>
<button
type="button"
className="btn-icon danger"
onClick={() => onChange(ingredients.filter((_, i) => i !== index))}
disabled={ingredients.length === 1}
title="Odebrat surovinu"
aria-label="Odebrat surovinu"
>
<FontAwesomeIcon icon={faTrash} />
</button>
</div>
);
})}
<Button
variant="outline-secondary"
size="sm"
onClick={() => onChange([...ingredients, emptyIngredient()])}
>
<FontAwesomeIcon icon={faPlus} /> Surovina
</Button>
</>
);
};
export default RecipeIngredientsEditor;