feat: implement base recipe references with customizable ingredients and instructions

Add comprehensive base recipe system allowing recipes to reference other recipes dynamically. References can include custom items before/after the base recipe content and render as unified lists.

Features:
- Mark recipes as base recipes with isBaseRecipe flag
- Insert base recipe references at any position in ingredients/instructions
- Add custom items before/after referenced content (itemsBefore/itemsAfter, stepsBefore/stepsAfter)
- Combined rendering displays all items in single unified lists
- Full edit/remove functionality for additional items with modal reuse
- Empty item validation prevents accidental blank entries
- HTML rendering in section titles for proper <wbr> and &shy; support
- Reference links in section headings with multiplier preservation
- Subtle hover effects (2% scale) on add buttons
- Translation support for all reference fields
- Deletion handling expands references before removing base recipes
This commit is contained in:
2026-01-04 15:21:17 +01:00
parent 2696f09653
commit b67e2434b5
14 changed files with 1499 additions and 114 deletions

View File

@@ -7,6 +7,74 @@ export type TranslationMetadata = {
fieldsModifiedSinceTranslation?: string[];
};
// Ingredient discriminated union types
export type IngredientSection = {
name?: string;
type: 'section';
list: [{
name: string;
unit: string;
amount: string;
}];
};
export type IngredientReference = {
name?: string;
type: 'reference';
baseRecipeRef: string; // ObjectId as string
includeIngredients: boolean;
showLabel: boolean;
labelOverride?: string;
itemsBefore?: [{
name: string;
unit: string;
amount: string;
}];
itemsAfter?: [{
name: string;
unit: string;
amount: string;
}];
// Populated after server-side resolution
resolvedRecipe?: {
_id: string;
name: string;
short_name: string;
ingredients?: IngredientSection[];
translations?: any;
};
};
export type IngredientItem = IngredientSection | IngredientReference;
// Instruction discriminated union types
export type InstructionSection = {
name?: string;
type: 'section';
steps: [string];
};
export type InstructionReference = {
name?: string;
type: 'reference';
baseRecipeRef: string; // ObjectId as string
includeInstructions: boolean;
showLabel: boolean;
labelOverride?: string;
stepsBefore?: [string];
stepsAfter?: [string];
// Populated after server-side resolution
resolvedRecipe?: {
_id: string;
name: string;
short_name: string;
instructions?: InstructionSection[];
translations?: any;
};
};
export type InstructionItem = InstructionSection | InstructionReference;
// Translated recipe type (English version)
export type TranslatedRecipeType = {
short_name: string;
@@ -17,18 +85,8 @@ export type TranslatedRecipeType = {
note?: string;
category: string;
tags?: string[];
ingredients?: [{
name?: string;
list: [{
name: string;
unit: string;
amount: string;
}]
}];
instructions?: [{
name?: string;
steps: string[];
}];
ingredients?: IngredientItem[];
instructions?: InstructionItem[];
images?: [{
alt: string;
caption?: string;
@@ -68,20 +126,11 @@ export type RecipeModelType = {
portions?: string;
cooking?: string;
total_time?: string;
ingredients?: [{
name?: string;
list: [{
name: string;
unit: string;
amount: string;
}]
}]
instructions?: [{
name?: string;
steps: [string]
}]
ingredients?: IngredientItem[];
instructions?: InstructionItem[];
preamble?: String
addendum?: string
isBaseRecipe?: boolean;
translations?: {
en?: TranslatedRecipeType;
};