feat: Enhance ProductCarousel with logging and response summarization, update category handling to support virtual categories 'neu' and 'bald'
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -418,8 +418,8 @@ class SharedCarousel extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Carousel for "neu" category */}
|
||||
<ProductCarousel categoryId="neu" />
|
||||
{/* Product Carousel: virtual categories Neuheiten + Demnächst */}
|
||||
<ProductCarousel categoryId={["neu", "bald"]} />
|
||||
|
||||
{/* Manufacturer logo carousel */}
|
||||
<ManufacturerCarousel />
|
||||
|
||||
Reference in New Issue
Block a user