Artikeldaten von Open Food Facts vergleichen und einzeln uebernehmen
Neuer Knopf unter "Nachschlagen" auf der Artikelseite. Er fragt OFF erneut und
stellt die Antwort den eigenen Daten gegenueber: Name, Marke, Packungsgroesse,
Kategorie und Bild, jeweils mit eigenem Uebernehmen-Knopf und einem
"Alle uebernehmen". Uebernommen wird nur ins Formular - gespeichert erst mit
"Speichern", damit man bis zuletzt bei den eigenen Daten bleiben kann.
Dafuer noetig: GET /products/{id}/off. /lookup taugt hier nicht, weil es bei
einem bekannten Barcode den eigenen Artikel meldet und OFF gar nicht erst
fragt. Der neue Endpunkt probiert auch die zusaetzlichen EAN-Codes durch - oft
ist nur einer davon bei OFF hinterlegt.
Damit ist das Bild unabhaengig davon nachholbar, ob ein Backup eine
Bildadresse enthielt.
Nebenbei: Der Nachschlagen-Knopf uebergab die Gruppenliste, wo die
Einheitenliste erwartet wird - die Einheit eines Vorschlags wurde deshalb nie
richtig zugeordnet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,12 @@ const PATHS = {
|
||||
check: <path d="M20 6 9 17l-5-5" />,
|
||||
close: <path d="M18 6 6 18M6 6l12 12" />,
|
||||
chevronRight: <path d="m9 18 6-6-6-6" />,
|
||||
download: (
|
||||
<>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<path d="M7 10l5 5 5-5M12 15V3" />
|
||||
</>
|
||||
),
|
||||
box: (
|
||||
<>
|
||||
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
|
||||
|
||||
86
web/src/components/OffVergleich.jsx
Normal file
86
web/src/components/OffVergleich.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import Icon from "./Icon";
|
||||
import ProduktBild from "./ProduktBild";
|
||||
|
||||
/**
|
||||
* Gegenüberstellung: eigene Daten links, Open Food Facts rechts.
|
||||
*
|
||||
* Nichts wird von allein übernommen. Jede Zeile hat ihren eigenen Knopf, und
|
||||
* auch der schreibt nur ins Formular – gespeichert wird erst mit „Speichern“.
|
||||
* So bleibt bis zuletzt die Möglichkeit, bei den eigenen Daten zu bleiben.
|
||||
*
|
||||
* felder: [{ schluessel, titel, eigen, fremd, anzeigeEigen?, anzeigeFremd? }]
|
||||
*/
|
||||
export default function OffVergleich({
|
||||
felder, bildEigenId, bildFremdUrl, onUebernehmen, onAlle, onSchliessen,
|
||||
}) {
|
||||
const leer = (wert) => wert === null || wert === undefined || wert === "";
|
||||
const abweichend = felder.filter((f) => !leer(f.fremd) && String(f.fremd) !== String(f.eigen));
|
||||
const bildAbweichend = Boolean(bildFremdUrl);
|
||||
|
||||
return (
|
||||
<section className="off-vergleich">
|
||||
<div className="off-kopf">
|
||||
<Icon name="search" size={16} />
|
||||
<h3>Daten von Open Food Facts</h3>
|
||||
<button type="button" className="btn sm" disabled={abweichend.length === 0}
|
||||
onClick={onAlle}>
|
||||
Alle übernehmen
|
||||
</button>
|
||||
<button type="button" className="btn-icon" title="Schließen" onClick={onSchliessen}>
|
||||
<Icon name="close" size={15} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{abweichend.length === 0 && !bildAbweichend ? (
|
||||
<p className="muted small">Keine abweichenden Angaben – deine Daten sind aktuell.</p>
|
||||
) : (
|
||||
<table className="table off-tabelle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feld</th><th>Bisher</th><th>Open Food Facts</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{felder.map((f) => {
|
||||
const gleich = leer(f.fremd) || String(f.fremd) === String(f.eigen);
|
||||
return (
|
||||
<tr key={f.schluessel} className={gleich ? "muted" : ""}>
|
||||
<td data-label="Feld">{f.titel}</td>
|
||||
<td data-label="Bisher">{f.anzeigeEigen ?? (leer(f.eigen) ? "–" : String(f.eigen))}</td>
|
||||
<td data-label="Open Food Facts">
|
||||
{f.anzeigeFremd ?? (leer(f.fremd) ? "–" : String(f.fremd))}
|
||||
</td>
|
||||
<td className="num">
|
||||
<button type="button" className="btn sm" disabled={gleich}
|
||||
onClick={() => onUebernehmen(f.schluessel)}>
|
||||
Übernehmen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
<tr className={bildAbweichend ? "" : "muted"}>
|
||||
<td data-label="Feld">Bild</td>
|
||||
<td data-label="Bisher"><ProduktBild productId={bildEigenId} className="off-bild" /></td>
|
||||
<td data-label="Open Food Facts">
|
||||
{/* Vorschau kommt hier direkt von OFF – lokal gibt es das Bild
|
||||
ja gerade noch nicht. Übernommen und gespeichert, holt der
|
||||
Server es einmal und liefert es danach selbst aus. */}
|
||||
{bildFremdUrl
|
||||
? <img className="off-bild" src={bildFremdUrl} alt="" />
|
||||
: <span className="muted">–</span>}
|
||||
</td>
|
||||
<td className="num">
|
||||
<button type="button" className="btn sm" disabled={!bildAbweichend}
|
||||
onClick={() => onUebernehmen("image_url")}>
|
||||
Übernehmen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import Icon from "./Icon";
|
||||
* Die Objekt-URL wird beim Verlassen wieder freigegeben, sonst hält der Browser
|
||||
* jedes angesehene Bild bis zum Neuladen der Seite im Speicher.
|
||||
*/
|
||||
function useBildUrl(productId) {
|
||||
function useBildUrl(productId, version = 0) {
|
||||
const [url, setUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -16,7 +16,9 @@ function useBildUrl(productId) {
|
||||
let abgebrochen = false;
|
||||
let erzeugt = null;
|
||||
|
||||
authorizedObjectUrl(`/products/${productId}/image`)
|
||||
// ``version`` haengt einen Wert an die Adresse, damit der Browser nach dem
|
||||
// Speichern nicht seine zwischengespeicherte Fassung ausliefert.
|
||||
authorizedObjectUrl(`/products/${productId}/image${version ? `?v=${version}` : ""}`)
|
||||
.then((neu) => {
|
||||
if (!neu) return;
|
||||
if (abgebrochen) {
|
||||
@@ -33,7 +35,7 @@ function useBildUrl(productId) {
|
||||
if (erzeugt) URL.revokeObjectURL(erzeugt);
|
||||
setUrl(null);
|
||||
};
|
||||
}, [productId]);
|
||||
}, [productId, version]);
|
||||
|
||||
return url;
|
||||
}
|
||||
@@ -44,8 +46,8 @@ function useBildUrl(productId) {
|
||||
* Rendert nichts, solange kein Bild vorliegt – ein leerer Rahmen neben einem
|
||||
* Artikel, der nie ein Bild bekommen wird, wäre nur Lärm.
|
||||
*/
|
||||
export default function ProduktBild({ productId, alt, className = "" }) {
|
||||
const url = useBildUrl(productId);
|
||||
export default function ProduktBild({ productId, alt, className = "", version = 0 }) {
|
||||
const url = useBildUrl(productId, version);
|
||||
if (!url) return null;
|
||||
return <img className={`produkt-bild ${className}`.trim()} src={url} alt={alt || ""} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user