diff --git a/package-lock.json b/package-lock.json
index a65c157..1208cb0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@mui/material": "^7.1.1",
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.1",
+ "async-mutex": "^0.5.0",
"chart.js": "^4.5.0",
"country-flag-icons": "^1.5.19",
"html-react-parser": "^5.2.5",
@@ -4200,6 +4201,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/async-mutex": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
+ "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -11829,7 +11839,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "devOptional": true,
"license": "0BSD"
},
"node_modules/type-check": {
diff --git a/package.json b/package.json
index 35d6adb..c540d5f 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"@mui/material": "^7.1.1",
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.1",
+ "async-mutex": "^0.5.0",
"chart.js": "^4.5.0",
"country-flag-icons": "^1.5.19",
"html-react-parser": "^5.2.5",
diff --git a/src/components/header/CategoryList.js b/src/components/header/CategoryList.js
index ea80a4c..143a80c 100644
--- a/src/components/header/CategoryList.js
+++ b/src/components/header/CategoryList.js
@@ -11,311 +11,129 @@ import CloseIcon from "@mui/icons-material/Close";
import { withI18n } from "../../i18n/withTranslation.js";
class CategoryList extends Component {
- findCategoryById = (category, targetId) => {
- if (!category) return null;
-
- if (category.seoName === targetId) {
- return category;
- }
-
- if (category.children) {
- for (let child of category.children) {
- const found = this.findCategoryById(child, targetId);
- if (found) return found;
- }
- }
-
- return null;
- };
-
- getPathToCategory = (category, targetId, currentPath = []) => {
- if (!category) return null;
-
- const newPath = [...currentPath, category];
-
- if (category.seoName === targetId) {
- return newPath;
- }
-
- if (category.children) {
- for (let child of category.children) {
- const found = this.getPathToCategory(child, targetId, newPath);
- if (found) return found;
- }
- }
-
- return null;
- };
-
constructor(props) {
super(props);
+ //const { i18n } = props;
+ const categories = window.categoryService.getSync(209);
- // Get current language from props (provided by withI18n HOC)
- const currentLanguage = props.languageContext?.currentLanguage || 'de';
-
- // Check for cached data during SSR/initial render
- let initialState = {
- categoryTree: null,
- level1Categories: [], // Children of category 209 (Home) - always shown
- level2Categories: [], // Children of active level 1 category
- level3Categories: [], // Children of active level 2 category
- activePath: [], // Array of active category objects for each level
- fetchedCategories: false,
- mobileMenuOpen: false, // State for mobile collapsible menu
- currentLanguage: currentLanguage,
+ this.state = {
+ categories: categories && categories.children && categories.children.length > 0 ? categories.children : [],
+ mobileMenuOpen: false,
+ activeCategoryId: null // Will be set properly after categories are loaded
};
-
- // Try to get cached data for SSR
- try {
- // @note Check both global.window (SSR) and window (browser) for cache
- const productCache = (typeof global !== "undefined" && global.window && global.window.productCache) ||
- (typeof window !== "undefined" && window.productCache);
-
- if (productCache) {
- const cacheKey = `categoryTree_209_${currentLanguage}`;
- const cachedData = productCache[cacheKey];
- if (cachedData && cachedData.categoryTree) {
- const { categoryTree, timestamp } = cachedData;
- const cacheAge = Date.now() - timestamp;
- const tenMinutes = 10 * 60 * 1000;
-
- // Use cached data if it's fresh
- if (cacheAge < tenMinutes) {
- initialState.categoryTree = categoryTree;
- initialState.fetchedCategories = true;
-
- // Process category tree to set up navigation
- const level1Categories =
- categoryTree && categoryTree.id === 209
- ? categoryTree.children || []
- : [];
- initialState.level1Categories = level1Categories;
-
- // Process active category path if needed
- if (props.activeCategoryId) {
- const activeCategory = this.findCategoryById(
- categoryTree,
- props.activeCategoryId
- );
- if (activeCategory) {
- const pathToActive = this.getPathToCategory(
- categoryTree,
- props.activeCategoryId
- );
- initialState.activePath = pathToActive
- ? pathToActive.slice(1)
- : [];
-
- if (initialState.activePath.length >= 1) {
- const level1Category = initialState.activePath[0];
- initialState.level2Categories = level1Category.children || [];
- }
-
- if (initialState.activePath.length >= 2) {
- const level2Category = initialState.activePath[1];
- initialState.level3Categories = level2Category.children || [];
- }
- }
- }
- }
- }
- }
- } catch (err) {
- console.error("Error reading cache in constructor:", err);
- }
-
- this.state = initialState;
}
componentDidMount() {
- this.fetchCategories();
+ if (!this.state.categories || this.state.categories.length === 0) {
+ window.categoryService.get(209).then((response) => {
+ console.log("response", response);
+ if (response.children && response.children.length > 0) {
+ this.setState({
+ categories: response.children,
+ activeCategoryId: this.getLevel1CategoryId(this.props.activeCategoryId)
+ });
+ }
+ });
+ } else {
+ // Categories are already loaded, set the initial activeCategoryId
+ this.setState({
+ activeCategoryId: this.getLevel1CategoryId(this.props.activeCategoryId)
+ });
+ }
}
componentDidUpdate(prevProps) {
- // Handle language changes
- const currentLanguage = this.props.languageContext?.currentLanguage || 'de';
- const prevLanguage = prevProps.languageContext?.currentLanguage || 'de';
-
- if (currentLanguage !== prevLanguage) {
- // Language changed, need to refetch categories
+ if (prevProps.activeCategoryId !== this.props.activeCategoryId) {
+ //detect path here
+ console.log("activeCategoryId updated", this.props.activeCategoryId);
+
+ // Get the active category ID of level 1 when prop is seoName
+ const level1CategoryId = this.getLevel1CategoryId(this.props.activeCategoryId);
+
this.setState({
- currentLanguage: currentLanguage,
- fetchedCategories: false,
- categoryTree: null,
- level1Categories: [],
- level2Categories: [],
- level3Categories: [],
- activePath: [],
- }, () => {
- this.fetchCategories();
+ activeCategoryId: level1CategoryId
});
- return;
- }
-
- // If activeCategoryId changes, update subcategories
- if (
- prevProps.activeCategoryId !== this.props.activeCategoryId &&
- this.state.categoryTree
- ) {
- this.processCategoryTree(this.state.categoryTree);
}
}
- fetchCategories = () => {
- if (this.state.fetchedCategories) {
- console.log('Categories already fetched, skipping');
- return;
- }
-
- const currentLanguage = this.state.currentLanguage || 'de';
- const windowObj = (typeof global !== "undefined" && global.window) ||
- (typeof window !== "undefined" && window);
+ // Helper method to get level 1 category ID from seoName or numeric ID
+ getLevel1CategoryId = (categoryIdOrSeoName) => {
+ if (!categoryIdOrSeoName) return null;
- // Ensure cache exists
- windowObj.productCache = windowObj.productCache || {};
-
- // The cache is PRERENDERED - always use it first!
- console.log('CategoryList: Checking prerendered cache', windowObj.productCache);
-
- // Use either language-specific or default cache
- const cacheKey = `categoryTree_209_${currentLanguage}`;
- const defaultCacheKey = "categoryTree_209";
-
- // Try language-specific cache first, then fall back to default
- const categoryTree =
- windowObj.productCache[cacheKey]?.categoryTree ||
- windowObj.productCache[defaultCacheKey]?.categoryTree;
-
- if (categoryTree) {
- console.log('CategoryList: Using prerendered cache');
- this.processCategoryTree(categoryTree);
- this.setState({ fetchedCategories: true });
- return;
+ // If it's already a numeric ID, check if it's a level 1 category
+ if (typeof categoryIdOrSeoName === 'number') {
+ // Check if this ID is directly under Home (209)
+ const level1Category = this.state.categories.find(cat => cat.id === categoryIdOrSeoName);
+ return level1Category ? categoryIdOrSeoName : this.findLevel1ParentId(categoryIdOrSeoName);
}
- // Only fetch if no prerendered cache exists
- console.log('CategoryList: No prerendered cache, fetching from socket');
- windowObj.productCache[cacheKey] = { fetching: true, timestamp: Date.now() };
- this.setState({ fetchedCategories: true });
- window.socketManager.emit("categoryList", { categoryId: 209, language: currentLanguage, requestTranslation: true }, (response) => {
- if (response && response.success) {
- // Use translated data if available, otherwise fall back to original
- const categoryTreeToUse = response.translation || response.categoryTree;
-
- if (categoryTreeToUse) {
- // Store in global cache with timestamp
- try {
- const cacheKey = `categoryTree_209_${currentLanguage}`;
- if (windowObj && windowObj.productCache) {
- windowObj.productCache[cacheKey] = {
- categoryTree: categoryTreeToUse,
- timestamp: Date.now(),
- fetching: false,
- };
- }
- } catch (err) {
- console.error("Error writing to cache:", err);
- }
- this.processCategoryTree(categoryTreeToUse);
- } else {
- console.error('No category tree found in response');
- // Clear cache on error
- try {
- const cacheKey = `categoryTree_209_${currentLanguage}`;
- if (windowObj && windowObj.productCache) {
- windowObj.productCache[cacheKey] = {
- categoryTree: null,
- timestamp: Date.now(),
- fetching: false,
- };
- }
- } catch (err) {
- console.error("Error writing to cache:", err);
- }
-
- this.setState({
- categoryTree: null,
- level1Categories: [],
- level2Categories: [],
- level3Categories: [],
- activePath: [],
- });
- }
- } else {
- console.error('Failed to fetch categories:', response);
- try {
- const cacheKey = `categoryTree_209_${currentLanguage}`;
- if (windowObj && windowObj.productCache) {
- windowObj.productCache[cacheKey] = {
- categoryTree: null,
- timestamp: Date.now(),
- fetching: false,
- };
- }
- } catch (err) {
- console.error("Error writing to cache:", err);
- }
-
- this.setState({
- categoryTree: null,
- level1Categories: [],
- level2Categories: [],
- level3Categories: [],
- activePath: [],
- });
+ // If it's a string (seoName), find the category and get its level 1 parent
+ if (typeof categoryIdOrSeoName === 'string') {
+ const categoryTreeCache = window.productCache && window.productCache['categoryTree_209'];
+ if (!categoryTreeCache || !categoryTreeCache.categoryTree) {
+ return null;
}
- });
- };
+
+ const category = this.findCategoryBySeoName(categoryTreeCache.categoryTree, categoryIdOrSeoName);
+ if (!category) return null;
+
+ // If the found category is already level 1 (direct child of Home)
+ if (category.parentId === 209) {
+ return category.id;
+ }
+
+ // Find the level 1 parent of this category
+ return this.findLevel1ParentId(category.id);
+ }
+
+ return null;
+ }
- processCategoryTree = (categoryTree) => {
- // Level 1 categories are always the children of category 209 (Home)
- const level1Categories =
- categoryTree && categoryTree.id === 209
- ? categoryTree.children || []
- : [];
-
- // Build the navigation path and determine what to show at each level
- let level2Categories = [];
- let level3Categories = [];
- let activePath = [];
-
- if (this.props.activeCategoryId) {
- const activeCategory = this.findCategoryById(
- categoryTree,
- this.props.activeCategoryId
- );
- if (activeCategory) {
- // Build the path from root to active category
- const pathToActive = this.getPathToCategory(
- categoryTree,
- this.props.activeCategoryId
- );
- activePath = pathToActive.slice(1); // Remove root (209) from path
-
- // Determine what to show at each level based on the path depth
- if (activePath.length >= 1) {
- // Show children of the level 1 category
- const level1Category = activePath[0];
- level2Categories = level1Category.children || [];
- }
-
- if (activePath.length >= 2) {
- // Show children of the level 2 category
- const level2Category = activePath[1];
- level3Categories = level2Category.children || [];
- }
+ // Helper method to find category by seoName (similar to Content.js)
+ findCategoryBySeoName = (categoryNode, seoName) => {
+ if (!categoryNode) return null;
+
+ if (categoryNode.seoName === seoName) {
+ return categoryNode;
+ }
+
+ if (categoryNode.children) {
+ for (const child of categoryNode.children) {
+ const found = this.findCategoryBySeoName(child, seoName);
+ if (found) return found;
}
}
+
+ return null;
+ }
- this.setState({
- categoryTree,
- level1Categories,
- level2Categories,
- level3Categories,
- activePath,
- fetchedCategories: true,
- });
- };
+ // Helper method to find the level 1 parent category ID
+ findLevel1ParentId = (categoryId) => {
+ const categoryTreeCache = window.productCache && window.productCache['categoryTree_209'];
+ if (!categoryTreeCache || !categoryTreeCache.categoryTree) {
+ return null;
+ }
+
+ const findParentPath = (node, targetId, path = []) => {
+ if (node.id === targetId) {
+ return [...path, node.id];
+ }
+
+ if (node.children) {
+ for (const child of node.children) {
+ const result = findParentPath(child, targetId, [...path, node.id]);
+ if (result) return result;
+ }
+ }
+
+ return null;
+ };
+
+ const path = findParentPath(categoryTreeCache.categoryTree, categoryId);
+ if (!path || path.length < 3) return null; // path should be [209, level1Id, ...]
+
+ return path[1]; // Return the level 1 category ID (second in path after Home/209)
+ }
handleMobileMenuToggle = () => {
this.setState(prevState => ({
@@ -331,10 +149,9 @@ class CategoryList extends Component {
};
render() {
- const { level1Categories, activePath, mobileMenuOpen } =
- this.state;
+ const { categories, mobileMenuOpen, activeCategoryId } = this.state;
- const renderCategoryRow = (categories, level = 1, isMobile = false) => (
+ const renderCategoryRow = (categories, isMobile = false) => (
- {level === 1 && (
{isMobile && (
@@ -405,7 +221,7 @@ class CategoryList extends Component {
className="bold-text"
sx={{
fontWeight: "bold",
- color: this.props.activeCategoryId === null ? "#2e7d32" : "transparent",
+ color: activeCategoryId === null ? "#2e7d32" : "transparent",
position: "relative",
zIndex: 2,
}}
@@ -417,7 +233,7 @@ class CategoryList extends Component {
className="thin-text"
sx={{
fontWeight: "400",
- color: this.props.activeCategoryId === null ? "transparent" : "inherit",
+ color: activeCategoryId === null ? "transparent" : "inherit",
position: "absolute",
top: 0,
left: 0,
@@ -429,14 +245,10 @@ class CategoryList extends Component {
)}
- )}
- {this.state.fetchedCategories && categories.length > 0 ? (
+
+ {categories.length > 0 ? (
<>
{categories.map((category) => {
- // Determine if this category is active at this level
- const isActiveAtThisLevel =
- activePath[level - 1] &&
- activePath[level - 1].id === category.id;
return (
- ) : (
- level === 1 && !isMobile && (
+ ) : ( !isMobile && (
- {/* Level 1 Categories Row - Always shown */}
- {renderCategoryRow(level1Categories, 1, false)}
-
- {/* Level 2 Categories Row - Show when level 1 is selected */}
- {/* DISABLED FOR NOW
- {level2Categories.length > 0 && (
-
- {renderCategoryRow(level2Categories, 2, false)}
-
- )}
-
- {/* Level 3 Categories Row - Show when level 2 is selected */}
- {/* DISABLED FOR NOW
- {level3Categories.length > 0 && (
-
- {renderCategoryRow(level3Categories, 3, false)}
-
- )}
- */}
+ {renderCategoryRow(categories, false)}
@@ -621,7 +414,7 @@ class CategoryList extends Component {
{/* Level 1 Categories - Only level shown in mobile menu */}
- {renderCategoryRow(level1Categories, 1, true)}
+ {renderCategoryRow(categories, true)}
diff --git a/src/services/CategoryService.js b/src/services/CategoryService.js
index 378a88d..5ae169e 100644
--- a/src/services/CategoryService.js
+++ b/src/services/CategoryService.js
@@ -1,3 +1,8 @@
+import { Mutex } from 'async-mutex';
+
+const mutex = new Mutex();
+
+
class CategoryService {
constructor() {
this.get = this.get.bind(this);
@@ -11,26 +16,37 @@ class CategoryService {
return null;
}
- get(categoryId, language = "de") {
- const cacheKey = `${categoryId}_${language}`;
- if (window.categoryCache && window.categoryCache[cacheKey]) {
- return Promise.resolve(window.categoryCache[cacheKey]);
- }
-
- return new Promise((resolve) => {
- window.socketManager.emit("categoryList", {categoryId: categoryId, language: language}, (response) => {
- console.log("CategoryService", cacheKey);
- if (response.categoryTree) {
- if (!window.categoryCache) {
- window.categoryCache = {};
+ async get(categoryId, language = "de") {
+
+ return await mutex.runExclusive(async () => {
+ console.log("mutex locked");
+
+
+ const cacheKey = `${categoryId}_${language}`;
+ if (window.categoryCache && window.categoryCache[cacheKey]) {
+ console.log("mutex unlocked and returning cached value");
+ return Promise.resolve(window.categoryCache[cacheKey]);
+ }
+
+ return new Promise((resolve) => {
+ window.socketManager.emit("categoryList", {categoryId: categoryId, language: language}, (response) => {
+ console.log("CategoryService", cacheKey);
+ if (response.categoryTree) {
+ if (!window.categoryCache) {
+ window.categoryCache = {};
+ }
+ window.categoryCache[cacheKey] = response.categoryTree;
+ console.log("mutex unlocked and returning new value");
+ resolve(response.categoryTree);
+ } else {
+ console.log("mutex unlocked and returning null");
+ resolve(null);
}
- window.categoryCache[cacheKey] = response.categoryTree;
- resolve(response.categoryTree);
- } else {
- resolve(null);
- }
+ });
});
+
});
+
}
}