- Introduce a new ProductCarousel component for displaying products in a scrollable format. - Implement auto-scrolling functionality and manual navigation controls. - Integrate ProductCarousel into SharedCarousel for the 'neu' category, enhancing the user interface and product visibility. - Update Product component rendering within the carousel to ensure proper display of product details.
411 lines
13 KiB
JavaScript
411 lines
13 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 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 (
|
|
<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 } = this.props;
|
|
const { categories } = this.state;
|
|
|
|
if(!categories || categories.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ mt: 3 }}>
|
|
<Typography
|
|
variant="h4"
|
|
component="h1"
|
|
sx={{
|
|
mb: 2,
|
|
fontFamily: "SwashingtonCP",
|
|
color: "primary.main",
|
|
textAlign: "center",
|
|
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
|
|
}}
|
|
>
|
|
{t('navigation.categories')}
|
|
</Typography>
|
|
|
|
<div
|
|
className="carousel-wrapper"
|
|
style={{
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
width: '100%',
|
|
maxWidth: '1200px',
|
|
margin: '0 auto',
|
|
padding: '0 20px',
|
|
boxSizing: 'border-box'
|
|
}}
|
|
>
|
|
{/* Left Arrow */}
|
|
<IconButton
|
|
aria-label="Vorherige Kategorien 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 Kategorien 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="carousel-container"
|
|
style={{
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
padding: '20px 0',
|
|
width: '100%',
|
|
maxWidth: '1080px',
|
|
margin: '0 auto',
|
|
zIndex: 1,
|
|
boxSizing: 'border-box'
|
|
}}
|
|
>
|
|
<div
|
|
className="home-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'
|
|
}}
|
|
>
|
|
{categories.map((category, index) => (
|
|
<div
|
|
key={`${category.id}-${index}`}
|
|
className="carousel-item"
|
|
style={{
|
|
flex: '0 0 130px',
|
|
width: '130px',
|
|
maxWidth: '130px',
|
|
minWidth: '130px',
|
|
height: '130px',
|
|
maxHeight: '130px',
|
|
minHeight: '130px',
|
|
boxSizing: 'border-box',
|
|
position: 'relative'
|
|
}}
|
|
>
|
|
<CategoryBox
|
|
id={category.id}
|
|
name={category.name}
|
|
seoName={category.seoName}
|
|
image={category.image}
|
|
bgcolor={category.bgcolor}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Virtual Scrollbar */}
|
|
{this.renderVirtualScrollbar()}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Product Carousel for "neu" category */}
|
|
<ProductCarousel categoryId="neu" />
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default withTranslation()(withLanguage(SharedCarousel));
|