181 lines
5.9 KiB
TypeScript
181 lines
5.9 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// 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<Recipe, "steps">;
|
|
|
|
// --- 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>-<meal>" (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<string, string>;
|
|
}
|
|
|
|
// --- 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<string, string>;
|
|
summary: GrocerySummary;
|
|
/** Human-readable notes, e.g. when filters had to be relaxed. */
|
|
notes: string[];
|
|
}
|