Skip to content

Commit 94752ad

Browse files
committed
- Creates MediaGridBlock component
- Creates useMediaQuery and useWindowHeight hooks
1 parent 711404b commit 94752ad

File tree

4 files changed

+208
-2
lines changed

4 files changed

+208
-2
lines changed

apps/web/src/app/components/Blocks/RenderBlocks.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,22 @@ const MediaContentCollageBlock = React.lazy(() =>
3333
default: module.MediaContentCollageBlock,
3434
})),
3535
)
36+
const MediaGridBlock = React.lazy(() =>
37+
import('./sections/MediaGridBlock').then((module) => ({
38+
default: module.MediaGridBlock,
39+
})),
40+
)
3641

3742
type BlockTypes = Page['layout'][number]['blockType']
3843

3944
const components: Record<BlockTypes, React.LazyExoticComponent<any>> = {
40-
content: ContentBlock,
41-
media: MediaBlock,
4245
cta: CTABlock,
4346
'cta-grid': CTAGridBlock,
47+
content: ContentBlock,
48+
media: MediaBlock,
4449
'media-collage': MediaCollageBlock,
4550
'media-content-collage': MediaContentCollageBlock,
51+
'media-grid': MediaGridBlock,
4652
}
4753

4854
type Props = {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import {
2+
type MotionValue,
3+
m,
4+
useReducedMotion,
5+
useScroll,
6+
useTransform,
7+
} from 'framer-motion'
8+
import React from 'react'
9+
import { LazyMotionDomAnimation } from '~/app/utils/framerMotion/LazyMotionFeatures'
10+
import { useMediaQuery } from '~/app/utils/useMediaQuery'
11+
import {
12+
type MediaGridBlockType,
13+
type Media as MediaType,
14+
} from '~/cms/payload-types'
15+
16+
import { BackgroundColor } from '../../BackgroundColor'
17+
import { Grid, Gutter } from '../../Layout'
18+
import { Media } from '../Media'
19+
import { RichText } from '../RichText'
20+
import { getParallaxTranslateYOutput, parallaxStyles } from '../parallaxUtils'
21+
22+
export const MediaGridBlock = (props: MediaGridBlockType): JSX.Element => {
23+
const shouldReduceMotion = useReducedMotion()
24+
const ref = React.useRef(null)
25+
const { scrollYProgress } = useScroll({
26+
offset: ['start end', 'end start'],
27+
target: ref,
28+
})
29+
const isLarge = useMediaQuery('only screen and (min-width: 1024px)', false)
30+
const y = useMediaGridScrollTransform(scrollYProgress)
31+
32+
const opacity = useTransform(scrollYProgress, [0, 1], [1, 0.5])
33+
const opacityStyle = { opacity }
34+
35+
return (
36+
<LazyMotionDomAnimation>
37+
<div className='pt-0 xs:pt-32 md:py-44' ref={ref}>
38+
{props.content ? (
39+
<div className='container'>
40+
<Grid>
41+
<div className='col-span-8 md:col-span-6'>
42+
<RichText content={props.content} />
43+
</div>
44+
</Grid>
45+
</div>
46+
) : null}
47+
48+
<div className='relative'>
49+
<Gutter
50+
left
51+
className='absolute top-[10%] right-0 bottom-[10%] left-0'
52+
>
53+
<BackgroundColor
54+
color={props.backgroundColor.color}
55+
className='h-full'
56+
/>
57+
</Gutter>
58+
59+
<div className='container'>
60+
<Grid>
61+
{props.media?.map(({ media, content, id }, i) => {
62+
if (typeof media === 'string') return null
63+
64+
let style
65+
if (shouldReduceMotion) {
66+
style = opacityStyle
67+
} else {
68+
style = isLarge ? { y: y.large[i % 3] } : { y: y.small }
69+
}
70+
71+
return (
72+
<div className='col-span-4' key={id ?? i}>
73+
<m.div
74+
className={parallaxStyles({ className: 'relative' })}
75+
style={style}
76+
>
77+
<Media
78+
{...media}
79+
className='text-antique align-middle'
80+
srcSizes={[
81+
'36w',
82+
'media(min-width: 480px) 40w',
83+
'media(min-width: 768px) 43w',
84+
'media(min-width: 1024px) 29w',
85+
'media(min-width: 1280px) 23w',
86+
'media(min-width: 1536px) 19w',
87+
'media(min-width: 1920px) 15w',
88+
]}
89+
srcBreakpoints={[{ width: 480, height: 640 }]}
90+
/>
91+
{content ? (
92+
<div className='p text-sm md:text-base flex items-end absolute inset-0 p-4 whitespace-pre-wrap leading-7 tracking-widest md:tracking-normal md:leading-8 bg-gradient-to-b from-gray/0 via-gray/20 to-gray/80 text-antique'>
93+
{content}
94+
</div>
95+
) : null}
96+
</m.div>
97+
</div>
98+
)
99+
})}
100+
</Grid>
101+
</div>
102+
</div>
103+
</div>
104+
</LazyMotionDomAnimation>
105+
)
106+
}
107+
108+
const useMediaGridScrollTransform = (scrollYProgress: MotionValue<number>) => {
109+
const inputRange = [0, 1]
110+
const slow = useTransform(
111+
scrollYProgress,
112+
inputRange,
113+
getParallaxTranslateYOutput(50),
114+
)
115+
const medium = useTransform(
116+
scrollYProgress,
117+
inputRange,
118+
getParallaxTranslateYOutput(100),
119+
)
120+
const fast = useTransform(
121+
scrollYProgress,
122+
inputRange,
123+
getParallaxTranslateYOutput(150),
124+
)
125+
126+
return {
127+
large: [slow, fast, medium],
128+
small: useTransform(
129+
scrollYProgress,
130+
inputRange,
131+
getParallaxTranslateYOutput(0),
132+
),
133+
}
134+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react'
2+
3+
/**
4+
* More performant than window.innerWidth and context (w/o useMemo)
5+
* @param {String} query
6+
* @param {Boolean} serverFallback
7+
* @return {Boolean} Whether devices matches the media query
8+
*/
9+
export function useMediaQuery(query: string, serverFallback: boolean): boolean {
10+
const getServerSnapshot = () => serverFallback
11+
12+
const subscribe = React.useCallback(
13+
(callback: () => void) => {
14+
const matchMedia = window.matchMedia(query)
15+
16+
if (matchMedia.addEventListener) {
17+
matchMedia.addEventListener('change', callback)
18+
} else {
19+
//https://caniuse.com/?search=MediaQueryList%20inherits%20EventTarget
20+
matchMedia.addListener(callback)
21+
}
22+
return () => {
23+
matchMedia.removeEventListener('change', callback)
24+
}
25+
},
26+
[query],
27+
)
28+
29+
const getSnapshot = () => {
30+
return window.matchMedia(query).matches
31+
}
32+
33+
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
34+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react'
2+
3+
type Listener = () => void
4+
const listeners = new Set<Listener>()
5+
6+
let currentHeight = window.innerHeight
7+
8+
function subscribe(listener: Listener) {
9+
listeners.add(listener)
10+
return () => {
11+
listeners.delete(listener)
12+
}
13+
}
14+
15+
function getSnapshot() {
16+
return currentHeight
17+
}
18+
19+
function getServerSnapshot() {
20+
return 0
21+
}
22+
23+
function resizeHandler() {
24+
currentHeight = window.innerHeight
25+
listeners.forEach((listener) => listener())
26+
}
27+
28+
window.addEventListener('resize', resizeHandler)
29+
30+
export function useWindowHeight() {
31+
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
32+
}

0 commit comments

Comments
 (0)