diff --git a/web/lib/hooks/useMediaQuery.ts b/web/lib/hooks/useMediaQuery.ts new file mode 100644 index 000000000..3ecba9804 --- /dev/null +++ b/web/lib/hooks/useMediaQuery.ts @@ -0,0 +1,43 @@ +import { useEffect, useLayoutEffect, useState } from 'react' + +const getMatches = (query: string): boolean => { + // Prevents SSR issues + if (typeof window !== 'undefined') { + return window.matchMedia(query).matches + } + return false +} + +/** + * Returns true if the given media query matches. + * Returns undefined until the first render occured to prevent hydration errors. + * @param query A media query as string + * @returns + */ +export function useMediaQuery(query: string): boolean | undefined { + const [hasMatch, setHasMatches] = useState(getMatches(query)) + const [initialLoad, setInitialLoad] = useState(true) + + useLayoutEffect(() => { + if (initialLoad) { + setInitialLoad(false) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + function handleChange() { + setHasMatches(getMatches(query)) + } + + useEffect(() => { + const matchMedia = window.matchMedia(query) + handleChange() + matchMedia.addEventListener('change', handleChange) + return () => { + matchMedia.removeEventListener('change', handleChange) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [query]) + + return initialLoad ? undefined : hasMatch +} diff --git a/web/lib/hooks/usePaginationPadding.ts b/web/lib/hooks/usePaginationPadding.ts index 1bf4a21d4..1bb37d2cf 100644 --- a/web/lib/hooks/usePaginationPadding.ts +++ b/web/lib/hooks/usePaginationPadding.ts @@ -1,8 +1,7 @@ -import useWindowSize from './useWindowSize' +import { useMediaQuery } from './useMediaQuery' function usePaginationPadding() { - const { width } = useWindowSize() - const isMobile = width && width < 375 + const isMobile = useMediaQuery(`(max-width: 375px)`) return isMobile ? 0 : 1 } diff --git a/web/lib/hooks/useWindowSize.ts b/web/lib/hooks/useWindowSize.ts deleted file mode 100644 index c8812dfce..000000000 --- a/web/lib/hooks/useWindowSize.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useState, useEffect } from 'react' - -interface Size { - width: number | undefined - height: number | undefined -} - -function useWindowSize(): Size { - const [windowSize, setWindowSize] = useState({ - width: undefined, - height: undefined, - }) - - useEffect(() => { - function handleResize() { - setWindowSize({ - width: window.innerWidth, - height: window.innerHeight, - }) - } - - window.addEventListener('resize', handleResize) - - handleResize() - - return () => window.removeEventListener('resize', handleResize) - }, []) - - return windowSize -} - -export default useWindowSize diff --git a/web/pageComponents/shared/Hero/FullImageHero.tsx b/web/pageComponents/shared/Hero/FullImageHero.tsx index 66bdaa28d..cd66040ea 100644 --- a/web/pageComponents/shared/Hero/FullImageHero.tsx +++ b/web/pageComponents/shared/Hero/FullImageHero.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components' import type { HeroType, ImageWithCaptionData } from 'types' -import useWindowSize from '../../../lib/hooks/useWindowSize' +import { useSanityLoader } from '../../../lib/hooks/useSanityLoader' import Image, { Ratios } from '../SanityImage' import { StyledCaption } from '../image/StyledCaption' @@ -25,11 +25,16 @@ const FullScreenHero = ({ figure }: FullImageHeroType) => { } const NarrowHero = ({ figure }: FullImageHeroType) => { - const { width } = useWindowSize() // 4:3 for small screens and 10:3 for large screens - const aspectRatio = width && width < 750 ? Ratios.THREE_TO_FOUR : Ratios.THREE_TO_TEN + const desktopUrl = useSanityLoader(figure.image, 4096, Ratios.THREE_TO_TEN) - return + // Using picture with mobile and dekstop source to avoid initial load layout shift between aspect ratio + return ( + + + + + ) } const RatioHero = ({ ratio, figure }: FullImageHeroType) => { diff --git a/web/pageComponents/topicPages/FullWidthImage.tsx b/web/pageComponents/topicPages/FullWidthImage.tsx index 8cd90f075..6871db403 100644 --- a/web/pageComponents/topicPages/FullWidthImage.tsx +++ b/web/pageComponents/topicPages/FullWidthImage.tsx @@ -1,7 +1,7 @@ import type { FullWidthImageData } from '../../types/types' import Image, { Ratios } from '../shared/SanityImage' import { StyledCaption } from '../shared/image/StyledCaption' -import useWindowSize from '../../lib/hooks/useWindowSize' +import { useMediaQuery } from '../../lib/hooks/useMediaQuery' type TeaserProps = { data: FullWidthImageData @@ -11,9 +11,9 @@ type TeaserProps = { const FullWidthImage = ({ data, anchor }: TeaserProps) => { const { image, attribution, caption } = data.image const { aspectRatio } = data.designOptions - const { width } = useWindowSize() + const isMobile = useMediaQuery(`(max-width: 750px)`) - const ratio = (width && width < 750) || aspectRatio === Ratios.ONE_TO_TWO ? Ratios.ONE_TO_TWO : Ratios.THREE_TO_TEN + const ratio = isMobile || aspectRatio === Ratios.ONE_TO_TWO ? Ratios.ONE_TO_TWO : Ratios.THREE_TO_TEN if (!image) return null return ( diff --git a/web/pageComponents/topicPages/KeyNumbers/KeyNumbers.tsx b/web/pageComponents/topicPages/KeyNumbers/KeyNumbers.tsx index a4673d1bf..aeb424db9 100644 --- a/web/pageComponents/topicPages/KeyNumbers/KeyNumbers.tsx +++ b/web/pageComponents/topicPages/KeyNumbers/KeyNumbers.tsx @@ -7,7 +7,7 @@ import KeyNumberItem from './KeyNumberItem' import ReadMoreLink from '../../../pageComponents/shared/ReadMoreLink' import RichText from '../../shared/portableText/RichText' import { Carousel } from '../../shared/Carousel' -import useWindowSize from '../../../lib/hooks/useWindowSize' +import { useMediaQuery } from '../../../lib/hooks/useMediaQuery' const Disclaimer = styled.div` @media (min-width: 1300px) { @@ -55,9 +55,9 @@ type KeyNumbersProps = { } export default function ({ data, anchor }: KeyNumbersProps) { const { title, items, designOptions, ingress, action, disclaimer, useHorizontalScroll } = data - const { width } = useWindowSize() + const isMobile = useMediaQuery(`(max-width: 800px)`) - const renderScroll = useHorizontalScroll && Boolean(width && width <= 800) + const renderScroll = useHorizontalScroll && isMobile const Wrapper = renderScroll ? ({ children }: { children: React.ReactNode }) => ( diff --git a/web/pageComponents/topicPages/PromoTileArray.tsx b/web/pageComponents/topicPages/PromoTileArray.tsx index 429f7160a..d2227419d 100644 --- a/web/pageComponents/topicPages/PromoTileArray.tsx +++ b/web/pageComponents/topicPages/PromoTileArray.tsx @@ -7,8 +7,8 @@ import type { PromoTileArrayData, PromoTileData } from '../../types/types' import Image, { Ratios } from '../shared/SanityImage' import PromotileTitleText from '../shared/portableText/PromoTileTitleText' import { PromoTileButton } from './PromoTileButton' -import useWindowSize from '../../lib/hooks/useWindowSize' import { Carousel } from '../shared/Carousel' +import { useMediaQuery } from '../../lib/hooks/useMediaQuery' const { Header, Action, Media } = Card @@ -57,11 +57,11 @@ const StyledCard = styled(Card)` ` const PromoTileArray = ({ data, anchor }: { data: PromoTileArrayData; anchor?: string }) => { - const { width } = useWindowSize() + const isMobile = useMediaQuery(`(max-width: 800px)`) if (!data.group) return null - const renderScroll = data.useHorizontalScroll || Boolean(width && width <= 800) + const renderScroll = data.useHorizontalScroll || isMobile const richTitle = (title: PortableTextBlock[], hasImage: boolean) => { return ( diff --git a/web/pageComponents/topicPages/promotions/MultipleEventCards.tsx b/web/pageComponents/topicPages/promotions/MultipleEventCards.tsx index 008817744..563841eb3 100644 --- a/web/pageComponents/topicPages/promotions/MultipleEventCards.tsx +++ b/web/pageComponents/topicPages/promotions/MultipleEventCards.tsx @@ -91,7 +91,7 @@ const MultipleEventCards = ({ ) }) } - if (eventPromotionSettings?.pastEventsCount) { + if (eventPromotionSettings?.promotePastEvents && eventPromotionSettings?.pastEventsCount) { data = data.slice(0, eventPromotionSettings.pastEventsCount) } diff --git a/web/pageComponents/topicPages/promotions/MultiplePromotions.tsx b/web/pageComponents/topicPages/promotions/MultiplePromotions.tsx index 14f417d61..8c5be2201 100644 --- a/web/pageComponents/topicPages/promotions/MultiplePromotions.tsx +++ b/web/pageComponents/topicPages/promotions/MultiplePromotions.tsx @@ -10,9 +10,9 @@ import NewsCard from '../../cards/NewsCard' import TopicPageCard from '../../cards/TopicPageCard' import PeopleCard from '../../cards/PeopleCard/PeopleCard' import MultipleEventCards from './MultipleEventCards' -import useWindowSize from '../../../lib/hooks/useWindowSize' import { Carousel } from '../../shared/Carousel' import { BackgroundContainer } from '@components/Backgrounds' +import { useMediaQuery } from '../../../lib/hooks/useMediaQuery' const CardsWrapper = styled.div` width: 100%; @@ -125,8 +125,8 @@ const MultiplePromotions = ({ } } - const { width } = useWindowSize() - const renderScroll = useHorizontalScroll || Boolean(width && width <= 800) + const isMobile = useMediaQuery(`(max-width: 800px)`) + const renderScroll = useHorizontalScroll || isMobile if (variant === 'promoteEvents') { return ( diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index b9d5681e4..4170a6146 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@algolia/cache-browser-local-storage': specifier: ^4.22.1 @@ -15880,3 +15876,7 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: true + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false