From defe3c95218ffa8e257d3ac42a5375ca5b715b79 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Thu, 26 Mar 2026 21:10:46 +0100 Subject: [PATCH] feat: Integrate IdleMainPagesSlideshow component into App.js and update links in MainPageLayout for improved navigation to articles --- src/App.js | 2 + src/components/IdleMainPagesSlideshow.js | 104 +++++++++++++++++++++++ src/components/MainPageLayout.js | 4 +- 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/components/IdleMainPagesSlideshow.js diff --git a/src/App.js b/src/App.js index 1fb30d8..a402362 100644 --- a/src/App.js +++ b/src/App.js @@ -33,6 +33,7 @@ import i18n from './i18n/index.js'; import Header from "./components/Header.js"; import Footer from "./components/Footer.js"; import MainPageLayout from "./components/MainPageLayout.js"; +import IdleMainPagesSlideshow from "./components/IdleMainPagesSlideshow.js"; import Content from "./components/Content.js"; import ProductDetail from "./components/ProductDetail.js"; @@ -253,6 +254,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => { ) }> + {/* Main pages using unified component */} } /> diff --git a/src/components/IdleMainPagesSlideshow.js b/src/components/IdleMainPagesSlideshow.js new file mode 100644 index 0000000..0515052 --- /dev/null +++ b/src/components/IdleMainPagesSlideshow.js @@ -0,0 +1,104 @@ +import { useEffect, useRef, useCallback } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; + +/** Same order as the main landing tiles (home → Aktionen → Filiale). */ +const MAIN_PAGE_PATHS = ["/", "/aktionen", "/filiale"]; + +/** No input for this long before the slideshow starts. */ +const IDLE_MS = 90_000; + +/** Time between automatic page changes once the slideshow is running. */ +const SLIDESHOW_STEP_MS = 14_000; + +/** Ignore duplicate events (mousemove etc.) within this window. */ +const ACTIVITY_THROTTLE_MS = 400; + +/** + * After idle on /, /aktionen, or /filiale, cycles those routes slowly. + * Lives outside MainPageLayout so it is not reset when the route changes. + */ +export default function IdleMainPagesSlideshow() { + const location = useLocation(); + const navigate = useNavigate(); + const idleTimerRef = useRef(null); + const slideTimerRef = useRef(null); + const pathRef = useRef(location.pathname); + const wasOnMainPageRef = useRef(false); + const lastActivityRef = useRef(0); + + pathRef.current = location.pathname; + + const clearTimers = useCallback(() => { + if (idleTimerRef.current != null) { + clearTimeout(idleTimerRef.current); + idleTimerRef.current = null; + } + if (slideTimerRef.current != null) { + clearInterval(slideTimerRef.current); + slideTimerRef.current = null; + } + }, []); + + const startSlideshow = useCallback(() => { + let idx = MAIN_PAGE_PATHS.indexOf(pathRef.current); + if (idx < 0) idx = 0; + const advance = () => { + idx = (idx + 1) % MAIN_PAGE_PATHS.length; + navigate(MAIN_PAGE_PATHS[idx], { replace: true }); + }; + slideTimerRef.current = setInterval(advance, SLIDESHOW_STEP_MS); + }, [navigate]); + + const resetIdle = useCallback(() => { + clearTimers(); + if (!MAIN_PAGE_PATHS.includes(pathRef.current)) return; + idleTimerRef.current = setTimeout(() => { + idleTimerRef.current = null; + startSlideshow(); + }, IDLE_MS); + }, [clearTimers, startSlideshow]); + + useEffect(() => { + const nowMain = MAIN_PAGE_PATHS.includes(location.pathname); + if (!nowMain) { + clearTimers(); + wasOnMainPageRef.current = false; + return; + } + if (!wasOnMainPageRef.current) { + resetIdle(); + } + wasOnMainPageRef.current = true; + }, [location.pathname, clearTimers, resetIdle]); + + useEffect(() => { + const onActivity = () => { + const t = Date.now(); + if (t - lastActivityRef.current < ACTIVITY_THROTTLE_MS) return; + lastActivityRef.current = t; + resetIdle(); + }; + + const events = [ + "mousedown", + "keydown", + "touchstart", + "touchmove", + "wheel", + "click", + "scroll", + ]; + events.forEach((ev) => + window.addEventListener(ev, onActivity, { passive: true }) + ); + window.addEventListener("mousemove", onActivity, { passive: true }); + + return () => { + events.forEach((ev) => window.removeEventListener(ev, onActivity)); + window.removeEventListener("mousemove", onActivity); + clearTimers(); + }; + }, [resetIdle, clearTimers]); + + return null; +} diff --git a/src/components/MainPageLayout.js b/src/components/MainPageLayout.js index e1e3e41..33a982b 100644 --- a/src/components/MainPageLayout.js +++ b/src/components/MainPageLayout.js @@ -320,8 +320,8 @@ const MainPageLayout = () => { { title: t('sections.konfigurator'), image: "/assets/images/konfigurator.avif", bgcolor: "#e8f5d6", link: "/Konfigurator" } ], aktionen: [ - { title: t('sections.oilPress'), image: "/assets/images/presse.jpg", bgcolor: "#e1f0d3", link: "/presseverleih" }, - { title: t('sections.thcTest'), image: "/assets/images/purpl.jpg", bgcolor: "#e8f5d6", link: "/thc-test" } + { title: t('sections.oilPress'), image: "/assets/images/presse.jpg", bgcolor: "#e1f0d3", link: "/Artikel/Graveda-10t-presse-tagesmiete-inkl-prepress-vorpressform" }, + { title: t('sections.thcTest'), image: "/assets/images/purpl.jpg", bgcolor: "#e8f5d6", link: "/Artikel/1x-messung-purplpro-thc-cbd-restfeuchte-wasseraktivitaet" } ], filiale: [ { title: t('sections.address1'), image: "/assets/images/filiale1.jpg", bgcolor: "#e1f0d3", link: "/filiale" },