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 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() {
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
Reference in New Issue
Block a user