Rezeptseite: Portionen drehen laesst die Seite stehen
Bisher setzte jede Aenderung an den Portionen das Rezept auf null - die ganze rechte Karte verschwand und zeigte "Links ein Rezept waehlen", bevor sie neu aufbaute. Bei einem Druck auf den Zahlenpfeil ist das ein Flackern, bei getippten Zahlen dreimal hintereinander. Jetzt bleibt stehen, was sich nicht aendert: Rezeptname, Portionenfeld und die Zutatenliste. Nur die drei Wertespalten (Gebraucht, Im Haus, Zustand) sind waehrend des Rechnens unklar und zeigen einen schimmernden Platzhalter; das Ergebnis blendet sich danach ein (.wert-ein, 0.28s, mit Ruecksicht auf prefers-reduced-motion). Das Kopf-Abzeichen sagt in der Zeit "wird gerechnet..." statt eine veraltete Zahl, und die roten/gelben Zeilenfarben setzen aus - sie gelten fuer die alten Mengen. Dazu zwei Dinge, die ohne das Standenbleiben nicht aufgefallen waeren: - Das Zahlenfeld reagiert sofort, die Anfrage wartet 280 ms. Wer von 4 auf 12 tippt, bekam sonst erst ein Ergebnis fuer 1. - Antworten koennen in falscher Reihenfolge eintreffen. Eine laufende Nummer je Anfrage verwirft alles, was nicht die neueste ist - sonst gewinnt beim schnellen Klicken zufaellig die langsamere Antwort. Ausserdem: Eine Knopfreihe direkt unter einer Angabenliste klebte am letzten Wert (Mealie-Karte in den Einstellungen). .kv hat bewusst keinen Aussenabstand und .field-inline nur einen nach unten - den fehlenden setzt jetzt eine Geschwisterregel, damit es ueberall gilt, wo dieses Paar vorkommt. Von Hand geprueft gegen eine Mealie-Attrappe: Pfannkuchen von 4 auf 12 Portionen - waehrend des Ladens 15 Platzhalter und die Zutatennamen stehen, danach 250 g -> 750 g und 2 EL -> 6 EL (60 g), keine leere Karte dazwischen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
53a8b9e0b6
commit
f02e9a3840
+81
-22
@@ -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() {
|
||||
<div className="card-head">
|
||||
<Icon name={rezept.kochbar ? "check" : "cart"} />
|
||||
<h2>{rezept.name}</h2>
|
||||
<span className={`badge ${rezept.kochbar ? "ok" : "warn"}`}
|
||||
style={{ marginLeft: "auto" }}>
|
||||
{rezept.kochbar ? "kochbar" : `${fehlende.length} fehlt`}
|
||||
</span>
|
||||
{rechnet ? (
|
||||
<span className="badge nowrap" style={{ marginLeft: "auto" }}>
|
||||
wird gerechnet…
|
||||
</span>
|
||||
) : (
|
||||
<span key={stand} className={`badge wert-ein ${rezept.kochbar ? "ok" : "warn"}`}
|
||||
style={{ marginLeft: "auto" }}>
|
||||
{rezept.kochbar ? "kochbar" : `${fehlende.length} fehlt`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="field-inline" style={{ flexWrap: "wrap" }}>
|
||||
@@ -158,8 +199,8 @@ export default function Rezepte() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{rezept.ingredients.map((z, i) => (
|
||||
<tr key={i} className={
|
||||
z.status === "fehlt" ? "row-danger"
|
||||
<tr key={i} className={rechnet ? ""
|
||||
: z.status === "fehlt" ? "row-danger"
|
||||
: z.status === "zu_wenig" ? "row-warn" : ""}>
|
||||
<td data-label="Zutat">
|
||||
<span className="strong">{z.text}</span>
|
||||
@@ -179,22 +220,40 @@ export default function Rezepte() {
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
{/* Nur die WERTE sind beim Umrechnen unklar, die Zutat
|
||||
selbst nicht. Deshalb bleibt die Zeile stehen und nur
|
||||
diese drei Zellen zeigen einen Platzhalter; das
|
||||
Ergebnis blendet sich danach ein. */}
|
||||
<td data-label="Gebraucht">
|
||||
{z.menge != null ? `${fmt(z.menge)} ${z.einheit}`.trim() : "–"}
|
||||
{z.benoetigt_base != null && (
|
||||
<div className="muted small">
|
||||
{fmt(z.benoetigt_base)} {kurz(z.basis_einheit)}
|
||||
</div>
|
||||
{rechnet ? <Skeleton w="70%" /> : (
|
||||
<span key={stand} className="wert-ein">
|
||||
{z.menge != null ? `${fmt(z.menge)} ${z.einheit}`.trim() : "–"}
|
||||
{z.benoetigt_base != null && (
|
||||
<div className="muted small">
|
||||
{fmt(z.benoetigt_base)} {kurz(z.basis_einheit)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td data-label="Im Haus">
|
||||
{z.bestand_base != null
|
||||
? `${fmt(z.bestand_base)} ${kurz(z.basis_einheit)}`.trim() : "–"}
|
||||
{z.fehlt_text && (
|
||||
<div className="muted small">fehlt: {z.fehlt_text}</div>
|
||||
{rechnet ? <Skeleton w="55%" /> : (
|
||||
<span key={stand} className="wert-ein">
|
||||
{z.bestand_base != null
|
||||
? `${fmt(z.bestand_base)} ${kurz(z.basis_einheit)}`.trim() : "–"}
|
||||
{z.fehlt_text && (
|
||||
<div className="muted small">fehlt: {z.fehlt_text}</div>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td data-label="Zustand">
|
||||
{rechnet ? <Skeleton w={84} /> : (
|
||||
<span key={stand} className="wert-ein">
|
||||
<Zustand status={z.status} />
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td data-label="Zustand"><Zustand status={z.status} /></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user