Projetos sobem sobre o palco do hero e o celular tem hero de uma tela sem mergulho; passagens com animation-timeline em CSS, sem GSAP nem motion; troca da vitrine sem texto duplicado; moldura de celular só com pôster e scrub reencodado em 960 px (9,4 para 5,2 MB); 3D monta na primeira interação e para quando parado; transição entre páginas do mesmo segmento; foco em anel duplo; texto de 15 px no celular; código morto removido.
84 lines
2.2 KiB
TypeScript
84 lines
2.2 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
type ReactNode,
|
|
useCallback,
|
|
useContext,
|
|
useMemo,
|
|
useState,
|
|
} from "react";
|
|
import type { PortfolioProjectView } from "@/lib/portfolio-data";
|
|
import { SHOWCASE, type ShowcaseConfig } from "@/lib/project-showcase";
|
|
import { ProjectLightbox } from "./ProjectLightbox";
|
|
|
|
export interface ShowcaseItem {
|
|
config: ShowcaseConfig;
|
|
project: PortfolioProjectView;
|
|
}
|
|
|
|
interface ShowcaseApi {
|
|
open(id: string): void;
|
|
items: ShowcaseItem[];
|
|
}
|
|
|
|
const ApiContext = createContext<ShowcaseApi | null>(null);
|
|
|
|
/** Só aparece como card com vídeo o projeto que existe na lista e tem vídeo gravado. */
|
|
export function buildShowcaseItems(
|
|
projects: PortfolioProjectView[],
|
|
): ShowcaseItem[] {
|
|
const items: ShowcaseItem[] = [];
|
|
for (const config of SHOWCASE) {
|
|
const project = projects.find((candidate) => candidate.id === config.id);
|
|
if (project) items.push({ config, project });
|
|
}
|
|
return items;
|
|
}
|
|
|
|
/** Itens da vitrine para quem está dentro do provedor. */
|
|
export function useShowcaseItems() {
|
|
return useContext(ApiContext)?.items ?? [];
|
|
}
|
|
|
|
/** Abre a janela ampliada de um projeto (nada acontece fora do provedor). */
|
|
export function useShowcaseOpen() {
|
|
const api = useContext(ApiContext);
|
|
return useCallback((id: string) => api?.open(id), [api]);
|
|
}
|
|
|
|
/**
|
|
* Guarda os projetos da vitrine e qual deles está ampliado, e mostra a janela ampliada. A vitrine animada
|
|
* (ProjectReel) e a lista do celular (ProjectList) leem a mesma lista de itens.
|
|
*/
|
|
export function ShowcaseProvider({
|
|
projects,
|
|
children,
|
|
}: {
|
|
projects: PortfolioProjectView[];
|
|
children: ReactNode;
|
|
}) {
|
|
const items = useMemo(() => buildShowcaseItems(projects), [projects]);
|
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
|
|
const api = useMemo<ShowcaseApi>(() => ({ items, open: setOpenId }), [items]);
|
|
|
|
const openIndex = openId
|
|
? items.findIndex((item) => item.config.id === openId)
|
|
: -1;
|
|
|
|
return (
|
|
<ApiContext.Provider value={api}>
|
|
{children}
|
|
{openIndex >= 0 && (
|
|
<ProjectLightbox
|
|
items={items}
|
|
index={openIndex}
|
|
onClose={() => setOpenId(null)}
|
|
onNavigate={(index) => setOpenId(items[index]?.config.id ?? null)}
|
|
/>
|
|
)}
|
|
</ApiContext.Provider>
|
|
);
|
|
}
|