Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions src/components/home/HeroPalmMedia.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as React from 'react'
import { PauseIcon, PlayIcon } from '@phosphor-icons/react'
import { Squircle } from '~/components/Squircle'

export function HeroPalmMedia() {
const videoRef = React.useRef<HTMLVideoElement>(null)
const [hasVideoSource, setHasVideoSource] = React.useState(false)
const [isVideoReady, setIsVideoReady] = React.useState(false)
const [isPlaying, setIsPlaying] = React.useState(false)

const playVideo = React.useCallback((video: HTMLVideoElement) => {
void video
.play()
.then(() => setIsVideoReady(true))
.catch((error: unknown) => {
console.error('Unable to play the homepage hero animation', error)
setIsPlaying(false)
setIsVideoReady(false)
})
}, [])

React.useEffect(() => {
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')

if (!reducedMotion.matches) {
setHasVideoSource(true)
}

const pauseForReducedMotion = () => {
if (reducedMotion.matches) videoRef.current?.pause()
}

pauseForReducedMotion()
reducedMotion.addEventListener('change', pauseForReducedMotion)
return () =>
reducedMotion.removeEventListener('change', pauseForReducedMotion)
}, [])

React.useEffect(() => {
if (
!hasVideoSource ||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
) {
return
}

const video = videoRef.current
if (video) playVideo(video)
}, [hasVideoSource, playVideo])

const togglePlayback = () => {
const video = videoRef.current
if (!video) return

if (video.paused) {
if (hasVideoSource) {
playVideo(video)
} else {
setHasVideoSource(true)
}
} else {
video.pause()
}
}

return (
<>
<Squircle
aria-hidden
className="absolute inset-0 -z-10 overflow-hidden rounded-xl [corner-shape:squircle]"
>
<picture className="contents">
<source
type="image/webp"
srcSet="/images/hero-palm-gradient-960.webp 960w, /images/hero-palm-gradient-1600.webp 1600w, /images/hero-palm-gradient-2400.webp 2400w"
sizes="100vw"
/>
<img
src="/images/hero-palm-gradient.jpg"
alt=""
width={2400}
height={1600}
loading="eager"
fetchPriority="high"
className="h-full w-full object-cover object-center"
/>
</picture>
<video
ref={videoRef}
src={hasVideoSource ? '/images/hero-palm-motion.mp4' : undefined}
autoPlay
loop
muted
playsInline
onPause={() => setIsPlaying(false)}
onPlay={() => setIsPlaying(true)}
onCanPlay={() => setIsVideoReady(true)}
onError={() => setIsVideoReady(false)}
style={{ visibility: isVideoReady ? 'visible' : 'hidden' }}
className="absolute inset-0 h-full w-full object-cover object-center motion-reduce:hidden"
/>
</Squircle>
<button
type="button"
onClick={togglePlayback}
aria-label={isPlaying ? 'Pause hero animation' : 'Play hero animation'}
className="absolute right-4 top-4 z-20 grid size-8 place-items-center rounded-full bg-ds-neutral-500/65 text-white opacity-0 backdrop-blur-sm transition-opacity hover:bg-ds-neutral-500/80 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white group-hover:opacity-100 motion-reduce:hidden"
>
{isPlaying ? (
<PauseIcon className="size-4" weight="fill" />
) : (
<PlayIcon className="size-4" weight="fill" />
)}
</button>
</>
)
}
86 changes: 1 addition & 85 deletions src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,16 @@ import {
StackIcon,
ShieldIcon,
LightningIcon,
PauseIcon,
PlayIcon,
type Icon,
} from '@phosphor-icons/react'
import { HeroPalmMedia } from '~/components/home/HeroPalmMedia'
import { HomeApplicationStarter } from '~/components/home/HomeApplicationStarter'
import { HomeCommunitySection } from '~/components/home/HomeCommunitySection'
import { HomeNewsletterSection } from '~/components/home/HomeNewsletterSection'
import { HomeSocialProofSection } from '~/components/home/HomeSocialProofSection'
import { HomeStatsSection } from '~/components/home/HomeStatsSection'
import { useQuery } from '@tanstack/react-query'
import { Button, Eyebrow } from '~/components/ds/ui'
import { Squircle } from '~/components/Squircle'
import { Scarf } from '~/components/Scarf'
import { useInView } from '~/hooks/useInView'
import { useNpmDownloadCounter } from '~/hooks/useNpmDownloadCounter'
Expand Down Expand Up @@ -156,88 +154,6 @@ function Index() {
)
}

function HeroPalmMedia() {
const videoRef = React.useRef<HTMLVideoElement>(null)
const [isPlaying, setIsPlaying] = React.useState(true)

React.useEffect(() => {
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')

const syncMotionPreference = () => {
if (reducedMotion.matches) {
videoRef.current?.pause()
}
}

syncMotionPreference()
reducedMotion.addEventListener('change', syncMotionPreference)
return () =>
reducedMotion.removeEventListener('change', syncMotionPreference)
}, [])

const togglePlayback = () => {
const video = videoRef.current
if (!video) return

if (video.paused) {
void video.play()
} else {
video.pause()
}
}

return (
<>
<Squircle
aria-hidden
className="absolute inset-0 -z-10 overflow-hidden rounded-xl [corner-shape:squircle]"
>
<picture className="contents">
<source
type="image/webp"
srcSet="/images/hero-palm-gradient-960.webp 960w, /images/hero-palm-gradient-1600.webp 1600w, /images/hero-palm-gradient-2400.webp 2400w"
sizes="100vw"
/>
<img
src="/images/hero-palm-gradient.jpg"
alt=""
width={2400}
height={1600}
loading="eager"
fetchPriority="high"
className="h-full w-full object-cover object-center"
/>
</picture>
<video
ref={videoRef}
autoPlay
loop
muted
playsInline
poster="/images/hero-palm-gradient.jpg"
onPause={() => setIsPlaying(false)}
onPlay={() => setIsPlaying(true)}
className="absolute inset-0 h-full w-full object-cover object-center motion-reduce:hidden"
>
<source src="/images/hero-palm-motion.mp4" type="video/mp4" />
</video>
</Squircle>
<button
type="button"
onClick={togglePlayback}
aria-label={isPlaying ? 'Pause hero animation' : 'Play hero animation'}
className="absolute right-4 top-4 z-20 grid size-8 place-items-center rounded-full bg-ds-neutral-500/65 text-white opacity-0 backdrop-blur-sm transition-opacity hover:bg-ds-neutral-500/80 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white group-hover:opacity-100 motion-reduce:hidden"
>
{isPlaying ? (
<PauseIcon className="size-4" weight="fill" />
) : (
<PlayIcon className="size-4" weight="fill" />
)}
</button>
</>
)
}

type PrincipleProof = 'adapters' | 'types' | 'adoption' | 'portable'

type WhyTanStackPrinciple = {
Expand Down
Loading
Loading