diff --git a/src/lib/components/recipes/IngredientsPage.svelte b/src/lib/components/recipes/IngredientsPage.svelte index cb5456c..67b8322 100644 --- a/src/lib/components/recipes/IngredientsPage.svelte +++ b/src/lib/components/recipes/IngredientsPage.svelte @@ -120,9 +120,71 @@ const isEnglish = $derived(data.lang === 'en'); const labels = $derived({ portions: isEnglish ? 'Portions:' : 'Portionen:', adjustAmount: isEnglish ? 'Adjust Amount:' : 'Menge anpassen:', - ingredients: isEnglish ? 'Ingredients' : 'Zutaten' + ingredients: isEnglish ? 'Ingredients' : 'Zutaten', + cakeForm: isEnglish ? 'Cake form:' : 'Backform:', + round: isEnglish ? 'Round' : 'Rund', + rectangular: isEnglish ? 'Rectangular' : 'Rechteckig', + diameter: isEnglish ? 'Diameter' : 'Durchmesser', + width: isEnglish ? 'Width' : 'Breite', + length: isEnglish ? 'Length' : 'Länge', + factor: isEnglish ? 'Factor' : 'Faktor', }); +// Cake form scaling +const hasDefaultForm = $derived(!!data.defaultForm?.shape); +let userFormShape = $state(data.defaultForm?.shape || 'round'); +let userFormDiameter = $state(data.defaultForm?.diameter || 26); +let userFormWidth = $state(data.defaultForm?.width || 20); +let userFormLength = $state(data.defaultForm?.length || 30); +let userFormInnerDiameter = $state(data.defaultForm?.innerDiameter || 8); + +function calcArea(shape, diameter, width, length, innerDiameter) { + if (shape === 'round') return Math.PI * (diameter / 2) ** 2; + if (shape === 'gugelhupf') return Math.PI * ((diameter / 2) ** 2 - (innerDiameter / 2) ** 2); + return width * length; +} + +const defaultFormArea = $derived( + hasDefaultForm + ? calcArea(data.defaultForm.shape, data.defaultForm.diameter, data.defaultForm.width, data.defaultForm.length, data.defaultForm.innerDiameter) + : 1 +); + +const userFormArea = $derived( + calcArea(userFormShape, userFormDiameter, userFormWidth, userFormLength, userFormInnerDiameter) +); + +const formMultiplier = $derived( + hasDefaultForm && defaultFormArea > 0 ? userFormArea / defaultFormArea : 1 +); + +// Track whether multiplier is driven by form or manual buttons +let formDriven = $state(false); + +function applyFormMultiplier() { + formDriven = true; +} + +// Reactively update multiplier when form dimensions change and form is driving +$effect(() => { + if (formDriven) { + multiplier = formMultiplier; + updateUrl(multiplier); + } +}); + +function updateUrl(value) { + if (browser) { + const url = new URL(window.location); + if (value === 1) { + url.searchParams.delete('multiplier'); + } else { + url.searchParams.set('multiplier', String(value)); + } + window.history.replaceState({}, '', url); + } +} + // Multiplier button options const multiplierOptions = [ { value: 0.5, label: '1/2x' }, @@ -176,15 +238,8 @@ function handleMultiplierClick(event, value) { if (browser) { event.preventDefault(); multiplier = value; - - // Update URL without reloading - const url = new URL(window.location); - if (value === 1) { - url.searchParams.delete('multiplier'); - } else { - url.searchParams.set('multiplier', value); - } - window.history.replaceState({}, '', url); + formDriven = false; + updateUrl(value); } // If no JS, form will submit normally } @@ -194,15 +249,8 @@ function handleCustomInput(event) { const value = parseFloat(event.target.value); if (!isNaN(value) && value > 0) { multiplier = value; - - // Update URL without reloading - const url = new URL(window.location); - if (value === 1) { - url.searchParams.delete('multiplier'); - } else { - url.searchParams.set('multiplier', value); - } - window.history.replaceState({}, '', url); + formDriven = false; + updateUrl(value); } } } @@ -386,6 +434,53 @@ function adjust_amount(string, multiplier){ box-shadow: none; } +.cake-form { + margin-block: 1rem; +} +.cake-form-shape { + display: flex; + gap: 0.75rem; + justify-content: center; + margin-bottom: 0.5rem; +} +.cake-form-shape label { + cursor: pointer; + padding: 0.25em 0.6em; + border-radius: var(--radius-sm); + transition: var(--transition-fast); +} +.cake-form-shape input[type="radio"] { + display: none; +} +.cake-form-selected { + background-color: var(--nord9); + color: white; + font-weight: bold; +} +.cake-form-inputs { + display: flex; + gap: 1rem; + justify-content: center; + align-items: center; + flex-wrap: wrap; +} +.cake-form-num { + width: 3.5em; + padding: 0.2em 0.4em; + border: 1px solid var(--nord4); + border-radius: var(--radius-sm); + text-align: center; + font-size: inherit; + background: transparent; + color: inherit; +} +.cake-form-factor { + text-align: center; + margin-top: 0.4rem; + font-weight: bold; + color: var(--nord10); +} + {#if data.ingredients}
@@ -419,6 +514,42 @@ function adjust_amount(string, multiplier){ +{#if hasDefaultForm} +
+

{labels.cakeForm}

+
+ + + {#if data.defaultForm?.shape === 'gugelhupf'} + + {/if} +
+
+ {#if userFormShape === 'round'} + + {:else if userFormShape === 'rectangular'} + + + {:else if userFormShape === 'gugelhupf'} + + + {/if} +
+ {#if formDriven} +
→ {labels.factor}: {formMultiplier.toFixed(2)}x
+ {/if} +
+{/if} +

{labels.ingredients}

{#each flattenedIngredients as list, listIndex} {#if list.name} diff --git a/src/models/Recipe.ts b/src/models/Recipe.ts index 82d29cc..7222658 100644 --- a/src/models/Recipe.ts +++ b/src/models/Recipe.ts @@ -28,6 +28,13 @@ const RecipeSchema = new mongoose.Schema( }, portions :{type:String, default: ""}, + defaultForm: { + shape: { type: String, enum: ['round', 'rectangular', 'gugelhupf'] }, + diameter: { type: Number }, + width: { type: Number }, + length: { type: Number }, + innerDiameter: { type: Number }, + }, cooking: {type:String, default: ""}, total_time : {type:String, default: ""}, ingredients: [{ diff --git a/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte b/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte index f8235f9..932c9ab 100644 --- a/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte +++ b/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte @@ -82,6 +82,7 @@ let datecreated = $state(data.recipe.datecreated); let datemodified = $state(new Date()); let isBaseRecipe = $state(data.recipe.isBaseRecipe || false); + let defaultForm = $state(data.recipe.defaultForm ? { ...data.recipe.defaultForm } : null); let ingredients = $state(data.recipe.ingredients || []); let instructions = $state(data.recipe.instructions || []); @@ -142,6 +143,7 @@ preamble, note, isBaseRecipe, + defaultForm, }; }); @@ -343,6 +345,29 @@ background-color: var(--nord6-dark); } } + .form-size-section { + max-width: 600px; + margin: 1rem auto; + text-align: center; + } + .form-size-controls { + display: flex; + gap: 1.5rem; + justify-content: center; + margin-bottom: 0.5rem; + } + .form-size-inputs { + display: flex; + gap: 1rem; + justify-content: center; + align-items: center; + flex-wrap: wrap; + } + .form-size-inputs input[type="number"] { + width: 4em; + display: inline; + margin: 0 0.3em; + } .error-message { background: var(--nord11); color: var(--nord6); @@ -422,6 +447,7 @@ +
+ +
+

Backform (Standard):

+
+ + + + +
+ {#if defaultForm?.shape === 'round'} +
+ +
+ {:else if defaultForm?.shape === 'rectangular'} +
+ + +
+ {:else if defaultForm?.shape === 'gugelhupf'} +
+ + +
+ {/if} +
+ diff --git a/src/types/types.ts b/src/types/types.ts index 8ceadb2..a63dd84 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -140,6 +140,13 @@ export type RecipeModelType = { final: string; } portions?: string; + defaultForm?: { + shape: 'round' | 'rectangular' | 'gugelhupf'; + diameter?: number; + width?: number; + length?: number; + innerDiameter?: number; + }; cooking?: string; total_time?: string; ingredients?: IngredientItem[]; diff --git a/src/utils/recipeFormHelpers.ts b/src/utils/recipeFormHelpers.ts index 03cf278..3475079 100644 --- a/src/utils/recipeFormHelpers.ts +++ b/src/utils/recipeFormHelpers.ts @@ -48,6 +48,15 @@ export interface RecipeFormData { caption: string; }>; + // Cake form + defaultForm?: { + shape: 'round' | 'rectangular' | 'gugelhupf'; + diameter?: number; + width?: number; + length?: number; + innerDiameter?: number; + }; + // Metadata isBaseRecipe?: boolean; datecreated?: Date; @@ -144,6 +153,20 @@ export function extractRecipeFromFormData(formData: FormData): RecipeFormData { } } + // Default form (cake pan size) + let defaultForm: RecipeFormData['defaultForm'] = undefined; + const defaultFormData = formData.get('defaultForm_json')?.toString(); + if (defaultFormData) { + try { + const parsed = JSON.parse(defaultFormData); + if (parsed && parsed.shape) { + defaultForm = parsed; + } + } catch (error) { + console.error('Failed to parse defaultForm:', error); + } + } + // Metadata const isBaseRecipe = formData.get('isBaseRecipe') === 'true'; const datecreated = formData.get('datecreated') @@ -189,6 +212,7 @@ export function extractRecipeFromFormData(formData: FormData): RecipeFormData { instructions, add_info, images, + defaultForm, isBaseRecipe, datecreated, datemodified, @@ -343,6 +367,7 @@ export function serializeRecipeForDatabase(data: RecipeFormData): any { if (data.preamble) recipe.preamble = data.preamble; if (data.addendum) recipe.addendum = data.addendum; if (data.note) recipe.note = data.note; + if (data.defaultForm) recipe.defaultForm = data.defaultForm; // Additional info if (data.add_info && Object.keys(data.add_info).length > 0) {