From dd8566eabc310010e1e3a7b2f0fa9369df46ba33 Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 4 Oct 2026 09:37:09 +0200 Subject: [PATCH] u --- src/components/tiles/ArticleTile.jsx | 89 +++-- src/components/tiles/CategoryTile.jsx | 10 +- src/routes/artikel/merkmale.jsx | 466 ++++++++++++-------------- 3 files changed, 284 insertions(+), 281 deletions(-) diff --git a/src/components/tiles/ArticleTile.jsx b/src/components/tiles/ArticleTile.jsx index 9ee8527..d74d87c 100644 --- a/src/components/tiles/ArticleTile.jsx +++ b/src/components/tiles/ArticleTile.jsx @@ -3,10 +3,15 @@ import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; import Box from '@mui/material/Box'; import ButtonBase from '@mui/material/ButtonBase'; import Typography from '@mui/material/Typography'; -import { Tile, tileBaseSx } from 'dashgui/tiles'; +import { TILE_SIZE, Tile, tileBaseSx } from 'dashgui/tiles'; import { formatPrice } from '../../utils/price'; import { articleSku } from '../article/articleRoute'; -import { getThumbHashDataUrl, getCachedImage, imageCache } from '../article/ThumbHashImage'; +import { + getThumbHashDataUrl, + getCachedImage, + isImageLoaded, + markImageLoaded, +} from '../article/ThumbHashImage'; import { BrandLabel } from '../brand'; import { thumb } from '../../api'; @@ -103,7 +108,26 @@ class ArticleTileBackdrop extends PureComponent { constructor(props) { super(props); this.thumbUrl = getThumbHashDataUrl(props.thumbHash); - this.img = getCachedImage(props.fullHash); + const fullHash = props.fullHash; + if (fullHash) { + const cached = getCachedImage(fullHash); + if (cached) { + this.img = cached; + } else { + const img = new Image(); + img.onload = () => { + markImageLoaded(fullHash, img); + if (this.img === img) { + this.paint(); + } + }; + img.src = thumb.url(fullHash); + this.img = img; + if (img.complete && img.naturalWidth) { + markImageLoaded(fullHash, img); + } + } + } } componentDidMount() { @@ -127,35 +151,37 @@ class ArticleTileBackdrop extends PureComponent { loadImage() { const { fullHash } = this.props; - if (fullHash) { - const cached = getCachedImage(fullHash); - if (cached) { - this.img = cached; + if (!fullHash) return; + const cached = getCachedImage(fullHash); + if (cached) { + this.img = cached; + this.paint(); + return; + } + if (this.img && this.img.src === thumb.url(fullHash)) { + if (this.img.complete && this.img.naturalWidth) { + markImageLoaded(fullHash, this.img); this.paint(); - return; } - const img = new Image(); - img.onload = () => { - imageCache.set(fullHash, img); - if (this.img === img) { - this.paint(); - } - }; - img.src = thumb.url(fullHash); - this.img = img; - if (img.complete && img.naturalWidth) { - imageCache.set(fullHash, img); + return; + } + const img = new Image(); + img.onload = () => { + markImageLoaded(fullHash, img); + if (this.img === img) { this.paint(); - return; } + }; + img.src = thumb.url(fullHash); + this.img = img; + if (img.complete && img.naturalWidth) { + markImageLoaded(fullHash, img); + this.paint(); } } setCanvas = (canvas) => { this.canvasRef = canvas; - if (!this.img && this.props.fullHash) { - this.img = getCachedImage(this.props.fullHash); - } if (canvas && typeof ResizeObserver !== 'undefined') { this.observer?.disconnect(); this.observer = new ResizeObserver(() => this.paint()); @@ -168,16 +194,18 @@ class ArticleTileBackdrop extends PureComponent { const canvas = this.canvasRef; if (!canvas) return; const rect = canvas.getBoundingClientRect(); - const dstW = Math.round(rect.width); - const dstH = Math.round(rect.height); + const dstW = Math.round(rect.width) || canvas.clientWidth || TILE_SIZE; + const dstH = Math.round(rect.height) || canvas.clientHeight || TILE_SIZE; if (!dstW || !dstH) return; - canvas.width = dstW; - canvas.height = dstH; + if (canvas.width !== dstW) canvas.width = dstW; + if (canvas.height !== dstH) canvas.height = dstH; const ctx = canvas.getContext('2d'); if (!ctx) return; - const img = this.img || getCachedImage(this.props.fullHash); + const img = (this.img && this.img.complete && this.img.naturalWidth) + ? this.img + : getCachedImage(this.props.fullHash); if (img && img.complete && img.naturalWidth && img.naturalHeight) { this.img = img; drawContainedWithStretchedEdges( @@ -192,7 +220,8 @@ class ArticleTileBackdrop extends PureComponent { }; render() { - const hasCachedImage = Boolean(this.img && this.img.complete && this.img.naturalWidth); + const { fullHash } = this.props; + const showThumb = !isImageLoaded(fullHash) && Boolean(this.thumbUrl); return ( - {!hasCachedImage && this.thumbUrl ? ( + {showThumb ? ( { if (this.props.fullHash && e.target) { - imageCache.set(this.props.fullHash, e.target); + markImageLoaded(this.props.fullHash, e.target); } }; render() { const { fullHash } = this.props; if (!this.thumbUrl && !fullHash) return null; - const isCached = this.isCached || Boolean(getCachedImage(fullHash)); + const isCached = this.isCached || isImageLoaded(fullHash); return ( {!isCached && this.thumbUrl ? ( diff --git a/src/routes/artikel/merkmale.jsx b/src/routes/artikel/merkmale.jsx index e274d48..7a33fd2 100644 --- a/src/routes/artikel/merkmale.jsx +++ b/src/routes/artikel/merkmale.jsx @@ -630,36 +630,44 @@ class ArtikelMerkmalePage extends PureComponent { `} - {/* Tile 1: Merkmale (Freitext) */} - + {/* Tile 1: Attribute (Auswahllisten) */} + - + - Merkmale + Auswahl-Merkmale - + - - Reihenfolge per Drag & Drop anpassen. - - - - {properties.length === 0 ? ( - - Noch keine Merkmale angelegt. - - ) : ( - properties.map((p) => ( + + {attributes.map((a) => { + const isSelected = selectedAttr?.id === a.id; + return ( this.handleSelectAttribute(a.id)} + sx={{ + gap: 1, + py: 0.75, + px: 0.5, + borderRadius: 1.5, + alignItems: 'center', + cursor: 'pointer', + bgcolor: isSelected ? 'rgba(59, 130, 246, 0.12)' : 'transparent', + border: isSelected ? '1px solid rgba(59, 130, 246, 0.3)' : '1px solid transparent', + transition: 'all 0.15s ease', + '&:hover': { + bgcolor: isSelected ? 'rgba(59, 130, 246, 0.16)' : 'rgba(0, 0, 0, 0.04)', + }, + }} + data-id={a.id} > e.stopPropagation()} sx={{ display: 'flex', alignItems: 'center', @@ -671,46 +679,57 @@ class ArtikelMerkmalePage extends PureComponent { > - - - this.setState((prev) => ({ - propDrafts: { ...prev.propDrafts, [p.id]: next }, - })) - } - onBlur={() => void this.handleRenameProperty(p)} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); - event.target.blur(); - } - if (event.key === 'Escape') { - event.preventDefault(); - this.setState((prev) => ({ - propDrafts: { ...prev.propDrafts, [p.id]: p.name }, - })); - event.target.blur(); - } + + + + e.stopPropagation()} sx={{ flex: 1, minWidth: 0 }}> + + this.setState((prev) => ({ + attrDrafts: { ...prev.attrDrafts, [a.id]: next }, + })) + } + onBlur={() => void this.handleRenameAttribute(a)} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + event.target.blur(); + } + if (event.key === 'Escape') { + event.preventDefault(); + this.setState((prev) => ({ + attrDrafts: { ...prev.attrDrafts, [a.id]: a.name }, + })); + event.target.blur(); + } + }} + sx={{ width: '100%' }} + /> + + void this.handleTogglePropertyExclusive(p, e)} + color={a.exclusive ? 'primary' : 'default'} + variant={a.exclusive ? 'filled' : 'outlined'} + onClick={(e) => void this.handleToggleAttributeExclusive(a, e)} sx={{ height: 22, fontSize: '0.72rem', @@ -720,206 +739,27 @@ class ArtikelMerkmalePage extends PureComponent { }} /> + void this.handleDeleteProperty(p)} + onClick={(e) => void this.handleDeleteAttribute(a, e)} + sx={{ flexShrink: 0 }} > - )) - )} + ); + })} this.setState({ newPropName: next })} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); - void this.handleCreateProperty(); - } - }} - sx={{ flex: 1 }} - /> - - - this.setState({ newPropExclusive: e.target.checked })} - sx={{ p: 0.5 }} - /> - } - label={ - - Einzelauswahl (max. 1 Wert pro Artikel) - - } - sx={{ ml: 0.5, mt: -0.5 }} - /> - - - - - {/* Tile 2: Attribute (Auswahllisten) */} - - - - - - Attribute - - - - - - Reihenfolge per Drag & Drop anpassen. - - - - {attributes.length === 0 ? ( - - Noch keine Attribute angelegt. - - ) : ( - attributes.map((a) => { - const isSelected = selectedAttr?.id === a.id; - return ( - this.handleSelectAttribute(a.id)} - sx={{ - gap: 1, - py: 0.75, - px: 0.5, - borderRadius: 1.5, - alignItems: 'center', - cursor: 'pointer', - bgcolor: isSelected ? 'rgba(59, 130, 246, 0.12)' : 'transparent', - border: isSelected ? '1px solid rgba(59, 130, 246, 0.3)' : '1px solid transparent', - transition: 'all 0.15s ease', - '&:hover': { - bgcolor: isSelected ? 'rgba(59, 130, 246, 0.16)' : 'rgba(0, 0, 0, 0.04)', - }, - }} - data-id={a.id} - > - e.stopPropagation()} - sx={{ - display: 'flex', - alignItems: 'center', - color: 'text.disabled', - flexShrink: 0, - '&:hover': { color: 'text.secondary' }, - transition: 'color 0.15s', - }} - > - - - - - - e.stopPropagation()} sx={{ flex: 1, minWidth: 0 }}> - - this.setState((prev) => ({ - attrDrafts: { ...prev.attrDrafts, [a.id]: next }, - })) - } - onBlur={() => void this.handleRenameAttribute(a)} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); - event.target.blur(); - } - if (event.key === 'Escape') { - event.preventDefault(); - this.setState((prev) => ({ - attrDrafts: { ...prev.attrDrafts, [a.id]: a.name }, - })); - event.target.blur(); - } - }} - sx={{ width: '100%' }} - /> - - - - void this.handleToggleAttributeExclusive(a, e)} - sx={{ - height: 22, - fontSize: '0.72rem', - fontWeight: 600, - cursor: 'pointer', - flexShrink: 0, - }} - /> - - - void this.handleDeleteAttribute(a, e)} - sx={{ flexShrink: 0 }} - > - - - - ); - }) - )} - - - - - this.setState({ newAttrName: next })} @@ -961,7 +801,7 @@ class ArtikelMerkmalePage extends PureComponent { - {/* Tile 3: Werte für das gewählte Attribut */} + {/* Tile 2: Werte für das gewählte Attribut */} @@ -978,14 +818,8 @@ class ArtikelMerkmalePage extends PureComponent { ) : null} - - {selectedAttr - ? 'Reihenfolge per Drag & Drop anpassen.' - : 'Wähle links ein Attribut aus, um Werte zu bearbeiten.'} - - {!selectedAttr ? ( - + Kein Attribut ausgewählt. ) : ( @@ -1060,7 +894,7 @@ class ArtikelMerkmalePage extends PureComponent { this.setState({ newValText: next })} @@ -1085,6 +919,146 @@ class ArtikelMerkmalePage extends PureComponent { )} + + {/* Tile 3: Merkmale (Freitext) */} + + + + + + Text-Merkmale + + + + + + {properties.map((p) => ( + + + + + + + this.setState((prev) => ({ + propDrafts: { ...prev.propDrafts, [p.id]: next }, + })) + } + onBlur={() => void this.handleRenameProperty(p)} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + event.target.blur(); + } + if (event.key === 'Escape') { + event.preventDefault(); + this.setState((prev) => ({ + propDrafts: { ...prev.propDrafts, [p.id]: p.name }, + })); + event.target.blur(); + } + }} + sx={{ flex: 1 }} + /> + + void this.handleTogglePropertyExclusive(p, e)} + sx={{ + height: 22, + fontSize: '0.72rem', + fontWeight: 600, + cursor: 'pointer', + flexShrink: 0, + }} + /> + + void this.handleDeleteProperty(p)} + > + + + + ))} + + + + + this.setState({ newPropName: next })} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + void this.handleCreateProperty(); + } + }} + sx={{ flex: 1 }} + /> + + + this.setState({ newPropExclusive: e.target.checked })} + sx={{ p: 0.5 }} + /> + } + label={ + + Einzelauswahl (max. 1 Wert pro Artikel) + + } + sx={{ ml: 0.5, mt: -0.5 }} + /> + + + );