diff --git a/src/components/ProductCarousel.js b/src/components/ProductCarousel.js
index cea9d13..e3c7401 100644
--- a/src/components/ProductCarousel.js
+++ b/src/components/ProductCarousel.js
@@ -14,6 +14,55 @@ 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 = [];
@@ -41,52 +90,282 @@ class ProductCarousel extends React.Component {
this._isMounted = true;
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language;
- console.log("ProductCarousel componentDidMount: Loading products for categoryId", this.props.categoryId, "language", currentLanguage);
+ 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) {
- console.log("ProductCarousel componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage);
- if (prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) {
+ 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(this.props.languageContext?.currentLanguage || this.props.i18n.language);
+ 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;
- window.socketManager.emit(
- "getCategoryProducts",
- {
- categoryId: categoryId === "neu" ? "neu" : categoryId,
- language: language,
- requestTranslation: language === 'de' ? false : true
- },
- (response) => {
- console.log("ProductCarousel getCategoryProducts response:", response);
- if (this._isMounted && response && response.products && response.products.length > 0) {
- // Filter products to only show those with pictures
- const productsWithPictures = response.products.filter(product =>
- product.pictureList && product.pictureList.length > 0
- );
- console.log("ProductCarousel: Filtered", productsWithPictures.length, "products with pictures from", response.products.length, "total");
+ logCarousel("loadProducts:start", {
+ language,
+ categoryId,
+ ids,
+ filter_availability: filterAvailability,
+ });
- if (productsWithPictures.length > 0) {
- // Take random 15 products and shuffle them
- const shuffledProducts = this.shuffleArray(productsWithPictures.slice(0, 15));
- console.log("ProductCarousel: Selected and shuffled", shuffledProducts.length, "products");
+ 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;
+ }
- this.originalProducts = shuffledProducts;
- // Duplicate for seamless looping
- this.products = [...shuffledProducts, ...shuffledProducts];
- this.setState({ products: this.products });
- this.startAutoScroll();
+ 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() {
diff --git a/src/components/SharedCarousel.js b/src/components/SharedCarousel.js
index 40d735b..7fb1a8c 100644
--- a/src/components/SharedCarousel.js
+++ b/src/components/SharedCarousel.js
@@ -418,8 +418,8 @@ class SharedCarousel extends React.Component {
- {/* Product Carousel for "neu" category */}
-
+ {/* Product Carousel: virtual categories Neuheiten + Demnächst */}
+
{/* Manufacturer logo carousel */}