import { PureComponent } from 'react';
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_SIZE, Tile, tileBaseSx } from 'dashgui/tiles';
import { formatPrice } from '../../utils/price';
import { articleSku } from '../article/articleRoute';
import {
getThumbHashDataUrl,
getCachedImage,
isImageLoaded,
markImageLoaded,
} from '../article/ThumbHashImage';
import { BrandLabel } from '../brand';
import { thumb } from '../../api';
const MIN_EDGE_STRETCH = 10;
function drawContainedWithStretchedEdges(
ctx,
source,
srcW,
srcH,
dstW,
dstH,
minStretch = MIN_EDGE_STRETCH,
) {
ctx.clearRect(0, 0, dstW, dstH);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
const stretch = Math.min(minStretch, Math.max(0, Math.floor(Math.min(dstW, dstH) / 4)));
const innerW = dstW - stretch * 2;
const innerH = dstH - stretch * 2;
const scale = Math.min(innerW / srcW, innerH / srcH);
const fitW = srcW * scale;
const fitH = srcH * scale;
const offsetX = (dstW - fitW) / 2;
const offsetY = (dstH - fitH) / 2;
const leftW = Math.ceil(offsetX);
const rightX = Math.floor(offsetX + fitW);
const rightW = dstW - rightX;
const topH = Math.ceil(offsetY);
const botY = Math.floor(offsetY + fitH);
const botH = dstH - botY;
// Edge fill: a strongly blurred layer, crossfaded smoothly into a lightly
// blurred layer near the image (masked by a blurred rect, no visible seams).
// Rects overdraw past the canvas and under the center so blur never fades
// into transparency.
const maxBlur = Math.max(2, stretch * 0.6);
const drawEdges = (c, blur) => {
const pad = Math.ceil(blur * 3);
c.filter = `blur(${blur}px)`;
// 1. Four corners (1x1 source border pixels stretched)
c.drawImage(source, 0, 0, 1, 1, -pad, -pad, leftW + pad * 2, topH + pad * 2);
c.drawImage(source, srcW - 1, 0, 1, 1, rightX - pad, -pad, rightW + pad * 2, topH + pad * 2);
c.drawImage(source, 0, srcH - 1, 1, 1, -pad, botY - pad, leftW + pad * 2, botH + pad * 2);
c.drawImage(
source, srcW - 1, srcH - 1, 1, 1,
rightX - pad, botY - pad, rightW + pad * 2, botH + pad * 2,
);
// 2. Four edge bars (stretched 1px border rows/columns)
c.drawImage(source, 0, 0, srcW, 1, offsetX, -pad, fitW, topH + pad * 2);
c.drawImage(source, 0, srcH - 1, srcW, 1, offsetX, botY - pad, fitW, botH + pad * 2);
c.drawImage(source, 0, 0, 1, srcH, -pad, offsetY, leftW + pad * 2, fitH);
c.drawImage(source, srcW - 1, 0, 1, srcH, rightX - pad, offsetY, rightW + pad * 2, fitH);
c.filter = 'none';
};
drawEdges(ctx, maxBlur);
const light = document.createElement('canvas');
light.width = dstW;
light.height = dstH;
const lctx = light.getContext('2d');
if (lctx) {
drawEdges(lctx, Math.max(0.5, maxBlur * 0.15));
// Mask: blurred rect around the image -> opaque near it, fading outward.
const grow = maxBlur * 0.5;
lctx.globalCompositeOperation = 'destination-in';
lctx.filter = `blur(${maxBlur}px)`;
lctx.fillStyle = '#000';
lctx.fillRect(
offsetX - grow,
offsetY - grow,
fitW + grow * 2,
fitH + grow * 2,
);
lctx.filter = 'none';
ctx.drawImage(light, 0, 0);
}
// 3. Center image (aspect ratio preserved / contained)
ctx.drawImage(source, 0, 0, srcW, srcH, offsetX, offsetY, fitW, fitH);
}
class ArticleTileBackdrop extends PureComponent {
canvasRef = null;
img = null;
observer = null;
constructor(props) {
super(props);
this.thumbUrl = getThumbHashDataUrl(props.thumbHash);
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() {
this.loadImage();
}
componentDidUpdate(prevProps) {
if (prevProps.thumbHash !== this.props.thumbHash) {
this.thumbUrl = getThumbHashDataUrl(this.props.thumbHash);
}
if (prevProps.fullHash !== this.props.fullHash) {
this.img = getCachedImage(this.props.fullHash);
this.loadImage();
}
}
componentWillUnmount() {
this.observer?.disconnect();
this.img = null;
}
loadImage() {
const { fullHash } = this.props;
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 = () => {
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);
this.paint();
}
}
setCanvas = (canvas) => {
this.canvasRef = canvas;
if (canvas && typeof ResizeObserver !== 'undefined') {
this.observer?.disconnect();
this.observer = new ResizeObserver(() => this.paint());
this.observer.observe(canvas);
}
this.paint();
};
paint = () => {
const canvas = this.canvasRef;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const dstW = Math.round(rect.width) || canvas.clientWidth || TILE_SIZE;
const dstH = Math.round(rect.height) || canvas.clientHeight || TILE_SIZE;
if (!dstW || !dstH) return;
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 && this.img.complete && this.img.naturalWidth)
? this.img
: getCachedImage(this.props.fullHash);
if (img && img.complete && img.naturalWidth && img.naturalHeight) {
this.img = img;
drawContainedWithStretchedEdges(
ctx,
img,
img.naturalWidth,
img.naturalHeight,
dstW,
dstH,
);
}
};
render() {
const { fullHash } = this.props;
const showThumb = !isImageLoaded(fullHash) && Boolean(this.thumbUrl);
return (
{showThumb ? (
) : null}
);
}
}
export class ArticleTile extends Tile {
hasPhoto() {
const { article } = this.props;
return article?.thumbHash != null || article?.fullHash != null;
}
renderIcon() {
return ;
}
renderSecondary() {
const { article } = this.props;
if (!article) return null;
const sku = articleSku(article);
const brand = article.brand;
return (
<>
{brand?.name ? (
) : null}
{sku ? (
{sku}
) : null}
{article.net != null && (
{formatPrice(article.net)}
)}
>
);
}
renderExtra() {
const { article } = this.props;
if (!article) return null;
const isParent = Boolean(article.isParent || article.is_parent);
const childCount = Number(article.childCount) || 0;
if (!isParent && childCount <= 0) return null;
return (
{childCount}
);
}
render() {
const hasPhoto = this.hasPhoto();
const { article, label, sx, disabled } = this.props;
const sku = articleSku(article);
return (
{hasPhoto ? (
) : (
{this.renderIcon()}
)}
{this.renderExtra()}
{article?.brand?.name ? (
) : null}
{label ? (
{label}
) : null}
{sku ? (
{sku}
) : null}
{article.net != null && (
{formatPrice(article.net)}
)}
);
}
}