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 */}