"use client"; import Image from "next/image"; import { type CSSProperties, type RefObject, useEffect, useId, useRef, useState, ViewTransition, } from "react"; import { coverTransitionProps } from "@/app/cases/_components/cover-transition"; import { type Device, getShowcaseAssets, projectKicker, } from "@/lib/project-showcase"; import { BrowserFrame, displayUrl } from "./BrowserFrame"; import { PhoneFrame } from "./PhoneFrame"; import { ProjectActions } from "./ProjectActions"; import { type ShowcaseItem, useShowcaseOpen } from "./ShowcaseProvider"; import { useNearViewport } from "./useShowcaseMedia"; /** * Qual cartão do carrossel está à frente: o índice do cartão cujo início está mais perto do começo da faixa (no fim da * rolagem, o último, que talvez não alcance o começo). Só lê a posição da rolagem; sem rolagem horizontal fica em 0. */ export function carouselIndex(list: HTMLElement) { const cards = Array.from(list.children) as HTMLElement[]; if (cards.length < 2 || list.scrollWidth <= list.clientWidth + 1) return 0; if (list.scrollLeft + list.clientWidth >= list.scrollWidth - 2) return cards.length - 1; const stride = cards[1].getBoundingClientRect().left - cards[0].getBoundingClientRect().left; if (stride <= 0) return 0; return Math.min( cards.length - 1, Math.max(0, Math.round(list.scrollLeft / stride)), ); } /** Acompanha a rolagem horizontal da faixa e devolve o índice do cartão à frente (em um quadro de animação por vez). */ function useCarouselIndex( ref: RefObject, enabled: boolean, ) { const [index, setIndex] = useState(0); useEffect(() => { const list = ref.current; if (!enabled || !list) return; let frame = 0; const read = () => { frame = 0; setIndex(carouselIndex(list)); }; const onScroll = () => { if (!frame) frame = requestAnimationFrame(read); }; read(); list.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { cancelAnimationFrame(frame); list.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, [ref, enabled]); return index; } function ListItem({ item, device, onExpand, }: { item: ShowcaseItem; /** `null` no servidor e até montar: o item mostra só o texto e os links. */ device: Device | null; onExpand?: () => void; }) { const { config, project } = item; const ref = useRef(null); const near = useNearViewport(ref); const assets = device ? getShowcaseAssets(config.id, device) : null; // só pôster (nunca vídeo): a imagem entra quando o item chega a ~1 tela da janela. É a mesma capa do topo do // case: viaja até lá na troca de página (o item tem uma moldura só, então o nome não se repete) const poster = assets && near ? ( {`Captura ) : null; return (
  • {device && (
    {device === "mobile" ? ( {poster} ) : (
    {poster}
    )}
    )}

    {projectKicker(project.category, project.status)}

    {project.title}

    {project.subtitle &&

    {project.subtitle}

    }
  • ); } /** * Projetos em lista simples: sem rolagem presa, sem vídeo que acompanha a rolagem. No celular (abaixo de 768 px) * é um carrossel por toque (rolagem horizontal com encaixe, o próximo projeto aparece pela borda); em telas * maiores, uma grade. Serve também a quem pede menos movimento e a janelas baixas demais para a vitrine fixa. * * É o que o servidor entrega (texto e links, sem mídia): sem JavaScript os projetos continuam todos ali, e * `pending` esconde a lista por CSS só onde a vitrine fixa vai assumir o lugar (largura e altura de desktop). * Pôster e "Ampliar vídeo" entram depois que o navegador monta; vídeo, só dentro da janela ampliada. */ export function ProjectList({ items, device, pending = false, }: { items: ShowcaseItem[]; device: Device | null; pending?: boolean; }) { const open = useShowcaseOpen(); const headingId = useId(); const listRef = useRef(null); // a contagem só existe no celular (o carrossel por toque); a grade das telas maiores não rola para o lado const carousel = device === "mobile"; const current = useCarouselIndex(listRef, carousel); return (

    Projetos de clientes

      {items.map((item) => ( open(item.config.id) : undefined} /> ))}
    {carousel && items.length > 1 && ( // quem lê com leitor de tela já tem a lista (com o número de itens): a contagem é só visual )}
    ); }