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

@@ -29,17 +29,56 @@ const RecipeSchema = new mongoose.Schema(
portions :{type:String, default: ""},
cooking: {type:String, default: ""},
total_time : {type:String, default: ""},
ingredients : [ { name: {type: String, default: ""},
list: [{name: {type: String, default: ""},
unit: String,
amount: String,
}]
ingredients: [{
// Common fields
name: { type: String, default: "" },
type: { type: String, enum: ['section', 'reference'], default: 'section' },
// For type='section' (existing structure)
list: [{
name: { type: String, default: "" },
unit: String,
amount: String,
}],
// For type='reference' (new base recipe references)
baseRecipeRef: { type: mongoose.Schema.Types.ObjectId, ref: 'Recipe' },
includeIngredients: { type: Boolean, default: true },
showLabel: { type: Boolean, default: true },
labelOverride: { type: String, default: "" },
itemsBefore: [{
name: { type: String, default: "" },
unit: String,
amount: String,
}],
itemsAfter: [{
name: { type: String, default: "" },
unit: String,
amount: String,
}],
}],
instructions : [{name: {type: String, default: ""},
steps: [String]}],
instructions: [{
// Common fields
name: { type: String, default: "" },
type: { type: String, enum: ['section', 'reference'], default: 'section' },
// For type='section' (existing structure)
steps: [String],
// For type='reference' (new base recipe references)
baseRecipeRef: { type: mongoose.Schema.Types.ObjectId, ref: 'Recipe' },
includeInstructions: { type: Boolean, default: true },
showLabel: { type: Boolean, default: true },
labelOverride: { type: String, default: "" },
stepsBefore: [String],
stepsAfter: [String],
}],
preamble : String,
addendum : String,
// Base recipe flag
isBaseRecipe: {type: Boolean, default: false},
// English translations
translations: {
en: {
@@ -65,16 +104,38 @@ const RecipeSchema = new mongoose.Schema(
final: {type: String},
},
ingredients: [{
name: {type: String, default: ""},
name: { type: String, default: "" },
type: { type: String, enum: ['section', 'reference'], default: 'section' },
list: [{
name: {type: String, default: ""},
name: { type: String, default: "" },
unit: String,
amount: String,
}]
}],
baseRecipeRef: { type: mongoose.Schema.Types.ObjectId, ref: 'Recipe' },
includeIngredients: { type: Boolean, default: true },
showLabel: { type: Boolean, default: true },
labelOverride: { type: String, default: "" },
itemsBefore: [{
name: { type: String, default: "" },
unit: String,
amount: String,
}],
itemsAfter: [{
name: { type: String, default: "" },
unit: String,
amount: String,
}],
}],
instructions: [{
name: {type: String, default: ""},
steps: [String]
name: { type: String, default: "" },
type: { type: String, enum: ['section', 'reference'], default: 'section' },
steps: [String],
baseRecipeRef: { type: mongoose.Schema.Types.ObjectId, ref: 'Recipe' },
includeInstructions: { type: Boolean, default: true },
showLabel: { type: Boolean, default: true },
labelOverride: { type: String, default: "" },
stepsBefore: [String],
stepsAfter: [String],
}],
images: [{
alt: String,