diff --git a/web/app/components/explore/banner/banner-item.tsx b/web/app/components/explore/banner/banner-item.tsx index 65585ed316..8d57d761ec 100644 --- a/web/app/components/explore/banner/banner-item.tsx +++ b/web/app/components/explore/banner/banner-item.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react' -import { useCallback, useMemo } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { RiArrowRightLine } from '@remixicon/react' import { useCarousel } from '@/app/components/base/carousel' import { IndicatorButton } from './indicator-button' @@ -27,6 +27,7 @@ type BannerItemProps = { export const BannerItem: FC = ({ banner, autoplayDelay }) => { const { t } = useTranslation() const { api, selectedIndex } = useCarousel() + const [resetKey, setResetKey] = useState(0) const slideInfo = useMemo(() => { const slides = api?.slideNodes() ?? [] @@ -35,22 +36,33 @@ export const BannerItem: FC = ({ banner, autoplayDelay }) => { return { slides, totalSlides, nextIndex } }, [api, selectedIndex]) + // Reset progress when slide changes + useEffect(() => { + setResetKey(prev => prev + 1) + }, [selectedIndex]) + const handleClick = useCallback(() => { + setResetKey(prev => prev + 1) if (banner.link) window.open(banner.link, '_blank', 'noopener,noreferrer') }, [banner.link]) + const handleIndicatorClick = useCallback((index: number) => { + setResetKey(prev => prev + 1) + api?.scrollTo(index) + }, [api]) + return (
{/* Left content area */}
-
+
{/* Text section */}
{/* Title area */} @@ -89,20 +101,22 @@ export const BannerItem: FC = ({ banner, autoplayDelay }) => { selectedIndex={selectedIndex} isNextSlide={index === slideInfo.nextIndex} autoplayDelay={autoplayDelay} - onClick={() => api?.scrollTo(index)} + resetKey={resetKey} + onClick={() => handleIndicatorClick(index)} /> ))}
+
- {/* Right image area */} -
+
{banner.content.title}
diff --git a/web/app/components/explore/banner/banner.tsx b/web/app/components/explore/banner/banner.tsx index 12616b73d9..331f198683 100644 --- a/web/app/components/explore/banner/banner.tsx +++ b/web/app/components/explore/banner/banner.tsx @@ -38,7 +38,7 @@ const Banner: FC = () => { plugins={[ Carousel.Plugin.Autoplay({ delay: AUTOPLAY_DELAY, - stopOnInteraction: true, + stopOnInteraction: false, }), ]} className="rounded-2xl" diff --git a/web/app/components/explore/banner/indicator-button.tsx b/web/app/components/explore/banner/indicator-button.tsx index 1ecb541c33..0bc5fac668 100644 --- a/web/app/components/explore/banner/indicator-button.tsx +++ b/web/app/components/explore/banner/indicator-button.tsx @@ -7,6 +7,7 @@ type IndicatorButtonProps = { selectedIndex: number isNextSlide: boolean autoplayDelay: number + resetKey: number onClick: () => void } @@ -18,11 +19,26 @@ export const IndicatorButton: FC = ({ selectedIndex, isNextSlide, autoplayDelay, + resetKey, onClick, }) => { const [progress, setProgress] = useState(0) - const animationIdRef = useRef(0) + const [isPageVisible, setIsPageVisible] = useState(true) const frameIdRef = useRef(undefined) + const pausedTimeRef = useRef(0) + const startTimeRef = useRef(0) + + // Listen to page visibility changes + useEffect(() => { + const handleVisibilityChange = () => { + setIsPageVisible(!document.hidden) + } + setIsPageVisible(!document.hidden) + document.addEventListener('visibilitychange', handleVisibilityChange) + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange) + } + }, []) useEffect(() => { if (!isNextSlide) { @@ -35,26 +51,32 @@ export const IndicatorButton: FC = ({ // reset and start new animation setProgress(0) - animationIdRef.current += 1 - - const startTime = Date.now() + startTimeRef.current = Date.now() + pausedTimeRef.current = 0 const animate = () => { - const elapsed = Date.now() - startTime - const newProgress = Math.min((elapsed / autoplayDelay) * PROGRESS_MAX, PROGRESS_MAX) - setProgress(newProgress) + // Only continue animation when page is visible + if (!document.hidden) { + const now = Date.now() + const elapsed = now - startTimeRef.current - pausedTimeRef.current + const newProgress = Math.min((elapsed / autoplayDelay) * PROGRESS_MAX, PROGRESS_MAX) + setProgress(newProgress) - if (newProgress < PROGRESS_MAX) + if (newProgress < PROGRESS_MAX) + frameIdRef.current = requestAnimationFrame(animate) + } + else { frameIdRef.current = requestAnimationFrame(animate) + } } - - frameIdRef.current = requestAnimationFrame(animate) + if (!document.hidden) + frameIdRef.current = requestAnimationFrame(animate) return () => { if (frameIdRef.current) cancelAnimationFrame(frameIdRef.current) } - }, [isNextSlide, autoplayDelay]) + }, [isNextSlide, autoplayDelay, resetKey, isPageVisible]) const isActive = index === selectedIndex @@ -76,7 +98,7 @@ export const IndicatorButton: FC = ({ {/* progress border for next slide */} {isNextSlide && !isActive && (