Files
reactShop/src/components/ProductCarousel.js
sebseb7 0c92591d32 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.
2025-11-16 07:34:39 +01:00

445 lines
14 KiB
JavaScript

import React from 'react';
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import ChevronRight from "@mui/icons-material/ChevronRight";
import Product from "./Product.js";
import { withTranslation } from 'react-i18next';
import { withLanguage } from '../i18n/withTranslation.js';
const ITEM_WIDTH = 250 + 16; // 250px width + 16px gap
const AUTO_SCROLL_SPEED = 0.5; // px per frame (~60fps, so ~30px/sec)
const AUTOSCROLL_RESTART_DELAY = 5000; // 5 seconds of inactivity before restarting autoscroll
const SCROLLBAR_FLASH_DURATION = 3000; // 3 seconds to show the virtual scrollbar
class ProductCarousel extends React.Component {
_isMounted = false;
products = [];
originalProducts = [];
animationFrame = null;
autoScrollActive = true;
translateX = 0;
inactivityTimer = null;
scrollbarTimer = null;
constructor(props) {
super(props);
const { i18n } = props;
this.state = {
products: [],
currentLanguage: (i18n && i18n.language) || 'de',
showScrollbar: false,
};
this.carouselTrackRef = React.createRef();
}
componentDidMount() {
this._isMounted = true;
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language;
console.log("ProductCarousel componentDidMount: Loading products for categoryId", this.props.categoryId, "language", currentLanguage);
this.loadProducts(currentLanguage);
}
componentDidUpdate(prevProps) {
console.log("ProductCarousel componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage);
if(prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) {
this.setState({ products: [] }, () => {
this.loadProducts(this.props.languageContext?.currentLanguage || this.props.i18n.language);
});
}
}
loadProducts = (language) => {
const { categoryId } = this.props;
window.socketManager.emit(
"getCategoryProducts",
{
categoryId: categoryId === "neu" ? "neu" : categoryId,
language: language,
requestTranslation: language === 'de' ? false : true
},
(response) => {
console.log("ProductCarousel getCategoryProducts response:", response);
if (this._isMounted && response && response.products && response.products.length > 0) {
// Filter products to only show those with pictures
const productsWithPictures = response.products.filter(product =>
product.pictureList && product.pictureList.length > 0
);
console.log("ProductCarousel: Filtered", productsWithPictures.length, "products with pictures from", response.products.length, "total");
if (productsWithPictures.length > 0) {
// Take random 15 products and shuffle them
const shuffledProducts = this.shuffleArray(productsWithPictures.slice(0, 15));
console.log("ProductCarousel: Selected and shuffled", shuffledProducts.length, "products");
this.originalProducts = shuffledProducts;
// Duplicate for seamless looping
this.products = [...shuffledProducts, ...shuffledProducts];
this.setState({ products: this.products });
this.startAutoScroll();
}
}
}
);
}
componentWillUnmount() {
this._isMounted = false;
this.stopAutoScroll();
this.clearInactivityTimer();
this.clearScrollbarTimer();
}
startAutoScroll = () => {
this.autoScrollActive = true;
if (!this.animationFrame) {
this.animationFrame = requestAnimationFrame(this.handleAutoScroll);
}
};
stopAutoScroll = () => {
this.autoScrollActive = false;
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
};
clearInactivityTimer = () => {
if (this.inactivityTimer) {
clearTimeout(this.inactivityTimer);
this.inactivityTimer = null;
}
};
clearScrollbarTimer = () => {
if (this.scrollbarTimer) {
clearTimeout(this.scrollbarTimer);
this.scrollbarTimer = null;
}
};
startInactivityTimer = () => {
this.clearInactivityTimer();
this.inactivityTimer = setTimeout(() => {
if (this._isMounted) {
this.startAutoScroll();
}
}, AUTOSCROLL_RESTART_DELAY);
};
showScrollbarFlash = () => {
this.clearScrollbarTimer();
this.setState({ showScrollbar: true });
this.scrollbarTimer = setTimeout(() => {
if (this._isMounted) {
this.setState({ showScrollbar: false });
}
}, SCROLLBAR_FLASH_DURATION);
};
handleAutoScroll = () => {
if (!this.autoScrollActive || this.originalProducts.length === 0) return;
this.translateX -= AUTO_SCROLL_SPEED;
this.updateTrackTransform();
const originalItemCount = this.originalProducts.length;
const maxScroll = ITEM_WIDTH * originalItemCount;
// Check if we've scrolled past the first set of items
if (Math.abs(this.translateX) >= maxScroll) {
// Reset to beginning seamlessly
this.translateX = 0;
this.updateTrackTransform();
}
this.animationFrame = requestAnimationFrame(this.handleAutoScroll);
};
updateTrackTransform = () => {
if (this.carouselTrackRef.current) {
this.carouselTrackRef.current.style.transform = `translateX(${this.translateX}px)`;
}
};
handleLeftClick = () => {
this.stopAutoScroll();
this.scrollBy(1);
this.showScrollbarFlash();
this.startInactivityTimer();
};
handleRightClick = () => {
this.stopAutoScroll();
this.scrollBy(-1);
this.showScrollbarFlash();
this.startInactivityTimer();
};
scrollBy = (direction) => {
if (this.originalProducts.length === 0) return;
// direction: 1 = left (scroll content right), -1 = right (scroll content left)
const originalItemCount = this.originalProducts.length;
const maxScroll = ITEM_WIDTH * originalItemCount;
this.translateX += direction * ITEM_WIDTH;
// Handle wrap-around when scrolling left (positive translateX)
if (this.translateX > 0) {
this.translateX = -(maxScroll - ITEM_WIDTH);
}
// Handle wrap-around when scrolling right (negative translateX beyond limit)
else if (Math.abs(this.translateX) >= maxScroll) {
this.translateX = 0;
}
this.updateTrackTransform();
// Force scrollbar to update immediately after wrap-around
if (this.state.showScrollbar) {
this.forceUpdate();
}
};
renderVirtualScrollbar = () => {
if (!this.state.showScrollbar || this.originalProducts.length === 0) {
return null;
}
const originalItemCount = this.originalProducts.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)
let currentItemIndex;
if (this.translateX === 0) {
currentItemIndex = 0;
} else if (this.translateX > 0) {
const maxScroll = ITEM_WIDTH * originalItemCount;
const effectivePosition = maxScroll + this.translateX;
currentItemIndex = Math.floor(effectivePosition / ITEM_WIDTH);
} else {
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
const lastPossibleFirstItem = Math.max(0, originalItemCount - itemsInView);
const thumbPosition = lastPossibleFirstItem > 0 ? Math.min((currentItemIndex / lastPossibleFirstItem) * 100, 100) : 0;
return (
<div
className="virtual-scrollbar"
style={{
position: 'absolute',
bottom: '5px',
left: '50%',
transform: 'translateX(-50%)',
width: '200px',
height: '4px',
backgroundColor: 'rgba(0, 0, 0, 0.1)',
borderRadius: '2px',
zIndex: 1000,
opacity: this.state.showScrollbar ? 1 : 0,
transition: 'opacity 0.3s ease-in-out'
}}
>
<div
className="scrollbar-thumb"
style={{
position: 'absolute',
top: '0',
left: `${thumbPosition}%`,
width: '20px',
height: '4px',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderRadius: '2px',
transform: 'translateX(-50%)',
transition: 'left 0.2s ease-out'
}}
/>
</div>
);
};
render() {
const { t, title } = this.props;
const { products } = this.state;
if(!products || products.length === 0) {
return null;
}
return (
<Box sx={{ mt: 3 }}>
<Typography
variant="h4"
component="h2"
sx={{
mb: 2,
fontFamily: "SwashingtonCP",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}}
>
{title || t('product.new')}
</Typography>
<div
className="product-carousel-wrapper"
style={{
position: 'relative',
overflowX: 'hidden',
overflowY: 'visible',
width: '100%',
maxWidth: '1200px',
margin: '0 auto',
padding: '0 20px',
boxSizing: 'border-box'
}}
>
{/* Left Arrow */}
<IconButton
aria-label="Vorherige Produkte anzeigen"
onClick={this.handleLeftClick}
style={{
position: 'absolute',
top: '50%',
left: '8px',
transform: 'translateY(-50%)',
zIndex: 1200,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
width: '48px',
height: '48px',
borderRadius: '50%'
}}
>
<ChevronLeft />
</IconButton>
{/* Right Arrow */}
<IconButton
aria-label="Nächste Produkte anzeigen"
onClick={this.handleRightClick}
style={{
position: 'absolute',
top: '50%',
right: '8px',
transform: 'translateY(-50%)',
zIndex: 1200,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
width: '48px',
height: '48px',
borderRadius: '50%'
}}
>
<ChevronRight />
</IconButton>
<div
className="product-carousel-container"
style={{
position: 'relative',
overflowX: 'hidden',
overflowY: 'visible',
padding: '20px 0',
width: '100%',
maxWidth: '1080px',
margin: '0 auto',
zIndex: 1,
boxSizing: 'border-box'
}}
>
<div
className="product-carousel-track"
ref={this.carouselTrackRef}
style={{
display: 'flex',
gap: '16px',
transition: 'none',
alignItems: 'flex-start',
width: 'fit-content',
overflow: 'visible',
position: 'relative',
transform: 'translateX(0px)',
margin: '0 auto'
}}
>
{products.map((product, index) => (
<div
key={`${product.id}-${index}`}
className="product-carousel-item"
style={{
flex: '0 0 250px',
width: '250px',
maxWidth: '250px',
minWidth: '250px',
boxSizing: 'border-box',
position: 'relative'
}}
>
<Product
id={product.id}
name={product.name}
seoName={product.seoName}
price={product.price}
currency={product.currency}
available={product.available}
manufacturer={product.manufacturer}
vat={product.vat}
cGrundEinheit={product.cGrundEinheit}
fGrundPreis={product.fGrundPreis}
incoming={product.incomingDate}
neu={product.neu}
thc={product.thc}
floweringWeeks={product.floweringWeeks}
versandklasse={product.versandklasse}
weight={product.weight}
pictureList={product.pictureList}
availableSupplier={product.availableSupplier}
komponenten={product.komponenten}
rebate={product.rebate}
categoryId={product.kategorien ? product.kategorien.split(',')[0] : undefined}
priority={index < 6 ? 'high' : 'auto'}
t={t}
/>
</div>
))}
</div>
{/* Virtual Scrollbar */}
{this.renderVirtualScrollbar()}
</div>
</div>
</Box>
);
}
// Shuffle array using Fisher-Yates algorithm
shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
}
export default withTranslation()(withLanguage(ProductCarousel));