"use client"; import { PlusIcon, StarIcon } from "@heroicons/react/24/outline"; import { useEffect, useMemo, useState } from "react"; import { FormField, inputClassName } from "@/app/_components/form-field"; import { CatalogFood, PortionUnit, Recipe, amountToGrams, calcRecipe, classifyRecipe, describeIngredientAmount, unitLabel, } from "@/lib/hellth-data"; import { trpc } from "@/lib/trpc"; const recipeUnits: PortionUnit[] = ["g", "ml", "tsp", "tbsp", "piece", "slice", "cup", "pack", "bottle"]; function toNumber(value: unknown) { return Number(value) || 0; } function toCatalogFood(food: { id: string; name: string; brand: string | null; category: string; kcalPer100g: unknown; proteinPer100g: unknown; carbsPer100g: unknown; fatPer100g: unknown; servingG: unknown; preferredUnit: PortionUnit | null; densityGPerMl: unknown | null; source: "seed" | "manual" | "openfoodfacts" | "ocr" | "community"; barcode: string | null; availableEverywhere: boolean; portions?: Array<{ id: string; label: string; unit: PortionUnit; amount: unknown; gramsEquivalent: unknown; }>; }): CatalogFood { return { id: food.id, name: food.name, brand: food.brand ?? undefined, category: food.category, kcalPer100g: toNumber(food.kcalPer100g), proteinPer100g: toNumber(food.proteinPer100g), carbsPer100g: toNumber(food.carbsPer100g), fatPer100g: toNumber(food.fatPer100g), servingG: toNumber(food.servingG) || 100, preferredUnit: food.preferredUnit ?? "g", densityGPerMl: food.densityGPerMl == null ? undefined : toNumber(food.densityGPerMl), portions: food.portions?.map((portion) => ({ id: portion.id, label: portion.label, unit: portion.unit, amount: toNumber(portion.amount), gramsEquivalent: toNumber(portion.gramsEquivalent), })), source: food.source, barcode: food.barcode ?? undefined, availableEverywhere: food.availableEverywhere, }; } function recipeFromApi(recipe: { id: string; name: string; portions: number; minutes: number; notes: string | null; ingredients: Array<{ foodId: string; amountG: unknown; displayAmount: unknown | null; displayUnit: PortionUnit | null; portionId: string | null; }>; }): Recipe { return { id: recipe.id, name: recipe.name, portions: recipe.portions, minutes: recipe.minutes, notes: recipe.notes ?? undefined, ingredients: recipe.ingredients.map((ingredient) => ({ foodId: ingredient.foodId, amountG: toNumber(ingredient.amountG), amount: ingredient.displayAmount == null ? undefined : toNumber(ingredient.displayAmount), unit: ingredient.displayUnit ?? undefined, portionId: ingredient.portionId ?? undefined, })), }; } function MacroBar({ distribution }: { distribution: { carbs: number; fat: number; protein: number } }) { return (
); } function RecipeNutritionInsight({ recipe, foods }: { recipe: Recipe; foods: CatalogFood[] }) { const totals = calcRecipe(recipe, foods); const classification = classifyRecipe(recipe, foods); return (
{classification.tags.map((tag) => ( {tag} ))}

{totals.kcal}

kcal

{totals.carbs} g

KH {classification.distribution.carbs}%

{totals.fat} g

Fett {classification.distribution.fat}%

{totals.protein} g

Eiweiß {classification.distribution.protein}%

{classification.criteria.map((criterion) => (
{criterion.ok ? "OK" : "!"} {criterion.label} {criterion.detail}
))}
); } export default function RezeptePage() { const utils = trpc.useUtils(); const [name, setName] = useState(""); const [notes, setNotes] = useState(""); const [minutes, setMinutes] = useState("20"); const [portions, setPortions] = useState("2"); const [mealPrepDays, setMealPrepDays] = useState("1"); const [foodId, setFoodId] = useState(""); const [amount, setAmount] = useState("100"); const [unit, setUnit] = useState("g"); const [portionId, setPortionId] = useState(""); const [draftIngredients, setDraftIngredients] = useState([]); const [status, setStatus] = useState(null); const [suggestMealPrepDays, setSuggestMealPrepDays] = useState("0"); const [suggestCooksPerDay, setSuggestCooksPerDay] = useState("1"); const foodsQuery = trpc.health.listFoods.useQuery({ query: "" }); const recipesQuery = trpc.health.listRecipes.useQuery({ query: "" }); const recommendationsQuery = trpc.health.recipeRecommendations.useQuery({ mealPrepDays: Number(suggestMealPrepDays) || 0, cooksPerDay: Number(suggestCooksPerDay) || 1, limit: 8, }); const submissionsQuery = trpc.health.myRecipeSubmissions.useQuery(); const saveRecipeMutation = trpc.health.saveRecipe.useMutation({ onSuccess: async () => { await utils.health.listRecipes.invalidate(); await utils.health.recipeRecommendations.invalidate(); setName(""); setNotes(""); setDraftIngredients([]); setStatus("Rezept gespeichert."); }, }); const submitRecipeMutation = trpc.health.submitRecipeSuggestion.useMutation({ onSuccess: async (result) => { await utils.health.myRecipeSubmissions.invalidate(); await utils.health.recipeRecommendations.invalidate(); await utils.health.clubLeaderboard.invalidate(); await utils.health.activityWeek.invalidate(); setName(""); setNotes(""); setDraftIngredients([]); setStatus(`Rezeptvorschlag wurde zur Prüfung eingereicht. +${result.points} Hellfireclub-Punkte.`); }, }); const foods = useMemo(() => (foodsQuery.data ?? []).map(toCatalogFood), [foodsQuery.data]); const recipes = useMemo(() => (recipesQuery.data ?? []).map(recipeFromApi), [recipesQuery.data]); const foodById = useMemo(() => new Map(foods.map((food) => [food.id, food])), [foods]); const selectedFood = foodId ? foodById.get(foodId) : foods[0]; const draftRecipe = useMemo(() => ({ id: "draft", name: name.trim() || "Entwurf", portions: Number(portions) || 1, minutes: Number(minutes) || 0, notes: notes.trim() || undefined, ingredients: draftIngredients, }), [draftIngredients, minutes, name, notes, portions]); useEffect(() => { if (foodId || !foods[0]) return; setFoodId(foods[0].id); setUnit(foods[0].preferredUnit ?? "g"); }, [foodId, foods]); function recipePayload() { return { name: name.trim(), portions: Number(portions) || 1, minutes: Number(minutes) || 0, notes: notes.trim() || null, mealPrepDays: Number(mealPrepDays) || 0, ingredients: draftIngredients.map((ingredient, index) => ({ foodId: ingredient.foodId, amountG: ingredient.amountG, displayAmount: ingredient.amount ?? null, displayUnit: ingredient.unit ?? null, portionId: ingredient.portionId ?? null, sortOrder: index, })), }; } function addIngredient() { if (!selectedFood) return; const amountNumber = Number(amount.replace(",", ".")) || 1; const amountG = amountToGrams(selectedFood, amountNumber, unit, portionId || undefined); setDraftIngredients((current) => [ ...current, { foodId: selectedFood.id, amount: amountNumber, unit, portionId: portionId || undefined, amountG: Math.round(amountG * 10) / 10, }, ]); } function saveRecipe() { if (!name.trim() || draftIngredients.length === 0) return; saveRecipeMutation.mutate({ ...recipePayload(), isPublic: false }); } function submitRecipeSuggestion() { if (!name.trim() || draftIngredients.length === 0) return; submitRecipeMutation.mutate(recipePayload()); } return (

Health-API

Gerichte & Rezepte

Zutaten aus dem Lebensmittelkatalog, Alltagseinheiten, Portionen, Nährwerte und Meal-Prep-Notizen.

{recipes.map((recipe) => { const totals = calcRecipe(recipe, foods); return (

{recipe.name}

{recipe.minutes} min · {recipe.portions} Portionen {recipe.notes ? ` · ${recipe.notes}` : ""}

{totals.kcal} kcal
Bewertung folgt

{totals.carbs} g

KH

{totals.fat} g

Fett

{totals.protein} g

Protein

{recipe.ingredients.map((ingredient, index) => (
{foodById.get(ingredient.foodId)?.name ?? "Unbekannt"} {describeIngredientAmount(ingredient)} (~{Math.round(ingredient.amountG)} g)
))}
); })} {!recipes.length && !recipesQuery.isLoading ? (

Noch keine Rezepte gespeichert.

) : null}