feat: Enhance ProductCarousel with logging and response summarization, update category handling to support virtual categories 'neu' and 'bald'

This commit is contained in:
sebseb7
2026-03-25 08:46:57 +01:00
parent af6893b5b0
commit ddc460f877
2 changed files with 310 additions and 31 deletions

View File

@@ -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 AUTOSCROLL_RESTART_DELAY = 5000; // 5 seconds of inactivity before restarting autoscroll
const SCROLLBAR_FLASH_DURATION = 3000; // 3 seconds to show the virtual scrollbar 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 { class ProductCarousel extends React.Component {
_isMounted = false; _isMounted = false;
products = []; products = [];
@@ -41,52 +90,282 @@ class ProductCarousel extends React.Component {
this._isMounted = true; this._isMounted = true;
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language; 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); 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) { componentDidUpdate(prevProps) {
console.log("ProductCarousel componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage); const lang = this.props.languageContext?.currentLanguage || this.props.i18n.language;
if (prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) { 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.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) => { loadProducts = (language) => {
const { categoryId } = this.props; const { categoryId } = this.props;
const ids = Array.isArray(categoryId) ? categoryId : [categoryId];
const filterAvailability =
typeof sessionStorage !== "undefined"
? sessionStorage.getItem("filter_availability")
: null;
window.socketManager.emit( logCarousel("loadProducts:start", {
"getCategoryProducts", language,
{ categoryId,
categoryId: categoryId === "neu" ? "neu" : categoryId, ids,
language: language, filter_availability: filterAvailability,
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");
if (productsWithPictures.length > 0) { if (ids.length === 1) {
// Take random 15 products and shuffle them const slug = this.normalizeSocketCategoryId(ids[0]);
const shuffledProducts = this.shuffleArray(productsWithPictures.slice(0, 15)); this.emitGetCategoryProductsOnce(ids[0], language, (response) => {
console.log("ProductCarousel: Selected and shuffled", shuffledProducts.length, "products"); 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; const neuBaldMode =
// Duplicate for seamless looping ids.length === 2 &&
this.products = [...shuffledProducts, ...shuffledProducts]; ids.map((x) => this.normalizeSocketCategoryId(x)).sort().join(",") ===
this.setState({ products: this.products }); "bald,neu";
this.startAutoScroll();
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() { componentWillUnmount() {

View File

@@ -418,8 +418,8 @@ class SharedCarousel extends React.Component {
</div> </div>
</div> </div>
{/* Product Carousel for "neu" category */} {/* Product Carousel: virtual categories Neuheiten + Demnächst */}
<ProductCarousel categoryId="neu" /> <ProductCarousel categoryId={["neu", "bald"]} />
{/* Manufacturer logo carousel */} {/* Manufacturer logo carousel */}
<ManufacturerCarousel /> <ManufacturerCarousel />