diff --git a/app/components/confetti.tsx b/app/components/confetti.tsx index 69fbecef..87b02ffa 100644 --- a/app/components/confetti.tsx +++ b/app/components/confetti.tsx @@ -1,7 +1,9 @@ import { Index as ConfettiShower } from 'confetti-react' import { ClientOnly } from 'remix-utils' +import { useWindowSize } from '#app/utils/useWindowSize.ts' export function Confetti({ id }: { id?: string | null }) { + const { width, height } = useWindowSize() if (!id) return null return ( @@ -12,8 +14,8 @@ export function Confetti({ id }: { id?: string | null }) { run={Boolean(id)} recycle={false} numberOfPieces={500} - width={window.innerWidth} - height={window.innerHeight} + width={width} + height={height} /> )} diff --git a/app/utils/useEventListener.ts b/app/utils/useEventListener.ts new file mode 100644 index 00000000..2b5745ad --- /dev/null +++ b/app/utils/useEventListener.ts @@ -0,0 +1,81 @@ +import { type RefObject, useEffect, useRef } from 'react' +import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect.ts' + +// MediaQueryList Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: MediaQueryListEventMap[K]) => void, + element: RefObject, + options?: boolean | AddEventListenerOptions, +): void + +// Window Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: WindowEventMap[K]) => void, + element?: undefined, + options?: boolean | AddEventListenerOptions, +): void + +// Element Event based useEventListener interface +function useEventListener< + K extends keyof HTMLElementEventMap, + T extends HTMLElement = HTMLDivElement, +>( + eventName: K, + handler: (event: HTMLElementEventMap[K]) => void, + element: RefObject, + options?: boolean | AddEventListenerOptions, +): void + +// Document Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: DocumentEventMap[K]) => void, + element: RefObject, + options?: boolean | AddEventListenerOptions, +): void + +function useEventListener< + KW extends keyof WindowEventMap, + KH extends keyof HTMLElementEventMap, + KM extends keyof MediaQueryListEventMap, + T extends HTMLElement | MediaQueryList | void = void, +>( + eventName: KW | KH | KM, + handler: ( + event: + | WindowEventMap[KW] + | HTMLElementEventMap[KH] + | MediaQueryListEventMap[KM] + | Event, + ) => void, + element?: RefObject, + options?: boolean | AddEventListenerOptions, +) { + // Create a ref that stores handler + const savedHandler = useRef(handler) + + useIsomorphicLayoutEffect(() => { + savedHandler.current = handler + }, [handler]) + + useEffect(() => { + // Define the listening target + const targetElement: T | Window = element?.current ?? window + + if (!(targetElement && targetElement.addEventListener)) return + + // Create event listener that calls handler function stored in ref + const listener: typeof handler = event => savedHandler.current(event) + + targetElement.addEventListener(eventName, listener, options) + + // Remove event listener on cleanup + return () => { + targetElement.removeEventListener(eventName, listener, options) + } + }, [eventName, element, options]) +} + +export { useEventListener } diff --git a/app/utils/useIsomorphicLayoutEffect.ts b/app/utils/useIsomorphicLayoutEffect.ts new file mode 100644 index 00000000..f6e5a114 --- /dev/null +++ b/app/utils/useIsomorphicLayoutEffect.ts @@ -0,0 +1,4 @@ +import { useEffect, useLayoutEffect } from 'react' + +export const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect diff --git a/app/utils/useWindowSize.ts b/app/utils/useWindowSize.ts new file mode 100644 index 00000000..46dd12a9 --- /dev/null +++ b/app/utils/useWindowSize.ts @@ -0,0 +1,32 @@ +import { useState } from 'react' +import { useEventListener } from './useEventListener.ts' +import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect.ts' + +interface WindowSize { + width: number + height: number +} + +export function useWindowSize(): WindowSize { + const [windowSize, setWindowSize] = useState({ + width: 0, + height: 0, + }) + + const handleSize = () => { + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }) + } + + useEventListener('resize', handleSize) + + // Set size at the first client-side load + useIsomorphicLayoutEffect(() => { + handleSize() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return windowSize +}