diff --git a/web/src/pages/Rezepte.jsx b/web/src/pages/Rezepte.jsx index 3e782ab..a6e5429 100644 --- a/web/src/pages/Rezepte.jsx +++ b/web/src/pages/Rezepte.jsx @@ -1,6 +1,7 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { api } from "../api"; import Icon from "../components/Icon"; +import { Skeleton } from "../components/Skeleton"; import ZuordnungDialog from "../components/ZuordnungDialog"; import { fmt } from "../units"; @@ -41,6 +42,17 @@ export default function Rezepte() { const [laedt, setLaedt] = useState(true); // Welche Zutat gerade von Hand zugeordnet wird (null = keine). const [zuordnen, setZuordnen] = useState(null); + // Beim Drehen an den Portionen bleibt das Rezept stehen und nur die Werte + // werden neu gerechnet. Die Zutatenliste selbst aendert sich dabei nie - + // sie darf deshalb auch nicht verschwinden. + const [rechnet, setRechnet] = useState(false); + // Zaehlt die fertigen Ladevorgaenge und dient als React-Schluessel, damit die + // Einblend-Animation bei jedem neuen Ergebnis erneut anlaeuft. + const [stand, setStand] = useState(0); + // Laufende Nummer der Anfrage: Beim schnellen Klicken am Zahlenfeld koennen + // Antworten in falscher Reihenfolge eintreffen - nur die neueste zaehlt. + const anfrage = useRef(0); + const tippTimer = useRef(null); async function ladeListe(q = "") { setLaedt(true); @@ -52,30 +64,53 @@ export default function Rezepte() { } useEffect(() => { ladeListe(); }, []); - async function oeffne(s, wunschPortionen) { + /** + * Rezept laden. ``behalten`` heisst: Es ist dasselbe Rezept, nur mit anderen + * Portionen - dann bleibt alles stehen und nur die Werte gelten als unklar. + * Ohne das flackerte die ganze Seite bei jedem Druck auf den Zahlenpfeil. + */ + async function oeffne(s, wunschPortionen, { behalten = false } = {}) { setSlug(s); - setRezept(null); + if (behalten) setRechnet(true); + else setRezept(null); + const lauf = ++anfrage.current; try { const daten = await api.recipe(s, wunschPortionen); + if (lauf !== anfrage.current) return; // eine neuere Anfrage laeuft setRezept(daten); + setStand((n) => n + 1); // Beim ersten Öffnen das Feld mit der Rezept-Portionszahl vorbelegen. if (wunschPortionen == null) setPortionen(daten.servings ? String(daten.servings) : ""); setFehler(null); - } catch (err) { setFehler(err.message); } + } catch (err) { + if (lauf === anfrage.current) setFehler(err.message); + } finally { + if (lauf === anfrage.current) setRechnet(false); + } } // Nach einer Zuordnung muss der Abgleich neu laufen: Die Zutat hat jetzt ein // Ziel, einen Bestand und einen Zustand. async function neuRechnen() { - if (slug) await oeffne(slug, Number(String(portionen).replace(",", ".")) || undefined); + if (slug) { + await oeffne(slug, Number(String(portionen).replace(",", ".")) || undefined, + { behalten: true }); + } } + // Das Zahlenfeld reagiert sofort, die Anfrage wartet kurz: Wer von 4 auf 12 + // tippt, soll nicht erst ein Ergebnis fuer 1 bekommen. function setzePortionen(wert) { setPortionen(wert); const zahl = Number(String(wert).replace(",", ".")); - if (slug && Number.isFinite(zahl) && zahl > 0) oeffne(slug, zahl); + if (tippTimer.current) clearTimeout(tippTimer.current); + if (!slug || !Number.isFinite(zahl) || zahl <= 0) return; + setRechnet(true); + tippTimer.current = setTimeout(() => oeffne(slug, zahl, { behalten: true }), 280); } + useEffect(() => () => { if (tippTimer.current) clearTimeout(tippTimer.current); }, []); + const fehlende = (rezept?.ingredients || []).filter( (z) => z.status === "fehlt" || z.status === "zu_wenig", ); @@ -129,10 +164,16 @@ export default function Rezepte() {