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
250 lines
5.0 KiB
Svelte
250 lines
5.0 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { browser } from '$app/environment';
|
|
import { do_on_key } from '$lib/components/do_on_key.js'
|
|
import Check from '$lib/assets/icons/Check.svelte'
|
|
|
|
export let type: 'ingredients' | 'instructions' = 'ingredients';
|
|
export let onSelect: (recipe: any, options: any) => void;
|
|
export let open = false;
|
|
|
|
// Unique dialog ID based on type to prevent conflicts when both are on the same page
|
|
const dialogId = `base-recipe-selector-modal-${type}`;
|
|
|
|
let baseRecipes: any[] = [];
|
|
let selectedRecipe: any = null;
|
|
let options = {
|
|
includeIngredients: false,
|
|
includeInstructions: false,
|
|
showLabel: true,
|
|
labelOverride: ''
|
|
};
|
|
|
|
// Reset options whenever type or modal state changes
|
|
$: {
|
|
if (open || type) {
|
|
options.includeIngredients = type === 'ingredients';
|
|
options.includeInstructions = type === 'instructions';
|
|
}
|
|
}
|
|
|
|
onMount(async () => {
|
|
const res = await fetch('/api/rezepte/base-recipes');
|
|
baseRecipes = await res.json();
|
|
});
|
|
|
|
function handleInsert() {
|
|
if (selectedRecipe) {
|
|
onSelect(selectedRecipe, options);
|
|
// Reset modal
|
|
selectedRecipe = null;
|
|
options.labelOverride = '';
|
|
options.showLabel = true;
|
|
closeModal();
|
|
}
|
|
}
|
|
|
|
function closeModal() {
|
|
open = false;
|
|
if (browser) {
|
|
const modal = document.querySelector(`#${dialogId}`) as HTMLDialogElement;
|
|
if (modal) {
|
|
modal.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
function openModal() {
|
|
if (browser) {
|
|
const modal = document.querySelector(`#${dialogId}`) as HTMLDialogElement;
|
|
if (modal) {
|
|
modal.showModal();
|
|
}
|
|
}
|
|
}
|
|
|
|
$: if (browser) {
|
|
if (open) {
|
|
setTimeout(openModal, 0);
|
|
} else {
|
|
closeModal();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
dialog {
|
|
box-sizing: content-box;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: transparent;
|
|
border: unset;
|
|
margin: 0;
|
|
transition: 500ms;
|
|
}
|
|
|
|
dialog[open]::backdrop {
|
|
animation: show 200ms ease forwards;
|
|
}
|
|
|
|
@keyframes show {
|
|
from {
|
|
backdrop-filter: blur(0px);
|
|
}
|
|
to {
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
}
|
|
|
|
dialog h2 {
|
|
font-size: 3rem;
|
|
font-family: sans-serif;
|
|
color: white;
|
|
text-align: center;
|
|
margin-top: 30vh;
|
|
margin-top: 30dvh;
|
|
filter: drop-shadow(0 0 0.4em black)
|
|
drop-shadow(0 0 1em black);
|
|
}
|
|
|
|
.selector-content {
|
|
box-sizing: border-box;
|
|
margin-inline: auto;
|
|
margin-top: 2rem;
|
|
max-width: 600px;
|
|
padding: 2rem;
|
|
border-radius: 20px;
|
|
background-color: var(--blue);
|
|
color: white;
|
|
box-shadow: 0 0 1em 0.2em rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.selector-content label {
|
|
display: block;
|
|
margin-block: 1rem;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.selector-content select,
|
|
.selector-content input[type="text"] {
|
|
width: 100%;
|
|
padding: 0.5em 1em;
|
|
margin-top: 0.5em;
|
|
border-radius: 1000px;
|
|
border: 2px solid var(--nord4);
|
|
background-color: white;
|
|
color: var(--nord0);
|
|
font-size: 1rem;
|
|
transition: 100ms;
|
|
}
|
|
|
|
.selector-content select:hover,
|
|
.selector-content select:focus,
|
|
.selector-content input[type="text"]:hover,
|
|
.selector-content input[type="text"]:focus {
|
|
border-color: var(--nord9);
|
|
transform: scale(1.02, 1.02);
|
|
}
|
|
|
|
.selector-content input[type="checkbox"] {
|
|
width: 1.2em;
|
|
height: 1.2em;
|
|
margin-right: 0.5em;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.button-group {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-top: 2rem;
|
|
justify-content: center;
|
|
}
|
|
|
|
.button-group button {
|
|
padding: 0.75em 2em;
|
|
font-size: 1.1rem;
|
|
border-radius: 1000px;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: 200ms;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.button-insert {
|
|
background-color: var(--nord14);
|
|
color: var(--nord0);
|
|
}
|
|
|
|
.button-cancel {
|
|
background-color: var(--nord3);
|
|
color: white;
|
|
}
|
|
|
|
.button-group button:hover {
|
|
transform: scale(1.1, 1.1);
|
|
box-shadow: 0 0 1em 0.3em rgba(0,0,0,0.3);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.selector-content {
|
|
background-color: var(--nord1);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<dialog id={dialogId}>
|
|
<h2>Basisrezept einfügen</h2>
|
|
|
|
<div class="selector-content">
|
|
<label>
|
|
Basisrezept auswählen:
|
|
<select bind:value={selectedRecipe}>
|
|
<option value={null}>-- Auswählen --</option>
|
|
{#each baseRecipes as recipe}
|
|
<option value={recipe}>{recipe.icon} {recipe.name}</option>
|
|
{/each}
|
|
</select>
|
|
</label>
|
|
|
|
{#if type === 'ingredients'}
|
|
<label>
|
|
<input type="checkbox" bind:checked={options.includeIngredients} />
|
|
Zutaten einbeziehen
|
|
</label>
|
|
{/if}
|
|
|
|
{#if type === 'instructions'}
|
|
<label>
|
|
<input type="checkbox" bind:checked={options.includeInstructions} />
|
|
Zubereitungsschritte einbeziehen
|
|
</label>
|
|
{/if}
|
|
|
|
<label>
|
|
<input type="checkbox" bind:checked={options.showLabel} />
|
|
Rezeptname als Überschrift anzeigen
|
|
</label>
|
|
|
|
{#if options.showLabel}
|
|
<label>
|
|
Eigene Überschrift (optional):
|
|
<input
|
|
type="text"
|
|
bind:value={options.labelOverride}
|
|
placeholder={selectedRecipe?.name || 'Überschrift eingeben...'}
|
|
on:keydown={(event) => do_on_key(event, 'Enter', false, handleInsert)}
|
|
/>
|
|
</label>
|
|
{/if}
|
|
|
|
<div class="button-group">
|
|
<button class="button-insert" on:click={handleInsert} disabled={!selectedRecipe}>
|
|
Einfügen
|
|
</button>
|
|
<button class="button-cancel" on:click={closeModal}>
|
|
Abbrechen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</dialog>
|