feat: add Categories page with refined layout and translation support

This commit is contained in:
sebseb7
2025-12-06 14:29:33 +01:00
parent 5d3e0832fe
commit e88370ff3e
6 changed files with 698 additions and 323 deletions

View File

@@ -159,6 +159,7 @@ const Batteriegesetzhinweise =
const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default; const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default;
const Sitemap = require("./src/pages/Sitemap.js").default; const Sitemap = require("./src/pages/Sitemap.js").default;
const PrerenderSitemap = require("./src/PrerenderSitemap.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 AGB = require("./src/pages/AGB.js").default;
const NotFound404 = require("./src/pages/NotFound404.js").default; const NotFound404 = require("./src/pages/NotFound404.js").default;
@@ -465,6 +466,13 @@ const renderApp = async (categoryData, socket) => {
description: "Sitemap page", description: "Sitemap page",
needsCategoryData: true, needsCategoryData: true,
}, },
{
component: PrerenderCategoriesPage,
path: "/Kategorien",
filename: "Kategorien",
description: "Categories page",
needsCategoryData: true,
},
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" }, { component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
{ component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" }, { component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" },
{ {
@@ -559,8 +567,7 @@ const renderApp = async (categoryData, socket) => {
try { try {
productData = await fetchCategoryProducts(socket, category.id); productData = await fetchCategoryProducts(socket, category.id);
console.log( console.log(
` ✅ Found ${ ` ✅ Found ${productData.products ? productData.products.length : 0
productData.products ? productData.products.length : 0
} products` } products`
); );

View File

@@ -50,6 +50,7 @@ const Datenschutz = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/D
const AGB = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/AGB.js")); const AGB = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/AGB.js"));
//const NotFound404 = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/NotFound404.js")); <Route path="/404" element={<NotFound404 />} /> //const NotFound404 = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/NotFound404.js")); <Route path="/404" element={<NotFound404 />} />
const Sitemap = lazy(() => import(/* webpackChunkName: "sitemap" */ "./pages/Sitemap.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 Impressum = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Impressum.js"));
const Batteriegesetzhinweise = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Batteriegesetzhinweise.js")); const Batteriegesetzhinweise = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Batteriegesetzhinweise.js"));
const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Widerrufsrecht.js")); const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Widerrufsrecht.js"));
@@ -296,6 +297,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
<Route path="/datenschutz" element={<Datenschutz />} /> <Route path="/datenschutz" element={<Datenschutz />} />
<Route path="/agb" element={<AGB />} /> <Route path="/agb" element={<AGB />} />
<Route path="/sitemap" element={<Sitemap />} /> <Route path="/sitemap" element={<Sitemap />} />
<Route path="/Kategorien" element={<CategoriesPage />} />
<Route path="/impressum" element={<Impressum />} /> <Route path="/impressum" element={<Impressum />} />
<Route <Route
path="/batteriegesetzhinweise" path="/batteriegesetzhinweise"

View File

@@ -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 (
<Paper
key={l1Node.id}
elevation={1}
sx={{
p: 2,
mb: 3,
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
alignItems: { xs: 'flex-start', md: 'flex-start' },
gap: 3
}}
>
{/* Level 1 Header/Box */}
<Box sx={{
minWidth: '150px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 1
}}>
<CategoryBox
id={l1Node.id}
name={l1Node.name}
seoName={l1Node.seoName}
sx={{
boxShadow: 4,
width: '150px',
height: '150px'
}}
/>
</Box>
{/* Descendants area */}
<Box sx={{ flex: 1 }}>
<Box sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2
}}>
{descendants.map((cat) => (
<CategoryBox
key={cat.id}
id={cat.id}
name={cat.name}
seoName={cat.seoName}
sx={{
width: '100px',
height: '100px',
minWidth: '100px',
minHeight: '100px',
boxShadow: 1,
fontSize: '0.9rem'
}}
/>
))}
</Box>
</Box>
</Paper>
);
};
const content = (
<Box>
<Box>
{rootTree && rootTree.children && rootTree.children.map((child) => (
renderLevel1Section(child)
))}
{(!rootTree || !rootTree.children || rootTree.children.length === 0) && (
<Typography>Keine Kategorien gefunden.</Typography>
)}
</Box>
</Box>
);
return <LegalPage title="Kategorien" content={content} />;
};
export default PrerenderCategoriesPage;

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
@@ -274,19 +275,35 @@ class SharedCarousel extends React.Component {
return ( return (
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<Box
component={Link}
to="/Kategorien"
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
textDecoration: "none",
color: "primary.main",
mb: 2,
transition: "all 0.3s ease",
"&:hover": {
transform: "translateX(5px)",
color: "primary.dark"
}
}}
>
<Typography <Typography
variant="h4" variant="h4"
component="h1" component="span"
sx={{ sx={{
mb: 2,
fontFamily: "SwashingtonCP", fontFamily: "SwashingtonCP",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)" textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}} }}
> >
{t('navigation.categories')} {t('navigation.categories')}
</Typography> </Typography>
<ChevronRight sx={{ fontSize: "2.5rem", ml: 1 }} />
</Box>
<div <div
className="carousel-wrapper" className="carousel-wrapper"

231
src/pages/CategoriesPage.js Normal file
View File

@@ -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 (
<Paper
key={l1Node.id}
elevation={1}
sx={{
p: 2,
mb: 3,
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
alignItems: { xs: 'flex-start', md: 'flex-start' },
gap: 3
}}
>
{/* Level 1 Header/Box */}
<Box sx={{
minWidth: '150px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 1
}}>
<CategoryBox
id={l1Node.id}
name={l1Node.name}
seoName={l1Node.seoName}
sx={{
boxShadow: 4,
width: '150px',
height: '150px'
}}
/>
<Typography
variant="h6"
sx={{
textAlign: 'center',
fontWeight: 'bold',
display: { xs: 'block', md: 'none' } // Only show text below box on mobile if needed, or rely on box text
}}
>
{/* Box already has text, so maybe no extra text needed here */}
</Typography>
</Box>
{/* Descendants area */}
<Box sx={{ flex: 1 }}>
<Box sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2
}}>
{descendants.map((cat) => (
<CategoryBox
key={cat.id}
id={cat.id}
name={cat.name}
seoName={cat.seoName}
sx={{
width: '100px',
height: '100px',
minWidth: '100px',
minHeight: '100px',
boxShadow: 1,
transition: 'transform 0.2s',
'&:hover': { transform: 'scale(1.05)', boxShadow: 3 },
fontSize: '0.9rem' // Smaller text for smaller boxes
}}
/>
))}
</Box>
</Box>
</Paper>
);
};
render() {
const { t } = this.props;
const { categoryTree, loading } = this.state;
const content = (
<Box>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
) : (
<Box>
{categoryTree && categoryTree.children && categoryTree.children.map((child) => (
this.renderLevel1Section(child)
))}
{(!categoryTree || !categoryTree.children || categoryTree.children.length === 0) && (
<Typography>Keine Kategorien gefunden.</Typography>
)}
</Box>
)}
</Box>
);
return <LegalPage title={t ? t('navigation.categories') : 'Kategorien'} content={content} />;
}
}
export default withI18n()(CategoriesPage);