init
This commit is contained in:
commit
c19b910c36
80 changed files with 16134 additions and 0 deletions
112
app/lib/recipes/index.ts
Normal file
112
app/lib/recipes/index.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
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<string, string[]> = {
|
||||
...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<string, Recipe> = 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<string>();
|
||||
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(", ")}`);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue