feat: import jen některých dnů

feat: násobení jídel/cviků
feat: refaktor napojování šablon
This commit is contained in:
Ondřej Anděl committed 2026-09-30 19:10:54 +02:00
1 parent c3990d80f8
commit 8dac67bdd1
37 files changed
+2314 -481

No files matched your search

@@ -0,0 +1,97 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPenToSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
import { MealTemplate, MealTemplateKind } from "../../../types";
import { MEAL_TYPE_ICONS, MEAL_TYPE_NAMES } from "../enums";
import {
deriveAmounts, deriveRecipe, formatCalories, formatPrice, formatWeight, getIngredientCalories,
} from "../Utils";
type Props = {
template: MealTemplate,
busy: boolean,
onEdit: () => void,
onDelete: () => void,
};
/**
* Karta šablony jídla na stránce Šablony.
*
* U receptu ukazuje suroviny, hodnoty celého hrnce i jedné porce a postup;
* u hotového jídla jen výsledné hodnoty, které se do dne zakládají.
*/
const MealTemplateCard = ({ template, busy, onEdit, onDelete }: Readonly<Props>) => {
const isRecipe = template.kind === MealTemplateKind.RECEPT;
const derived = deriveAmounts(template.price, template.pricePer100g, template.weight, template.caloriesPer100g);
const calories = derived.calories ?? template.calories;
const recipe = isRecipe
? deriveRecipe(template.ingredients ?? [], template.servings, template.cookedWeight)
: undefined;
return (
<div className="card-panel template-card">
<div className="template-head">
<div className="template-title">
{template.name}
{template.mealType && (
<span className="tag">
{MEAL_TYPE_ICONS[template.mealType]} {MEAL_TYPE_NAMES[template.mealType]}
</span>
)}
{template.source && <span className="tag">{template.source}</span>}
{derived.weight != null && (
<span className="tag">{isRecipe ? 'porce ' : ''}{formatWeight(derived.weight)}</span>
)}
{isRecipe && (template.servings ?? 1) > 1 && (
<span className="tag">{template.servings} porcí</span>
)}
</div>
{calories != null && <span className="template-calories">{formatCalories(calories)}</span>}
{template.price != null && (
<span className="meal-template-price">{formatPrice(template.price)}</span>
)}
<button className="btn-icon" onClick={() => onEdit()} title="Upravit" aria-label="Upravit šablonu">
<FontAwesomeIcon icon={faPenToSquare} />
</button>
<button
className="btn-icon danger"
onClick={() => onDelete()}
disabled={busy}
title="Smazat"
aria-label="Smazat šablonu"
>
<FontAwesomeIcon icon={faTrash} />
</button>
</div>
{isRecipe && !!template.ingredients?.length && (
<ul className="template-items">
{template.ingredients.map((ingredient, index) => (
// Suroviny nemají vlastní id, v rámci jednoho receptu je index stabilní
<li key={index}>
<span className="template-item-name">{ingredient.name}</span>
<span className="template-item-quantity">
{ingredient.weight != null ? formatWeight(ingredient.weight) : ''}
</span>
<span className="template-item-calories">
{formatCalories(getIngredientCalories(ingredient))}
</span>
</li>
))}
</ul>
)}
{recipe && (
<div className="recipe-summary">
Celkem {formatCalories(recipe.totalCalories)}
{recipe.totalWeight != null && <> · {formatWeight(recipe.totalWeight)}</>}
{recipe.caloriesPer100g != null && <> · {recipe.caloriesPer100g} kcal/100 g</>}
{recipe.totalPrice != null && <> · {formatPrice(recipe.totalPrice)}</>}
</div>
)}
{template.note && <p className="meal-template-note">{template.note}</p>}
</div>
);
};
export default MealTemplateCard;