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`
); );
@@ -849,7 +856,7 @@ const fetchCategoryDataAndRender = () => {
const socket = io(socketUrl, { const socket = io(socketUrl, {
path: "/socket.io/", path: "/socket.io/",
transports: [ "websocket"], transports: ["websocket"],
reconnection: false, reconnection: false,
timeout: 10000, timeout: 10000,
}); });

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"));
@@ -260,19 +261,19 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
{/* Category page - Render Content in parallel */} {/* Category page - Render Content in parallel */}
<Route <Route
path="/Kategorie/:categoryId" path="/Kategorie/:categoryId"
element={<Content/>} element={<Content />}
/> />
{/* Single product page */} {/* Single product page */}
<Route <Route
path="/Artikel/:seoName" path="/Artikel/:seoName"
element={<ProductDetail/>} element={<ProductDetail />}
/> />
{/* Search page - Render Content in parallel */} {/* Search page - Render Content in parallel */}
<Route path="/search" element={<Content/>} /> <Route path="/search" element={<Content />} />
{/* Profile page */} {/* Profile page */}
<Route path="/profile" element={<ProfilePage/>} /> <Route path="/profile" element={<ProfilePage />} />
{/* Payment success page for Mollie redirects */} {/* Payment success page for Mollie redirects */}
<Route path="/payment/success" element={<PaymentSuccess />} /> <Route path="/payment/success" element={<PaymentSuccess />} />
@@ -280,22 +281,23 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
{/* Reset password page */} {/* Reset password page */}
<Route <Route
path="/resetPassword" path="/resetPassword"
element={<ResetPassword/>} element={<ResetPassword />}
/> />
{/* Admin page */} {/* Admin page */}
<Route path="/admin" element={<AdminPage/>} /> <Route path="/admin" element={<AdminPage />} />
{/* Admin Users page */} {/* Admin Users page */}
<Route path="/admin/users" element={<UsersPage/>} /> <Route path="/admin/users" element={<UsersPage />} />
{/* Admin Server Logs page */} {/* Admin Server Logs page */}
<Route path="/admin/logs" element={<ServerLogsPage/>} /> <Route path="/admin/logs" element={<ServerLogsPage />} />
{/* Legal pages */} {/* Legal pages */}
<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"
@@ -304,7 +306,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} /> <Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
{/* Grow Tent Configurator */} {/* Grow Tent Configurator */}
<Route path="/Konfigurator" element={<GrowTentKonfigurator/>} /> <Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
{/* Separate pages that are truly different */} {/* Separate pages that are truly different */}
<Route path="/presseverleih" element={<PresseverleihPage />} /> <Route path="/presseverleih" element={<PresseverleihPage />} />
@@ -457,11 +459,11 @@ const App = () => {
<ProductContextProvider> <ProductContextProvider>
<CategoryContextProvider> <CategoryContextProvider>
<CssBaseline /> <CssBaseline />
<AppContent <AppContent
currentTheme={currentTheme} currentTheme={currentTheme}
dynamicTheme={dynamicTheme} dynamicTheme={dynamicTheme}
onThemeChange={handleThemeChange} onThemeChange={handleThemeChange}
/> />
</CategoryContextProvider> </CategoryContextProvider>
</ProductContextProvider> </ProductContextProvider>
</ThemeProvider> </ThemeProvider>

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";
@@ -60,9 +61,9 @@ class SharedCarousel extends React.Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
console.log("componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage); 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: [] },() => { this.setState({ categories: [] }, () => {
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); console.log("response", response);
if (response.children && response.children.length > 0) { if (response.children && response.children.length > 0) {
this.originalCategories = response.children; this.originalCategories = response.children;
@@ -268,25 +269,41 @@ class SharedCarousel extends React.Component {
const { t } = this.props; const { t } = this.props;
const { categories } = this.state; const { categories } = this.state;
if(!categories || categories.length === 0) { if (!categories || categories.length === 0) {
return null; return null;
} }
return ( return (
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<Typography <Box
variant="h4" component={Link}
component="h1" to="/Kategorien"
sx={{ sx={{
mb: 2, display: "flex",
fontFamily: "SwashingtonCP", alignItems: "center",
justifyContent: "center",
textDecoration: "none",
color: "primary.main", color: "primary.main",
textAlign: "center", mb: 2,
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)" transition: "all 0.3s ease",
"&:hover": {
transform: "translateX(5px)",
color: "primary.dark"
}
}} }}
> >
{t('navigation.categories')} <Typography
</Typography> variant="h4"
component="span"
sx={{
fontFamily: "SwashingtonCP",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}}
>
{t('navigation.categories')}
</Typography>
<ChevronRight sx={{ fontSize: "2.5rem", ml: 1 }} />
</Box>
<div <div
className="carousel-wrapper" className="carousel-wrapper"

View File

@@ -53,12 +53,12 @@ class CategoryList extends Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
console.log("componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage); 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({ this.setState({
categories: [], categories: [],
activeCategoryId: null 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); console.log("response", response);
if (response.children && response.children.length > 0) { if (response.children && response.children.length > 0) {
this.setState({ this.setState({
@@ -69,14 +69,14 @@ class CategoryList extends Component {
}); });
}); });
} }
if (prevProps.activeCategoryId !== this.props.activeCategoryId) { if (prevProps.activeCategoryId !== this.props.activeCategoryId) {
this.setLevel1CategoryId(this.props.activeCategoryId); this.setLevel1CategoryId(this.props.activeCategoryId);
} }
} }
setLevel1CategoryId = (input) => { setLevel1CategoryId = (input) => {
if(input) { if (input) {
const language = this.props.languageContext?.currentLanguage || this.props.i18n.language; const language = this.props.languageContext?.currentLanguage || this.props.i18n.language;
const categoryTreeCache = window.categoryService.getSync(209, language); const categoryTreeCache = window.categoryService.getSync(209, language);
@@ -173,141 +173,141 @@ class CategoryList extends Component {
py: 0.5, // Add vertical padding to prevent border clipping py: 0.5, // Add vertical padding to prevent border clipping
}} }}
> >
<Button <Button
component={Link} component={Link}
to="/" to="/"
color="inherit" color="inherit"
size="small" size="small"
aria-label="Zur Startseite" aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined} onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{ sx={{
fontSize: "0.75rem", fontSize: "0.75rem",
textTransform: "none", textTransform: "none",
whiteSpace: "nowrap", whiteSpace: "nowrap",
opacity: 0.9, opacity: 0.9,
mx: isMobile ? 0 : 0.5, mx: isMobile ? 0 : 0.5,
my: 0.25, my: 0.25,
minWidth: isMobile ? "100%" : "auto", minWidth: isMobile ? "100%" : "auto",
borderRadius: 1, borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center", justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease", transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)", textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative", position: "relative",
...(activeCategoryId === null && { ...(activeCategoryId === null && {
bgcolor: "#fff", bgcolor: "#fff",
textShadow: "none", textShadow: "none",
opacity: 1, opacity: 1,
}), }),
"&:hover": { "&:hover": {
opacity: 1, opacity: 1,
bgcolor: "#fff", bgcolor: "#fff",
textShadow: "none", textShadow: "none",
"& .MuiSvgIcon-root": { "& .MuiSvgIcon-root": {
color: "#2e7d32 !important", color: "#2e7d32 !important",
},
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
}, },
}} "& .bold-text": {
> color: "#2e7d32 !important",
<HomeIcon sx={{ },
fontSize: "1rem", "& .thin-text": {
mr: isMobile ? 1 : 0, color: "transparent !important",
color: activeCategoryId === null ? "#2e7d32" : "inherit" },
}} /> },
{isMobile && ( }}
<Box sx={{ position: "relative", display: "inline-block" }}> >
{/* Bold text (always rendered to set width) */} <HomeIcon sx={{
<Box fontSize: "1rem",
className="bold-text" mr: isMobile ? 1 : 0,
sx={{ color: activeCategoryId === null ? "#2e7d32" : "inherit"
fontWeight: "bold", }} />
color: activeCategoryId === null ? "#2e7d32" : "transparent", {isMobile && (
position: "relative", <Box sx={{ position: "relative", display: "inline-block" }}>
zIndex: 2, {/* Bold text (always rendered to set width) */}
}} <Box
> className="bold-text"
{this.props.t ? this.props.t('navigation.home') : 'Startseite'} sx={{
</Box> fontWeight: "bold",
{/* Thin text (positioned on top) */} color: activeCategoryId === null ? "#2e7d32" : "transparent",
<Box position: "relative",
className="thin-text" zIndex: 2,
sx={{ }}
fontWeight: "400", >
color: activeCategoryId === null ? "transparent" : "inherit", {this.props.t ? this.props.t('navigation.home') : 'Startseite'}
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
</Box> </Box>
)} {/* Thin text (positioned on top) */}
</Button> <Box
className="thin-text"
sx={{
fontWeight: "400",
color: activeCategoryId === null ? "transparent" : "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
</Box>
)}
</Button>
<Button <Button
component={Link} component={Link}
to="/Kategorie/neu" to="/Kategorie/neu"
color="inherit" color="inherit"
size="small" size="small"
aria-label="Neuheiten" aria-label="Neuheiten"
onClick={isMobile ? this.handleMobileCategoryClick : undefined} onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{ sx={{
fontSize: "0.75rem", fontSize: "0.75rem",
textTransform: "none", textTransform: "none",
whiteSpace: "nowrap", whiteSpace: "nowrap",
opacity: 0.9, opacity: 0.9,
mx: isMobile ? 0 : 0.5, mx: isMobile ? 0 : 0.5,
my: 0.25, my: 0.25,
minWidth: isMobile ? "100%" : "auto", minWidth: isMobile ? "100%" : "auto",
borderRadius: 1, borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center", justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease", transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)", textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative" position: "relative"
}} }}
> >
<FiberNewIcon sx={{ <FiberNewIcon sx={{
fontSize: "1rem", fontSize: "1rem",
mr: isMobile ? 1 : 0 mr: isMobile ? 1 : 0
}} /> }} />
{isMobile && ( {isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}> <Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */} {/* Bold text (always rendered to set width) */}
<Box <Box
className="bold-text" className="bold-text"
sx={{ sx={{
fontWeight: "bold", fontWeight: "bold",
color: "transparent", color: "transparent",
position: "relative", position: "relative",
zIndex: 2, zIndex: 2,
}} }}
> >
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'} {this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
{/* Thin text (positioned on top) */}
<Box
className="thin-text"
sx={{
fontWeight: "400",
color: "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
</Box> </Box>
)} {/* Thin text (positioned on top) */}
</Button> <Box
className="thin-text"
sx={{
fontWeight: "400",
color: "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
</Box>
)}
</Button>
{categories.length > 0 ? ( {categories.length > 0 ? (
@@ -385,100 +385,100 @@ class CategoryList extends Component {
); );
})} })}
</> </>
) : ( !isMobile && ( ) : (!isMobile && (
<Typography <Typography
variant="caption" variant="caption"
color="inherit"
sx={{
display: "inline-flex",
alignItems: "center",
height: "33px", // Match small button height
px: 1,
fontSize: "0.75rem",
opacity: 0.9,
}}
>
&nbsp;
</Typography>
)
)}
<Button
component={Link}
to="/Konfigurator"
color="inherit" color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{ sx={{
display: "inline-flex",
alignItems: "center",
height: "33px", // Match small button height
px: 1,
fontSize: "0.75rem", fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
opacity: 0.9, opacity: 0.9,
mx: isMobile ? 0 : 0.5,
my: 0.25,
minWidth: isMobile ? "100%" : "auto",
borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative",
...(activeCategoryId === null && {
bgcolor: "#fff",
textShadow: "none",
opacity: 1,
}),
"&:hover": {
opacity: 1,
bgcolor: "#fff",
textShadow: "none",
"& .MuiSvgIcon-root": {
color: "#2e7d32 !important",
},
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
},
}} }}
> >
<SettingsIcon sx={{ &nbsp;
fontSize: "1rem", </Typography>
mr: isMobile ? 1 : 0, )
color: activeCategoryId === null ? "#2e7d32" : "inherit" )}
}} /> <Button
{isMobile && ( component={Link}
<Box sx={{ position: "relative", display: "inline-block" }}> to="/Konfigurator"
{/* Bold text (always rendered to set width) */} color="inherit"
<Box size="small"
className="bold-text" aria-label="Zur Startseite"
sx={{ onClick={isMobile ? this.handleMobileCategoryClick : undefined}
fontWeight: "bold", sx={{
color: activeCategoryId === null ? "#2e7d32" : "transparent", fontSize: "0.75rem",
position: "relative", textTransform: "none",
zIndex: 2, whiteSpace: "nowrap",
}} opacity: 0.9,
> mx: isMobile ? 0 : 0.5,
{this.props.t ? this.props.t('navigation.home') : 'Startseite'} my: 0.25,
</Box> minWidth: isMobile ? "100%" : "auto",
{/* Thin text (positioned on top) */} borderRadius: 1,
<Box justifyContent: isMobile ? "flex-start" : "center",
className="thin-text" transition: "all 0.2s ease",
sx={{ textShadow: "0 1px 2px rgba(0,0,0,0.3)",
fontWeight: "400", position: "relative",
color: activeCategoryId === null ? "transparent" : "inherit", ...(activeCategoryId === null && {
position: "absolute", bgcolor: "#fff",
top: 0, textShadow: "none",
left: 0, opacity: 1,
zIndex: 1, }),
}} "&:hover": {
> opacity: 1,
{this.props.t ? this.props.t('navigation.home') : 'Startseite'} bgcolor: "#fff",
</Box> textShadow: "none",
"& .MuiSvgIcon-root": {
color: "#2e7d32 !important",
},
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
},
}}
>
<SettingsIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0,
color: activeCategoryId === null ? "#2e7d32" : "inherit"
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: activeCategoryId === null ? "#2e7d32" : "transparent",
position: "relative",
zIndex: 2,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box> </Box>
)} {/* Thin text (positioned on top) */}
</Button> <Box
className="thin-text"
sx={{
fontWeight: "400",
color: activeCategoryId === null ? "transparent" : "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
</Box>
)}
</Button>
</Box> </Box>
); );
@@ -545,7 +545,7 @@ class CategoryList extends Component {
fontWeight: "bold", fontWeight: "bold",
textShadow: "0 1px 2px rgba(0,0,0,0.3)" textShadow: "0 1px 2px rgba(0,0,0,0.3)"
}}> }}>
{this.props.t ? this.props.t('navigation.categories') : 'Kategorien'} {this.props.t ? this.props.t('navigation.categories') : 'Kategorien'}
</Typography> </Typography>
<Box sx={{ display: "flex", alignItems: "center" }}> <Box sx={{ display: "flex", alignItems: "center" }}>
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />} {mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}

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);