Legenda do mergulho até Projetos cobrir a cena; transição de página sem blur e com fundo escuro; foco no conteúdo ao trocar de página; listas com marcador e dois pesos de título; vídeos da SEV e da SC Plus regravados sem quadros brancos; scrub a 12-15 fps (-30%); vitrine com uma parada de Tab e setas, contador no celular e CSS sob demanda; âncoras num módulo só (fragment-nav); FramePulse com setInterval; CSS morto removido.
87 lines
2.3 KiB
TypeScript
87 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
type ReactNode,
|
|
Suspense,
|
|
useCallback,
|
|
useContext,
|
|
useMemo,
|
|
useState,
|
|
} from "react";
|
|
import type { PortfolioProjectView } from "@/lib/portfolio-data";
|
|
import { SHOWCASE, type ShowcaseConfig } from "@/lib/project-showcase";
|
|
import { ProjectLightboxLazy } from "./ProjectLightboxLazy";
|
|
|
|
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 && (
|
|
<Suspense fallback={null}>
|
|
<ProjectLightboxLazy
|
|
items={items}
|
|
index={openIndex}
|
|
onClose={() => setOpenId(null)}
|
|
onNavigate={(index) => setOpenId(items[index]?.config.id ?? null)}
|
|
/>
|
|
</Suspense>
|
|
)}
|
|
</ApiContext.Provider>
|
|
);
|
|
}
|