feat(projects): video de rolagem na capa dos cards, carregado so quando o card aparece
video autoplay muted loop playsinline preload=none com poster; as fontes entram via IntersectionObserver e o video pausa fora da tela. Com prefers-reduced-motion aparece so o poster. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
ca9c0604b8
commit
a67892613b
6 files changed
+276
-2
No files matched your search
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
|
||||
import type { ProjectVideoAsset } from "@/lib/project-videos";
|
||||
|
||||
/**
|
||||
* Vídeo de rolagem do projeto na capa do card. Nada de vídeo é baixado até o card entrar na tela
|
||||
* (IntersectionObserver); fora da tela ele pausa. Com prefers-reduced-motion só o poster aparece.
|
||||
*/
|
||||
export function ProjectVideo({
|
||||
video,
|
||||
title,
|
||||
}: {
|
||||
video: ProjectVideoAsset;
|
||||
title: string;
|
||||
}) {
|
||||
const reducedMotion = useReducedMotion();
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
const [activated, setActivated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (reducedMotion || !element) return;
|
||||
if (typeof IntersectionObserver === "undefined") {
|
||||
setActivated(true);
|
||||
return;
|
||||
}
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry?.isIntersecting) {
|
||||
setActivated(true);
|
||||
element.play().catch(() => {});
|
||||
} else {
|
||||
element.pause();
|
||||
}
|
||||
},
|
||||
{ rootMargin: "120px 0px", threshold: 0.2 },
|
||||
);
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
}, [reducedMotion]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!activated || reducedMotion || !element) return;
|
||||
element.muted = true; // o React não reflete `muted` no DOM e o autoplay exige o valor real
|
||||
element.load();
|
||||
element.play().catch(() => {});
|
||||
}, [activated, reducedMotion]);
|
||||
|
||||
if (reducedMotion) {
|
||||
return (
|
||||
<Image
|
||||
src={video.poster}
|
||||
alt={`Captura de tela de ${title}`}
|
||||
width={video.width}
|
||||
height={video.height}
|
||||
sizes="(min-width: 1360px) 33vw, (min-width: 1025px) 50vw, 100vw"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={ref}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="none"
|
||||
poster={video.poster}
|
||||
width={video.width}
|
||||
height={video.height}
|
||||
aria-label={`Demonstração em vídeo de ${title}`}
|
||||
disablePictureInPicture
|
||||
disableRemotePlayback
|
||||
>
|
||||
{activated &&
|
||||
video.sources.map((source) => (
|
||||
<source key={source.src} src={source.src} type={source.type} />
|
||||
))}
|
||||
</video>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user