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 CategoryBox from "./CategoryBox.js"; import ProductCarousel from "./ProductCarousel.js"; import { withTranslation } from 'react-i18next'; import { withLanguage } from '../i18n/withTranslation.js'; const ITEM_WIDTH = 130 + 16; // 130px 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 SharedCarousel extends React.Component { _isMounted = false; categories = []; originalCategories = []; animationFrame = null; autoScrollActive = true; translateX = 0; inactivityTimer = null; scrollbarTimer = null; constructor(props) { super(props); const { i18n } = props; // Don't load categories in constructor - will be loaded in componentDidMount with correct language this.state = { categories: [], 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; // ALWAYS reload categories to ensure correct language console.log("SharedCarousel componentDidMount: ALWAYS RELOADING categories for language", currentLanguage); window.categoryService.get(209, currentLanguage).then((response) => { console.log("SharedCarousel categoryService.get response for language '" + currentLanguage + "':", response); if (this._isMounted && response.children && response.children.length > 0) { console.log("SharedCarousel: Setting categories with", response.children.length, "items"); console.log("SharedCarousel: First category name:", response.children[0]?.name); this.originalCategories = response.children; // Duplicate for seamless looping this.categories = [...response.children, ...response.children]; this.setState({ categories: this.categories }); this.startAutoScroll(); } }); } 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) => { console.log("response", response); if (response.children && response.children.length > 0) { this.originalCategories = response.children; this.categories = [...response.children, ...response.children]; this.setState({ categories: this.categories }); 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.originalCategories.length === 0) return; this.translateX -= AUTO_SCROLL_SPEED; this.updateTrackTransform(); const originalItemCount = this.originalCategories.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.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; 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.originalCategories.length === 0) { return null; } 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; } else if (this.translateX > 0) { // Wrapped to show end items (this happens when scrolling left past beginning) const maxScroll = ITEM_WIDTH * originalItemCount; const effectivePosition = maxScroll + this.translateX; currentItemIndex = Math.floor(effectivePosition / ITEM_WIDTH); } else { // 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; return (
); }; render() { const { t } = this.props; const { categories } = this.state; if(!categories || categories.length === 0) { return null; } return ( {t('navigation.categories')}
{/* Left Arrow */} {/* Right Arrow */}
{categories.map((category, index) => (
))}
{/* Virtual Scrollbar */} {this.renderVirtualScrollbar()}
{/* Product Carousel for "neu" category */}
); } } export default withTranslation()(withLanguage(SharedCarousel));