feat(navigation): enhance article category handling and product navigation

- Introduce state management for article categories in App component to track active categories.
- Implement logic to clear article category state when navigating away from article pages.
- Update Product component to navigate to article pages with associated category information in the state.
- Modify Header and CategoryList components to accommodate new category handling logic.
- Ensure ProductCarousel and ProductDetailPage components receive and utilize category IDs for improved product organization.
This commit is contained in:
sebseb7
2025-11-16 07:34:39 +01:00
parent 8ea2e50432
commit 0c92591d32
7 changed files with 206 additions and 75 deletions

View File

@@ -5,7 +5,7 @@ import {
Route,
Navigate,
useLocation,
useNavigate,
useNavigate
} from "react-router-dom";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
@@ -82,6 +82,9 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
const [authVersion, setAuthVersion] = useState(0);
// @note Theme customizer state for development mode
const [isThemeCustomizerOpen, setThemeCustomizerOpen] = useState(false);
// State to track active category for article pages
const [articleCategoryId, setArticleCategoryId] = useState(null);
// Remove duplicate theme state since it's passed as prop
// const [dynamicTheme, setDynamicTheme] = useState(createTheme(defaultTheme));
@@ -112,10 +115,44 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
};
}, []);
// Extract categoryId from pathname if on category route
// Clear article category when navigating away from article pages
useEffect(() => {
const isArticlePage = location.pathname.startsWith('/Artikel/');
const isCategoryPage = location.pathname.startsWith('/Kategorie/');
const isHomePage = location.pathname === '/';
// Only clear article category when navigating to non-article pages
// (but keep it when going from category to article)
if (!isArticlePage && !isCategoryPage && !isHomePage) {
setArticleCategoryId(null);
}
}, [location.pathname]);
// Read article category from navigation state (when coming from product click)
useEffect(() => {
if (location.state && location.state.articleCategoryId !== undefined) {
if (location.state.articleCategoryId !== null) {
setArticleCategoryId(location.state.articleCategoryId);
}
// Clear the state so it doesn't persist on page refresh
navigate(location.pathname, { replace: true, state: {} });
}
}, [location.state, navigate, location.pathname]);
// Extract categoryId from pathname if on category route, or use article category
const getCategoryId = () => {
const match = location.pathname.match(/^\/Kategorie\/(.+)$/);
return match ? match[1] : null;
if (match) {
return match[1];
}
// For article pages, use the article category if available
const isArticlePage = location.pathname.startsWith('/Artikel/');
if (isArticlePage && articleCategoryId) {
return articleCategoryId;
}
return null;
};
const categoryId = getCategoryId();

View File

@@ -91,7 +91,7 @@ class Header extends Component {
</Box>
</Container>
</Toolbar>
{(isHomePage || this.props.categoryId || isProfilePage || isAktionenPage || isFilialePage) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId}/>}
{(isHomePage || this.props.categoryId || isProfilePage || isAktionenPage || isFilialePage || this.props.isArtikel) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId}/>}
</AppBar>
);
}
@@ -104,10 +104,11 @@ const HeaderWithContext = (props) => {
const isProfilePage = location.pathname === '/profile';
const isAktionenPage = location.pathname === '/aktionen';
const isFilialePage = location.pathname === '/filiale';
const isArtikel = location.pathname.startsWith('/Artikel/');
return (
<Header {...props} isHomePage={isHomePage} isProfilePage={isProfilePage} isAktionenPage={isAktionenPage} isFilialePage={isFilialePage} />
<Header {...props} isHomePage={isHomePage} isArtikel={isArtikel} isProfilePage={isProfilePage} isAktionenPage={isAktionenPage} isFilialePage={isFilialePage} />
);
};

View File

