Compare commits
4 Commits
de8e59f1bb
...
47ed2ec231
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47ed2ec231 | ||
|
|
188c883450 | ||
|
|
ba66b82b2b | ||
|
|
defe3c9521 |
@@ -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 }) => {
|
||||
)
|
||||
}>
|
||||
<CarouselProvider>
|
||||
<IdleMainPagesSlideshow />
|
||||
<Routes>
|
||||
{/* Main pages using unified component */}
|
||||
<Route path="/" element={<MainPageLayout />} />
|
||||
|
||||
@@ -15,6 +15,8 @@ import NotificationsActiveIcon from "@mui/icons-material/NotificationsActive";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { withI18n } from "../i18n/withTranslation.js";
|
||||
import {
|
||||
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||
emitPushSubscriptionsChanged,
|
||||
isPushApiSupported,
|
||||
fetchPushConfiguration,
|
||||
registerPushServiceWorker,
|
||||
@@ -109,9 +111,10 @@ class AddToCartButton extends Component {
|
||||
this.setState({ pushSubscribed: false, pushBusy: false });
|
||||
return;
|
||||
}
|
||||
const res = await articlePushUnsubscribe(subscription.endpoint);
|
||||
const res = await articlePushUnsubscribe(subscription.endpoint, kArtikel);
|
||||
if (parseSuccess(res)) {
|
||||
this.setState({ pushSubscribed: false });
|
||||
emitPushSubscriptionsChanged();
|
||||
} else {
|
||||
this.setState({
|
||||
pushError:
|
||||
@@ -146,6 +149,7 @@ class AddToCartButton extends Component {
|
||||
const res = await articlePushSubscribe(kArtikel, subscription);
|
||||
if (parseSuccess(res)) {
|
||||
this.setState({ pushSubscribed: true });
|
||||
emitPushSubscriptionsChanged();
|
||||
} else {
|
||||
this.setState({
|
||||
pushError:
|
||||
@@ -174,7 +178,14 @@ class AddToCartButton extends Component {
|
||||
if (this.state.quantity !== newQuantity)
|
||||
this.setState({ quantity: newQuantity });
|
||||
};
|
||||
this.onPushSubscriptionsChanged = () => {
|
||||
this.refreshIncomingPushStatus();
|
||||
};
|
||||
window.addEventListener("cart", this.cart);
|
||||
window.addEventListener(
|
||||
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||
this.onPushSubscriptionsChanged
|
||||
);
|
||||
this.refreshIncomingPushStatus();
|
||||
}
|
||||
|
||||
@@ -190,6 +201,10 @@ class AddToCartButton extends Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("cart", this.cart);
|
||||
window.removeEventListener(
|
||||
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||
this.onPushSubscriptionsChanged
|
||||
);
|
||||
}
|
||||
|
||||
handleIncrement = () => {
|
||||
|
||||
121
src/components/IdleMainPagesSlideshow.js
Normal file
121
src/components/IdleMainPagesSlideshow.js
Normal file
@@ -0,0 +1,121 @@
|
||||
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 auto-navigation, ignore user-activity handlers briefly — route changes
|
||||
* often emit scroll / mousemove / focus events that would call resetIdle() and
|
||||
* clear the slideshow interval (only one slide before stopping).
|
||||
*/
|
||||
const POST_NAV_GRACE_MS = 3_000;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
const ignoreActivityUntilRef = useRef(0);
|
||||
|
||||
const resetIdleRef = useRef(() => {});
|
||||
const clearTimersRef = useRef(() => {});
|
||||
|
||||
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;
|
||||
}
|
||||
}, []);
|
||||
|
||||
clearTimersRef.current = clearTimers;
|
||||
|
||||
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;
|
||||
ignoreActivityUntilRef.current = Date.now() + POST_NAV_GRACE_MS;
|
||||
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]);
|
||||
|
||||
resetIdleRef.current = resetIdle;
|
||||
|
||||
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 now = Date.now();
|
||||
if (now < ignoreActivityUntilRef.current) return;
|
||||
if (now - lastActivityRef.current < ACTIVITY_THROTTLE_MS) return;
|
||||
lastActivityRef.current = now;
|
||||
resetIdleRef.current();
|
||||
};
|
||||
|
||||
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);
|
||||
clearTimersRef.current();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -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" },
|
||||
|
||||
@@ -12,6 +12,8 @@ import { useParams, useSearchParams, useNavigate, useLocation } from 'react-rout
|
||||
import { setSessionSetting, removeSessionSetting, clearAllSessionSettings } from '../utils/sessionStorage.js';
|
||||
import { withI18n } from '../i18n/withTranslation.js';
|
||||
import {
|
||||
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||
emitPushSubscriptionsChanged,
|
||||
isPushApiSupported,
|
||||
fetchPushConfiguration,
|
||||
registerPushServiceWorker,
|
||||
@@ -68,14 +70,25 @@ class ProductFilters extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onPushSubscriptionsChanged = () => {
|
||||
this.refreshCategoryPushStatus();
|
||||
};
|
||||
this.adjustPaperHeight();
|
||||
window.addEventListener('resize', this.adjustPaperHeight);
|
||||
window.addEventListener(
|
||||
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||
this.onPushSubscriptionsChanged
|
||||
);
|
||||
this._loadManufacturerImages();
|
||||
this.refreshCategoryPushStatus();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.adjustPaperHeight);
|
||||
window.removeEventListener(
|
||||
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||
this.onPushSubscriptionsChanged
|
||||
);
|
||||
this._manufacturerImageUrls.forEach(url => URL.revokeObjectURL(url));
|
||||
}
|
||||
|
||||
@@ -204,9 +217,10 @@ class ProductFilters extends Component {
|
||||
this.setState({ pushSubscribed: false, pushBusy: false });
|
||||
return;
|
||||
}
|
||||
const res = await categoryPushUnsubscribe(subscription.endpoint);
|
||||
const res = await categoryPushUnsubscribe(subscription.endpoint, kKat);
|
||||
if (parseSuccess(res)) {
|
||||
this.setState({ pushSubscribed: false });
|
||||
emitPushSubscriptionsChanged();
|
||||
} else {
|
||||
this.setState({
|
||||
pushError:
|
||||
@@ -237,6 +251,7 @@ class ProductFilters extends Component {
|
||||
const res = await categoryPushSubscribe(kKat, subscription);
|
||||
if (parseSuccess(res)) {
|
||||
this.setState({ pushSubscribed: true });
|
||||
emitPushSubscriptionsChanged();
|
||||
} else {
|
||||
this.setState({
|
||||
pushError:
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
* Web Push helpers for article notifications (VAPID + backend API).
|
||||
*/
|
||||
|
||||
/** Fired after any article/category push subscribe or unsubscribe so all UIs re-query status. */
|
||||
export const PUSH_SUBSCRIPTIONS_CHANGED_EVENT = "growheads-push-subscriptions-changed";
|
||||
|
||||
export function emitPushSubscriptionsChanged() {
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent(PUSH_SUBSCRIPTIONS_CHANGED_EVENT));
|
||||
}
|
||||
}
|
||||
|
||||
export const ARTICLE_PUSH_SW_PATH = "/api/check/push/service-worker";
|
||||
export const ARTICLE_PUSH_VAPID_URL = "/api/check/push/vapid-public-key";
|
||||
|
||||
@@ -88,14 +97,22 @@ export async function articlePushSubscribe(kArtikel, subscription) {
|
||||
return { ...data, httpOk: res.ok };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} endpoint
|
||||
*/
|
||||
export async function articlePushUnsubscribe(endpoint) {
|
||||
/** POST /api/article/push/unsubscribe — body is always `{ endpoint, kArtikel }` (scoped row only). */
|
||||
export async function articlePushUnsubscribe(endpoint, kArtikel) {
|
||||
const parsed =
|
||||
kArtikel != null && kArtikel !== ""
|
||||
? typeof kArtikel === "number"
|
||||
? kArtikel
|
||||
: parseInt(String(kArtikel), 10)
|
||||
: NaN;
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return { success: false, httpOk: false, error: "missing_kArtikel" };
|
||||
}
|
||||
const body = { endpoint, kArtikel: parsed };
|
||||
const res = await fetch("/api/article/push/unsubscribe", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ endpoint }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return { ...data, httpOk: res.ok };
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
*/
|
||||
|
||||
export {
|
||||
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||
emitPushSubscriptionsChanged,
|
||||
fetchPushConfiguration,
|
||||
registerPushServiceWorker,
|
||||
ensurePushSubscription,
|
||||
@@ -43,14 +45,22 @@ export async function categoryPushSubscribe(kKategorie, subscription) {
|
||||
return { ...data, httpOk: res.ok };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} endpoint
|
||||
*/
|
||||
export async function categoryPushUnsubscribe(endpoint) {
|
||||
/** POST /api/category/push/unsubscribe — body is always `{ endpoint, kKategorie }` (scoped row only). */
|
||||
export async function categoryPushUnsubscribe(endpoint, kKategorie) {
|
||||
const parsed =
|
||||
kKategorie != null && kKategorie !== ""
|
||||
? typeof kKategorie === "number"
|
||||
? kKategorie
|
||||
: parseInt(String(kKategorie), 10)
|
||||
: NaN;
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return { success: false, httpOk: false, error: "missing_kKategorie" };
|
||||
}
|
||||
const body = { endpoint, kKategorie: parsed };
|
||||
const res = await fetch("/api/category/push/unsubscribe", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ endpoint }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return { ...data, httpOk: res.ok };
|
||||
|
||||
Reference in New Issue
Block a user