- cards com moldura de navegador (endereco real) e brilho na cor do site no card que toca - um video por vez: o do card sob o mouse/foco, ou o mais perto do centro da tela; nada baixa antes de tocar, e um botao pausa os videos que se mexem sozinhos - janela ampliada em <dialog> nativo: troca computador/celular mantendo o ponto do site, capitulos clicaveis, controles proprios, setas entre projetos e foco devolvido ao fechar - dados e capitulos de cada projeto em lib/project-showcase.ts (substitui project-videos.ts) Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
85 lines
2.4 KiB
TypeScript
85 lines
2.4 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
|
|
import type { ShowcaseAssets } from "@/lib/project-showcase";
|
|
|
|
/**
|
|
* Vídeo de rolagem na capa do card. Quem decide se ele toca é a vitrine (`active`: só um card por vez, o que
|
|
* está sob o mouse ou o mais perto do centro da tela). Nada do vídeo é baixado antes da primeira vez que o
|
|
* card fica ativo; fora de cena ele pausa. Com prefers-reduced-motion só o poster aparece.
|
|
* O poster também fica por baixo (fundo da tela da moldura): o vídeo aparece em fade quando começa a tocar.
|
|
*/
|
|
export function ProjectVideo({
|
|
video,
|
|
title,
|
|
active,
|
|
}: {
|
|
video: ShowcaseAssets;
|
|
title: string;
|
|
active: boolean;
|
|
}) {
|
|
const reducedMotion = useReducedMotion();
|
|
const ref = useRef<HTMLVideoElement>(null);
|
|
const [attached, setAttached] = useState(false);
|
|
const [ready, setReady] = useState(false);
|
|
const playing = active && !reducedMotion;
|
|
|
|
useEffect(() => {
|
|
if (playing) setAttached(true);
|
|
}, [playing]);
|
|
|
|
useEffect(() => {
|
|
// as fontes entram depois do primeiro render: o elemento precisa ler a lista nova
|
|
if (attached) ref.current?.load();
|
|
}, [attached]);
|
|
|
|
useEffect(() => {
|
|
const element = ref.current;
|
|
if (!element || !attached) return;
|
|
if (playing) {
|
|
element.muted = true; // o React não reflete `muted` no DOM e o autoplay exige o valor real
|
|
element.play().catch(() => {});
|
|
} else {
|
|
element.pause(); // também cancela um autoplay que ainda esperava o arquivo chegar
|
|
}
|
|
}, [attached, playing]);
|
|
|
|
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}
|
|
data-ready={ready}
|
|
onPlaying={() => setReady(true)}
|
|
aria-label={`Demonstração em vídeo de ${title}`}
|
|
disablePictureInPicture
|
|
disableRemotePlayback
|
|
>
|
|
{attached &&
|
|
video.sources.map((source) => (
|
|
<source key={source.src} src={source.src} type={source.type} />
|
|
))}
|
|
</video>
|
|
);
|
|
}
|