Implement progressive enhancement for recipe multiplier with form fallbacks
Add form-based multiplier controls that work without JavaScript while providing enhanced UX when JS is available. Fixed fraction display and NaN flash issues. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		@@ -1,21 +1,70 @@
 | 
				
			|||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { onMount } from 'svelte';
 | 
					import { onMount } from 'svelte';
 | 
				
			||||||
import { onNavigate } from "$app/navigation";
 | 
					import { onNavigate } from "$app/navigation";
 | 
				
			||||||
 | 
					import { browser } from '$app/environment';
 | 
				
			||||||
import HefeSwapper from './HefeSwapper.svelte';
 | 
					import HefeSwapper from './HefeSwapper.svelte';
 | 
				
			||||||
export let data
 | 
					export let data
 | 
				
			||||||
let multiplier;
 | 
					let multiplier = data.multiplier || 1;
 | 
				
			||||||
let custom_mul = "…"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Progressive enhancement - use JS if available
 | 
				
			||||||
onMount(() => {
 | 
					onMount(() => {
 | 
				
			||||||
	// Apply multiplier from URL
 | 
						if (browser) {
 | 
				
			||||||
	const urlParams = new URLSearchParams(window.location.search);
 | 
							const urlParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
	multiplier = urlParams.get('multiplier') || 1;
 | 
							multiplier = parseFloat(urlParams.get('multiplier')) || 1;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onNavigate(() => {
 | 
					onNavigate(() => {
 | 
				
			||||||
	const urlParams = new URLSearchParams(window.location.search);
 | 
						if (browser) {
 | 
				
			||||||
	multiplier = urlParams.get('multiplier') || 1;
 | 
							const urlParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
 | 
							multiplier = parseFloat(urlParams.get('multiplier')) || 1;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// If no JS, form will submit normally
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function handleCustomInput(event) {
 | 
				
			||||||
 | 
						if (browser) {
 | 
				
			||||||
 | 
							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);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function handleCustomSubmit(event) {
 | 
				
			||||||
 | 
						if (browser) {
 | 
				
			||||||
 | 
							event.preventDefault();
 | 
				
			||||||
 | 
							// Value already updated by handleCustomInput
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// If no JS, form will submit normally
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function convertFloatsToFractions(inputString) {
 | 
					function convertFloatsToFractions(inputString) {
 | 
				
			||||||
  // Split the input string into individual words
 | 
					  // Split the input string into individual words
 | 
				
			||||||
  const words = inputString.split(' ');
 | 
					  const words = inputString.split(' ');
 | 
				
			||||||
@@ -104,22 +153,6 @@ function adjust_amount(string, multiplier){
 | 
				
			|||||||
	return temp
 | 
						return temp
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function apply_if_not_NaN(custom){
 | 
					 | 
				
			||||||
	const multipliers = [0.5, 1, 1.5, 2, 3]
 | 
					 | 
				
			||||||
	if((!isNaN(custom * 1)) && custom != ""){
 | 
					 | 
				
			||||||
		if(multipliers.includes(parseFloat(custom))){
 | 
					 | 
				
			||||||
			multiplier = custom
 | 
					 | 
				
			||||||
			custom_mul = "…"
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		else{
 | 
					 | 
				
			||||||
			custom_mul = convertFloatsToFractions(custom)
 | 
					 | 
				
			||||||
			multiplier = custom
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	else{
 | 
					 | 
				
			||||||
		custom_mul = "…"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function handleHefeToggle(event, item) {
 | 
					function handleHefeToggle(event, item) {
 | 
				
			||||||
	item.name = event.detail.name;
 | 
						item.name = event.detail.name;
 | 
				
			||||||
@@ -202,9 +235,67 @@ span
 | 
				
			|||||||
	padding: 0;
 | 
						padding: 0;
 | 
				
			||||||
	margin: 0;
 | 
						margin: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.multipliers button:last-child{
 | 
					.custom-multiplier {
 | 
				
			||||||
	display: flex;
 | 
						display: flex;
 | 
				
			||||||
	align-items: center;
 | 
						align-items: center;
 | 
				
			||||||
 | 
						min-width: 2em;
 | 
				
			||||||
 | 
						font-size: 1.1rem;
 | 
				
			||||||
 | 
						border-radius: 0.3rem;
 | 
				
			||||||
 | 
						border: none;
 | 
				
			||||||
 | 
						cursor: pointer;
 | 
				
			||||||
 | 
						transition: 100ms;
 | 
				
			||||||
 | 
						color: var(--nord0);
 | 
				
			||||||
 | 
						background-color: var(--nord5);
 | 
				
			||||||
 | 
						box-shadow: 0px 0px 0.4em 0.05em rgba(0,0,0, 0.2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.custom-input {
 | 
				
			||||||
 | 
						width: 3em;
 | 
				
			||||||
 | 
						padding: 0;
 | 
				
			||||||
 | 
						margin: 0;
 | 
				
			||||||
 | 
						border: none;
 | 
				
			||||||
 | 
						background: transparent;
 | 
				
			||||||
 | 
						text-align: center;
 | 
				
			||||||
 | 
						color: inherit;
 | 
				
			||||||
 | 
						font-size: inherit;
 | 
				
			||||||
 | 
						outline: none;
 | 
				
			||||||
 | 
						box-shadow: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Remove number input arrows */
 | 
				
			||||||
 | 
					.custom-input::-webkit-outer-spin-button,
 | 
				
			||||||
 | 
					.custom-input::-webkit-inner-spin-button {
 | 
				
			||||||
 | 
						-webkit-appearance: none;
 | 
				
			||||||
 | 
						margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.custom-input[type=number] {
 | 
				
			||||||
 | 
						-moz-appearance: textfield;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.custom-button {
 | 
				
			||||||
 | 
						padding: 0;
 | 
				
			||||||
 | 
						margin: 0;
 | 
				
			||||||
 | 
						border: none;
 | 
				
			||||||
 | 
						background: transparent;
 | 
				
			||||||
 | 
						color: inherit;
 | 
				
			||||||
 | 
						font-size: inherit;
 | 
				
			||||||
 | 
						cursor: pointer;
 | 
				
			||||||
 | 
						box-shadow: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (prefers-color-scheme: dark){
 | 
				
			||||||
 | 
						.custom-multiplier {
 | 
				
			||||||
 | 
							color: var(--tag-font);
 | 
				
			||||||
 | 
							background-color: var(--nord6-dark);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.custom-multiplier:hover,
 | 
				
			||||||
 | 
					.custom-multiplier:focus-within {
 | 
				
			||||||
 | 
						scale: 1.2;
 | 
				
			||||||
 | 
						background-color: var(--orange);
 | 
				
			||||||
 | 
						box-shadow: 0px 0px 0.5em 0.1em rgba(0,0,0, 0.3);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
{#if data.ingredients}
 | 
					{#if data.ingredients}
 | 
				
			||||||
@@ -216,23 +307,39 @@ span
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<h3>Menge anpassen:</h3>
 | 
					<h3>Menge anpassen:</h3>
 | 
				
			||||||
<div class=multipliers>
 | 
					<div class=multipliers>
 | 
				
			||||||
	<button class:selected={multiplier==0.5} on:click={() => multiplier=0.5}><sup>1</sup>⁄<sub>2</sub>x</button>
 | 
						<form method="get" style="display: inline;">
 | 
				
			||||||
	<button class:selected={multiplier==1} on:click={() => {multiplier=1; custom_mul="…"}}>1x</button>
 | 
							<input type="hidden" name="multiplier" value="0.5" />
 | 
				
			||||||
	<button class:selected={multiplier==1.5} on:click={() => {multiplier=1.5; custom_mul="…"}}><sup>3</sup>⁄<sub>2</sub>x</button>
 | 
							<button type="submit" class:selected={multiplier==0.5} on:click={(e) => handleMultiplierClick(e, 0.5)}>{@html "<sup>1</sup>/<sub>2</sub>x"}</button>
 | 
				
			||||||
	<button class:selected={multiplier==2} on:click="{() => {multiplier=2; custom_mul="…"}}">2x</button>
 | 
						</form>
 | 
				
			||||||
	<button class:selected={multiplier==3} on:click="{() => {multiplier=3; custom_mul="…"}}">3x</button>
 | 
						<form method="get" style="display: inline;">
 | 
				
			||||||
	<button class:selected={multiplier==custom_mul} on:click={(e) => { const el = e.composedPath()[0].children[0]; if(el){ el.focus()}}}>
 | 
							<input type="hidden" name="multiplier" value="1" />
 | 
				
			||||||
		<span class:selected={multiplier==custom_mul}
 | 
							<button type="submit" class:selected={multiplier==1} on:click={(e) => handleMultiplierClick(e, 1)}>1x</button>
 | 
				
			||||||
			on:focus={() => { custom_mul="" }
 | 
						</form>
 | 
				
			||||||
				}
 | 
						<form method="get" style="display: inline;">
 | 
				
			||||||
			on:blur="{() => { apply_if_not_NaN(custom_mul);
 | 
							<input type="hidden" name="multiplier" value="1.5" />
 | 
				
			||||||
					if(custom_mul == "")
 | 
							<button type="submit" class:selected={multiplier==1.5} on:click={(e) => handleMultiplierClick(e, 1.5)}>{@html "<sup>3</sup>/<sub>2</sub>x"}</button>
 | 
				
			||||||
						{custom_mul = "…"}
 | 
						</form>
 | 
				
			||||||
						}}"
 | 
						<form method="get" style="display: inline;">
 | 
				
			||||||
					bind:innerHTML={custom_mul}
 | 
							<input type="hidden" name="multiplier" value="2" />
 | 
				
			||||||
					contenteditable > </span>
 | 
							<button type="submit" class:selected={multiplier==2} on:click={(e) => handleMultiplierClick(e, 2)}>2x</button>
 | 
				
			||||||
		x
 | 
						</form>
 | 
				
			||||||
	</button>
 | 
						<form method="get" style="display: inline;">
 | 
				
			||||||
 | 
							<input type="hidden" name="multiplier" value="3" />
 | 
				
			||||||
 | 
							<button type="submit" class:selected={multiplier==3} on:click={(e) => handleMultiplierClick(e, 3)}>3x</button>
 | 
				
			||||||
 | 
						</form>
 | 
				
			||||||
 | 
						<form method="get" style="display: inline;" class="custom-multiplier" on:submit={handleCustomSubmit}>
 | 
				
			||||||
 | 
							<input 
 | 
				
			||||||
 | 
								type="text" 
 | 
				
			||||||
 | 
								name="multiplier" 
 | 
				
			||||||
 | 
								pattern="[0-9]+(\.[0-9]*)?" 
 | 
				
			||||||
 | 
								title="Enter a positive number (e.g., 2.5, 0.75, 3.14)" 
 | 
				
			||||||
 | 
								placeholder="…" 
 | 
				
			||||||
 | 
								class="custom-input"
 | 
				
			||||||
 | 
								value={multiplier != 0.5 && multiplier != 1 && multiplier != 1.5 && multiplier != 2 && multiplier != 3 ? multiplier : ''}
 | 
				
			||||||
 | 
								on:input={handleCustomInput}
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
							<button type="submit" class="custom-button">x</button>
 | 
				
			||||||
 | 
						</form>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h2>Zutaten</h2>
 | 
					<h2>Zutaten</h2>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import { error } from "@sveltejs/kit";
 | 
					import { error } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function load({ fetch, params}) {
 | 
					export async function load({ fetch, params, url}) {
 | 
				
			||||||
    const res = await fetch(`/api/rezepte/items/${params.name}`);
 | 
					    const res = await fetch(`/api/rezepte/items/${params.name}`);
 | 
				
			||||||
    let item = await res.json();
 | 
					    let item = await res.json();
 | 
				
			||||||
    if(!res.ok){
 | 
					    if(!res.ok){
 | 
				
			||||||
@@ -19,8 +19,12 @@ export async function load({ fetch, params}) {
 | 
				
			|||||||
        // Silently fail if not authenticated or other error
 | 
					        // Silently fail if not authenticated or other error
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // Get multiplier from URL parameters
 | 
				
			||||||
 | 
					    const multiplier = parseFloat(url.searchParams.get('multiplier') || '1');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        ...item,
 | 
					        ...item,
 | 
				
			||||||
        isFavorite
 | 
					        isFavorite,
 | 
				
			||||||
 | 
					        multiplier
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user