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 ­ 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:
@@ -57,6 +57,7 @@
|
||||
let short_name = ""
|
||||
let datecreated = new Date()
|
||||
let datemodified = datecreated
|
||||
let isBaseRecipe = false
|
||||
|
||||
import type { PageData } from './$types';
|
||||
import CardAdd from '$lib/components/CardAdd.svelte';
|
||||
@@ -118,6 +119,7 @@
|
||||
ingredients,
|
||||
preamble,
|
||||
addendum,
|
||||
isBaseRecipe,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -318,6 +320,13 @@ button.action_button{
|
||||
<h3>Kurzname (für URL):</h3>
|
||||
<input bind:value={short_name} placeholder="Kurzname"/>
|
||||
|
||||
<div style="text-align: center; margin: 1rem;">
|
||||
<label style="font-size: 1.1rem; cursor: pointer;">
|
||||
<input type="checkbox" bind:checked={isBaseRecipe} style="width: auto; display: inline; margin-right: 0.5em;" />
|
||||
Als Basisrezept markieren (kann von anderen Rezepten referenziert werden)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class=title_container>
|
||||
<div class=title>
|
||||
<h4>Eine etwas längere Beschreibung:</h4>
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
let short_name = data.recipe.short_name
|
||||
let datecreated = data.recipe.datecreated
|
||||
let datemodified = new Date()
|
||||
let isBaseRecipe = data.recipe.isBaseRecipe || false
|
||||
|
||||
import type { PageData } from './$types';
|
||||
import CardAdd from '$lib/components/CardAdd.svelte';
|
||||
@@ -117,6 +118,7 @@
|
||||
addendum,
|
||||
preamble,
|
||||
note,
|
||||
isBaseRecipe,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -190,7 +192,25 @@
|
||||
}
|
||||
|
||||
async function doDelete(){
|
||||
const response = confirm("Bist du dir sicher, dass du das Rezept löschen willst?")
|
||||
// Check for references if this is a base recipe
|
||||
const checkRes = await fetch(`/api/rezepte/check-references/${data.recipe._id}`);
|
||||
const checkData = await checkRes.json();
|
||||
|
||||
let response;
|
||||
if (checkData.isReferenced) {
|
||||
const refList = checkData.references
|
||||
.map(r => ` • ${r.name}`)
|
||||
.join('\n');
|
||||
|
||||
response = confirm(
|
||||
`Dieses Rezept wird von folgenden Rezepten referenziert:\n\n${refList}\n\n` +
|
||||
`Die Referenzen werden in regulären Inhalt umgewandelt.\n` +
|
||||
`Möchtest du fortfahren?`
|
||||
);
|
||||
} else {
|
||||
response = confirm("Bist du dir sicher, dass du das Rezept löschen willst?");
|
||||
}
|
||||
|
||||
if(!response){
|
||||
return
|
||||
}
|
||||
@@ -454,6 +474,42 @@ button.action_button{
|
||||
<h3>Kurzname (für URL):</h3>
|
||||
<input bind:value={short_name} placeholder="Kurzname"/>
|
||||
|
||||
<div style="text-align: center; margin: 1rem;">
|
||||
<label style="font-size: 1.1rem; cursor: pointer;">
|
||||
<input type="checkbox" bind:checked={isBaseRecipe} style="width: auto; display: inline; margin-right: 0.5em;" />
|
||||
Als Basisrezept markieren (kann von anderen Rezepten referenziert werden)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if isBaseRecipe}
|
||||
<div style="background-color: var(--nord14); padding: 1.5rem; margin: 1rem auto; max-width: 600px; border-radius: 10px; border: 2px solid var(--nord9);">
|
||||
<h3 style="margin-top: 0; color: var(--nord0);">📋 Basisrezept-Informationen</h3>
|
||||
{#await fetch(`/api/rezepte/check-references/${data.recipe._id}`).then(r => r.json())}
|
||||
<p style="color: var(--nord3);">Lade Referenzen...</p>
|
||||
{:then refData}
|
||||
{#if refData.isReferenced}
|
||||
<h4 style="color: var(--nord0);">Wird referenziert von:</h4>
|
||||
<ul style="color: var(--nord1); list-style-position: inside;">
|
||||
{#each refData.references as ref}
|
||||
<li>
|
||||
<a href="/{data.lang}/edit/{ref.short_name}" style="color: var(--nord10); font-weight: bold; text-decoration: underline;">
|
||||
{ref.name}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p style="color: var(--nord11); font-weight: bold; margin-top: 1rem;">
|
||||
⚠️ Änderungen an diesem Basisrezept wirken sich auf alle referenzierenden Rezepte aus.
|
||||
</p>
|
||||
{:else}
|
||||
<p style="color: var(--nord3);">Dieses Basisrezept wird noch nicht referenziert.</p>
|
||||
{/if}
|
||||
{:catch error}
|
||||
<p style="color: var(--nord11);">Fehler beim Laden der Referenzen.</p>
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=title_container>
|
||||
<div class=title>
|
||||
<h4>Eine etwas längere Beschreibung:</h4>
|
||||
|
||||
16
src/routes/api/rezepte/base-recipes/+server.ts
Normal file
16
src/routes/api/rezepte/base-recipes/+server.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { Recipe } from '../../../../models/Recipe';
|
||||
import { dbConnect } from '../../../../utils/db';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
// GET: List all base recipes for selector UI
|
||||
export const GET: RequestHandler = async () => {
|
||||
await dbConnect();
|
||||
|
||||
const baseRecipes = await Recipe.find({ isBaseRecipe: true })
|
||||
.select('_id short_name name category icon')
|
||||
.sort({ name: 1 })
|
||||
.lean();
|
||||
|
||||
return json(baseRecipes);
|
||||
};
|
||||
21
src/routes/api/rezepte/check-references/[id]/+server.ts
Normal file
21
src/routes/api/rezepte/check-references/[id]/+server.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { Recipe } from '../../../../../models/Recipe';
|
||||
import { dbConnect } from '../../../../../utils/db';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
// GET: Check which recipes reference this recipe
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
await dbConnect();
|
||||
|
||||
const referencingRecipes = await Recipe.find({
|
||||
$or: [
|
||||
{ 'ingredients.baseRecipeRef': params.id },
|
||||
{ 'instructions.baseRecipeRef': params.id }
|
||||
]
|
||||
}).select('short_name name').lean();
|
||||
|
||||
return json({
|
||||
isReferenced: referencingRecipes.length > 0,
|
||||
references: referencingRecipes
|
||||
});
|
||||
};
|
||||
@@ -21,6 +21,45 @@ export const POST: RequestHandler = async ({request, locals}) => {
|
||||
throw error(404, "Recipe not found");
|
||||
}
|
||||
|
||||
// Check if this recipe is referenced by others
|
||||
const referencingRecipes = await Recipe.find({
|
||||
$or: [
|
||||
{ 'ingredients.baseRecipeRef': recipe._id },
|
||||
{ 'instructions.baseRecipeRef': recipe._id }
|
||||
]
|
||||
});
|
||||
|
||||
// Expand all references into regular content before deletion
|
||||
for (const depRecipe of referencingRecipes) {
|
||||
// Expand ingredient references
|
||||
if (depRecipe.ingredients) {
|
||||
depRecipe.ingredients = depRecipe.ingredients.flatMap((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef && item.baseRecipeRef.equals(recipe._id)) {
|
||||
if (item.includeIngredients && recipe.ingredients) {
|
||||
return recipe.ingredients.filter((i: any) => i.type === 'section' || !i.type);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
return [item];
|
||||
});
|
||||
}
|
||||
|
||||
// Expand instruction references
|
||||
if (depRecipe.instructions) {
|
||||
depRecipe.instructions = depRecipe.instructions.flatMap((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef && item.baseRecipeRef.equals(recipe._id)) {
|
||||
if (item.includeInstructions && recipe.instructions) {
|
||||
return recipe.instructions.filter((i: any) => i.type === 'section' || !i.type);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
return [item];
|
||||
});
|
||||
}
|
||||
|
||||
await depRecipe.save();
|
||||
}
|
||||
|
||||
// Remove this recipe from all users' favorites
|
||||
await UserFavorites.updateMany(
|
||||
{ favorites: recipe._id },
|
||||
|
||||
@@ -6,11 +6,40 @@ import { error } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async ({params}) => {
|
||||
await dbConnect();
|
||||
let recipe = (await Recipe.findOne({ short_name: params.name}).lean()) as RecipeModelType[];
|
||||
let recipe = await Recipe.findOne({ short_name: params.name})
|
||||
.populate({
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients translations'
|
||||
})
|
||||
.populate({
|
||||
path: 'instructions.baseRecipeRef',
|
||||
select: 'short_name name instructions translations'
|
||||
})
|
||||
.lean() as RecipeModelType[];
|
||||
|
||||
recipe = JSON.parse(JSON.stringify(recipe));
|
||||
if(recipe == null){
|
||||
throw error(404, "Recipe not found")
|
||||
}
|
||||
|
||||
// Map populated refs to resolvedRecipe field
|
||||
if (recipe?.ingredients) {
|
||||
recipe.ingredients = recipe.ingredients.map((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||
return { ...item, resolvedRecipe: item.baseRecipeRef };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
if (recipe?.instructions) {
|
||||
recipe.instructions = recipe.instructions.map((item: any) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||
return { ...item, resolvedRecipe: item.baseRecipeRef };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return json(recipe);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user