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'}
+
+
+ )}
+
-
{categories.length > 0 ? (
@@ -385,100 +385,100 @@ class CategoryList extends Component {
);
})}
>
- ) : ( !isMobile && (
-
-
-
- )
- )}
-
-
- {isMobile && (
-
- {/* Bold text (always rendered to set width) */}
-
- {this.props.t ? this.props.t('navigation.home') : 'Startseite'}
-
- {/* Thin text (positioned on top) */}
-
- {this.props.t ? this.props.t('navigation.home') : 'Startseite'}
-
+
+
+ )
+ )}
+
+
+ {isMobile && (
+
+ {/* Bold text (always rendered to set width) */}
+
+ {this.props.t ? this.props.t('navigation.home') : 'Startseite'}
- )}
-
+ {/* 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);