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 && ( - )} - {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 (