Files
Evandro Fonseca Junior de6af74a0c feat(cases): prova certa por serviço, páginas internas com trabalho no topo
Diagnóstico sem case que não cabe; desktop com o GCC Installer como projeto próprio; IoT com o Twins Lab e a nota da Apolo. Mosaico de pôsteres em /servicos e /cases, faixa baixa em uma coluna no currículo, privacidade e laboratório; Laboratório como lista compacta; SEV começando por catálogo, pedido no WhatsApp e painel; Joinville e SC nos títulos; imagens do LCP com fetchPriority e sizes reais; pré-carregamento só no hover; 404 com Open Graph próprio.
2026-10-14 17:01:35 -03:00

91 lines
2.9 KiB
TypeScript

"use client";
import { Play } from "lucide-react";
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import type { Device, ShowcaseAssets } from "@/lib/project-showcase";
const deviceLabel: Record<Device, string> = {
desktop: "no computador",
mobile: "no celular",
};
/**
* Pôster com um botão de play. O vídeo só é montado, e só baixa, quando a pessoa aperta o botão:
* antes disso a página mostra a imagem do site, não um vídeo parado em 0:00 com controles do navegador.
* Vai dentro de um quadro com `position: relative` (moldura de navegador ou de celular) que dá o tamanho.
* `sizes` segue a grade de CaseMedia: a coluna da esquerda de um bloco de até 1040 px (88 vw da página abaixo de 1182 px)
* com a moldura do celular ao lado (220 px mais 32 px de vão); abaixo de 768 px, a largura da página menos 40 px.
*/
export function CasePlayer({
assets,
title,
device,
sizes = "(min-width: 1182px) 788px, (min-width: 768px) calc(88vw - 252px), calc(100vw - 40px)",
}: {
assets: ShowcaseAssets;
title: string;
device: Device;
sizes?: string;
}) {
const [playing, setPlaying] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
// o botão sai de cena ao tocar: o foco vai para o vídeo, para quem usa o teclado não o perder
useEffect(() => {
if (playing) videoRef.current?.focus();
}, [playing]);
if (playing) {
return (
<video
ref={videoRef}
className="absolute inset-0 h-full w-full object-cover object-top"
autoPlay
controls
muted
loop
playsInline
poster={assets.poster}
width={assets.width}
height={assets.height}
tabIndex={0}
aria-label={`Vídeo do site ${title} ${deviceLabel[device]}`}
disablePictureInPicture
>
{assets.sources.map((source) => (
<source key={source.src} src={source.src} type={source.type} />
))}
</video>
);
}
return (
<>
{/* o quadro do celular já mostra o pôster como fundo; no computador a imagem entra aqui */}
{device === "desktop" ? (
<Image
src={assets.poster}
alt=""
fill
sizes={sizes}
className="object-cover object-top"
/>
) : null}
<button
type="button"
onClick={() => setPlaying(true)}
aria-label={`Reproduzir o vídeo do site ${title} ${deviceLabel[device]}`}
className="group absolute inset-0 grid place-items-center focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-white"
>
<span
aria-hidden="true"
className="grid size-14 place-items-center rounded-full border border-white/70 bg-black/65 text-white transition-colors group-hover:bg-black/85 motion-reduce:transition-none sm:size-16"
>
<Play className="ml-1 size-6" fill="currentColor" />
</span>
</button>
</>
);
}