add lazy loading attributes to images and fix potential runaway timers in main page scroll logic
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Box, Image, Button, HStack, VStack, Text } from '@chakra-ui/react'
|
import { Box, Image, Button, HStack, VStack, Text } from '@chakra-ui/react'
|
||||||
import { useState, useEffect, CSSProperties } from 'react'
|
import { useState, useEffect, useMemo, useRef, CSSProperties } from 'react'
|
||||||
|
|
||||||
interface CyclingImageProps {
|
interface CyclingImageProps {
|
||||||
src: string
|
src: string
|
||||||
@@ -49,6 +49,12 @@ const CyclingImage = ({
|
|||||||
const [currentIntensity, setCurrentIntensity] = useState(intensity)
|
const [currentIntensity, setCurrentIntensity] = useState(intensity)
|
||||||
const [currentImage, setCurrentImage] = useState(src)
|
const [currentImage, setCurrentImage] = useState(src)
|
||||||
|
|
||||||
|
// Keep refs to latest src/src2 so the setInterval callback is never stale
|
||||||
|
const srcRef = useRef(src)
|
||||||
|
const src2Ref = useRef(src2)
|
||||||
|
useEffect(() => { srcRef.current = src }, [src])
|
||||||
|
useEffect(() => { src2Ref.current = src2 }, [src2])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSpeed(cycleDuration)
|
setSpeed(cycleDuration)
|
||||||
}, [cycleDuration])
|
}, [cycleDuration])
|
||||||
@@ -58,11 +64,12 @@ const CyclingImage = ({
|
|||||||
if (!src2 || !isPlaying) return
|
if (!src2 || !isPlaying) return
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setCurrentImage(prev => prev === src ? src2 : src)
|
// Use refs to read latest prop values, avoiding stale closures
|
||||||
|
setCurrentImage(prev => prev === srcRef.current ? src2Ref.current! : srcRef.current)
|
||||||
}, speed * 1000)
|
}, speed * 1000)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [src, src2, speed, isPlaying])
|
}, [src2, speed, isPlaying])
|
||||||
|
|
||||||
const togglePlayPause = () => {
|
const togglePlayPause = () => {
|
||||||
setIsPlaying(!isPlaying)
|
setIsPlaying(!isPlaying)
|
||||||
@@ -76,9 +83,10 @@ const CyclingImage = ({
|
|||||||
setCurrentIntensity(value)
|
setCurrentIntensity(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSS keyframes animation
|
// CSS keyframes animation — memoized so the <style> tag is only
|
||||||
|
// re-written when the intensity value actually changes.
|
||||||
const animationName = 'lightToDarkCycle'
|
const animationName = 'lightToDarkCycle'
|
||||||
const animationStyle = `
|
const animationStyle = useMemo(() => `
|
||||||
@keyframes ${animationName} {
|
@keyframes ${animationName} {
|
||||||
0% {
|
0% {
|
||||||
filter: brightness(${1 - currentIntensity * 1.5});
|
filter: brightness(${1 - currentIntensity * 1.5});
|
||||||
@@ -90,7 +98,7 @@ const CyclingImage = ({
|
|||||||
filter: brightness(${1 - currentIntensity * 1.5});
|
filter: brightness(${1 - currentIntensity * 1.5});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`, [currentIntensity])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ function Advantages() {
|
|||||||
src='/images/new/heart.webp'
|
src='/images/new/heart.webp'
|
||||||
title="獅球嘜營萃護心油"
|
title="獅球嘜營萃護心油"
|
||||||
alt="獅球嘜營萃護心油"
|
alt="獅球嘜營萃護心油"
|
||||||
|
loading="lazy"
|
||||||
w={{ base: '75vw', sm: '75vw', md: '40vw', lg: '40vw', xl: '40vw' }}
|
w={{ base: '75vw', sm: '75vw', md: '40vw', lg: '40vw', xl: '40vw' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box, Image, Stack, Text } from '@chakra-ui/react'
|
import { Box, Image, Stack, Text } from '@chakra-ui/react'
|
||||||
import CyclingImage from './CyclingImage'
|
import CyclingImage from './CyclingImage'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
const MotionImage = motion.create(Image)
|
const MotionImage = motion.create(Image)
|
||||||
@@ -9,6 +9,7 @@ const MotionText = motion.create(Text)
|
|||||||
function Hero1() {
|
function Hero1() {
|
||||||
const [headerHeight, setHeaderHeight] = useState(80);
|
const [headerHeight, setHeaderHeight] = useState(80);
|
||||||
const bigWarningSize = { base: "17vw", sm: "15vw", md: "9vw", lg: "9vw", xl: "8vw" };
|
const bigWarningSize = { base: "17vw", sm: "15vw", md: "9vw", lg: "9vw", xl: "8vw" };
|
||||||
|
const delayedUpdateTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateHeaderHeight = () => {
|
const updateHeaderHeight = () => {
|
||||||
@@ -20,13 +21,15 @@ function Hero1() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const delayedUpdate = () => {
|
const delayedUpdate = () => {
|
||||||
setTimeout(updateHeaderHeight, 100);
|
clearTimeout(delayedUpdateTimerRef.current);
|
||||||
|
delayedUpdateTimerRef.current = setTimeout(updateHeaderHeight, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
delayedUpdate();
|
delayedUpdate();
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
setTimeout(updateHeaderHeight, 50);
|
clearTimeout(delayedUpdateTimerRef.current);
|
||||||
|
delayedUpdateTimerRef.current = setTimeout(updateHeaderHeight, 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
const header = document.querySelector('[data-header="true"]');
|
const header = document.querySelector('[data-header="true"]');
|
||||||
@@ -38,6 +41,7 @@ function Hero1() {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', delayedUpdate);
|
window.removeEventListener('resize', delayedUpdate);
|
||||||
|
clearTimeout(delayedUpdateTimerRef.current);
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ function Hero2() {
|
|||||||
left="50%"
|
left="50%"
|
||||||
title="獅球嘜營萃護心油"
|
title="獅球嘜營萃護心油"
|
||||||
alt="獅球嘜營萃護心油"
|
alt="獅球嘜營萃護心油"
|
||||||
|
loading="lazy"
|
||||||
top={{ base: "10px", sm: "20px", md: "30px", lg: "30px", xl: "40px" }}
|
top={{ base: "10px", sm: "20px", md: "30px", lg: "30px", xl: "40px" }}
|
||||||
w={{ base: "70vw", sm: "70vw", md: "35vw", lg: "35vw", xl: "28vw" }}
|
w={{ base: "70vw", sm: "70vw", md: "35vw", lg: "35vw", xl: "28vw" }}
|
||||||
initial={{ opacity: 0, y: -30, x: '-50%' }}
|
initial={{ opacity: 0, y: -30, x: '-50%' }}
|
||||||
@@ -45,6 +46,7 @@ function Hero2() {
|
|||||||
src="/images/new/hero2subtitle.webp"
|
src="/images/new/hero2subtitle.webp"
|
||||||
position={'absolute'}
|
position={'absolute'}
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
|
loading="lazy"
|
||||||
top={{ base: "35vw", sm: "35vw", md: "17vw", lg: "15vw", xl: "12vw" }}
|
top={{ base: "35vw", sm: "35vw", md: "17vw", lg: "15vw", xl: "12vw" }}
|
||||||
right={{ base: "55vw", sm: "55vw", md: "24vw", lg: "25vw", xl: "30vw" }}
|
right={{ base: "55vw", sm: "55vw", md: "24vw", lg: "25vw", xl: "30vw" }}
|
||||||
w={{ base: "34vw", sm: "34vw", md: "22vw", lg: "22vw", xl: "18vw" }}
|
w={{ base: "34vw", sm: "34vw", md: "22vw", lg: "22vw", xl: "18vw" }}
|
||||||
@@ -59,6 +61,7 @@ function Hero2() {
|
|||||||
alt="獅球嘜營萃護心油"
|
alt="獅球嘜營萃護心油"
|
||||||
title="獅球嘜營萃護心油"
|
title="獅球嘜營萃護心油"
|
||||||
zIndex={0}
|
zIndex={0}
|
||||||
|
loading="lazy"
|
||||||
position={'absolute'}
|
position={'absolute'}
|
||||||
left="50%"
|
left="50%"
|
||||||
bottom={0}
|
bottom={0}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ function Info() {
|
|||||||
src='/images/new/info_title.webp'
|
src='/images/new/info_title.webp'
|
||||||
w={{ base: '90vw', sm: '80vw', md: '45vw', lg: '35vw', xl: '22vw' }}
|
w={{ base: '90vw', sm: '80vw', md: '45vw', lg: '35vw', xl: '22vw' }}
|
||||||
mt={'50px'}
|
mt={'50px'}
|
||||||
|
loading="lazy"
|
||||||
initial={{ opacity: 0, y: -30 }}
|
initial={{ opacity: 0, y: -30 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, amount: 0.3 }}
|
viewport={{ once: true, amount: 0.3 }}
|
||||||
@@ -156,7 +157,7 @@ function Info() {
|
|||||||
src={infoData.find(info => info.id === selectedInfo)?.image}
|
src={infoData.find(info => info.id === selectedInfo)?.image}
|
||||||
w="100%"
|
w="100%"
|
||||||
p={5}
|
p={5}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<Box px={{ base: 5, sm: 5, md: 6, lg: 5, xl: 5 }}
|
<Box px={{ base: 5, sm: 5, md: 6, lg: 5, xl: 5 }}
|
||||||
pb={{ base: 5, sm: 5, md: 6, lg: 9, xl: 9 }}>
|
pb={{ base: 5, sm: 5, md: 6, lg: 9, xl: 9 }}>
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ function OilInfo() {
|
|||||||
src='/images/cookmethods.webp'
|
src='/images/cookmethods.webp'
|
||||||
alt='烹調方法'
|
alt='烹調方法'
|
||||||
title='烹調方法'
|
title='烹調方法'
|
||||||
|
loading="lazy"
|
||||||
w={{ base: '100%', sm: '90%', md: '85%' }}
|
w={{ base: '100%', sm: '90%', md: '85%' }}
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
||||||
@@ -109,6 +110,7 @@ function OilInfo() {
|
|||||||
<MotionImage
|
<MotionImage
|
||||||
src='/images/new/buttons.webp'
|
src='/images/new/buttons.webp'
|
||||||
w={{ base: '100%', sm: '100%', md: '90%' }}
|
w={{ base: '100%', sm: '100%', md: '90%' }}
|
||||||
|
loading="lazy"
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
||||||
transition={{ duration: 0.5, delay: 1.0, ease: "easeOut" }}
|
transition={{ duration: 0.5, delay: 1.0, ease: "easeOut" }}
|
||||||
|
|||||||
@@ -91,7 +91,11 @@ function Truth() {
|
|||||||
// Parse XFBML when SDK is loaded
|
// Parse XFBML when SDK is loaded
|
||||||
const handleSDKLoad = () => {
|
const handleSDKLoad = () => {
|
||||||
if ((window as any).FB && fbContainerRef.current) {
|
if ((window as any).FB && fbContainerRef.current) {
|
||||||
(window as any).FB.XFBML.parse(fbContainerRef.current);
|
try {
|
||||||
|
(window as any).FB.XFBML.parse(fbContainerRef.current);
|
||||||
|
} catch (_e) {
|
||||||
|
// FB SDK may fail (e.g. blocked by ad-blockers) — ignore silently
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useRouterState } from '@tanstack/react-router'
|
import { useRouterState } from '@tanstack/react-router'
|
||||||
import Hero1 from '../components/new_ui/hero1'
|
import Hero1 from '../components/new_ui/hero1'
|
||||||
import Hero2 from '../components/new_ui/hero2'
|
import Hero2 from '../components/new_ui/hero2'
|
||||||
@@ -16,23 +16,26 @@ import Header from '@/components/header'
|
|||||||
function MainPage() {
|
function MainPage() {
|
||||||
const { location } = useRouterState()
|
const { location } = useRouterState()
|
||||||
|
|
||||||
|
// Refs must live at component top level (Rules of Hooks).
|
||||||
|
// Storing timer IDs in refs means the cleanup closure always reads the
|
||||||
|
// latest value, preventing runaway timers after the component unmounts.
|
||||||
|
const animationFrameRef = useRef(0)
|
||||||
|
const retryTimeoutRef = useRef<number | undefined>(undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.pathname !== '/40plus') {
|
if (location.pathname !== '/40plus') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let animationFrame = 0
|
|
||||||
let retryTimeout: number | undefined
|
|
||||||
|
|
||||||
// Replay pending section scroll requests written to sessionStorage
|
// Replay pending section scroll requests written to sessionStorage
|
||||||
const clearTimers = () => {
|
const clearTimers = () => {
|
||||||
if (animationFrame) {
|
if (animationFrameRef.current) {
|
||||||
window.cancelAnimationFrame(animationFrame)
|
window.cancelAnimationFrame(animationFrameRef.current)
|
||||||
animationFrame = 0
|
animationFrameRef.current = 0
|
||||||
}
|
}
|
||||||
if (retryTimeout) {
|
if (retryTimeoutRef.current) {
|
||||||
window.clearTimeout(retryTimeout)
|
window.clearTimeout(retryTimeoutRef.current)
|
||||||
retryTimeout = undefined
|
retryTimeoutRef.current = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,17 +47,17 @@ function MainPage() {
|
|||||||
if (element) {
|
if (element) {
|
||||||
window.sessionStorage.removeItem('pendingScrollSection')
|
window.sessionStorage.removeItem('pendingScrollSection')
|
||||||
element.scrollIntoView({ behavior: 'smooth' })
|
element.scrollIntoView({ behavior: 'smooth' })
|
||||||
animationFrame = 0
|
animationFrameRef.current = 0
|
||||||
retryTimeout = undefined
|
retryTimeoutRef.current = undefined
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
retryTimeout = window.setTimeout(() => {
|
retryTimeoutRef.current = window.setTimeout(() => {
|
||||||
animationFrame = window.requestAnimationFrame(scrollToTarget)
|
animationFrameRef.current = window.requestAnimationFrame(scrollToTarget)
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
animationFrame = window.requestAnimationFrame(scrollToTarget)
|
animationFrameRef.current = window.requestAnimationFrame(scrollToTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
const runScroll = () => {
|
const runScroll = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user