feat: add Categories page with refined layout and translation support
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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`
|
||||
);
|
||||
|
||||
@@ -849,7 +856,7 @@ const fetchCategoryDataAndRender = () => {
|
||||
|
||||
const socket = io(socketUrl, {
|
||||
path: "/socket.io/",
|
||||
transports: [ "websocket"],
|
||||
transports: ["websocket"],
|
||||
reconnection: false,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
20
src/App.js
20
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")); <Route path="/404" element={<NotFound404 />} />
|
||||
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 */}
|
||||
<Route
|
||||
path="/Kategorie/:categoryId"
|
||||
element={<Content/>}
|
||||
element={<Content />}
|
||||
/>
|
||||
{/* Single product page */}
|
||||
<Route
|
||||
path="/Artikel/:seoName"
|
||||
element={<ProductDetail/>}
|
||||
element={<ProductDetail />}
|
||||
/>
|
||||
|
||||
{/* Search page - Render Content in parallel */}
|
||||
<Route path="/search" element={<Content/>} />
|
||||
<Route path="/search" element={<Content />} />
|
||||
|
||||
{/* Profile page */}
|
||||
<Route path="/profile" element={<ProfilePage/>} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
|
||||
{/* Payment success page for Mollie redirects */}
|
||||
<Route path="/payment/success" element={<PaymentSuccess />} />
|
||||
@@ -280,22 +281,23 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
|
||||
{/* Reset password page */}
|
||||
<Route
|
||||
path="/resetPassword"
|
||||
element={<ResetPassword/>}
|
||||
element={<ResetPassword />}
|
||||
/>
|
||||
|
||||
{/* Admin page */}
|
||||
<Route path="/admin" element={<AdminPage/>} />
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
|
||||
{/* Admin Users page */}
|
||||
<Route path="/admin/users" element={<UsersPage/>} />
|
||||
<Route path="/admin/users" element={<UsersPage />} />
|
||||
|
||||
{/* Admin Server Logs page */}
|
||||
<Route path="/admin/logs" element={<ServerLogsPage/>} />
|
||||
<Route path="/admin/logs" element={<ServerLogsPage />} />
|
||||
|
||||
{/* Legal pages */}
|
||||
<Route path="/datenschutz" element={<Datenschutz />} />
|
||||
<Route path="/agb" element={<AGB />} />
|
||||
<Route path="/sitemap" element={<Sitemap />} />
|
||||
<Route path="/Kategorien" element={<CategoriesPage />} />
|
||||
<Route path="/impressum" element={<Impressum />} />
|
||||
<Route
|
||||
path="/batteriegesetzhinweise"
|
||||
@@ -304,7 +306,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
|
||||
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
|
||||
|
||||
{/* Grow Tent Configurator */}
|
||||
<Route path="/Konfigurator" element={<GrowTentKonfigurator/>} />
|
||||
<Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
|
||||
|
||||
{/* Separate pages that are truly different */}
|
||||
<Route path="/presseverleih" element={<PresseverleihPage />} />
|
||||
|
||||
118
src/PrerenderCategoriesPage.js
Normal file
118
src/PrerenderCategoriesPage.js
Normal 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;
|
||||
@@ -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";
|
||||
@@ -60,9 +61,9 @@ 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;
|
||||
@@ -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 (
|
||||
<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
|
||||
variant="h4"
|
||||
component="h1"
|
||||
component="span"
|
||||
sx={{
|
||||
mb: 2,
|
||||
fontFamily: "SwashingtonCP",
|
||||
color: "primary.main",
|
||||
textAlign: "center",
|
||||
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
|
||||
}}
|
||||
>
|
||||
{t('navigation.categories')}
|
||||
</Typography>
|
||||
<ChevronRight sx={{ fontSize: "2.5rem", ml: 1 }} />
|
||||
</Box>
|
||||
|
||||
<div
|
||||
className="carousel-wrapper"
|
||||
|
||||
@@ -53,12 +53,12 @@ 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({
|
||||
@@ -76,7 +76,7 @@ class CategoryList extends Component {
|
||||
|
||||
|
||||
setLevel1CategoryId = (input) => {
|
||||
if(input) {
|
||||
if (input) {
|
||||
const language = this.props.languageContext?.currentLanguage || this.props.i18n.language;
|
||||
const categoryTreeCache = window.categoryService.getSync(209, language);
|
||||
|
||||
@@ -385,7 +385,7 @@ class CategoryList extends Component {
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : ( !isMobile && (
|
||||
) : (!isMobile && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="inherit"
|
||||
@@ -545,7 +545,7 @@ class CategoryList extends Component {
|
||||
fontWeight: "bold",
|
||||
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>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
|
||||
|
||||
231
src/pages/CategoriesPage.js
Normal file
231
src/pages/CategoriesPage.js
Normal 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);
|
||||
Reference in New Issue
Block a user