774 lines
23 KiB
JavaScript
774 lines
23 KiB
JavaScript
import React from 'react';
|
|
import { Link } from "react-router-dom";
|
|
import Box from "@mui/material/Box";
|
|
import Typography from "@mui/material/Typography";
|
|
import IconButton from "@mui/material/IconButton";
|
|
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
|
import ChevronRight from "@mui/icons-material/ChevronRight";
|
|
import Product from "./Product.js";
|
|
import { withTranslation } from 'react-i18next';
|
|
import { withLanguage } from '../i18n/withTranslation.js';
|
|
import {
|
|
getProductCarouselItemStridePx,
|
|
PRODUCT_CARD_WIDTH_SM_PX,
|
|
PRODUCT_CARD_WIDTH_XS_PX,
|
|
} from "../utils/productCardLayout.js";
|
|
const AUTO_SCROLL_SPEED = 0.5; // px per frame (~60fps, so ~30px/sec)
|
|
const AUTOSCROLL_RESTART_DELAY = 5000; // 5 seconds of inactivity before restarting autoscroll
|
|
const SCROLLBAR_FLASH_DURATION = 3000; // 3 seconds to show the virtual scrollbar
|
|
|
|
const CAROUSEL_LOG = "[ProductCarousel]";
|
|
|
|
function logCarousel(phase, detail) {
|
|
try {
|
|
console.log(CAROUSEL_LOG, phase, detail != null ? detail : "");
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
/** Debug summary of getCategoryProducts callback / socket payload. */
|
|
function summarizeResponse(response, maxProducts = 8) {
|
|
if (response == null) {
|
|
return { type: "null" };
|
|
}
|
|
if (typeof response !== "object") {
|
|
return { type: typeof response, value: response };
|
|
}
|
|
const keys = Object.keys(response);
|
|
const products = response.products;
|
|
const n = Array.isArray(products) ? products.length : null;
|
|
const sample =
|
|
Array.isArray(products) && products.length > 0
|
|
? products.slice(0, maxProducts).map((p) => ({
|
|
id: p?.id,
|
|
seoName: p?.seoName,
|
|
available: p?.available,
|
|
availableSupplier: p?.availableSupplier,
|
|
hasPictureList: Boolean(
|
|
p?.pictureList &&
|
|
(typeof p.pictureList === "string"
|
|
? p.pictureList.trim().length > 0
|
|
: Array.isArray(p.pictureList)
|
|
? p.pictureList.length > 0
|
|
: false)
|
|
),
|
|
}))
|
|
: [];
|
|
return { keys, productsLength: n, sample };
|
|
}
|
|
|
|
function productHasImage(p) {
|
|
const pl = p?.pictureList;
|
|
if (!pl) return false;
|
|
if (typeof pl === "string") return pl.trim().length > 0;
|
|
if (Array.isArray(pl)) return pl.length > 0;
|
|
return false;
|
|
}
|
|
|
|
class ProductCarousel extends React.Component {
|
|
_isMounted = false;
|
|
products = [];
|
|
originalProducts = [];
|
|
animationFrame = null;
|
|
autoScrollActive = true;
|
|
translateX = 0;
|
|
inactivityTimer = null;
|
|
scrollbarTimer = null;
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
const { i18n } = props;
|
|
|
|
this.state = {
|
|
products: [],
|
|
currentLanguage: (i18n && i18n.language) || 'de',
|
|
showScrollbar: false,
|
|
itemStride:
|
|
typeof window !== "undefined"
|
|
? getProductCarouselItemStridePx()
|
|
: PRODUCT_CARD_WIDTH_SM_PX + 16,
|
|
};
|
|
|
|
this.carouselTrackRef = React.createRef();
|
|
}
|
|
|
|
handleCarouselResize = () => {
|
|
if (!this._isMounted) return;
|
|
const next = getProductCarouselItemStridePx();
|
|
if (next !== this.state.itemStride) {
|
|
this.translateX = 0;
|
|
this.updateTrackTransform();
|
|
this.setState({ itemStride: next });
|
|
}
|
|
};
|
|
|
|
componentDidMount() {
|
|
this._isMounted = true;
|
|
if (typeof window !== "undefined") {
|
|
window.addEventListener("resize", this.handleCarouselResize);
|
|
this.setState({ itemStride: getProductCarouselItemStridePx() });
|
|
}
|
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language;
|
|
|
|
logCarousel("mount", {
|
|
categoryId: this.props.categoryId,
|
|
language: currentLanguage,
|
|
filter_availability:
|
|
typeof sessionStorage !== "undefined"
|
|
? sessionStorage.getItem("filter_availability")
|
|
: "(no sessionStorage)",
|
|
});
|
|
this.loadProducts(currentLanguage);
|
|
}
|
|
|
|
categoryIdsEqual(a, b) {
|
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
}
|
|
return a === b;
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
const lang = this.props.languageContext?.currentLanguage || this.props.i18n.language;
|
|
const langChanged =
|
|
prevProps.languageContext?.currentLanguage !==
|
|
this.props.languageContext?.currentLanguage;
|
|
const catChanged = !this.categoryIdsEqual(
|
|
prevProps.categoryId,
|
|
this.props.categoryId
|
|
);
|
|
if (langChanged || catChanged) {
|
|
logCarousel("didUpdate:reload", {
|
|
langChanged,
|
|
catChanged,
|
|
prevCategoryId: prevProps.categoryId,
|
|
categoryId: this.props.categoryId,
|
|
language: lang,
|
|
});
|
|
}
|
|
if (langChanged) {
|
|
this.setState({ products: [] }, () => {
|
|
this.loadProducts(lang);
|
|
});
|
|
return;
|
|
}
|
|
if (catChanged) {
|
|
this.setState({ products: [] }, () => {
|
|
this.loadProducts(lang);
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Virtual slugs go to the socket as strings; other ids pass through. */
|
|
normalizeSocketCategoryId(id) {
|
|
if (id === "neu" || id === "bald") return id;
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* neu: stub only (no `full`). bald: `full: true` — one emit ack with the full list.
|
|
* Carousel does not subscribe to `productList:*` (no second update).
|
|
*/
|
|
buildGetCategoryProductsPayload(id, language) {
|
|
const slug = this.normalizeSocketCategoryId(id);
|
|
const payload = {
|
|
categoryId: slug,
|
|
language,
|
|
requestTranslation: language === "de" ? false : true,
|
|
};
|
|
if (slug === "bald") {
|
|
payload.full = true;
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
/** Single getCategoryProducts response (emit ack only). */
|
|
emitGetCategoryProductsOnce = (rawId, language, onAck) => {
|
|
const slug = this.normalizeSocketCategoryId(rawId);
|
|
const payload = this.buildGetCategoryProductsPayload(rawId, language);
|
|
logCarousel("emit getCategoryProducts (ack only)", { slug, payload });
|
|
window.socketManager.emit("getCategoryProducts", payload, (response) => {
|
|
logCarousel("getCategoryProducts CALLBACK", {
|
|
slug,
|
|
summary: summarizeResponse(response),
|
|
});
|
|
onAck(response);
|
|
});
|
|
};
|
|
|
|
finalizeCarouselList = (products) => {
|
|
if (!this._isMounted) {
|
|
logCarousel("finalizeCarouselList:skip (unmounted)", {
|
|
incomingLen: products?.length,
|
|
});
|
|
return;
|
|
}
|
|
if (!products || products.length === 0) {
|
|
logCarousel("finalizeCarouselList:skip (empty)", {});
|
|
return;
|
|
}
|
|
const shuffledProducts = this.shuffleArray(products).slice(0, 15);
|
|
logCarousel("finalizeCarouselList:ok", {
|
|
inputCount: products.length,
|
|
afterShuffleCap15: shuffledProducts.length,
|
|
ids: shuffledProducts.map((p) => p?.id),
|
|
});
|
|
|
|
this.originalProducts = shuffledProducts;
|
|
this.products = [...shuffledProducts, ...shuffledProducts];
|
|
this.setState({ products: this.products });
|
|
this.startAutoScroll();
|
|
};
|
|
|
|
processLoadedProducts = (rawProducts, sourceTag = "processLoadedProducts") => {
|
|
if (!this._isMounted) {
|
|
logCarousel(`${sourceTag}:skip (unmounted)`, {
|
|
rawLen: rawProducts?.length,
|
|
});
|
|
return;
|
|
}
|
|
if (!rawProducts || rawProducts.length === 0) {
|
|
logCarousel(`${sourceTag}:skip (empty products)`, {});
|
|
return;
|
|
}
|
|
logCarousel(`${sourceTag}:incoming`, {
|
|
count: rawProducts.length,
|
|
idsSample: rawProducts.slice(0, 12).map((p) => p?.id),
|
|
});
|
|
this.finalizeCarouselList(rawProducts.slice());
|
|
};
|
|
|
|
/** Merge neu + bald; neu ids win on duplicate. */
|
|
processNeuAndBaldMerged = (neuRaw, baldRaw) => {
|
|
const neuList = neuRaw || [];
|
|
const baldList = baldRaw || [];
|
|
|
|
const seen = new Set();
|
|
const merged = [];
|
|
for (const p of neuList) {
|
|
if (p && p.id != null && !seen.has(p.id)) {
|
|
seen.add(p.id);
|
|
merged.push(p);
|
|
}
|
|
}
|
|
let baldWithImage = 0;
|
|
for (const p of baldList) {
|
|
if (!p || p.id == null || seen.has(p.id)) continue;
|
|
if (!productHasImage(p)) continue;
|
|
baldWithImage += 1;
|
|
seen.add(p.id);
|
|
merged.push(p);
|
|
}
|
|
|
|
logCarousel("processNeuAndBaldMerged:counts", {
|
|
neu: neuList.length,
|
|
bald: baldList.length,
|
|
baldWithImage,
|
|
mergedUnique: merged.length,
|
|
neuIdsSample: neuList.slice(0, 8).map((p) => p?.id),
|
|
baldIdsSample: baldList.slice(0, 8).map((p) => p?.id),
|
|
});
|
|
|
|
if (merged.length === 0) {
|
|
logCarousel("processNeuAndBaldMerged:skip (merged empty)", {});
|
|
return;
|
|
}
|
|
this.finalizeCarouselList(merged);
|
|
};
|
|
|
|
loadProducts = (language) => {
|
|
const { categoryId } = this.props;
|
|
const ids = Array.isArray(categoryId) ? categoryId : [categoryId];
|
|
const filterAvailability =
|
|
typeof sessionStorage !== "undefined"
|
|
? sessionStorage.getItem("filter_availability")
|
|
: null;
|
|
|
|
logCarousel("loadProducts:start", {
|
|
language,
|
|
categoryId,
|
|
ids,
|
|
filter_availability: filterAvailability,
|
|
});
|
|
|
|
if (ids.length === 1) {
|
|
const slug = this.normalizeSocketCategoryId(ids[0]);
|
|
this.emitGetCategoryProductsOnce(ids[0], language, (response) => {
|
|
let products = response?.products ?? [];
|
|
if (slug === "bald") {
|
|
const before = products.length;
|
|
products = products.filter(productHasImage);
|
|
logCarousel("loadProducts:single:bald filter image", {
|
|
before,
|
|
after: products.length,
|
|
});
|
|
}
|
|
if (products.length > 0) {
|
|
this.processLoadedProducts(products, "single");
|
|
} else {
|
|
logCarousel("loadProducts:single: empty", { slug });
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
const neuBaldMode =
|
|
ids.length === 2 &&
|
|
ids.map((x) => this.normalizeSocketCategoryId(x)).sort().join(",") ===
|
|
"bald,neu";
|
|
|
|
if (neuBaldMode) {
|
|
logCarousel("loadProducts:mode neu+bald (stub neu + full bald → merge → shuffle → max 15)", {
|
|
ids,
|
|
});
|
|
|
|
let neuProducts = null;
|
|
let baldProducts = null;
|
|
|
|
const tryMerge = () => {
|
|
if (neuProducts === null || baldProducts === null) {
|
|
return;
|
|
}
|
|
logCarousel("loadProducts:neu+bald merge inputs", {
|
|
neuLen: neuProducts.length,
|
|
baldLen: baldProducts.length,
|
|
});
|
|
this.processNeuAndBaldMerged(neuProducts, baldProducts);
|
|
};
|
|
|
|
this.emitGetCategoryProductsOnce("neu", language, (response) => {
|
|
neuProducts = response?.products ?? [];
|
|
tryMerge();
|
|
});
|
|
this.emitGetCategoryProductsOnce("bald", language, (response) => {
|
|
baldProducts = response?.products ?? [];
|
|
tryMerge();
|
|
});
|
|
return;
|
|
}
|
|
|
|
logCarousel("loadProducts:mode generic merge", { ids });
|
|
|
|
const mergedById = new Map();
|
|
let remaining = ids.length;
|
|
|
|
const onResponse = (response, idForLog) => {
|
|
logCarousel("loadProducts:merge CALLBACK", {
|
|
id: idForLog,
|
|
summary: summarizeResponse(response),
|
|
});
|
|
let products = response?.products ?? [];
|
|
if (this.normalizeSocketCategoryId(idForLog) === "bald") {
|
|
products = products.filter(productHasImage);
|
|
}
|
|
if (products.length > 0) {
|
|
for (const p of products) {
|
|
if (p && p.id != null && !mergedById.has(p.id)) {
|
|
mergedById.set(p.id, p);
|
|
}
|
|
}
|
|
}
|
|
remaining -= 1;
|
|
if (remaining === 0) {
|
|
logCarousel("loadProducts:merge complete", {
|
|
unique: mergedById.size,
|
|
idsSample: Array.from(mergedById.keys()).slice(0, 15),
|
|
});
|
|
this.processLoadedProducts(
|
|
Array.from(mergedById.values()),
|
|
"genericMerge"
|
|
);
|
|
}
|
|
};
|
|
|
|
ids.forEach((id) => {
|
|
this.emitGetCategoryProductsOnce(id, language, (response) => {
|
|
onResponse(response, id);
|
|
});
|
|
});
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this._isMounted = false;
|
|
if (typeof window !== "undefined") {
|
|
window.removeEventListener("resize", this.handleCarouselResize);
|
|
}
|
|
this.stopAutoScroll();
|
|
this.clearInactivityTimer();
|
|
this.clearScrollbarTimer();
|
|
}
|
|
|
|
startAutoScroll = () => {
|
|
this.autoScrollActive = true;
|
|
if (!this.animationFrame) {
|
|
this.animationFrame = requestAnimationFrame(this.handleAutoScroll);
|
|
}
|
|
};
|
|
|
|
stopAutoScroll = () => {
|
|
this.autoScrollActive = false;
|
|
if (this.animationFrame) {
|
|
cancelAnimationFrame(this.animationFrame);
|
|
this.animationFrame = null;
|
|
}
|
|
};
|
|
|
|
clearInactivityTimer = () => {
|
|
if (this.inactivityTimer) {
|
|
clearTimeout(this.inactivityTimer);
|
|
this.inactivityTimer = null;
|
|
}
|
|
};
|
|
|
|
clearScrollbarTimer = () => {
|
|
if (this.scrollbarTimer) {
|
|
clearTimeout(this.scrollbarTimer);
|
|
this.scrollbarTimer = null;
|
|
}
|
|
};
|
|
|
|
startInactivityTimer = () => {
|
|
this.clearInactivityTimer();
|
|
this.inactivityTimer = setTimeout(() => {
|
|
if (this._isMounted) {
|
|
this.startAutoScroll();
|
|
}
|
|
}, AUTOSCROLL_RESTART_DELAY);
|
|
};
|
|
|
|
showScrollbarFlash = () => {
|
|
this.clearScrollbarTimer();
|
|
this.setState({ showScrollbar: true });
|
|
|
|
this.scrollbarTimer = setTimeout(() => {
|
|
if (this._isMounted) {
|
|
this.setState({ showScrollbar: false });
|
|
}
|
|
}, SCROLLBAR_FLASH_DURATION);
|
|
};
|
|
|
|
handleAutoScroll = () => {
|
|
if (!this.autoScrollActive || this.originalProducts.length === 0) return;
|
|
|
|
this.translateX -= AUTO_SCROLL_SPEED;
|
|
this.updateTrackTransform();
|
|
|
|
const { itemStride } = this.state;
|
|
const originalItemCount = this.originalProducts.length;
|
|
const maxScroll = itemStride * originalItemCount;
|
|
|
|
// Check if we've scrolled past the first set of items
|
|
if (Math.abs(this.translateX) >= maxScroll) {
|
|
// Reset to beginning seamlessly
|
|
this.translateX = 0;
|
|
this.updateTrackTransform();
|
|
}
|
|
|
|
this.animationFrame = requestAnimationFrame(this.handleAutoScroll);
|
|
};
|
|
|
|
updateTrackTransform = () => {
|
|
if (this.carouselTrackRef.current) {
|
|
this.carouselTrackRef.current.style.transform = `translateX(${this.translateX}px)`;
|
|
}
|
|
};
|
|
|
|
handleLeftClick = () => {
|
|
this.stopAutoScroll();
|
|
this.scrollBy(1);
|
|
this.showScrollbarFlash();
|
|
this.startInactivityTimer();
|
|
};
|
|
|
|
handleRightClick = () => {
|
|
this.stopAutoScroll();
|
|
this.scrollBy(-1);
|
|
this.showScrollbarFlash();
|
|
this.startInactivityTimer();
|
|
};
|
|
|
|
scrollBy = (direction) => {
|
|
if (this.originalProducts.length === 0) return;
|
|
|
|
// direction: 1 = left (scroll content right), -1 = right (scroll content left)
|
|
const { itemStride } = this.state;
|
|
const originalItemCount = this.originalProducts.length;
|
|
const maxScroll = itemStride * originalItemCount;
|
|
|
|
this.translateX += direction * itemStride;
|
|
|
|
// Handle wrap-around when scrolling left (positive translateX)
|
|
if (this.translateX > 0) {
|
|
this.translateX = -(maxScroll - itemStride);
|
|
}
|
|
// Handle wrap-around when scrolling right (negative translateX beyond limit)
|
|
else if (Math.abs(this.translateX) >= maxScroll) {
|
|
this.translateX = 0;
|
|
}
|
|
|
|
this.updateTrackTransform();
|
|
|
|
// Force scrollbar to update immediately after wrap-around
|
|
if (this.state.showScrollbar) {
|
|
this.forceUpdate();
|
|
}
|
|
};
|
|
|
|
renderVirtualScrollbar = () => {
|
|
if (!this.state.showScrollbar || this.originalProducts.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const { itemStride } = this.state;
|
|
const originalItemCount = this.originalProducts.length;
|
|
const viewportWidth =
|
|
typeof window !== "undefined"
|
|
? Math.min(1080, Math.max(0, window.innerWidth - 56))
|
|
: 1080;
|
|
const itemsInView = Math.max(1, Math.floor(viewportWidth / itemStride));
|
|
|
|
// Calculate which item is currently at the left edge (first visible)
|
|
let currentItemIndex;
|
|
|
|
if (this.translateX === 0) {
|
|
currentItemIndex = 0;
|
|
} else if (this.translateX > 0) {
|
|
const maxScroll = itemStride * originalItemCount;
|
|
const effectivePosition = maxScroll + this.translateX;
|
|
currentItemIndex = Math.floor(effectivePosition / itemStride);
|
|
} else {
|
|
currentItemIndex = Math.floor(Math.abs(this.translateX) / itemStride);
|
|
}
|
|
|
|
// Ensure we stay within bounds
|
|
currentItemIndex = Math.max(0, Math.min(currentItemIndex, originalItemCount - 1));
|
|
|
|
// Calculate scrollbar position
|
|
const lastPossibleFirstItem = Math.max(0, originalItemCount - itemsInView);
|
|
const thumbPosition = lastPossibleFirstItem > 0 ? Math.min((currentItemIndex / lastPossibleFirstItem) * 100, 100) : 0;
|
|
|
|
return (
|
|
<div
|
|
className="virtual-scrollbar"
|
|
style={{
|
|
position: 'absolute',
|
|
bottom: '5px',
|
|
left: '50%',
|
|
transform: 'translateX(-50%)',
|
|
width: '200px',
|
|
height: '4px',
|
|
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
|
borderRadius: '2px',
|
|
zIndex: 1000,
|
|
opacity: this.state.showScrollbar ? 1 : 0,
|
|
transition: 'opacity 0.3s ease-in-out'
|
|
}}
|
|
>
|
|
<div
|
|
className="scrollbar-thumb"
|
|
style={{
|
|
position: 'absolute',
|
|
top: '0',
|
|
left: `${thumbPosition}%`,
|
|
width: '20px',
|
|
height: '4px',
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
borderRadius: '2px',
|
|
transform: 'translateX(-50%)',
|
|
transition: 'left 0.2s ease-out'
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
render() {
|
|
const { t, title } = this.props;
|
|
const { products } = this.state;
|
|
|
|
if (!products || products.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ mt: 3 }}>
|
|
<Box
|
|
component={Link}
|
|
to="/Kategorie/neu"
|
|
sx={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
textDecoration: "none",
|
|
color: "primary.main",
|
|
mb: 2,
|
|
transition: "all 0.3s ease",
|
|
"&:hover": {
|
|
transform: "translateX(5px)",
|
|
color: "primary.dark"
|
|
}
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="h4"
|
|
component="span"
|
|
sx={{
|
|
fontFamily: "SwashingtonCP",
|
|
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
|
|
}}
|
|
>
|
|
{title || t('product.new')}
|
|
</Typography>
|
|
<ChevronRight sx={{ fontSize: "2.5rem", ml: 1 }} />
|
|
</Box>
|
|
|
|
<div
|
|
className="product-carousel-wrapper"
|
|
style={{
|
|
position: 'relative',
|
|
overflowX: 'hidden',
|
|
overflowY: 'visible',
|
|
width: '100%',
|
|
maxWidth: '1200px',
|
|
margin: '0 auto',
|
|
padding: '0 20px',
|
|
boxSizing: 'border-box'
|
|
}}
|
|
>
|
|
{/* Left Arrow */}
|
|
<IconButton
|
|
aria-label="Vorherige Produkte anzeigen"
|
|
onClick={this.handleLeftClick}
|
|
style={{
|
|
position: 'absolute',
|
|
top: '50%',
|
|
left: '8px',
|
|
transform: 'translateY(-50%)',
|
|
zIndex: 1000,
|
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
|
width: '48px',
|
|
height: '48px',
|
|
borderRadius: '50%'
|
|
}}
|
|
>
|
|
<ChevronLeft />
|
|
</IconButton>
|
|
|
|
{/* Right Arrow */}
|
|
<IconButton
|
|
aria-label="Nächste Produkte anzeigen"
|
|
onClick={this.handleRightClick}
|
|
style={{
|
|
position: 'absolute',
|
|
top: '50%',
|
|
right: '8px',
|
|
transform: 'translateY(-50%)',
|
|
zIndex: 1000,
|
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
|
width: '48px',
|
|
height: '48px',
|
|
borderRadius: '50%'
|
|
}}
|
|
>
|
|
<ChevronRight />
|
|
</IconButton>
|
|
|
|
<div
|
|
className="product-carousel-container"
|
|
style={{
|
|
position: 'relative',
|
|
overflowX: 'hidden',
|
|
overflowY: 'visible',
|
|
padding: '20px 0',
|
|
width: '100%',
|
|
maxWidth: '1080px',
|
|
margin: '0 auto',
|
|
zIndex: 1,
|
|
boxSizing: 'border-box'
|
|
}}
|
|
>
|
|
<div
|
|
className="product-carousel-track"
|
|
ref={this.carouselTrackRef}
|
|
style={{
|
|
display: 'flex',
|
|
gap: '16px',
|
|
transition: 'none',
|
|
alignItems: 'flex-start',
|
|
width: 'fit-content',
|
|
overflow: 'visible',
|
|
position: 'relative',
|
|
transform: 'translateX(0px)',
|
|
margin: '0 auto'
|
|
}}
|
|
>
|
|
{products.map((product, index) => (
|
|
<Box
|
|
key={`${product.id}-${index}`}
|
|
className="product-carousel-item"
|
|
sx={{
|
|
flex: {
|
|
xs: `0 0 ${PRODUCT_CARD_WIDTH_XS_PX}px`,
|
|
sm: `0 0 ${PRODUCT_CARD_WIDTH_SM_PX}px`,
|
|
},
|
|
width: { xs: PRODUCT_CARD_WIDTH_XS_PX, sm: PRODUCT_CARD_WIDTH_SM_PX },
|
|
maxWidth: { xs: PRODUCT_CARD_WIDTH_XS_PX, sm: PRODUCT_CARD_WIDTH_SM_PX },
|
|
minWidth: { xs: PRODUCT_CARD_WIDTH_XS_PX, sm: PRODUCT_CARD_WIDTH_SM_PX },
|
|
boxSizing: "border-box",
|
|
position: "relative",
|
|
}}
|
|
>
|
|
<Product
|
|
id={product.id}
|
|
name={product.name}
|
|
seoName={product.seoName}
|
|
price={product.price}
|
|
currency={product.currency}
|
|
available={product.available}
|
|
manufacturer={product.manufacturer}
|
|
vat={product.vat}
|
|
cGrundEinheit={product.cGrundEinheit}
|
|
fGrundPreis={product.fGrundPreis}
|
|
incoming={product.incomingDate}
|
|
neu={product.neu}
|
|
thc={product.thc}
|
|
floweringWeeks={product.floweringWeeks}
|
|
versandklasse={product.versandklasse}
|
|
weight={product.weight}
|
|
pictureList={product.pictureList}
|
|
availableSupplier={product.availableSupplier}
|
|
komponenten={product.komponenten}
|
|
rebate={product.rebate}
|
|
categoryId={product.kategorien ? product.kategorien.split(',')[0] : undefined}
|
|
priority={index < 6 ? 'high' : 'auto'}
|
|
t={t}
|
|
/>
|
|
</Box>
|
|
))}
|
|
</div>
|
|
|
|
{/* Virtual Scrollbar */}
|
|
{this.renderVirtualScrollbar()}
|
|
</div>
|
|
</div>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Shuffle array using Fisher-Yates algorithm
|
|
shuffleArray(array) {
|
|
const shuffled = [...array];
|
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
}
|
|
return shuffled;
|
|
}
|
|
}
|
|
|
|
export default withTranslation()(withLanguage(ProductCarousel));
|
|
|