blocks/main/latest-news/mascot/index.tsx (93 lines of code) (raw):

import { memo, useEffect, useRef, useState } from 'react'; import cn from 'classnames'; import { useInView } from 'react-intersection-observer'; import { AnimationItem, LottiePlayer } from 'lottie-web/build/player/lottie_light'; import styles from './mascot.module.css'; type MascotProps = { className?: string; }; const ANIMATION_INITIAL_DELAY = 500 as const; const ANIMATION_AFTER_DELAY = 5000 as const; async function noop() {} function sleep(ms: number) { return new Promise<void>((resolve) => setTimeout(() => resolve(), ms)); } function createAnimation( lottie: LottiePlayer, node: Element, animationData: Record<string, unknown> ): [AnimationItem, () => Promise<void>] { const animation = lottie.loadAnimation({ container: node, renderer: 'svg', loop: false, autoplay: false, animationData, }); const animationComplete = new Promise<void>((resolve) => { function done() { animation.removeEventListener('complete', done); resolve(); } animation.addEventListener('complete', done); }); return [ animation, async () => { animation.play(); await animationComplete; animation.destroy(); }, ]; } function MascotContent({ className, onFinish }: MascotProps & { onFinish: () => void }) { const mascotNode = useRef<HTMLSpanElement>(null); const { ref: inViewRef, inView } = useInView(); useEffect(() => { const node = mascotNode.current; let animation: AnimationItem; let play: ReturnType<typeof createAnimation>[1]; let wasOnceStarted = false; function done() { if (wasOnceStarted) onFinish(); } let skipInactiveHook: (body: () => Promise<void>) => ReturnType<typeof body> = (body) => body(); async function playAnimation() { const [lottie, initialData] = await Promise.all([ import('lottie-web/build/player/lottie_light').then((l) => l.default), import('./option3.json'), sleep(ANIMATION_INITIAL_DELAY), ]); await skipInactiveHook(async function initialPlay() { [animation, play] = createAnimation(lottie, node, initialData); wasOnceStarted = true; await play(); }); let afterData: Parameters<typeof createAnimation>[2]; await skipInactiveHook(async function afterPrepare() { [afterData] = await Promise.all([import('./option4.json'), sleep(ANIMATION_AFTER_DELAY)]); }); await skipInactiveHook(async function afterPlay() { [animation, play] = createAnimation(lottie, node, afterData); await play(); }); done(); } if (node && inView) { playAnimation(); return function playCleanup() { skipInactiveHook = noop; animation?.destroy(); done(); }; } }, [mascotNode.current, inView]); return ( <div aria-hidden="true" ref={inViewRef} className={cn(styles.container, className)}> <span ref={mascotNode} className={styles.animation} /> </div> ); } export default memo(function Mascot(props: MascotProps) { const [isComplete, setComplete] = useState(false); return isComplete ? null : <MascotContent {...props} onFinish={() => setComplete(true)} />; });