feat: import jen některých dnů
feat: násobení jídel/cviků feat: refaktor napojování šablon
This commit is contained in:
1 parent
c3990d80f8
commit
8dac67bdd1
37 files changed
+2314
-481
No files matched your search
@@ -0,0 +1,137 @@
|
||||
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;
|
||||
Reference in new issue
Block a user