@@ -7,10 +7,67 @@ import Typography from '@mui/material/Typography';
import CircularProgress from '@mui/material/CircularProgress';
import IconButton from '@mui/material/IconButton';
import AddToCartButton from './AddToCartButton.js';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { withI18n } from '../i18n/withTranslation.js';
import ZoomInIcon from '@mui/icons-material/ZoomIn';
// Helper function to find level 1 category ID from any category ID
const findLevel1CategoryId = (categoryId) => {
try {
const currentLanguage = 'de'; // Default to German
const categoryTreeCache = window.categoryService?.getSync(209, currentLanguage);
if (!categoryTreeCache || !categoryTreeCache.children) {
return null;
}
// Helper function to find category by ID and get its level 1 parent
const findCategoryAndLevel1 = (categories, targetId) => {
for (const category of categories) {
if (category.id === targetId) {
// Found the category, now find its level 1 parent
return findLevel1Parent(categoryTreeCache.children, category);
}
if (category.children && category.children.length > 0) {
const result = findCategoryAndLevel1(category.children, targetId);
if (result) return result;
}
}
return null;
};
// Helper function to find the level 1 parent (direct child of root category 209)
const findLevel1Parent = (level1Categories, category) => {
// If this category's parent is 209, it's already level 1
if (category.parentId === 209) {
return category.id;
}
// Otherwise, find the parent and check if it's level 1
for (const level1Category of level1Categories) {
if (level1Category.id === category.parentId) {
return level1Category.id;
}
// If parent has children, search recursively
if (level1Category.children && level1Category.children.length > 0) {
const result = findLevel1Parent(level1Category.children, category);
if (result) return result;
}
}
return null;
};
return findCategoryAndLevel1(categoryTreeCache.children, parseInt(categoryId));
} catch (error) {
console.error('Error finding level 1 category:', error);
return null;
}
};
class Product extends Component {
constructor(props) {
super(props);
@@ -73,6 +130,23 @@ class Product extends Component {
// In a real app, this would update a cart state in a parent component or Redux store
}
handleProductClick = (e) => {
e.preventDefault();
const { categoryId } = this.props;
// Find the level 1 category for this product
const level1CategoryId = categoryId ? findLevel1CategoryId(categoryId) : null;
// Navigate to the product page WITH the category information in the state
const navigate = this.props.navigate;
if (navigate) {
navigate(`/Artikel/${this.props.seoName}`, {
state: { articleCategoryId: level1CategoryId }
});
}
}
render() {
const {
id, name, price, available, manufacturer, seoName,
@@ -253,15 +327,15 @@ class Product extends Component {
)}
<Box
component={Link}
to={`/Artikel/${seoName}`}
onClick={this.handleProductClick}
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
textDecoration: 'none',
color: 'inherit'
color: 'inherit',
cursor: 'pointer'
}}
>
<Box sx={{
@@ -420,4 +494,10 @@ class Product extends Component {
}
}
export default withI18n()(Product);
// Wrapper component to provide navigate hook
const ProductWithNavigation = (props) => {
const navigate = useNavigate();
return <Product {...props} navigate={navigate} />;
};
export default withI18n()(ProductWithNavigation);

View File

@@ -413,6 +413,7 @@ class ProductCarousel extends React.Component {
availableSupplier={product.availableSupplier}
komponenten={product.komponenten}
rebate={product.rebate}
categoryId={product.kategorien ? product.kategorien.split(',')[0] : undefined}
priority={index < 6 ? 'high' : 'auto'}
t={t}
/>

View File

@@ -62,6 +62,7 @@ class ProductDetailPage extends Component {
if (cachedData) {
// Complete cached data found
// Clean up prerender fallback since we have cached data
if (typeof window !== "undefined" && window.__PRERENDER_FALLBACK__) {
delete window.__PRERENDER_FALLBACK__;
@@ -900,10 +901,10 @@ class ProductDetailPage extends Component {
mb: 2,
position: ["-webkit-sticky", "sticky"], // Provide both prefixed and standard
top: {
xs: "80px",
sm: "80px",
md: "80px",
lg: "80px",
xs: "110px",
sm: "110px",
md: "110px",
lg: "110px",
} /* Offset to sit below the header 120 mith menu for md and lg*/,
left: 0,
width: "100%",
@@ -1681,6 +1682,7 @@ class ProductDetailPage extends Component {
availableSupplier={product.availableSupplier}
komponenten={product.komponenten}
rebate={product.rebate}
categoryId={product.kategorien ? product.kategorien.split(',')[0] : undefined}
priority={index < 6 ? 'high' : 'auto'}
t={this.props.t}
/>

View File

@@ -475,6 +475,7 @@ class ProductList extends Component {
availableSupplier={product.availableSupplier}
komponenten={product.komponenten}
rebate={product.rebate}
categoryId={product.kategorien ? product.kategorien.split(',')[0] : undefined}
priority={index < 6 ? 'high' : 'auto'}
t={this.props.t}
/>

View File

@@ -23,6 +23,7 @@ class CategoryList extends Component {
mobileMenuOpen: false,
activeCategoryId: null // Will be set properly after categories are loaded
};
this.productCategoryCheckInterval = null;
}
componentDidMount() {
@@ -69,60 +70,70 @@ class CategoryList extends Component {
});
}
if (prevProps.activeCategoryId !== this.props.activeCategoryId) {
//detect path here
console.log("activeCategoryId updated", this.props.activeCategoryId);
this.setLevel1CategoryId(this.props.activeCategoryId);
}
}
setLevel1CategoryId = (seoName) => {
console.log("setLevel1CategoryId called with seoName:", seoName);
if(seoName) {
setLevel1CategoryId = (input) => {
if(input) {
const language = this.props.languageContext?.currentLanguage || this.props.i18n.language;
console.log("setLevel1CategoryId - using language:", language);
console.log("setLevel1CategoryId - languageContext:", this.props.languageContext);
console.log("setLevel1CategoryId - i18n.language:", this.props.i18n?.language);
const categoryTreeCache = window.categoryService.getSync(209, language);
console.log("setLevel1CategoryId - categoryTreeCache (language: " + language + "):", categoryTreeCache, seoName);
// Helper function to recursively search for seoName in category tree
const findLevel1CategoryId = (categories, targetSeoName, level1Id = null) => {
if (categoryTreeCache && categoryTreeCache.children) {
let level1CategoryId = null;
// Check if input is already a numeric level 1 category ID
const inputAsNumber = parseInt(input);
if (!isNaN(inputAsNumber)) {
// Check if this is already a level 1 category ID
const level1Category = categoryTreeCache.children.find(cat => cat.id === inputAsNumber);
if (level1Category) {
console.log("Input is already a level 1 category ID:", inputAsNumber);
level1CategoryId = inputAsNumber;
} else {
// It's a category ID, find its level 1 parent
const findLevel1FromId = (categories, targetId) => {
for (const category of categories) {
if (category.id === targetId) {
return category.parentId === 209 ? category.id : findLevel1FromId(categoryTreeCache.children, category.parentId);
}
if (category.children && category.children.length > 0) {
const result = findLevel1FromId(category.children, targetId);
if (result) return result;
}
}
return null;
};
level1CategoryId = findLevel1FromId(categoryTreeCache.children, inputAsNumber);
}
} else {
// It's an SEO name, find the level 1 category
const findLevel1FromSeoName = (categories, targetSeoName, level1Id = null) => {
for (const category of categories) {
// If we're at level 1 (direct children of root), set this as potential level1Id
const currentLevel1Id = level1Id || category.id;
// Check if current category matches the seoName
if (category.seoName === targetSeoName) {
return currentLevel1Id;
}
// If category has children, search recursively
if (category.children && category.children.length > 0) {
const result = findLevel1CategoryId(category.children, targetSeoName, currentLevel1Id);
if (result) {
return result;
}
const result = findLevel1FromSeoName(category.children, targetSeoName, currentLevel1Id);
if (result) return result;
}
}
return null;
};
level1CategoryId = findLevel1FromSeoName(categoryTreeCache.children, input);
}
// Search in the children of the root category (209)
if (categoryTreeCache && categoryTreeCache.children) {
const level1CategoryId = findLevel1CategoryId(categoryTreeCache.children, seoName);
console.log("Found level1CategoryId:", level1CategoryId, "for seoName:", seoName);
this.setState({
activeCategoryId: level1CategoryId
});
return;
}
this.setState({ activeCategoryId: null });
}else{
this.setState({ activeCategoryId: null });
}
this.setState({ activeCategoryId: null });
}
@@ -140,18 +151,16 @@ class CategoryList extends Component {
});
};
componentWillUnmount() {
if (this.productCategoryCheckInterval) {
clearInterval(this.productCategoryCheckInterval);
this.productCategoryCheckInterval = null;
}
}
render() {
const { categories, mobileMenuOpen, activeCategoryId } = this.state;
console.log("RENDER DEBUG - About to render categories:");
console.log(" categories.length:", categories.length);
if (categories.length > 0) {
console.log(" First category name:", categories[0].name);
console.log(" First category id:", categories[0].id);
}
console.log(" Current language context:", this.props.languageContext?.currentLanguage);
console.log(" Current i18n language:", this.props.i18n?.language);
const renderCategoryRow = (categories, isMobile = false) => (
<Box
sx={{