import type { Appliance, CookingStyle, Cuisine, MealKind, RecipeData, RecipeIngredient, RecipeUnit } from "@/app/types"; // --------------------------------------------------------------------------- // Compact recipe format, used for the big recipe library in ./extra/. // // Instead of a 12-line object per recipe, each dish is one entry with its // ingredients written like a shopping list: // // { title: "Beef Stroganoff", mins: 30, tools: ["stovetop"], styles: ["comfort"], // about: "Creamy mushroom sauce over egg noodles.", // items: "ground_beef 1 lb; egg_noodles 12 oz; mushrooms 8 oz; sour_cream 0.5 cup" } // // Each ingredient is " ". Bad units throw at // module load, so typos fail loudly in dev instead of producing $0 groceries. // --------------------------------------------------------------------------- interface DishInput { title: string; /** Defaults to a slug of the title. Set it explicitly to keep saved plans stable if you rename. */ id?: string; about: string; mins: number; tools: Appliance[]; styles?: CookingStyle[]; /** Defaults to the file's cuisine/kind (see `dishes`). */ cuisine?: Cuisine; kind?: MealKind; /** People one batch feeds. Default 4. */ serves?: number; /** "item_id amount unit; item_id amount unit; …" */ items: string; /** Optional cook's tip shown under the steps. */ tip?: string; } const UNITS = new Set(["g", "oz", "lb", "ml", "tsp", "tbsp", "cup", "each"]); const slugify = (text: string) => text .normalize("NFD") // "Étouffée" → "Etouffee" + combining accents… .replace(/[\u0300-\u036f]/g, "") // …which this strips .replace(/ı/g, "i") // Turkish dotless i (doesn't decompose) .toLowerCase() .replace(/['’]/g, "") // "Shepherd's" → "shepherds", not "shepherd-s" .replace(/&/g, "and") .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, ""); function parseItems(items: string, title: string): RecipeIngredient[] { return items .split(";") .map((part) => part.trim()) .filter(Boolean) .map((part) => { const [itemId, amountText, unit] = part.split(/\s+/); const amount = Number(amountText); if (!itemId || !Number.isFinite(amount) || amount <= 0 || !UNITS.has(unit as RecipeUnit)) { throw new Error(`Recipe "${title}": can't parse ingredient "${part}" (expected "item_id amount unit")`); } return { itemId, amount, unit: unit as RecipeUnit }; }); } /** Build a list of recipes that share a default cuisine and meal kind. */ export function dishes(defaults: { cuisine: Cuisine; kind?: MealKind }, list: DishInput[]): RecipeData[] { return list.map((d) => ({ id: d.id ?? slugify(d.title), title: d.title, description: d.about, cuisine: d.cuisine ?? defaults.cuisine, styles: d.styles ?? [], kind: d.kind ?? defaults.kind ?? "main", prepTime: d.mins, requiredAppliances: d.tools, servings: d.serves ?? 4, ingredients: parseItems(d.items, d.title), ...(d.tip ? { tip: d.tip } : {}), })); }