import type { CategoryId, CatalogItem, ExtraItem, FoodCategory, FoodItem, HowToRecipe, LocalizedString, MealTime, NutritionBasis, Recipe, TagType, } from "@/types/domain" import extrasRaw from "@/data/extras.json" import foodsRaw from "@/data/foods.json" import howtosRaw from "@/data/howtos.json" import recipesRaw from "@/data/recipes.json" import stepsRaw from "@/data/steps.json" interface RawFoodItem { id: string name: LocalizedString v: LocalizedString basis?: NutritionBasis kcal?: number proteinG?: number f?: TagType approx?: boolean note?: LocalizedString cautionWhy?: LocalizedString } interface RawFoodCategory { id: CategoryId cls: string title: LocalizedString thumb: LocalizedString items: RawFoodItem[] sub?: { title: LocalizedString items: RawFoodItem[] } } export interface PlateStep { n: number cls: string title: LocalizedString qty: LocalizedString text: LocalizedString } function formatProteinG(n: number, lang: "de" | "en"): string { if (Number.isInteger(n)) return String(n) return lang === "de" ? String(n).replace(".", ",") : String(n) } /** Build display valueInfo from structured macros when present. */ export function formatFoodValueInfo( item: Pick, ): LocalizedString { if (item.kcal != null && item.proteinG != null) { const prefix = item.approx ? "≈" : "" const noteDe = item.note?.de ? ` ${item.note.de}` : "" const noteEn = item.note?.en ? ` ${item.note.en}` : "" return { de: `${prefix}${item.kcal} kcal · ${formatProteinG(item.proteinG, "de")} g${noteDe}`, en: `${prefix}${item.kcal} kcal · ${formatProteinG(item.proteinG, "en")} g${noteEn}`, } } return item.v } function mapItem(cat: CategoryId, item: RawFoodItem): FoodItem { const basis: NutritionBasis = item.basis ?? (cat === "snack" ? "perServing" : item.id === "egg" ? "perUnit" : "per100g") return { id: item.id, cat, name: item.name, valueInfo: formatFoodValueInfo(item), basis, kcal: item.kcal, proteinG: item.proteinG, tag: item.f, cautionWhy: item.cautionWhy, isApprox: item.approx, } } function mapCategory(category: RawFoodCategory): FoodCategory { const items = [ ...category.items.map((item) => mapItem(category.id, item)), ...(category.sub?.items.map((item) => mapItem(category.id, item)) ?? []), ] return { id: category.id, cls: category.cls, title: category.title, thumb: category.thumb, items, } } /** * Sort: protein high→low within preferred basis first, then other bases, * then A–Z. Preferred basis is switchable later (perServing / perUnit views). */ export function sortFoodItems( items: FoodItem[], lang: "de" | "en", preferredBasis: NutritionBasis = "per100g", ): FoodItem[] { function rank(item: FoodItem): number { if (item.proteinG != null && item.basis === preferredBasis) return 0 if (item.proteinG != null) return 1 return 2 } return [...items].sort((a, b) => { const rankDiff = rank(a) - rank(b) if (rankDiff !== 0) return rankDiff if (a.proteinG != null && b.proteinG != null && a.proteinG !== b.proteinG) { return b.proteinG - a.proteinG } return a.name[lang].localeCompare(b.name[lang], lang) }) } export const foodCategories: FoodCategory[] = (foodsRaw as RawFoodCategory[]).map( mapCategory, ) export const foods: FoodItem[] = foodCategories.flatMap((category) => category.items) export const foodById: Record = Object.fromEntries( foods.map((item) => [item.id, item]), ) export const extras: ExtraItem[] = extrasRaw as ExtraItem[] export const extraById: Record = Object.fromEntries( extras.map((item) => [item.id, item]), ) /** Plate suggestions (RECIPES). */ export const recipes: Recipe[] = recipesRaw as Recipe[] /** Prep / how-to instructions — not plate suggestions. */ export const howtos: HowToRecipe[] = howtosRaw as HowToRecipe[] export const plateSteps: PlateStep[] = stepsRaw as PlateStep[] export const MEAL_TIME_ORDER: MealTime[] = [ "breakfast", "lunch", "snack", "dinner", ] export function foodsInCategory(cat: CategoryId): FoodItem[] { return foodCategories.find((category) => category.id === cat)?.items ?? [] } export function foodCategoryById(cat: CategoryId): FoodCategory | undefined { return foodCategories.find((category) => category.id === cat) } export function resolveCatalogId(id: string): CatalogItem | null { const food = foodById[id] if (food) { return { id: food.id, name: food.name, source: "cat", cat: food.cat, valueInfo: food.valueInfo, tag: food.tag, cautionWhy: food.cautionWhy, } } const extra = extraById[id] if (extra) { return { id: extra.id, name: extra.name, source: "extra", } } return null } export function resolveRecipeUses( recipe: Pick | Pick, ): CatalogItem[] { return recipe.uses .map((id) => resolveCatalogId(id)) .filter((item): item is CatalogItem => Boolean(item)) } export function recipesForMealTime(time: MealTime): Recipe[] { return recipes.filter((recipe) => recipe.times.includes(time)) } /** Category color token for ingredient chips / plate segments. */ export function categoryDotColor(cat: CategoryId | undefined): string { switch (cat) { case "protein": case "snack": return "var(--cat-protein)" case "obst": case "gemuese": return "var(--cat-produce)" case "fett": return "var(--cat-fat)" case "kh": return "var(--cat-berry)" default: return "var(--ink-faint)" } } /** Four plate slots for completeness scan (snack → protein). */ export type PlateSlot = "protein" | "produce" | "fat" | "berry" export function plateSlotForCategory(cat: CategoryId): PlateSlot { if (cat === "protein" || cat === "snack") return "protein" if (cat === "obst" || cat === "gemuese") return "produce" if (cat === "fett") return "fat" return "berry" } export const PLATE_SLOT_COLOR: Record = { protein: "var(--cat-protein)", produce: "var(--cat-produce)", fat: "var(--cat-fat)", berry: "var(--cat-berry)", }