init
This commit is contained in:
commit
c19b910c36
80 changed files with 16134 additions and 0 deletions
79
app/lib/recipes/dish.ts
Normal file
79
app/lib/recipes/dish.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
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 "<catalog item id> <amount> <unit>". Bad units throw at
|
||||
// module load, so typos fail loudly in dev instead of producing $0 groceries.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export 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<RecipeUnit>(["g", "oz", "lb", "ml", "tsp", "tbsp", "cup", "each"]);
|
||||
|
||||
export 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 } : {}),
|
||||
}));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue