feat(Context): integrate Product and Category context providers into App

- Wrapped AppContent with ProductContextProvider and CategoryContextProvider to manage product and category states.
- Added TitleUpdater component for dynamic title management.
- Enhanced Content and ProductDetailPage components to utilize the new context for setting and clearing current product and category states.
- Updated ProductDetailWithSocket to pass setCurrentProduct function from context.
This commit is contained in:
sebseb7
2025-11-19 09:25:21 +01:00
parent b599e6424b
commit 43e67ee4c4
8 changed files with 232 additions and 10 deletions

View File

@@ -0,0 +1,5 @@
---
alwaysApply: false
---
never run your own dev sever, it can be restarted with ```pm2 restart dev_seedheads_fron```
get logoutput lioke this ```pm2 log dev_seedheads_fron --lines 20 --nostream```

View File

@@ -18,6 +18,9 @@ import PaletteIcon from "@mui/icons-material/Palette";
import ScienceIcon from "@mui/icons-material/Science";
import { CarouselProvider } from "./contexts/CarouselContext.js";
import { ProductContextProvider } from "./context/ProductContext.js";
import { CategoryContextProvider } from "./context/CategoryContext.js";
import TitleUpdater from "./components/TitleUpdater.js";
import config from "./config.js";
import ScrollToTop from "./components/ScrollToTop.js";
@@ -222,6 +225,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
bgcolor: "background.default",
}}
>
<TitleUpdater />
<ScrollToTop />
<Header active categoryId={categoryId} key={authVersion} />
<Box sx={{ flexGrow: 1 }}>
@@ -450,12 +454,16 @@ const App = () => {
return (
<LanguageProvider i18n={i18n}>
<ThemeProvider theme={dynamicTheme}>
<CssBaseline />
<AppContent
currentTheme={currentTheme}
dynamicTheme={dynamicTheme}
onThemeChange={handleThemeChange}
/>
<ProductContextProvider>
<CategoryContextProvider>
<CssBaseline />
<AppContent
currentTheme={currentTheme}
dynamicTheme={dynamicTheme}
onThemeChange={handleThemeChange}
/>
</CategoryContextProvider>
</ProductContextProvider>
</ThemeProvider>
</LanguageProvider>
);

View File

@@ -14,6 +14,7 @@ import CategoryBox from './CategoryBox.js';
import { useParams, useSearchParams } from 'react-router-dom';
import { getAllSettingsWithPrefix } from '../utils/sessionStorage.js';
import { withI18n } from '../i18n/withTranslation.js';
import { withCategory } from '../context/CategoryContext.js';
const isNew = (neu) => neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
@@ -221,6 +222,11 @@ class Content extends Component {
const searchChanged = this.props.searchParams?.get('q') && (prevProps.searchParams?.get('q') !== this.props.searchParams?.get('q'));
if(categoryChanged) {
// Clear context for new category loading
if (this.props.categoryContext && this.props.categoryContext.setCurrentCategory) {
this.props.categoryContext.setCurrentCategory(null);
}
window.currentSearchQuery = null;
this.setState({loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage}, () => {
this.fetchCategoryData(this.props.params.categoryId);
@@ -299,6 +305,26 @@ class Content extends Component {
attributes: response.attributes,
childCategories: response.childCategories || [],
loaded: true
}, () => {
console.log('Content: processData finished', {
hasContext: !!this.props.categoryContext,
categoryName: response.categoryName,
name: response.name
});
if (this.props.categoryContext && this.props.categoryContext.setCurrentCategory) {
if (response.categoryName || response.name) {
console.log('Content: Setting category context');
this.props.categoryContext.setCurrentCategory({
id: this.props.params.categoryId,
name: response.categoryName || response.name
});
} else {
console.log('Content: No category name found to set in context');
}
} else {
console.warn('Content: categoryContext prop is missing!');
}
});
}
@@ -375,6 +401,27 @@ class Content extends Component {
...response,
childCategories
};
// Attempt to set category name from the tree if missing in response
if (!enhancedResponse.categoryName && !enhancedResponse.name) {
// Try to find name in the tree using the ID or SEO name
try {
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
const categoryTreeCache = window.categoryService.getSync(209, currentLanguage);
if (categoryTreeCache) {
const targetCategory = typeof categoryId === 'string'
? this.findCategoryBySeoName(categoryTreeCache, categoryId)
: this.findCategoryById(categoryTreeCache, categoryId);
if (targetCategory && targetCategory.name) {
enhancedResponse.categoryName = targetCategory.name;
}
}
} catch (err) {
console.error('Error finding category name in tree:', err);
}
}
this.processData(enhancedResponse);
}
@@ -452,6 +499,12 @@ class Content extends Component {
return category ? category.id : null;
}
componentWillUnmount() {
if (this.props.categoryContext && this.props.categoryContext.setCurrentCategory) {
this.props.categoryContext.setCurrentCategory(null);
}
}
renderParentCategoryNavigation = () => {
const currentCategoryId = this.getCurrentCategoryId();
if (!currentCategoryId) return null;
@@ -490,6 +543,7 @@ class Content extends Component {
}
render() {
// console.log('Content props:', this.props);
// Check if we should show category boxes instead of product list
const showCategoryBoxes = this.state.loaded &&
this.state.unfilteredProducts.length === 0 &&
@@ -780,4 +834,4 @@ class Content extends Component {
}
}
export default withRouter(withI18n()(Content));
export default withRouter(withI18n()(withCategory(Content)));

View File

@@ -201,6 +201,17 @@ class ProductDetailPage extends Component {
}
componentDidMount() {
// Update context with cached product if available
if (this.state.product && this.props.setCurrentProduct) {
console.log('ProductDetailPage: Setting product context from cache', this.state.product.name);
this.props.setCurrentProduct({
name: this.state.product.name,
categoryId: this.state.product.kategorien ? this.state.product.kategorien.split(',')[0] : undefined
});
} else if (this.state.product) {
console.warn('ProductDetailPage: setCurrentProduct prop is missing despite having product');
}
// Load product data if we have no product or if we're in upgrading state
if (!this.state.product || this.state.upgrading) {
this.loadProductData();
@@ -221,6 +232,11 @@ class ProductDetailPage extends Component {
componentDidUpdate(prevProps) {
// Check for seoName changes
if (prevProps.seoName !== this.props.seoName) {
// Clear context when navigating to new product
if (this.props.setCurrentProduct) {
this.props.setCurrentProduct(null);
}
this.setState(
{ product: null, loading: true, upgrading: false, error: null, imageDialogOpen: false, similarProducts: [] },
this.loadProductData
@@ -617,6 +633,17 @@ class ProductDetailPage extends Component {
komponentenLoaded: komponenten.length === 0, // If no komponenten, mark as loaded
similarProducts: res.similarProducts || []
}, () => {
// Update context
if (this.props.setCurrentProduct) {
console.log('ProductDetailPage: Setting product context from fetch', productData.name);
this.props.setCurrentProduct({
name: productData.name,
categoryId: productData.kategorien ? productData.kategorien.split(',')[0] : undefined
});
} else {
console.warn('ProductDetailPage: setCurrentProduct prop is missing after fetch');
}
if(komponenten.length > 0) {
for(const komponent of komponenten) {
this.loadKomponent(komponent.id, komponent.count);
@@ -1034,7 +1061,13 @@ class ProductDetailPage extends Component {
});
};
render() {
componentWillUnmount() {
if (this.props.setCurrentProduct) {
this.props.setCurrentProduct(null);
}
}
render() {
const { product, loading, upgrading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings, shareAnchorEl, sharePopperOpen, snackbarOpen, snackbarMessage, snackbarSeverity } =
this.state;

View File

@@ -1,15 +1,22 @@
import React from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import ProductDetailPage from './ProductDetailPage.js';
import { useProduct } from '../context/ProductContext.js';
const ProductDetailWithSocket = () => {
const { seoName } = useParams();
const navigate = useNavigate();
const location = useLocation();
const { setCurrentProduct } = useProduct();
return (
<ProductDetailPage seoName={seoName} navigate={navigate} location={location} />
<ProductDetailPage
seoName={seoName}
navigate={navigate}
location={location}
setCurrentProduct={setCurrentProduct}
/>
);
};
export default ProductDetailWithSocket;
export default ProductDetailWithSocket;

View File

@@ -0,0 +1,53 @@
import React, { Component } from 'react';
import { withProduct } from '../context/ProductContext.js';
import { withCategory } from '../context/CategoryContext.js';
// Utility function to clean product names (duplicated from ProductDetailPage to ensure consistency)
const cleanProductName = (name) => {
if (!name) return "";
// Remove patterns like " (1)", " (3)", " (10)" at the end of the string
return name.replace(/\s*\(\d+\)\s*$/, "").trim();
};
class TitleUpdater extends Component {
componentDidMount() {
this.updateTitle();
}
componentDidUpdate(prevProps) {
console.log('TitleUpdater: Update triggered', {
prevProduct: prevProps.productContext.currentProduct,
currProduct: this.props.productContext.currentProduct,
prevCategory: prevProps.categoryContext.currentCategory,
currCategory: this.props.categoryContext.currentCategory
});
if (
prevProps.productContext.currentProduct !== this.props.productContext.currentProduct ||
prevProps.categoryContext.currentCategory !== this.props.categoryContext.currentCategory
) {
this.updateTitle();
}
}
updateTitle() {
const { currentProduct } = this.props.productContext;
const { currentCategory } = this.props.categoryContext;
console.log('TitleUpdater: Updating title with', { currentProduct, currentCategory });
if (currentProduct && currentProduct.name) {
document.title = `GrowHeads.de - ${cleanProductName(currentProduct.name)}`;
} else if (currentCategory && currentCategory.name) {
document.title = `GrowHeads.de - ${currentCategory.name}`;
} else {
document.title = 'GrowHeads.de';
}
}
render() {
return null;
}
}
export default withCategory(withProduct(TitleUpdater));

View File

@@ -0,0 +1,31 @@
import React, { createContext, useState, useContext } from 'react';
const CategoryContext = createContext({
currentCategory: null,
setCurrentCategory: () => {}
});
export const useCategory = () => useContext(CategoryContext);
export const withCategory = (Component) => {
return (props) => {
const categoryContext = useCategory();
return <Component {...props} categoryContext={categoryContext} />;
};
};
export const CategoryContextProvider = ({ children }) => {
const [currentCategory, setCurrentCategory] = useState(null);
const setCurrentCategoryWithLog = (category) => {
console.log('CategoryContext: Setting current category to:', category);
setCurrentCategory(category);
};
return (
<CategoryContext.Provider value={{ currentCategory, setCurrentCategory: setCurrentCategoryWithLog }}>
{children}
</CategoryContext.Provider>
);
};

View File

@@ -0,0 +1,31 @@
import React, { createContext, useState, useContext } from 'react';
const ProductContext = createContext({
currentProduct: null,
setCurrentProduct: () => {}
});
export const useProduct = () => useContext(ProductContext);
export const withProduct = (Component) => {
return (props) => {
const productContext = useProduct();
return <Component {...props} productContext={productContext} />;
};
};
export const ProductContextProvider = ({ children }) => {
const [currentProduct, setCurrentProduct] = useState(null);
const setCurrentProductWithLog = (product) => {
console.log('ProductContext: Setting current product to:', product);
setCurrentProduct(product);
};
return (
<ProductContext.Provider value={{ currentProduct, setCurrentProduct: setCurrentProductWithLog }}>
{children}
</ProductContext.Provider>
);
};