Rezepte aus Mealie gegen den Bestand pruefen
Neue Seite "Rezepte" (nur sichtbar, wenn Mealie eingerichtet ist): links
die Rezeptliste mit Suche, rechts das gewaehlte Rezept mit allen Zutaten
und der einen Frage, die zaehlt - kann ich das heute kochen, oder muss ich
vorher einkaufen? Je Zutat steht der Zustand: vorhanden, zu wenig, fehlt,
Menge unklar oder nicht zugeordnet; Fehlendes ist rot.
Die Portionenzahl laesst sich hochdrehen. Vorrania rechnet die Mengen dann
hoch, bevor es sie mit dem Bestand vergleicht - ein Rezept fuer 4, das man
fuer 8 kocht, braucht die doppelte Menge, und genau danach soll der
Abgleich gehen. Nennt das Rezept keine Portionszahl, wird nicht skaliert
und die Seite sagt das auch.
Drei Dinge entscheidet der Abgleich nacheinander (services/rezepte.py,
bewusst ohne HTTP, damit sich alles mit erfundenen Zutaten pruefen laesst):
1. Welcher Artikel ist gemeint? Unscharf ueber den Namen, gegen Artikel
UND Gruppen - "Milch" ist markenuebergreifend gemeint, und genau dafuer
gibt es Gruppen. Die Schwelle liegt hoeher als beim Kassenzettel: Dort
sieht ein Mensch die Vorschlaege durch, hier darf nicht stillschweigend
das Falsche als vorhanden gelten. Unter der Schwelle bleibt es bei
"nicht zugeordnet" samt Kandidatenliste.
2. Wie viel ist das in Gramm? Ueber die Einheiten und die Hausmasse der
Kategorie - "2 EL Zucker" wird zu 24 g. Fehlt das Mass fuer diese
Kategorie, meldet die Zutat genau das zurueck (Massname und
Kategorie-ID), damit daraus ein "Mass jetzt anlegen" werden kann.
3. Reicht der Bestand? Bei Gruppen ueber dieselbe gewichtete Summe wie in
der Einkaufsliste, damit Untergruppen und Zweiteinheiten gleich zaehlen.
Die Fuzzy-Heuristik lag in routers/products.py und ist nach
services/matching.py gezogen: Der Rezeptabgleich stellt dieselbe Frage, und
zwei Kopien waeren mit der Zeit auseinandergelaufen. Der Kassenzettel-Scan
nutzt sie unveraendert weiter, test_receipt_match.py bleibt gruen.
Dazu zwei Dinge aus deiner Rueckmeldung zur Einstellungs-Karte:
- Der verbundene Zustand bleibt jetzt stehen, statt nur als Meldung
aufzublitzen: Adresse, Version und "Dauertoken hinterlegt" samt den
Knoepfen Pruefen / Zugang aendern / Trennen. Das Formular erscheint nur,
solange nichts eingerichtet ist - ein Dauerzustand gehoert nicht in ein
Eingabefeld, sonst sieht man nie, ob es geklappt hat.
- Leere Felder melden sich auf Deutsch ("Bitte das Passwort eingeben.")
statt mit der rohen Schema-Meldung "String should have at least 1
character", die niemandem sagt, welches Feld gemeint ist.
Der Knopf "Fehlende zur Einkaufsliste" fehlt noch: Vorranias Einkaufsliste
wird heute vollstaendig aus Mindestbestaenden gerechnet und hat keinen Ort
fuer einen eigenen Eintrag. Das ist der naechste Schritt; die Seite sagt es
auch so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7d8a54a3fc
commit
2247c8cf69
@@ -26,6 +26,7 @@ import PackageTypes from "./pages/PackageTypes";
|
||||
import Units from "./pages/Units";
|
||||
import Users from "./pages/Users";
|
||||
import ShoppingList from "./pages/ShoppingList";
|
||||
import Rezepte from "./pages/Rezepte";
|
||||
import History from "./pages/History";
|
||||
import Verliehen from "./pages/Verliehen";
|
||||
import Transfer from "./pages/Transfer";
|
||||
@@ -59,6 +60,9 @@ function Sidebar() {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const [dashboards, setDashboards] = useState([]);
|
||||
// Der Rezept-Eintrag erscheint nur, wenn Mealie eingerichtet ist. Ein Fehler
|
||||
// hier darf die Leiste nicht stoeren, deshalb still abfangen.
|
||||
const [mealie, setMealie] = useState(false);
|
||||
// Zugeklappt als Vorgabe; der Zustand bleibt im Browser, wie die
|
||||
// Spalteneinstellungen der Tabellen.
|
||||
const [listenOffen, setListenOffen] = useState(
|
||||
@@ -88,6 +92,10 @@ function Sidebar() {
|
||||
return () => window.removeEventListener("dashboards-geaendert", laden);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
api.mealieVerfuegbar().then((a) => setMealie(!!a.available)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
function handleLogout() {
|
||||
logout();
|
||||
navigate("/login");
|
||||
@@ -132,6 +140,7 @@ function Sidebar() {
|
||||
<span className="label">{e.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
{mealie && <NavItem to="/rezepte" icon="food" label="Rezepte" />}
|
||||
<NavItem to="/shopping" icon="cart" label="Einkaufsliste" />
|
||||
<NavItem to="/mindestbestaende" icon="check" label="Mindestbestände" />
|
||||
<NavItem to="/verliehen" icon="loan" label="Verliehen" />
|
||||
@@ -237,6 +246,7 @@ export default function App() {
|
||||
<Route path="/groups" element={<Protected adminOnly wide><Groups /></Protected>} />
|
||||
<Route path="/categories" element={<Protected adminOnly wide><Categories /></Protected>} />
|
||||
<Route path="/shopping" element={<Protected><ShoppingList /></Protected>} />
|
||||
<Route path="/rezepte" element={<Protected wide><Rezepte /></Protected>} />
|
||||
<Route path="/mindestbestaende" element={<Protected wide><MinStock /></Protected>} />
|
||||
{/* Ohne adminOnly: Verleihen und Zurücknehmen ist eine Buchung wie Ein-
|
||||
und Auslagern, keine Stammdatenänderung. */}
|
||||
|
||||
@@ -451,6 +451,14 @@ export const api = {
|
||||
mealieKonfigurieren: (body) => request("/mealie/config", { method: "PUT", body }),
|
||||
mealieToken: (body) => request("/mealie/token", { method: "PUT", body }),
|
||||
mealieTrennen: () => request("/mealie/config", { method: "DELETE" }),
|
||||
// Rezepte aus Mealie, gegen den Bestand geprueft.
|
||||
recipes: ({ q, page = 1, perPage = 50 } = {}) => {
|
||||
const s = new URLSearchParams({ page: String(page), per_page: String(perPage) });
|
||||
if (q) s.set("q", q);
|
||||
return request(`/recipes?${s.toString()}`);
|
||||
},
|
||||
recipe: (slug, servings) =>
|
||||
request(`/recipes/${encodeURIComponent(slug)}${servings ? `?servings=${servings}` : ""}`),
|
||||
listSettings: () => request("/settings"),
|
||||
setSetting: (key, value) =>
|
||||
request(`/settings/${key}?value=${encodeURIComponent(value)}`, { method: "PUT" }),
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../api";
|
||||
import Icon from "../components/Icon";
|
||||
import { fmt } from "../units";
|
||||
|
||||
/**
|
||||
* Rezepte aus Mealie – mit der einen Frage, die zählt: Kann ich das heute
|
||||
* kochen, oder muss ich vorher einkaufen?
|
||||
*
|
||||
* Links die Liste, rechts das gewählte Rezept mit allen Zutaten und ihrem
|
||||
* Zustand. Die Portionenzahl lässt sich hochdrehen; der Server rechnet die
|
||||
* Mengen dann entsprechend hoch, bevor er sie mit dem Bestand vergleicht.
|
||||
*/
|
||||
|
||||
// Die Basiseinheit kommt als technischer Wert („gram"); in der Tabelle soll
|
||||
// stehen, was auf der Packung steht.
|
||||
const BASIS_KURZ = { gram: "g", milliliter: "ml", piece: "Stk" };
|
||||
const kurz = (basis) => BASIS_KURZ[basis] || basis || "";
|
||||
|
||||
const ZUSTAND = {
|
||||
vorhanden: { text: "vorhanden", klasse: "ok", icon: "check" },
|
||||
zu_wenig: { text: "zu wenig", klasse: "warn", icon: "alert" },
|
||||
fehlt: { text: "fehlt", klasse: "danger", icon: "alert" },
|
||||
menge_unbekannt: { text: "Menge unklar", klasse: "muted", icon: "alert" },
|
||||
kein_treffer: { text: "nicht zugeordnet", klasse: "muted", icon: "search" },
|
||||
};
|
||||
|
||||
function Zustand({ status }) {
|
||||
const z = ZUSTAND[status] || ZUSTAND.kein_treffer;
|
||||
return <span className={`badge ${z.klasse}`}>{z.text}</span>;
|
||||
}
|
||||
|
||||
export default function Rezepte() {
|
||||
const [liste, setListe] = useState([]);
|
||||
const [suche, setSuche] = useState("");
|
||||
const [slug, setSlug] = useState(null);
|
||||
const [rezept, setRezept] = useState(null);
|
||||
const [portionen, setPortionen] = useState("");
|
||||
const [fehler, setFehler] = useState(null);
|
||||
const [laedt, setLaedt] = useState(true);
|
||||
|
||||
async function ladeListe(q = "") {
|
||||
setLaedt(true);
|
||||
try {
|
||||
const daten = await api.recipes({ q: q || undefined });
|
||||
setListe(daten.items || []);
|
||||
setFehler(null);
|
||||
} catch (err) { setFehler(err.message); } finally { setLaedt(false); }
|
||||
}
|
||||
useEffect(() => { ladeListe(); }, []);
|
||||
|
||||
async function oeffne(s, wunschPortionen) {
|
||||
setSlug(s);
|
||||
setRezept(null);
|
||||
try {
|
||||
const daten = await api.recipe(s, wunschPortionen);
|
||||
setRezept(daten);
|
||||
// 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); }
|
||||
}
|
||||
|
||||
function setzePortionen(wert) {
|
||||
setPortionen(wert);
|
||||
const zahl = Number(String(wert).replace(",", "."));
|
||||
if (slug && Number.isFinite(zahl) && zahl > 0) oeffne(slug, zahl);
|
||||
}
|
||||
|
||||
const fehlende = (rezept?.ingredients || []).filter(
|
||||
(z) => z.status === "fehlt" || z.status === "zu_wenig",
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>Rezepte</h1>
|
||||
<div className="sub">Aus Mealie – geprüft gegen deinen Bestand</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{fehler && <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>}
|
||||
|
||||
<div className="card" style={{ marginBottom: "var(--sp-3)" }}>
|
||||
<form className="field-inline" style={{ marginBottom: 0 }}
|
||||
onSubmit={(e) => { e.preventDefault(); ladeListe(suche); }}>
|
||||
<input value={suche} placeholder="Rezept suchen…"
|
||||
onChange={(e) => setSuche(e.target.value)} />
|
||||
<button className="btn"><Icon name="search" size={16} />Suchen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gap: "var(--sp-4)",
|
||||
gridTemplateColumns: "minmax(240px, 320px) 1fr" }}>
|
||||
<div className="card">
|
||||
<div className="card-head"><Icon name="food" /><h2>{liste.length} Rezepte</h2></div>
|
||||
{laedt && <p className="muted small">Wird geladen…</p>}
|
||||
{!laedt && liste.length === 0 && (
|
||||
<p className="muted small">Mealie liefert keine Rezepte.</p>
|
||||
)}
|
||||
<ul className="clean-list">
|
||||
{liste.map((r) => (
|
||||
<li key={r.slug}>
|
||||
<button type="button"
|
||||
className={`btn sm ghost${r.slug === slug ? " primary" : ""}`}
|
||||
style={{ width: "100%", justifyContent: "flex-start" }}
|
||||
onClick={() => oeffne(r.slug, null)}>
|
||||
{r.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
{!rezept && <p className="muted">Links ein Rezept wählen.</p>}
|
||||
{rezept && (
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="field-inline" style={{ flexWrap: "wrap" }}>
|
||||
<label style={{ margin: 0, maxWidth: 160 }}>
|
||||
Portionen
|
||||
<input type="number" min="0.5" step="0.5" value={portionen}
|
||||
onChange={(e) => setzePortionen(e.target.value)} />
|
||||
</label>
|
||||
{rezept.base_servings ? (
|
||||
<span className="muted small">
|
||||
Rezept ist für {fmt(rezept.base_servings)} gedacht.
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{rezept.skalierung_hinweis && (
|
||||
<p className="muted small mt-0">{rezept.skalierung_hinweis}</p>
|
||||
)}
|
||||
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr><th>Zutat</th><th>Gebraucht</th><th>Im Haus</th><th>Zustand</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rezept.ingredients.map((z, i) => (
|
||||
<tr key={i} className={
|
||||
z.status === "fehlt" ? "row-danger"
|
||||
: z.status === "zu_wenig" ? "row-warn" : ""}>
|
||||
<td data-label="Zutat">
|
||||
<span className="strong">{z.text}</span>
|
||||
{z.ziel_name && (
|
||||
<div className="muted small">
|
||||
{z.art === "group" ? "Gruppe " : ""}{z.ziel_name}
|
||||
</div>
|
||||
)}
|
||||
{z.hinweis && <div className="muted small">{z.hinweis}</div>}
|
||||
</td>
|
||||
<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>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
</td>
|
||||
<td data-label="Zustand"><Zustand status={z.status} /></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{!rezept.kochbar && (
|
||||
<p className="muted small">
|
||||
Was fehlt, steht oben rot. Der Knopf „Fehlende zur Einkaufsliste"
|
||||
kommt, sobald die Einkaufsliste eigene Einträge speichern kann.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+84
-55
@@ -198,6 +198,10 @@ function MealieCard() {
|
||||
const [mitToken, setMitToken] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [fehler, setFehler] = useState(null);
|
||||
// Das Formular steht nur offen, solange nichts eingerichtet ist – oder wenn
|
||||
// man es ausdruecklich wieder aufmacht. Ein Dauerzustand gehoert nicht in ein
|
||||
// Eingabeformular, sonst sieht man nie, ob es geklappt hat.
|
||||
const [aendern, setAendern] = useState(false);
|
||||
|
||||
async function laden(pruefen = false) {
|
||||
try {
|
||||
@@ -208,8 +212,17 @@ function MealieCard() {
|
||||
}
|
||||
useEffect(() => { laden(); }, []);
|
||||
|
||||
const verbunden = !!status?.configured;
|
||||
|
||||
async function verbinden(e) {
|
||||
e.preventDefault();
|
||||
// Eigene Pruefung vor dem Absenden: Der Server antwortet sonst mit der
|
||||
// rohen Schema-Meldung („String should have at least 1 character"), und
|
||||
// die erklaert niemandem, welches Feld gemeint ist.
|
||||
if (!url.trim()) { setFehler("Bitte die Adresse von Mealie eintragen."); return; }
|
||||
if (mitToken && !token.trim()) { setFehler("Bitte das Token aus Mealie einfügen."); return; }
|
||||
if (!mitToken && !benutzer.trim()) { setFehler("Bitte den Benutzernamen in Mealie eintragen."); return; }
|
||||
if (!mitToken && !passwort) { setFehler("Bitte das Passwort eingeben."); return; }
|
||||
setBusy(true);
|
||||
setFehler(null);
|
||||
try {
|
||||
@@ -219,6 +232,7 @@ function MealieCard() {
|
||||
setStatus(s);
|
||||
setPasswort(""); // bewusst sofort leeren – es ist nirgends gespeichert
|
||||
setToken("");
|
||||
setAendern(false); // Formular zu, der Zustand oben sagt jetzt alles
|
||||
toast(s.reachable ? "Mit Mealie verbunden." : "Gespeichert – Mealie antwortet aber nicht.");
|
||||
} catch (err) { setFehler(err.message); } finally { setBusy(false); }
|
||||
}
|
||||
@@ -235,6 +249,7 @@ function MealieCard() {
|
||||
await api.mealieTrennen();
|
||||
setStatus({ configured: false, base_url: "", token_set: false });
|
||||
setUrl(""); setBenutzer(""); setPasswort(""); setToken("");
|
||||
setAendern(false);
|
||||
toast("Verbindung gelöst.");
|
||||
} catch (err) { setFehler(err.message); }
|
||||
}
|
||||
@@ -249,68 +264,82 @@ function MealieCard() {
|
||||
Dauertoken ausstellen und vergisst es.
|
||||
</p>
|
||||
|
||||
{status?.configured && (
|
||||
<div className={`alert ${status.reachable === false ? "error" : "ok"}`}>
|
||||
<Icon name={status.reachable === false ? "alert" : "check"} size={16} />
|
||||
<span>
|
||||
{status.base_url}
|
||||
{status.version ? ` · Mealie ${status.version}` : ""}
|
||||
{" · "}
|
||||
{status.reachable === false
|
||||
? (status.error || "antwortet nicht")
|
||||
: status.reachable === true ? "erreichbar" : "Token gesetzt"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{fehler && <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>}
|
||||
|
||||
<label>
|
||||
Adresse
|
||||
<input value={url} placeholder="https://mealie.example.com"
|
||||
onChange={(e) => setUrl(e.target.value)} />
|
||||
</label>
|
||||
|
||||
{!mitToken ? (
|
||||
<div className="row">
|
||||
<label className="grow">
|
||||
Benutzername in Mealie
|
||||
<input value={benutzer} onChange={(e) => setBenutzer(e.target.value)} />
|
||||
</label>
|
||||
<label className="grow">
|
||||
Passwort
|
||||
<input type="password" value={passwort} autoComplete="new-password"
|
||||
onChange={(e) => setPasswort(e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<label>
|
||||
Dauertoken aus Mealie
|
||||
<input value={token} onChange={(e) => setToken(e.target.value)}
|
||||
placeholder="in Mealie unter Profil ▸ API-Tokens erzeugen" />
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label className="check-inline">
|
||||
<input type="checkbox" checked={mitToken}
|
||||
onChange={(e) => setMitToken(e.target.checked)} />
|
||||
Ich gebe lieber selbst ein Token ein
|
||||
</label>
|
||||
|
||||
<div className="field-inline" style={{ flexWrap: "wrap", marginBottom: 0 }}>
|
||||
<button className="btn primary" disabled={busy || !url}>
|
||||
<Icon name="check" size={16} />{busy ? "Verbinde…" : "Verbinden"}
|
||||
</button>
|
||||
{status?.configured && (
|
||||
<>
|
||||
{verbunden && !aendern ? (
|
||||
<>
|
||||
<div className={`alert ${status.reachable === false ? "error" : "ok"}`}>
|
||||
<Icon name={status.reachable === false ? "alert" : "check"} size={16} />
|
||||
<span>
|
||||
{status.reachable === false
|
||||
? `Nicht erreichbar: ${status.error || "unbekannter Grund"}`
|
||||
: "Verbunden"}
|
||||
</span>
|
||||
</div>
|
||||
<dl className="kv">
|
||||
<dt>Adresse</dt><dd>{status.base_url}</dd>
|
||||
{status.version && (<><dt>Mealie-Version</dt><dd>{status.version}</dd></>)}
|
||||
<dt>Zugang</dt>
|
||||
<dd>Dauertoken hinterlegt – dein Passwort ist nicht gespeichert.</dd>
|
||||
</dl>
|
||||
<div className="field-inline" style={{ flexWrap: "wrap", marginBottom: 0 }}>
|
||||
<button type="button" className="btn" onClick={() => laden(true)}>
|
||||
Verbindung prüfen
|
||||
<Icon name="check" size={16} />Verbindung prüfen
|
||||
</button>
|
||||
<button type="button" className="btn" onClick={() => setAendern(true)}>
|
||||
<Icon name="edit" size={16} />Zugang ändern
|
||||
</button>
|
||||
<button type="button" className="btn danger" onClick={trennen}>
|
||||
Trennen
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<label>
|
||||
Adresse
|
||||
<input value={url} placeholder="https://mealie.example.com"
|
||||
onChange={(e) => setUrl(e.target.value)} />
|
||||
</label>
|
||||
|
||||
{!mitToken ? (
|
||||
<div className="row">
|
||||
<label className="grow">
|
||||
Benutzername in Mealie
|
||||
<input value={benutzer} onChange={(e) => setBenutzer(e.target.value)} />
|
||||
</label>
|
||||
<label className="grow">
|
||||
Passwort
|
||||
<input type="password" value={passwort} autoComplete="new-password"
|
||||
onChange={(e) => setPasswort(e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<label>
|
||||
Dauertoken aus Mealie
|
||||
<input value={token} onChange={(e) => setToken(e.target.value)}
|
||||
placeholder="in Mealie unter Profil ▸ API-Tokens erzeugen" />
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label className="check-inline">
|
||||
<input type="checkbox" checked={mitToken}
|
||||
onChange={(e) => setMitToken(e.target.checked)} />
|
||||
Ich gebe lieber selbst ein Token ein
|
||||
</label>
|
||||
|
||||
<div className="field-inline" style={{ flexWrap: "wrap", marginBottom: 0 }}>
|
||||
<button className="btn primary" disabled={busy}>
|
||||
<Icon name="check" size={16} />{busy ? "Verbinde…" : "Verbinden"}
|
||||
</button>
|
||||
{verbunden && (
|
||||
<button type="button" className="btn" onClick={() => { setAendern(false); setFehler(null); }}>
|
||||
Abbrechen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user