diff --git a/prerender.cjs b/prerender.cjs index be6ae26..e31751c 100644 --- a/prerender.cjs +++ b/prerender.cjs @@ -28,7 +28,7 @@ class CategoryService { const cacheKey = `${categoryId}_${language}`; return null; } - + async get(categoryId, language = "de") { const cacheKey = `${categoryId}_${language}`; return null; @@ -159,6 +159,7 @@ const Batteriegesetzhinweise = const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default; const Sitemap = require("./src/pages/Sitemap.js").default; const PrerenderSitemap = require("./src/PrerenderSitemap.js").default; +const PrerenderCategoriesPage = require("./src/PrerenderCategoriesPage.js").default; const AGB = require("./src/pages/AGB.js").default; const NotFound404 = require("./src/pages/NotFound404.js").default; @@ -189,7 +190,7 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback, try { const productDetails = await fetchProductDetails(workerSocket, productSeoName); - + const actualSeoName = productDetails.product.seoName || productSeoName; const productComponent = React.createElement(PrerenderProduct, { productData: productDetails, @@ -205,7 +206,7 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback, }, shopConfig.baseUrl, shopConfig); // Get category info from categoryMap if available const categoryInfo = productDetails.product.categoryId ? categoryMap[productDetails.product.categoryId] : null; - + const jsonLdScript = generateProductJsonLd({ ...productDetails.product, seoName: actualSeoName, @@ -234,9 +235,9 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback, success, workerId }; - + results.push(result); - + // Call progress callback if provided if (progressCallback) { progressCallback(result); @@ -252,14 +253,14 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback, error: error.message, workerId }; - + results.push(result); - + // Call progress callback if provided if (progressCallback) { progressCallback(result); } - + setTimeout(processNextProduct, 25); } }; @@ -291,16 +292,16 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu const barLength = 30; const filledLength = Math.round((barLength * current) / total); const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength); - + // @note Single line progress update to prevent flickering const truncatedName = productName ? ` - ${productName.substring(0, 25)}${productName.length > 25 ? '...' : ''}` : ''; - + // Build worker stats on one line let workerStats = ''; for (let i = 0; i < Math.min(maxWorkers, 8); i++) { // Limit to 8 workers to fit on screen workerStats += `W${i + 1}:${workerCounts[i]}/${workerSuccess[i]} `; } - + // Single line update without complex cursor movements process.stdout.write(`\r [${bar}] ${percentage}% (${current}/${total})${truncatedName}\n ${workerStats}${current < total ? '\x1b[1A' : '\n'}`); }; @@ -308,26 +309,26 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu // Split products among workers const productsPerWorker = Math.ceil(allProductsArray.length / maxWorkers); const workerPromises = []; - + // Initial progress bar updateProgressBar(0, totalProducts); - + for (let i = 0; i < maxWorkers; i++) { const start = i * productsPerWorker; const end = Math.min(start + productsPerWorker, allProductsArray.length); const productsForWorker = allProductsArray.slice(start, end); - + if (productsForWorker.length > 0) { const promise = renderProductWorker(productsForWorker, i + 1, (result) => { // Progress callback - called each time a product is completed completedProducts++; progressResults.push(result); lastProductName = result.productName; - + // Update per-worker counters const workerIndex = result.workerId - 1; // Convert to 0-based index workerCounts[workerIndex]++; - + if (result.success) { totalSuccessCount++; workerSuccess[workerIndex]++; @@ -335,11 +336,11 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu // Don't log errors immediately to avoid interfering with progress bar // Errors will be shown after completion } - + // Update progress bar with worker stats updateProgressBar(completedProducts, totalProducts, lastProductName); }, categoryMap); - + workerPromises.push(promise); } } @@ -347,10 +348,10 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu try { // Wait for all workers to complete await Promise.all(workerPromises); - + // Ensure final progress update updateProgressBar(totalProducts, totalProducts, lastProductName); - + // Show any errors that occurred const errorResults = progressResults.filter(r => !r.success && r.error); if (errorResults.length > 0) { @@ -359,7 +360,7 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu console.log(` - ${result.productSeoName}: ${result.error}`); }); } - + return totalSuccessCount; } catch (error) { console.error('Error in parallel rendering:', error); @@ -465,6 +466,13 @@ const renderApp = async (categoryData, socket) => { description: "Sitemap page", needsCategoryData: true, }, + { + component: PrerenderCategoriesPage, + path: "/Kategorien", + filename: "Kategorien", + description: "Categories page", + needsCategoryData: true, + }, { component: AGB, path: "/agb", filename: "agb", description: "AGB page" }, { component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" }, { @@ -559,8 +567,7 @@ const renderApp = async (categoryData, socket) => { try { productData = await fetchCategoryProducts(socket, category.id); console.log( - ` ✅ Found ${ - productData.products ? productData.products.length : 0 + ` ✅ Found ${productData.products ? productData.products.length : 0 } products` ); @@ -644,7 +651,7 @@ const renderApp = async (categoryData, socket) => { const totalProducts = allProducts.size; const numCPUs = os.cpus().length; const maxWorkers = Math.min(numCPUs, totalProducts, 8); // Cap at 8 workers to avoid overwhelming the server - + // Create category map for breadcrumbs const categoryMap = {}; allCategories.forEach(category => { @@ -653,11 +660,11 @@ const renderApp = async (categoryData, socket) => { seoName: category.seoName }; }); - + console.log( `\n📦 Rendering ${totalProducts} individual product pages using ${maxWorkers} parallel workers...` ); - + const productPagesRendered = await renderProductsInParallel( Array.from(allProducts), maxWorkers, @@ -709,21 +716,21 @@ const renderApp = async (categoryData, socket) => { // Generate products.xml (Google Shopping feed) in parallel to sitemap.xml if (allProductsData.length > 0) { console.log("\n🛒 Generating products.xml (Google Shopping feed)..."); - + try { const productsXml = generateProductsXml(allProductsData, shopConfig.baseUrl, shopConfig); - + const productsXmlPath = path.resolve(__dirname, config.outputDir, "products.xml"); - + // Write with explicit UTF-8 encoding fs.writeFileSync(productsXmlPath, productsXml, { encoding: 'utf8' }); - + console.log(`✅ products.xml generated: ${productsXmlPath}`); console.log(` - Products included: ${allProductsData.length}`); console.log(` - Format: Google Shopping RSS 2.0 feed`); console.log(` - Encoding: UTF-8`); console.log(` - Includes: title, description, price, availability, images`); - + // Verify the file is valid UTF-8 try { const verification = fs.readFileSync(productsXmlPath, 'utf8'); @@ -731,18 +738,18 @@ const renderApp = async (categoryData, socket) => { } catch (verifyError) { console.log(` - File verification: ⚠️ ${verifyError.message}`); } - + // Validate XML against Google Shopping schema try { const ProductsXmlValidator = require('./scripts/validate-products-xml.cjs'); const validator = new ProductsXmlValidator(productsXmlPath); const validationResults = await validator.validate(); - + if (validationResults.valid) { console.log(` - Schema validation: ✅ Valid Google Shopping RSS 2.0`); } else { console.log(` - Schema validation: ⚠️ ${validationResults.summary.errorCount} errors, ${validationResults.summary.warningCount} warnings`); - + // Show first few errors for quick debugging if (validationResults.errors.length > 0) { console.log(` - First error: ${validationResults.errors[0].message}`); @@ -751,7 +758,7 @@ const renderApp = async (categoryData, socket) => { } catch (validationError) { console.log(` - Schema validation: ⚠️ Validation failed: ${validationError.message}`); } - + } catch (error) { console.error(`❌ Error generating products.xml: ${error.message}`); console.log("\n⚠️ Skipping products.xml generation due to errors"); @@ -762,18 +769,18 @@ const renderApp = async (categoryData, socket) => { // Generate llms.txt (LLM-friendly markdown sitemap) and category-specific files console.log("\n🤖 Generating LLM sitemap files..."); - + try { // Generate main llms.txt overview file const llmsTxt = generateLlmsTxt(allCategories, allProductsData, shopConfig.baseUrl, shopConfig); const llmsTxtPath = path.resolve(__dirname, config.outputDir, "llms.txt"); fs.writeFileSync(llmsTxtPath, llmsTxt, { encoding: 'utf8' }); - + console.log(`✅ Main llms.txt generated: ${llmsTxtPath}`); console.log(` - Static pages: 8 pages`); console.log(` - Categories: ${allCategories.length} with links to detailed files`); console.log(` - File size: ${Math.round(llmsTxt.length / 1024)}KB`); - + // Group products by category for category-specific files const productsByCategory = {}; allProductsData.forEach((product) => { @@ -783,20 +790,20 @@ const renderApp = async (categoryData, socket) => { } productsByCategory[categoryId].push(product); }); - + // Generate category-specific LLM files with pagination let categoryFilesGenerated = 0; let totalCategoryProducts = 0; let totalPaginatedFiles = 0; - + for (const category of allCategories) { if (category.seoName) { const categoryProducts = productsByCategory[category.id] || []; const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-'); - + // Generate all paginated files for this category const categoryPages = generateAllCategoryLlmsPages(category, categoryProducts, shopConfig.baseUrl, shopConfig); - + // Write each paginated file for (const page of categoryPages) { const pagePath = path.resolve(__dirname, config.outputDir, page.fileName); @@ -814,22 +821,22 @@ const renderApp = async (categoryData, socket) => { console.log(` ✅ llms-${categorySlug}-page-*.txt - ${categoryProducts.length} products across ${pageCount} pages (${Math.round(totalSize / 1024)}KB total)`); console.log(` 📋 ${productList.fileName} - ${productList.productCount} products (${Math.round(productList.content.length / 1024)}KB)`); - + categoryFilesGenerated++; totalCategoryProducts += categoryProducts.length; } } - + console.log(` 📄 Total paginated files generated: ${totalPaginatedFiles}`); console.log(` 📦 Total products across all categories: ${totalCategoryProducts}`); - + try { const verification = fs.readFileSync(llmsTxtPath, 'utf8'); console.log(` - File verification: ✅ All files valid UTF-8`); } catch (verifyError) { console.log(` - File verification: ⚠️ ${verifyError.message}`); } - + } catch (error) { console.error(`❌ Error generating LLM sitemap files: ${error.message}`); console.log("\n⚠️ Skipping LLM sitemap generation due to errors"); @@ -849,7 +856,7 @@ const fetchCategoryDataAndRender = () => { const socket = io(socketUrl, { path: "/socket.io/", - transports: [ "websocket"], + transports: ["websocket"], reconnection: false, timeout: 10000, }); diff --git a/src/App.js b/src/App.js index 1f3978f..68c96fc 100644 --- a/src/App.js +++ b/src/App.js @@ -50,6 +50,7 @@ const Datenschutz = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/D const AGB = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/AGB.js")); //const NotFound404 = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/NotFound404.js")); } /> const Sitemap = lazy(() => import(/* webpackChunkName: "sitemap" */ "./pages/Sitemap.js")); +const CategoriesPage = lazy(() => import(/* webpackChunkName: "categories" */ "./pages/CategoriesPage.js")); const Impressum = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Impressum.js")); const Batteriegesetzhinweise = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Batteriegesetzhinweise.js")); const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Widerrufsrecht.js")); @@ -260,19 +261,19 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => { {/* Category page - Render Content in parallel */} } + element={} /> {/* Single product page */} } + element={} /> {/* Search page - Render Content in parallel */} - } /> + } /> {/* Profile page */} - } /> + } /> {/* Payment success page for Mollie redirects */} } /> @@ -280,22 +281,23 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => { {/* Reset password page */} } + element={} /> {/* Admin page */} - } /> + } /> {/* Admin Users page */} - } /> + } /> {/* Admin Server Logs page */} - } /> + } /> {/* Legal pages */} } /> } /> } /> + } /> } /> { } /> {/* Grow Tent Configurator */} - } /> + } /> {/* Separate pages that are truly different */} } /> @@ -457,11 +459,11 @@ const App = () => { - + diff --git a/src/PrerenderCategoriesPage.js b/src/PrerenderCategoriesPage.js new file mode 100644 index 0000000..62bbcbf --- /dev/null +++ b/src/PrerenderCategoriesPage.js @@ -0,0 +1,118 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Paper from '@mui/material/Paper'; +import LegalPage from './pages/LegalPage.js'; +import CategoryBox from './components/CategoryBox.js'; + +const PrerenderCategoriesPage = ({ categoryData }) => { + // Helper function to recursively collect all categories from the tree + const collectAllCategories = (categoryNode, categories = [], level = 0) => { + if (!categoryNode) return categories; + + // Add current category (skip root category 209) + if (categoryNode.id !== 209 && categoryNode.seoName) { + categories.push({ + id: categoryNode.id, + name: categoryNode.name, + seoName: categoryNode.seoName, + level: level + }); + } + + // Recursively add children + if (categoryNode.children) { + for (const child of categoryNode.children) { + collectAllCategories(child, categories, level + 1); + } + } + + return categories; + }; + + // The categoryData passed prop is the root tree (id: 209) + const rootTree = categoryData; + + const renderLevel1Section = (l1Node) => { + // Collect all descendants (excluding the L1 node itself, which collectAllCategories would include first) + const descendants = collectAllCategories(l1Node).slice(1); + + return ( + + {/* Level 1 Header/Box */} + + + + + {/* Descendants area */} + + + {descendants.map((cat) => ( + + ))} + + + + ); + }; + + const content = ( + + + {rootTree && rootTree.children && rootTree.children.map((child) => ( + renderLevel1Section(child) + ))} + {(!rootTree || !rootTree.children || rootTree.children.length === 0) && ( + Keine Kategorien gefunden. + )} + + + ); + + return ; +}; + +export default PrerenderCategoriesPage; diff --git a/src/components/SharedCarousel.js b/src/components/SharedCarousel.js index bd79be9..5ef5c7b 100644 --- a/src/components/SharedCarousel.js +++ b/src/components/SharedCarousel.js @@ -1,4 +1,5 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; @@ -27,7 +28,7 @@ class SharedCarousel extends React.Component { constructor(props) { super(props); const { i18n } = props; - + // Don't load categories in constructor - will be loaded in componentDidMount with correct language this.state = { categories: [], @@ -41,7 +42,7 @@ class SharedCarousel extends React.Component { componentDidMount() { this._isMounted = true; const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language; - + // ALWAYS reload categories to ensure correct language console.log("SharedCarousel componentDidMount: ALWAYS RELOADING categories for language", currentLanguage); window.categoryService.get(209, currentLanguage).then((response) => { @@ -60,12 +61,12 @@ class SharedCarousel extends React.Component { componentDidUpdate(prevProps) { console.log("componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage); - if(prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) { - this.setState({ categories: [] },() => { - window.categoryService.get(209,this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => { + if (prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) { + this.setState({ categories: [] }, () => { + window.categoryService.get(209, this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => { console.log("response", response); if (response.children && response.children.length > 0) { - this.originalCategories = response.children; + this.originalCategories = response.children; this.categories = [...response.children, ...response.children]; this.setState({ categories: this.categories }); this.startAutoScroll(); @@ -123,7 +124,7 @@ class SharedCarousel extends React.Component { showScrollbarFlash = () => { this.clearScrollbarTimer(); this.setState({ showScrollbar: true }); - + this.scrollbarTimer = setTimeout(() => { if (this._isMounted) { this.setState({ showScrollbar: false }); @@ -133,7 +134,7 @@ class SharedCarousel extends React.Component { handleAutoScroll = () => { if (!this.autoScrollActive || this.originalCategories.length === 0) return; - + this.translateX -= AUTO_SCROLL_SPEED; this.updateTrackTransform(); @@ -172,7 +173,7 @@ class SharedCarousel extends React.Component { scrollBy = (direction) => { if (this.originalCategories.length === 0) return; - + // direction: 1 = left (scroll content right), -1 = right (scroll content left) const originalItemCount = this.originalCategories.length; const maxScroll = ITEM_WIDTH * originalItemCount; @@ -189,7 +190,7 @@ class SharedCarousel extends React.Component { } this.updateTrackTransform(); - + // Force scrollbar to update immediately after wrap-around if (this.state.showScrollbar) { this.forceUpdate(); @@ -204,11 +205,11 @@ class SharedCarousel extends React.Component { const originalItemCount = this.originalCategories.length; const viewportWidth = 1080; // carousel container max-width const itemsInView = Math.floor(viewportWidth / ITEM_WIDTH); - + // Calculate which item is currently at the left edge (first visible) // Map translateX directly to item index using the same logic as scrollBy let currentItemIndex; - + if (this.translateX === 0) { // At the beginning - item 0 is visible currentItemIndex = 0; @@ -221,10 +222,10 @@ class SharedCarousel extends React.Component { // Normal negative scrolling - calculate which item is at left edge currentItemIndex = Math.floor(Math.abs(this.translateX) / ITEM_WIDTH); } - + // Ensure we stay within bounds currentItemIndex = Math.max(0, Math.min(currentItemIndex, originalItemCount - 1)); - + // Calculate scrollbar position: 0% when item 0 is first visible, 100% when last item is first visible const lastPossibleFirstItem = Math.max(0, originalItemCount - itemsInView); const thumbPosition = lastPossibleFirstItem > 0 ? Math.min((currentItemIndex / lastPossibleFirstItem) * 100, 100) : 0; @@ -268,25 +269,41 @@ class SharedCarousel extends React.Component { const { t } = this.props; const { categories } = this.state; - if(!categories || categories.length === 0) { + if (!categories || categories.length === 0) { return null; } return ( - - {t('navigation.categories')} - + + {t('navigation.categories')} + + +
))}
- + {/* Virtual Scrollbar */} {this.renderVirtualScrollbar()} diff --git a/src/components/header/CategoryList.js b/src/components/header/CategoryList.js index f107853..6a4c2ff 100644 --- a/src/components/header/CategoryList.js +++ b/src/components/header/CategoryList.js @@ -32,9 +32,9 @@ class CategoryList extends Component { console.log(" i18n.language:", this.props.i18n?.language); console.log(" sessionStorage i18nextLng:", typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('i18nextLng') : 'N/A'); console.log(" localStorage i18nextLng:", typeof localStorage !== 'undefined' ? localStorage.getItem('i18nextLng') : 'N/A'); - + const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language; - + // ALWAYS reload categories to ensure correct language console.log("CategoryList componentDidMount: ALWAYS RELOADING categories for language", currentLanguage); this.setState({ categories: [] }); // Clear any cached categories @@ -53,15 +53,15 @@ class CategoryList extends Component { componentDidUpdate(prevProps) { console.log("componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage); - if(prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) { + if (prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) { this.setState({ categories: [], activeCategoryId: null - },() => { - window.categoryService.get(209,this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => { + }, () => { + window.categoryService.get(209, this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => { console.log("response", response); if (response.children && response.children.length > 0) { - this.setState({ + this.setState({ categories: response.children, activeCategoryId: this.setLevel1CategoryId(this.props.activeCategoryId) }); @@ -69,14 +69,14 @@ class CategoryList extends Component { }); }); } - if (prevProps.activeCategoryId !== this.props.activeCategoryId) { - this.setLevel1CategoryId(this.props.activeCategoryId); - } + if (prevProps.activeCategoryId !== this.props.activeCategoryId) { + this.setLevel1CategoryId(this.props.activeCategoryId); + } } setLevel1CategoryId = (input) => { - if(input) { + if (input) { const language = this.props.languageContext?.currentLanguage || this.props.i18n.language; const categoryTreeCache = window.categoryService.getSync(209, language); @@ -136,7 +136,7 @@ class CategoryList extends Component { this.setState({ activeCategoryId: null }); } - + handleMobileMenuToggle = () => { this.setState(prevState => ({ @@ -173,141 +173,141 @@ class CategoryList extends Component { py: 0.5, // Add vertical padding to prevent border clipping }} > - + {/* Thin text (positioned on top) */} + + {this.props.t ? this.props.t('navigation.home') : 'Startseite'} + + + )} + - + {/* Thin text (positioned on top) */} + + {this.props.t ? this.props.t('navigation.new') : 'Neuheiten'} + + + )} + {categories.length > 0 ? ( @@ -385,100 +385,100 @@ class CategoryList extends Component { ); })} - ) : ( !isMobile && ( - -   - - ) - )} - + {/* Thin text (positioned on top) */} + + {this.props.t ? this.props.t('navigation.home') : 'Startseite'} + + + )} + ); @@ -516,11 +516,11 @@ class CategoryList extends Component { > {/* Toggle Button */} - - -{this.props.t ? this.props.t('navigation.categories') : 'Kategorien'} + {this.props.t ? this.props.t('navigation.categories') : 'Kategorien'} {mobileMenuOpen ? : } diff --git a/src/pages/CategoriesPage.js b/src/pages/CategoriesPage.js new file mode 100644 index 0000000..cdddfa2 --- /dev/null +++ b/src/pages/CategoriesPage.js @@ -0,0 +1,231 @@ +import React, { Component } from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import CircularProgress from '@mui/material/CircularProgress'; +import Paper from '@mui/material/Paper'; +import LegalPage from './LegalPage.js'; +import CategoryBox from '../components/CategoryBox.js'; +import { withI18n } from '../i18n/withTranslation.js'; + +// Helper function to recursively collect all categories from the tree +const collectAllCategories = (categoryNode, categories = [], level = 0) => { + if (!categoryNode) return categories; + + // Add current category (skip root category 209) + if (categoryNode.id !== 209 && categoryNode.seoName) { + categories.push({ + id: categoryNode.id, + name: categoryNode.name, + seoName: categoryNode.seoName, + level: level + }); + } + + // Recursively add children + if (categoryNode.children) { + for (const child of categoryNode.children) { + collectAllCategories(child, categories, level + 1); + } + } + + return categories; +}; + +// Check for cached data - handle both browser and prerender environments +const getProductCache = () => { + if (typeof window !== "undefined" && window.productCache) { + return window.productCache; + } + if ( + typeof global !== "undefined" && + global.window && + global.window.productCache + ) { + return global.window.productCache; + } + return null; +}; + +// Initialize categories from cache if available (for prerendering) +const initializeCategoryTree = (language = 'de') => { + // Try synchronous get from service first if available + if (typeof window !== "undefined" && window.categoryService) { + const syncData = window.categoryService.getSync(209, language); + if (syncData) return syncData; + } + + const productCache = getProductCache(); + // Fallback to productCache checks (mostly for prerender context if service isn't init) + const cacheKey = `categoryTree_209_${language}`; // Note: Service uses simpler keys, might mismatch if strictly relying on this + + // Check old style cache just in case + if (productCache && productCache[cacheKey]) { + const cached = productCache[cacheKey]; + if (cached.categoryTree) return cached.categoryTree; + } + + return null; +}; + +class CategoriesPage extends Component { + constructor(props) { + super(props); + + // Use languageContext if available, otherwise fallback to i18n or 'de' + const currentLanguage = props.languageContext?.currentLanguage || props.i18n?.language || 'de'; + const initialTree = initializeCategoryTree(currentLanguage); + + console.log("CategoriesPage constructor: currentLanguage =", currentLanguage); + + this.state = { + categoryTree: initialTree, + loading: !initialTree + }; + } + + componentDidMount() { + const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de'; + + // If we don't have data yet, or if we want to ensure freshness/socket connection + if (!this.state.categoryTree) { + this.fetchCategories(currentLanguage); + } + } + + componentDidUpdate(prevProps) { + const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de'; + const prevLanguage = prevProps.languageContext?.currentLanguage || prevProps.i18n?.language || 'de'; + + if (currentLanguage !== prevLanguage) { + console.log(`CategoriesPage: Language changed from ${prevLanguage} to ${currentLanguage}. Refetching.`); + this.setState({ loading: true, categoryTree: [] }); // Clear tree to force re-render/loading state + this.fetchCategories(currentLanguage); + } + } + + fetchCategories = (language) => { + // Use categoryService which handles caching and translated vs untranslated responses correctly + console.log(`CategoriesPage: Fetching categories for ${language} using categoryService`); + + window.categoryService.get(209, language).then((tree) => { + if (tree) { + this.setState({ + categoryTree: tree, + loading: false + }); + } else { + console.error('Failed to fetch categories via service'); + this.setState({ loading: false }); + } + }).catch(err => { + console.error("Error in categoryService:", err); + this.setState({ loading: false }); + }); + }; + + renderLevel1Section = (l1Node) => { + // Collect all descendants (excluding the L1 node itself, which collectAllCategories would include first) + const descendants = collectAllCategories(l1Node).slice(1); + + return ( + + {/* Level 1 Header/Box */} + + + + {/* Box already has text, so maybe no extra text needed here */} + + + + {/* Descendants area */} + + + {descendants.map((cat) => ( + + ))} + + + + ); + }; + + render() { + const { t } = this.props; + const { categoryTree, loading } = this.state; + + const content = ( + + {loading ? ( + + + + ) : ( + + {categoryTree && categoryTree.children && categoryTree.children.map((child) => ( + this.renderLevel1Section(child) + ))} + {(!categoryTree || !categoryTree.children || categoryTree.children.length === 0) && ( + Keine Kategorien gefunden. + )} + + )} + + ); + + return ; + } +} + +export default withI18n()(CategoriesPage);