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

@@ -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 (
<Box sx={{ mt: 3 }}>
<Typography
variant="h4"
component="h1"
<Box
component={Link}
to="/Kategorien"
sx={{
mb: 2,
fontFamily: "SwashingtonCP",
display: "flex",
alignItems: "center",
justifyContent: "center",
textDecoration: "none",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
mb: 2,
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
className="carousel-wrapper"
@@ -394,7 +411,7 @@ class SharedCarousel extends React.Component {
</div>
))}
</div>
{/* Virtual Scrollbar */}
{this.renderVirtualScrollbar()}
</div>

View File

@@ -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
}}
>
<Button
component={Link}
to="/"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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",
},
<Button
component={Link}
to="/"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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",
},
}}
>
<HomeIcon 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>
{/* Thin text (positioned on top) */}
<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>
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
},
}}
>
<HomeIcon 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>
)}
</Button>
{/* Thin text (positioned on top) */}
<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
component={Link}
to="/Kategorie/neu"
color="inherit"
size="small"
aria-label="Neuheiten"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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"
}}
>
<FiberNewIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: "transparent",
position: "relative",
zIndex: 2,
}}
>
{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>
<Button
component={Link}
to="/Kategorie/neu"
color="inherit"
size="small"
aria-label="Neuheiten"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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"
}}
>
<FiberNewIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: "transparent",
position: "relative",
zIndex: 2,
}}
>
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
)}
</Button>
{/* 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>
)}
</Button>
{categories.length > 0 ? (
@@ -385,100 +385,100 @@ class CategoryList extends Component {
);
})}
</>
) : ( !isMobile && (
<Typography
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"
) : (!isMobile && (
<Typography
variant="caption"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
display: "inline-flex",
alignItems: "center",
height: "33px", // Match small button height
px: 1,
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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={{
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>
{/* Thin text (positioned on top) */}
<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>
&nbsp;
</Typography>
)
)}
<Button
component={Link}
to="/Konfigurator"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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={{
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>
)}
</Button>
{/* Thin text (positioned on top) */}
<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>
);
@@ -516,11 +516,11 @@ class CategoryList extends Component {
>
<Container maxWidth="lg" sx={{ px: 2 }}>
{/* Toggle Button */}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
py: 1,
cursor: "pointer",
"&:hover": {
@@ -530,7 +530,7 @@ class CategoryList extends Component {
onClick={this.handleMobileMenuToggle}
role="button"
tabIndex={0}
aria-label={this.props.t ?
aria-label={this.props.t ?
(mobileMenuOpen ? this.props.t('navigation.categoriesClose') : this.props.t('navigation.categoriesOpen')) :
(mobileMenuOpen ? "Kategorien schließen" : "Kategorien öffnen")
}
@@ -541,11 +541,11 @@ class CategoryList extends Component {
}
}}
>
<Typography variant="subtitle2" color="inherit" sx={{
<Typography variant="subtitle2" color="inherit" sx={{
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 />}