import type { Recipe, RecipeData } from "@/app/types"; import { CATALOG_BY_ID } from "../catalog"; import { toBaseUnit } from "../units"; import { BREAKFASTS } from "./breakfasts"; import { CAYDENS_CORNER_DESSERTS, CAYDENS_CORNER_MAINS, CAYDENS_CORNER_SIDES } from "./caydens-corner"; import { EXTRA_AMERICAN, EXTRA_SOUTHERN } from "./extra/americas-north"; import { EXTRA_CARIBBEAN, EXTRA_MEXICAN } from "./extra/americas-south"; import { EXTRA_CHINESE, EXTRA_INDIAN, EXTRA_JAPANESE, EXTRA_KOREAN, EXTRA_THAI } from "./extra/asia"; import { EXTRA_BREAKFASTS } from "./extra/breakfasts"; import { EXTRA_ITALIAN, EXTRA_MEDITERRANEAN, EXTRA_MIDDLE_EASTERN } from "./extra/mediterranean"; import { EXTRA_SEAFOOD } from "./extra/seafood"; import { MAINS_AMERICAS } from "./mains-americas"; import { MAINS_ASIA } from "./mains-asia"; import { MAINS_MEDITERRANEAN } from "./mains-mediterranean"; import { SEAFOOD } from "./seafood"; import { CAYDENS_CORNER_STEPS } from "./steps/caydens-corner"; import { CORE_STEPS } from "./steps/core"; import { EXTRA_AMERICAS_STEPS } from "./steps/extra-americas"; import { EXTRA_ASIA_STEPS } from "./steps/extra-asia"; import { EXTRA_BREAKFAST_STEPS } from "./steps/extra-breakfasts"; import { EXTRA_MEDITERRANEAN_STEPS } from "./steps/extra-mediterranean"; import { EXTRA_SEAFOOD_STEPS } from "./steps/extra-seafood"; // --------------------------------------------------------------------------- // Recipe database. // // ./*.ts the original recipes, written as full objects // ./extra/*.ts the big library, written in the compact `dishes()` format // ./caydens-corner.ts family recipes // ./steps/*.ts cooking instructions for all of the above, keyed by recipe id // // Ingredients deliberately overlap (rice, chicken, onion, garlic, soy sauce, // canned tomatoes, tortillas…) so the planner has real overlap to exploit. // Amounts are for `servings` people and get scaled to household size. // --------------------------------------------------------------------------- const CORE: RecipeData[] = [...BREAKFASTS, ...MAINS_AMERICAS, ...MAINS_MEDITERRANEAN, ...MAINS_ASIA, ...SEAFOOD]; const EXTRA: RecipeData[] = [ ...EXTRA_BREAKFASTS, ...EXTRA_AMERICAN, ...EXTRA_SOUTHERN, ...EXTRA_MEXICAN, ...EXTRA_CARIBBEAN, ...EXTRA_ITALIAN, ...EXTRA_MEDITERRANEAN, ...EXTRA_MIDDLE_EASTERN, ...EXTRA_INDIAN, ...EXTRA_CHINESE, ...EXTRA_JAPANESE, ...EXTRA_KOREAN, ...EXTRA_THAI, ...EXTRA_SEAFOOD, ]; const STEPS: Record = { ...CORE_STEPS, ...EXTRA_BREAKFAST_STEPS, ...EXTRA_AMERICAS_STEPS, ...EXTRA_MEDITERRANEAN_STEPS, ...EXTRA_ASIA_STEPS, ...EXTRA_SEAFOOD_STEPS, ...CAYDENS_CORNER_STEPS, }; /** Cayden's Corner: family recipes. */ const FAMILY: RecipeData[] = [...CAYDENS_CORNER_SIDES, ...CAYDENS_CORNER_MAINS, ...CAYDENS_CORNER_DESSERTS]; const ALL: RecipeData[] = [...CORE, ...EXTRA, ...FAMILY]; // --- Load-time checks --------------------------------------------------------- // A typo in recipe data should crash the app on startup (and fail `next build`), // not quietly drop an ingredient from someone's grocery bill. // 1. Every recipe must have instructions, and every instruction set must belong to a recipe. const missingSteps = ALL.filter((r) => !STEPS[r.id]?.length).map((r) => r.id); const recipeIds = new Set(ALL.map((r) => r.id)); const orphanSteps = Object.keys(STEPS).filter((id) => !recipeIds.has(id)); if (missingSteps.length || orphanSteps.length) { throw new Error(`Recipe steps out of sync. Missing: [${missingSteps.join(", ")}] Orphaned: [${orphanSteps.join(", ")}]`); } // 2. Every ingredient must be a real store item, in a unit that converts to how it's sold. const badIngredients: string[] = []; for (const recipe of ALL) { for (const { itemId, amount, unit } of recipe.ingredients) { const item = CATALOG_BY_ID.get(itemId); if (!item) { badIngredients.push(`${recipe.id}: unknown item "${itemId}"`); continue; } try { toBaseUnit(amount, unit, item); } catch (error) { badIngredients.push(`${recipe.id}: ${(error as Error).message}`); } } } if (badIngredients.length) { throw new Error(`Bad recipe ingredients:\n ${badIngredients.join("\n ")}`); } export const RECIPES: Recipe[] = ALL.map((r) => ({ ...r, steps: STEPS[r.id] })); export const RECIPES_BY_ID: ReadonlyMap = new Map(RECIPES.map((r) => [r.id, r])); // 3. Duplicate ids would silently shadow each other in RECIPES_BY_ID; fail loudly instead. if (RECIPES_BY_ID.size !== RECIPES.length) { const seen = new Set(); const dupes = RECIPES.filter((r) => (seen.has(r.id) ? true : (seen.add(r.id), false))).map((r) => r.id); throw new Error(`Duplicate recipe ids: ${dupes.join(", ")}`); }