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:
2025-09-04 17:34:43 +02:00
parent aeec3b4865
commit 88f9531a6f
2 changed files with 154 additions and 43 deletions

View File

@@ -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>&frasl;<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>&frasl;<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>

View File

@@ -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
}; };
} }