// --------------------------------------------------------------------------- // Shared types for the whole app (client + server). // Keep this file dependency-free so it can be imported anywhere. // --------------------------------------------------------------------------- export type Appliance = "stovetop" | "oven" | "air_fryer" | "slow_cooker" | "microwave" | "blender" | "rice_cooker"; export type Cuisine = | "american" | "southern" | "mexican" | "caribbean" | "italian" | "mediterranean" | "middle_eastern" | "indian" | "chinese" | "japanese" | "korean" | "thai"; /** "caydens_corner" marks the family recipes collection. */ export type CookingStyle = "comfort" | "quick" | "healthy" | "meal_prep" | "vegetarian" | "seafood" | "caydens_corner"; /** * Breakfast slots only get breakfast recipes; lunch/dinner slots get mains. * Sides and desserts live in the library and can be added to a slot by hand, * but auto-fill never picks them. */ export type MealKind = "breakfast" | "main" | "side" | "dessert"; // --- Units ----------------------------------------------------------------- /** Units recipes are written in (what a human would type). */ export type RecipeUnit = "g" | "oz" | "lb" | "ml" | "tsp" | "tbsp" | "cup" | "each"; /** Units the store catalog is priced in. Every recipe unit converts to one of these. */ export type BaseUnit = "g" | "ml" | "each"; // --- Store catalog --------------------------------------------------------- export type ItemId = string; export type Aisle = "Meat & Eggs" | "Seafood" | "Dairy" | "Produce" | "Frozen" | "Pantry" | "International" | "Spices & Sauces" | "Bakery"; /** One purchasable product, e.g. "Long Grain White Rice, 2 lb bag". */ export interface StoreItem { id: ItemId; name: string; aisle: Aisle; /** How much is in one package, in `baseUnit`. */ packageSize: number; baseUnit: BaseUnit; /** Human label for one package, e.g. "2 lb bag". */ packageLabel: string; /** Price of one package, USD. */ price: number; /** * Lets weight-priced items be measured by volume in recipes * (e.g. rice: 1 cup ≈ 185 g → 0.78 g/ml). Only needed for `g` items. */ gramsPerMl?: number; /** Salt, oil, etc. Marked "have it" by default for new users. */ pantryStaple?: boolean; } // --- Recipes --------------------------------------------------------------- export interface RecipeIngredient { /** Points at a StoreItem. Cost is derived from the catalog, never hard-coded per recipe. */ itemId: ItemId; amount: number; unit: RecipeUnit; /** Optional display note, e.g. "diced". */ note?: string; } export interface Recipe { id: string; title: string; description: string; cuisine: Cuisine; styles: CookingStyle[]; kind: MealKind; /** Minutes, hands-on + cook. */ prepTime: number; /** ALL of these are needed. An empty array means no-cook. */ requiredAppliances: Appliance[]; /** How many people one batch feeds. Ingredients scale to household size. */ servings: number; ingredients: RecipeIngredient[]; /** Cooking instructions, in order. Kept in recipes/steps/ and joined in by recipes/index.ts. */ steps: string[]; /** Optional cook's tip shown under the steps. */ tip?: string; } /** A recipe as written in the data files, before its steps are attached. */ export type RecipeData = Omit; // --- Plan state -------------------------------------------------------------- export interface MealSlot { dayIndex: number; // 0-6 mealNumber: number; // 1-3 recipeId: string | null; } export interface MealPlanState { targetBudget: number; daysCount: number; // 1-7 mealsPerDay: number; // 1-3 householdSize: number; // 1-6 selectedAppliances: Appliance[]; /** Empty = any cuisine. */ preferredCuisines: Cuisine[]; /** Empty = any style. */ preferredStyles: CookingStyle[]; /** Never plan recipes that use these store items. */ avoidedItems: ItemId[]; /** Already in the kitchen: listed on the grocery list but cost $0. */ onHandItems: ItemId[]; /** * Keyed "-" (day 0 = Mon; meal 1/2/3 = Breakfast/Lunch/Dinner) → recipe id. * Only filled slots are stored. Slots outside daysCount/mealsPerDay are kept but hidden. */ slots: Record; } // --- Grocery output ---------------------------------------------------------- export interface GroceryLine { item: StoreItem; /** Total amount all recipes need, in the item's base unit. */ neededAmount: number; /** Whole packages to buy. */ packages: number; /** What you pay at checkout for this line. */ checkoutCost: number; /** Value of what actually gets eaten this week. */ usedCost: number; /** Left over after the week, in base unit (goes to your pantry). */ leftoverAmount: number; /** User marked this item as already in their kitchen. */ onHand: boolean; /** Recipe titles that use this item. Length > 1 means ingredient overlap. */ usedBy: string[]; } export interface GrocerySummary { lines: GroceryLine[]; /** Sum of checkout costs (excludes on-hand lines). */ checkoutTotal: number; /** Value of food actually eaten this plan. */ usedTotal: number; /** checkoutTotal - usedTotal: what you carry into next week. */ carryoverValue: number; /** How many store items are shared by 2+ recipes. */ sharedItemCount: number; } /** Per-ingredient setting in the Ingredients panel. */ export type ItemPreference = "avoid" | "use" | "have"; // --- Server action contract ------------------------------------------------- export type FillMode = "empty" | "all"; export interface GeneratePlanInput { state: MealPlanState; fillMode: FillMode; /** Changing the seed gives a different (but still cost-optimized) plan. */ seed: number; } export interface GeneratePlanResult { slots: Record; summary: GrocerySummary; /** Human-readable notes, e.g. when filters had to be relaxed. */ notes: string[]; }