diff --git a/web/src/components/ProduktBild.jsx b/web/src/components/ProduktBild.jsx index fa4cb2d..2373816 100644 --- a/web/src/components/ProduktBild.jsx +++ b/web/src/components/ProduktBild.jsx @@ -12,8 +12,15 @@ import Lightbox from "./Lightbox"; * ``onLoaded`` meldet, ob ein Bild vorliegt – so kann die Artikelseite z.B. den * „Entfernen"-Knopf ausblenden, wenn es nichts zu entfernen gibt. */ +// Modulweiter Cache: Produkt-ID+Version → Objekt-URL (oder null = „kein Bild"). +// So werden Thumbnails beim erneuten Öffnen einer Liste nicht jedes Mal neu +// geholt. Die Objekt-URLs bleiben absichtlich bestehen (werden nicht widerrufen), +// bis eine neue Version dieselbe ID ablöst. +const bildCache = new Map(); + function useBildUrl(productId, version = 0, onLoaded) { - const [url, setUrl] = useState(null); + const key = `${productId}:${version}`; + const [url, setUrl] = useState(() => (bildCache.has(key) ? bildCache.get(key) : null)); // Über einen Ref, damit ein bei jedem Render neu erzeugtes onLoaded den Effekt // nicht erneut auslöst. const onLoadedRef = useRef(onLoaded); @@ -21,8 +28,13 @@ function useBildUrl(productId, version = 0, onLoaded) { useEffect(() => { if (!productId) return undefined; + if (bildCache.has(key)) { + const cached = bildCache.get(key); + onLoadedRef.current?.(Boolean(cached)); + setUrl(cached); + return undefined; + } let abgebrochen = false; - let erzeugt = null; // ``version`` haengt einen Wert an die Adresse, damit der Browser nach dem // Speichern nicht seine zwischengespeicherte Fassung ausliefert. @@ -32,24 +44,26 @@ function useBildUrl(productId, version = 0, onLoaded) { if (neu) URL.revokeObjectURL(neu); return; } - onLoadedRef.current?.(Boolean(neu)); - if (!neu) { - setUrl(null); - return; + // Alte Version derselben ID freigeben, dann neu cachen. + const alt = `${productId}:`; + for (const k of bildCache.keys()) { + if (k !== key && k.startsWith(alt)) { + const u = bildCache.get(k); + if (u) URL.revokeObjectURL(u); + bildCache.delete(k); + } } - erzeugt = neu; - setUrl(neu); + bildCache.set(key, neu || null); + onLoadedRef.current?.(Boolean(neu)); + setUrl(neu || null); }) .catch(() => { if (!abgebrochen) onLoadedRef.current?.(false); }); - return () => { - abgebrochen = true; - if (erzeugt) URL.revokeObjectURL(erzeugt); - setUrl(null); - }; - }, [productId, version]); + // Objekt-URL NICHT widerrufen – sie bleibt im Cache für den nächsten Mount. + return () => { abgebrochen = true; }; + }, [productId, version, key]); return url; } diff --git a/web/src/styles.css b/web/src/styles.css index 0208f82..6a2d35b 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -400,7 +400,12 @@ td select { width: auto; min-width: 0; max-width: 100%; } width: 34px; height: 34px; object-fit: cover; border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--surface-2); } -.thumb-fallback { display: grid; place-items: center; color: var(--muted); } +/* Gleiche Größe wie das echte Bild, damit die Zeilenhöhe mit und ohne Bild + identisch ist und beim Nachladen nicht springt. */ +.thumb-fallback { + width: 34px; height: 34px; display: grid; place-items: center; color: var(--muted); + border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--surface-2); +} /* ---------- Badges / pills ---------- */ .badge {