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) { this.originalProducts = productsWithPictures; // Duplicate for seamless looping this.products = [...productsWithPictures, ...productsWithPictures]; 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 (