Hero e movimento - cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py): marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao cursor e, parada no topo, passa sozinha pelos estados com legenda - três níveis de aparelho (full, lite, static); sem WebGL fica o pôster - título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha." - faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções - rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa compartilhada entre a lista de cases e o case Ofertas - atendimento no WhatsApp com IA e nota fiscal para a reforma tributária (páginas de serviço, cartões da home, formulário e aviso com data de validade) - página /ia atualizada, com fontes - peças 3D em loop nos cartões e topos de página (3d/pecas.py) Design - hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta - um destino por rótulo: "Falar sobre meu projeto" leva ao formulário, "Chamar no WhatsApp" abre o WhatsApp - logo montado em blocos, 404 redesenhado, grades sem célula vazia - ícones do PWA e manifesto novos Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
92 lines
3.3 KiB
TypeScript
92 lines
3.3 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"
|
|
>
|
|
{/* sob o ponteiro ou no foco o círculo cresce e fica verde (só o `scale` anima; a cor troca na hora) */}
|
|
<span
|
|
aria-hidden="true"
|
|
className="grid size-14 place-items-center rounded-full border border-white/70 bg-black/65 text-white transition-[scale] duration-[280ms] ease-[var(--ease-out)] group-hover:scale-108 group-hover:border-[var(--slash)] group-hover:bg-[var(--slash)] group-hover:text-[var(--ink)] group-focus-visible:scale-108 group-focus-visible:border-[var(--slash)] group-focus-visible:bg-[var(--slash)] group-focus-visible:text-[var(--ink)] motion-reduce:transition-none sm:size-16"
|
|
>
|
|
<Play className="ml-1 size-6" fill="currentColor" />
|
|
</span>
|
|
</button>
|
|
</>
|
|
);
|
|
}
|