diff --git a/src/components/Product.js b/src/components/Product.js
index 081d68a..9eee0d1 100644
--- a/src/components/Product.js
+++ b/src/components/Product.js
@@ -393,9 +393,11 @@ class Product extends Component {
({this.props.t ? this.props.t('product.inclVatFooter', { vat }) : `incl. ${vat}% USt.,*`})
- {cGrundEinheit && fGrundPreis && fGrundPreis != price && (
- ({new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(fGrundPreis)}/{cGrundEinheit})
- )}
+
+ {cGrundEinheit && fGrundPreis && fGrundPreis != price && (
+ ({new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(fGrundPreis)}/{cGrundEinheit})
+ )}
+
{/*incoming*/}
diff --git a/src/components/ProductCarousel.js b/src/components/ProductCarousel.js
new file mode 100644
index 0000000..edd8745
--- /dev/null
+++ b/src/components/ProductCarousel.js
@@ -0,0 +1,429 @@
+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 (
+
+ );
+ };
+
+ render() {
+ const { t, title } = this.props;
+ const { products } = this.state;
+
+ if(!products || products.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ {title || t('product.new')}
+
+
+
+ {/* Left Arrow */}
+
+
+
+
+ {/* Right Arrow */}
+
+
+
+
+
+
+ {products.map((product, index) => (
+
+ ))}
+
+
+ {/* Virtual Scrollbar */}
+ {this.renderVirtualScrollbar()}
+
+
+
+ );
+ }
+}
+
+export default withTranslation()(withLanguage(ProductCarousel));
+
diff --git a/src/components/SharedCarousel.js b/src/components/SharedCarousel.js
index 8e95112..bd79be9 100644
--- a/src/components/SharedCarousel.js
+++ b/src/components/SharedCarousel.js
@@ -5,6 +5,7 @@ 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';
@@ -398,6 +399,9 @@ class SharedCarousel extends React.Component {
{this.renderVirtualScrollbar()}
+
+ {/* Product Carousel for "neu" category */}
+
);
}