import React, { Component } from 'react'; import Container from '@mui/material/Container'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { Link } from 'react-router-dom'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import ProductFilters from './ProductFilters.js'; import ProductList from './ProductList.js'; import CategoryBoxGrid from './CategoryBoxGrid.js'; import CategoryBox from './CategoryBox.js'; import { useParams, useSearchParams } from 'react-router-dom'; import { getAllSettingsWithPrefix } from '../utils/sessionStorage.js'; import { withI18n } from '../i18n/withTranslation.js'; import { withCategory } from '../context/CategoryContext.js'; const isNew = (neu) => neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000); // @note SwashingtonCP font is now loaded globally via index.css const withRouter = (ClassComponent) => { return (props) => { const params = useParams(); const [searchParams] = useSearchParams(); return ; }; }; function getCachedCategoryData(categoryId, language = 'de') { if (!window.productCache) { window.productCache = {}; } try { const cacheKey = `categoryProducts_${categoryId}_${language}`; const cachedData = window.productCache[cacheKey]; if (cachedData) { const { timestamp } = cachedData; const cacheAge = Date.now() - timestamp; const tenMinutes = 10 * 60 * 1000; if (cacheAge < tenMinutes) { return cachedData; } } } catch (err) { console.error('Error reading from cache:', err); } return null; } function getFilteredProducts(unfilteredProducts, attributes, t) { const attributeSettings = getAllSettingsWithPrefix('filter_attribute_'); const manufacturerSettings = getAllSettingsWithPrefix('filter_manufacturer_'); const availabilitySettings = getAllSettingsWithPrefix('filter_availability_'); const attributeFilters = []; Object.keys(attributeSettings).forEach(key => { if (attributeSettings[key] === 'true') { attributeFilters.push(key.split('_')[2]); } }); const manufacturerFilters = []; Object.keys(manufacturerSettings).forEach(key => { if (manufacturerSettings[key] === 'true') { manufacturerFilters.push(key.split('_')[2]); } }); const availabilityFilters = []; Object.keys(availabilitySettings).forEach(key => { if (availabilitySettings[key] === 'true') { availabilityFilters.push(key.split('_')[2]); } }); const uniqueAttributes = [...new Set((attributes || []).map(attr => attr.kMerkmalWert ? attr.kMerkmalWert.toString() : ''))]; const uniqueManufacturers = [...new Set((unfilteredProducts || []).filter(product => product.manufacturerId).map(product => product.manufacturerId ? product.manufacturerId.toString() : ''))]; const uniqueManufacturersWithName = [...new Set((unfilteredProducts || []).filter(product => product.manufacturerId).map(product => ({id:product.manufacturerId ? product.manufacturerId.toString() : '',value:product.manufacturer})))]; const activeAttributeFilters = attributeFilters.filter(filter => uniqueAttributes.includes(filter)); const activeManufacturerFilters = manufacturerFilters.filter(filter => uniqueManufacturers.includes(filter)); const attributeFiltersByGroup = {}; for (const filterId of activeAttributeFilters) { const attribute = attributes.find(attr => attr.kMerkmalWert.toString() === filterId); if (attribute) { if (!attributeFiltersByGroup[attribute.cName]) { attributeFiltersByGroup[attribute.cName] = []; } attributeFiltersByGroup[attribute.cName].push(filterId); } } let filteredProducts = (unfilteredProducts || []).filter(product => { const availabilityFilter = sessionStorage.getItem('filter_availability'); let inStockMatch = availabilityFilter == 1 ? true : (product.available>0); // Check if there are any new products in the entire set const hasNewProducts = (unfilteredProducts || []).some(product => isNew(product.neu)); // Only apply the new filter if there are actually new products and the filter is active const isNewMatch = availabilityFilters.includes('2') && hasNewProducts ? isNew(product.neu) : true; let soonMatch = availabilityFilters.includes('3') ? !product.available && product.incoming : true; const soon2Match = (availabilityFilter != 1)&&availabilityFilters.includes('3') ? (product.available) || (!product.available && product.incoming) : true; if( (availabilityFilter != 1)&&availabilityFilters.includes('3') && ((product.available) || (!product.available && product.incoming))){ inStockMatch = true; soonMatch = true; console.log("soon2Match", product.cName); } const manufacturerMatch = activeManufacturerFilters.length === 0 || (product.manufacturerId && activeManufacturerFilters.includes(product.manufacturerId.toString())); if (Object.keys(attributeFiltersByGroup).length === 0) { return manufacturerMatch && soon2Match && inStockMatch && soonMatch && isNewMatch; } const productAttributes = attributes .filter(attr => attr.kArtikel === product.id); const attributeMatch = Object.entries(attributeFiltersByGroup).every(([groupName, groupFilters]) => { const productGroupAttributes = productAttributes .filter(attr => attr.cName === groupName) .map(attr => attr.kMerkmalWert ? attr.kMerkmalWert.toString() : ''); return groupFilters.some(filter => productGroupAttributes.includes(filter)); }); return manufacturerMatch && attributeMatch && soon2Match && inStockMatch && soonMatch && isNewMatch; }); const activeAttributeFiltersWithNames = activeAttributeFilters.map(filter => { const attribute = attributes.find(attr => attr.kMerkmalWert.toString() === filter); return {name: attribute.cName, value: attribute.cWert, id: attribute.kMerkmalWert}; }); const activeManufacturerFiltersWithNames = activeManufacturerFilters.map(filter => { const manufacturer = uniqueManufacturersWithName.find(manufacturer => manufacturer.id === filter); return {name: manufacturer.value, value: manufacturer.id}; }); // Extract active availability filters const availabilityFilter = sessionStorage.getItem('filter_availability'); const activeAvailabilityFilters = []; // Check if there are actually products with these characteristics const hasNewProducts = (unfilteredProducts || []).some(product => isNew(product.neu)); const hasComingSoonProducts = (unfilteredProducts || []).some(product => !product.available && product.incoming); // Check for "auf Lager" filter (in stock) - it's active when filter_availability is NOT set to '1' if (availabilityFilter !== '1') { activeAvailabilityFilters.push({id: '1', name: t ? t('product.inStock') : 'auf Lager'}); } // Check for "Neu" filter (new) - only show if there are actually new products and filter is active if (availabilityFilters.includes('2') && hasNewProducts) { activeAvailabilityFilters.push({id: '2', name: t ? t('product.new') : 'Neu'}); } // Check for "Bald verfügbar" filter (coming soon) - only show if there are actually coming soon products and filter is active if (availabilityFilters.includes('3') && hasComingSoonProducts) { activeAvailabilityFilters.push({id: '3', name: t ? t('product.comingSoon') : 'Bald verfügbar'}); } return {filteredProducts,activeAttributeFilters:activeAttributeFiltersWithNames,activeManufacturerFilters:activeManufacturerFiltersWithNames,activeAvailabilityFilters}; } function setCachedCategoryData(categoryId, data, language = 'de') { if (!window.productCache) { window.productCache = {}; } if (!window.productDetailCache) { window.productDetailCache = {}; } try { const cacheKey = `categoryProducts_${categoryId}_${language}`; if(data.products) for(const product of data.products) { const productCacheKey = `product_${product.id}_${language}`; window.productDetailCache[productCacheKey] = product; } window.productCache[cacheKey] = { ...data, timestamp: Date.now() }; } catch (err) { console.error('Error writing to cache:', err); } } class Content extends Component { constructor(props) { super(props); this.state = { loaded: false, categoryName: null, unfilteredProducts: [], filteredProducts: [], attributes: [], childCategories: [], lastFetchedLanguage: props.i18n?.language || 'de' }; } componentDidMount() { const currentLanguage = this.props.i18n?.language || 'de'; if(this.props.params.categoryId) {this.setState({loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage}, () => { this.fetchCategoryData(this.props.params.categoryId); })} else if (this.props.searchParams?.get('q')) { this.setState({loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage}, () => { this.fetchSearchData(this.props.searchParams?.get('q')); }) } } componentDidUpdate(prevProps) { const currentLanguage = this.props.i18n?.language || 'de'; const categoryChanged = this.props.params.categoryId && (prevProps.params.categoryId !== this.props.params.categoryId); const searchChanged = this.props.searchParams?.get('q') && (prevProps.searchParams?.get('q') !== this.props.searchParams?.get('q')); if(categoryChanged) { // Clear context for new category loading if (this.props.categoryContext && this.props.categoryContext.setCurrentCategory) { this.props.categoryContext.setCurrentCategory(null); } window.currentSearchQuery = null; this.setState({loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage}, () => { this.fetchCategoryData(this.props.params.categoryId); }); return; // Don't check language change if category changed } else if (searchChanged) { this.setState({loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage}, () => { this.fetchSearchData(this.props.searchParams?.get('q')); }); return; // Don't check language change if search changed } // Re-fetch products when language changes to get translated content const languageChanged = currentLanguage !== this.state.lastFetchedLanguage; console.log('Content componentDidUpdate:', { languageChanged, lastFetchedLang: this.state.lastFetchedLanguage, currentLang: currentLanguage, prevPropsLang: prevProps.i18n?.language, hasCategoryId: !!this.props.params.categoryId, categoryId: this.props.params.categoryId, hasSearchQuery: !!this.props.searchParams?.get('q') }); if(languageChanged) { console.log('Content: Language changed! Re-fetching data...'); // Re-fetch current data with new language // Note: Language is now part of the cache key, so it will automatically fetch fresh data if(this.props.params.categoryId) { // Re-fetch category data with new language console.log('Content: Re-fetching category', this.props.params.categoryId); this.setState({loaded: false, lastFetchedLanguage: currentLanguage}, () => { this.fetchCategoryData(this.props.params.categoryId); }); } else if(this.props.searchParams?.get('q')) { // Re-fetch search data with new language console.log('Content: Re-fetching search', this.props.searchParams?.get('q')); this.setState({loaded: false, lastFetchedLanguage: currentLanguage}, () => { this.fetchSearchData(this.props.searchParams?.get('q')); }); } else { // If not viewing category or search, just re-filter existing products console.log('Content: Just re-filtering existing products'); this.setState({lastFetchedLanguage: currentLanguage}); this.filterProducts(); } } } processData(response) { const unfilteredProducts = response.products; if (!window.individualProductCache) { window.individualProductCache = {}; } //console.log("processData", unfilteredProducts); if(unfilteredProducts) unfilteredProducts.forEach(product => { window.individualProductCache[product.id] = { data: product, timestamp: Date.now() }; }); this.setState({ unfilteredProducts: unfilteredProducts, ...getFilteredProducts( unfilteredProducts, response.attributes, this.props.t ), categoryName: response.categoryName || response.name || null, dataType: response.dataType, dataParam: response.dataParam, attributes: response.attributes, childCategories: response.childCategories || [], loaded: true }, () => { console.log('Content: processData finished', { hasContext: !!this.props.categoryContext, categoryName: response.categoryName, name: response.name }); if (this.props.categoryContext && this.props.categoryContext.setCurrentCategory) { if (response.categoryName || response.name) { console.log('Content: Setting category context'); this.props.categoryContext.setCurrentCategory({ id: this.props.params.categoryId, name: response.categoryName || response.name }); } else { console.log('Content: No category name found to set in context'); } } else { console.warn('Content: categoryContext prop is missing!'); } }); } fetchCategoryData(categoryId) { const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de'; const cachedData = getCachedCategoryData(categoryId, currentLanguage); if (cachedData) { this.processDataWithCategoryTree(cachedData, categoryId); return; } console.log(`productList:${categoryId}`); window.socketManager.off(`productList:${categoryId}`); // Track if we've received the full response to ignore stub response if needed let receivedFullResponse = false; window.socketManager.on(`productList:${categoryId}`,(response) => { console.log("getCategoryProducts full response", response); receivedFullResponse = true; setCachedCategoryData(categoryId, response, currentLanguage); if (response && response.products !== undefined) { this.processDataWithCategoryTree(response, categoryId); } else { console.log("fetchCategoryData in Content failed", response); } }); window.socketManager.emit( "getCategoryProducts", { categoryId: categoryId, language: currentLanguage, requestTranslation: currentLanguage === 'de' ? false : true }, (response) => { console.log("getCategoryProducts stub response", response); // Only process stub response if we haven't received the full response yet if (!receivedFullResponse) { setCachedCategoryData(categoryId, response, currentLanguage); if (response && response.products !== undefined) { this.processDataWithCategoryTree(response, categoryId); } else { console.log("fetchCategoryData in Content failed", response); } } else { console.log("Ignoring stub response - full response already received"); } } ); } processDataWithCategoryTree(response, categoryId) { console.log("---------------processDataWithCategoryTree", response, categoryId); // Get child categories from the cached category tree let childCategories = []; try { const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de'; const categoryTreeCache = window.categoryService.getSync(209, currentLanguage); if (categoryTreeCache) { // If categoryId is a string (SEO name), find by seoName, otherwise by ID const targetCategory = typeof categoryId === 'string' ? this.findCategoryBySeoName(categoryTreeCache, categoryId) : this.findCategoryById(categoryTreeCache, categoryId); if (targetCategory && targetCategory.children) { childCategories = targetCategory.children; } } } catch (err) { console.error('Error getting child categories from tree:', err); } // Add child categories to the response const enhancedResponse = { ...response, childCategories }; // Attempt to set category name from the tree if missing in response if (!enhancedResponse.categoryName && !enhancedResponse.name) { // Try to find name in the tree using the ID or SEO name try { const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de'; const categoryTreeCache = window.categoryService.getSync(209, currentLanguage); if (categoryTreeCache) { const targetCategory = typeof categoryId === 'string' ? this.findCategoryBySeoName(categoryTreeCache, categoryId) : this.findCategoryById(categoryTreeCache, categoryId); if (targetCategory && targetCategory.name) { enhancedResponse.categoryName = targetCategory.name; } } } catch (err) { console.error('Error finding category name in tree:', err); } } this.processData(enhancedResponse); } findCategoryById(category, targetId) { if (!category) return null; if (category.id === targetId) { return category; } if (category.children) { for (let child of category.children) { const found = this.findCategoryById(child, targetId); if (found) return found; } } return null; } fetchSearchData(query) { const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de'; window.socketManager.emit( "getSearchProducts", { query, language: currentLanguage, requestTranslation: currentLanguage === 'de' ? false : true }, (response) => { if (response && response.products) { this.processData(response); } else { console.log("fetchSearchData in Content failed", response); } } ); } filterProducts() { this.setState({ ...getFilteredProducts( this.state.unfilteredProducts, this.state.attributes, this.props.t ) }); } // Helper function to find category by seoName 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; } // Helper function to get current category ID from seoName getCurrentCategoryId = () => { const seoName = this.props.params.categoryId; // Get the category tree from cache const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de'; const categoryTreeCache = window.categoryService.getSync(209, currentLanguage); // Find the category by seoName const category = this.findCategoryBySeoName(categoryTreeCache, seoName); return category ? category.id : null; } componentWillUnmount() { if (this.props.categoryContext && this.props.categoryContext.setCurrentCategory) { this.props.categoryContext.setCurrentCategory(null); } } renderParentCategoryNavigation = () => { const currentCategoryId = this.getCurrentCategoryId(); if (!currentCategoryId) return null; // Get the category tree from cache const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de'; const categoryTreeCache = window.categoryService.getSync(209, currentLanguage); // Find the current category in the tree const currentCategory = this.findCategoryById(categoryTreeCache, currentCategoryId); if (!currentCategory) { return null; } // Check if this category has a parent (not root category 209) if (!currentCategory.parentId || currentCategory.parentId === 209) { return null; // Don't show for top-level categories } // Find the parent category const parentCategory = this.findCategoryById(categoryTreeCache, currentCategory.parentId); if (!parentCategory) { return null; } // Create parent category object for CategoryBox const parentCategoryForDisplay = { id: parentCategory.id, seoName: parentCategory.seoName, name: parentCategory.name, image: parentCategory.image, isParentNav: true }; return parentCategoryForDisplay; } render() { // console.log('Content props:', this.props); // Check if we should show category boxes instead of product list const showCategoryBoxes = this.state.loaded && this.state.unfilteredProducts.length === 0 && this.state.childCategories.length > 0; console.log("showCategoryBoxes", showCategoryBoxes, this.state.unfilteredProducts.length, this.state.childCategories.length); return ( {showCategoryBoxes ? ( // Show category boxes layout when no products but have child categories ) : ( <> {/* Show subcategories above main layout when there are both products and child categories */} {this.state.loaded && this.state.unfilteredProducts.length > 0 && this.state.childCategories.length > 0 && ( {(() => { const parentCategory = this.renderParentCategoryNavigation(); if (parentCategory) { // Show parent category to the left of subcategories return ( {/* Parent Category Box */} {/* Up Arrow Overlay */} {/* Subcategories Grid */} ); } else { // No parent category, just show subcategories return ; } })()} )} {/* Show standalone parent category navigation when there are only products */} {this.state.loaded && this.props.params.categoryId && !(this.state.unfilteredProducts.length > 0 && this.state.childCategories.length > 0) && (() => { const parentCategory = this.renderParentCategoryNavigation(); if (parentCategory) { return ( {/* Up Arrow Overlay */} ); } return null; })()} {/* Show normal product list layout */} {this.filterProducts()}} dataType={this.state.dataType} dataParam={this.state.dataParam} /> {(this.props.params.categoryId == 'Stecklinge' || this.props.params.categoryId == 'Seeds') && {this.props.t ? this.props.t('navigation.otherCategories') : 'Andere Kategorien'} } {this.props.params.categoryId == 'Stecklinge' && {/* Image Container - Place your seeds image here */} Seeds {/* Overlay text - optional */} {this.props.t('sections.seeds')} } {this.props.params.categoryId == 'Seeds' && {/* Image Container - Place your cutlings image here */} Stecklinge {/* Overlay text - optional */} {this.props.t('sections.stecklinge')} } {this.filterProducts()}} dataType={this.state.dataType} dataParam={this.state.dataParam} /> )} ); } } export default withRouter(withI18n()(withCategory(Content)));