327aa6824b
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 ­ 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
167 lines
5.5 KiB
TypeScript
167 lines
5.5 KiB
TypeScript
import mongoose from 'mongoose';
|
|
|
|
const RecipeSchema = new mongoose.Schema(
|
|
{
|
|
short_name: {type: String, required: true, unique: true},
|
|
name : {type: String, required: true,},
|
|
category : {type: String, required: true,},
|
|
icon: {type: String, required: true},
|
|
dateCreated: {type: Date, default: Date.now},
|
|
dateModified: {type: Date, default: Date.now},
|
|
images: [ {
|
|
mediapath: {type: String, required: true}, // filename with hash for cache busting: e.g., "maccaroni.a1b2c3d4.webp"
|
|
alt: String,
|
|
caption: String,
|
|
}],
|
|
description: {type: String, required: true},
|
|
note: {type: String},
|
|
tags : [String],
|
|
season : [Number],
|
|
baking: { temperature: {type:String, default: ""},
|
|
length: {type:String, default: ""},
|
|
mode: {type:String, default: ""},
|
|
},
|
|
preparation : {type:String, default: ""},
|
|
fermentation: { bulk: {type:String, default: ""},
|
|
final: {type:String, default: ""},
|
|
|
|
},
|
|
portions :{type:String, default: ""},
|
|
cooking: {type:String, default: ""},
|
|
total_time : {type:String, default: ""},
|
|
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: [{
|
|
// 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: {
|
|
short_name: {type: String}, // English slug for URLs
|
|
name: {type: String},
|
|
description: {type: String},
|
|
preamble: {type: String},
|
|
addendum: {type: String},
|
|
note: {type: String},
|
|
category: {type: String},
|
|
tags: [String],
|
|
portions: {type: String},
|
|
preparation: {type: String},
|
|
cooking: {type: String},
|
|
total_time: {type: String},
|
|
baking: {
|
|
temperature: {type: String},
|
|
length: {type: String},
|
|
mode: {type: String},
|
|
},
|
|
fermentation: {
|
|
bulk: {type: String},
|
|
final: {type: String},
|
|
},
|
|
ingredients: [{
|
|
name: { type: String, default: "" },
|
|
type: { type: String, enum: ['section', 'reference'], default: 'section' },
|
|
list: [{
|
|
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: "" },
|
|
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,
|
|
caption: String,
|
|
}],
|
|
translationStatus: {
|
|
type: String,
|
|
enum: ['pending', 'approved', 'needs_update'],
|
|
default: 'pending'
|
|
},
|
|
lastTranslated: {type: Date},
|
|
changedFields: [String],
|
|
}
|
|
},
|
|
|
|
// Translation metadata for tracking changes
|
|
translationMetadata: {
|
|
lastModifiedGerman: {type: Date},
|
|
fieldsModifiedSinceTranslation: [String],
|
|
},
|
|
}, {timestamps: true}
|
|
);
|
|
|
|
// Indexes for efficient querying
|
|
RecipeSchema.index({ "translations.en.short_name": 1 });
|
|
RecipeSchema.index({ "translations.en.translationStatus": 1 });
|
|
|
|
export const Recipe = mongoose.model("Recipe", RecipeSchema);
|