diff --git a/src/components/Footer.js b/src/components/Footer.js index 0abcfea..0cb3c4b 100644 --- a/src/components/Footer.js +++ b/src/components/Footer.js @@ -352,9 +352,6 @@ class Footer extends Component { © {new Date().getFullYear()} GrowHeads.de - - Telegraf - sicherer Chat mit unseren Mitarbeitern - diff --git a/src/components/ManufacturerCarousel.js b/src/components/ManufacturerCarousel.js new file mode 100644 index 0000000..84f09d0 --- /dev/null +++ b/src/components/ManufacturerCarousel.js @@ -0,0 +1,189 @@ +import React from 'react'; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { withTranslation } from 'react-i18next'; +import { withLanguage } from '../i18n/withTranslation.js'; + +const ITEM_WIDTH = 140 + 16; // 140px width + 16px gap +const AUTO_SCROLL_SPEED = 0.4; + +class ManufacturerCarousel extends React.Component { + _isMounted = false; + originalItems = []; + animationFrame = null; + translateX = 0; + + constructor(props) { + super(props); + this.state = { + items: [], // [{ id, name, src }] + }; + this.carouselTrackRef = React.createRef(); + } + + componentDidMount() { + this._isMounted = true; + this.loadImages(); + } + + componentWillUnmount() { + this._isMounted = false; + if (this.animationFrame) { + cancelAnimationFrame(this.animationFrame); + this.animationFrame = null; + } + // Revoke object URLs to avoid memory leaks + for (const item of this.originalItems) { + if (item.src) URL.revokeObjectURL(item.src); + } + } + + loadImages = () => { + window.socketManager.emit('getHerstellerImages', {}, (res) => { + if (!this._isMounted || !res?.success || !res.manufacturers?.length) return; + + const items = res.manufacturers + .filter(m => m.imageBuffer) + .map(m => { + const blob = new Blob([m.imageBuffer], { type: 'image/avif' }); + return { id: m.id, name: m.name || '', src: URL.createObjectURL(blob) }; + }) + .sort(() => Math.random() - 0.5); + + if (items.length === 0) return; + + this.originalItems = items; + this.setState({ items: [...items, ...items] }, () => { + this.startAutoScroll(); + }); + }); + }; + + startAutoScroll = () => { + if (!this.animationFrame) { + this.animationFrame = requestAnimationFrame(this.tick); + } + }; + + tick = () => { + if (!this._isMounted || this.originalItems.length === 0) return; + + this.translateX -= AUTO_SCROLL_SPEED; + + const maxScroll = ITEM_WIDTH * this.originalItems.length; + if (Math.abs(this.translateX) >= maxScroll) { + this.translateX = 0; + } + + if (this.carouselTrackRef.current) { + this.carouselTrackRef.current.style.transform = `translateX(${this.translateX}px)`; + } + + this.animationFrame = requestAnimationFrame(this.tick); + }; + + render() { + const { t } = this.props; + const { items } = this.state; + + if (!items || items.length === 0) return null; + + return ( + + + {t('product.manufacturer')} + + + + {/* Fade edges */} + + + + + + {items.map((item, index) => ( + + + + ))} + + + + + ); + } +} + +export default withTranslation()(withLanguage(ManufacturerCarousel)); diff --git a/src/components/SharedCarousel.js b/src/components/SharedCarousel.js index 5ef5c7b..40d735b 100644 --- a/src/components/SharedCarousel.js +++ b/src/components/SharedCarousel.js @@ -7,6 +7,7 @@ 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 ManufacturerCarousel from "./ManufacturerCarousel.js"; import { withTranslation } from 'react-i18next'; import { withLanguage } from '../i18n/withTranslation.js'; @@ -419,6 +420,9 @@ class SharedCarousel extends React.Component { {/* Product Carousel for "neu" category */} + + {/* Manufacturer logo carousel */} + ); }