O botão das ofertas rola até o contato com o tipo marcado e não quebra mais as âncoras; ofertas com o que você recebe e primeiro passo, logo depois da vitrine; bloco de IA fora da home; cartão largo da SEV no hero do celular; separador da faixa de clientes só entre itens; favicon de 13 para 1,8 KB; pré-carregamento só no hover e nunca de /#... fora da home; X do menu e cabeçalho opaco; WhatsApp com "o case"; /ia com 3 passos e modelo de relatório ilustrativo; Sobre alinhado ao currículo; CSP Report-Only com os domínios do Ads.
86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
"use client";
|
|
|
|
import type { MouseEvent, ReactNode } from "react";
|
|
import { prefersReducedMotion } from "@/lib/device";
|
|
import {
|
|
elementForHash,
|
|
focusFragment,
|
|
isPlainPrimaryClick,
|
|
} from "./cinematic/fragment-nav";
|
|
import {
|
|
CONTACT_KIND_EVENT,
|
|
CONTACT_KIND_PARAM,
|
|
type ContactKind,
|
|
contactKindHref,
|
|
} from "./contact-subject";
|
|
import HoverPrefetchLink from "./HoverPrefetchLink";
|
|
|
|
const CONTACT_HASH = "#contact";
|
|
|
|
/**
|
|
* Deixa o endereço da home como o do link do cabeçalho (`/#contact`): com o fragmento do formulário e sem o `?tipo=`
|
|
* que uma oferta anterior ou a chegada de outra página deixou (o tipo já está no formulário). `replaceState`, e não
|
|
* uma navegação do Next: nada recarrega, e as âncoras seguintes (cabeçalho, rodapé) continuam a rolar a página.
|
|
*/
|
|
function showContactInAddress() {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.delete(CONTACT_KIND_PARAM);
|
|
url.hash = CONTACT_HASH;
|
|
const next = `${url.pathname}${url.search}${url.hash}`;
|
|
const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
if (next !== current)
|
|
window.history.replaceState(window.history.state, "", next);
|
|
}
|
|
|
|
/**
|
|
* Botão de uma oferta da home: leva ao formulário de contato com o tipo de projeto já escolhido.
|
|
* Na própria home o tipo segue por evento e a página rola até `#contact` (sem navegar pelo Next); o `href` com
|
|
* `?tipo=` cobre quem chega de outra página ou abre o link em outra aba.
|
|
*
|
|
* O normal é o gerenciador de âncoras (fragment-nav + useCinematicScroll) tratar o clique antes deste código, do
|
|
* mesmo jeito que trata o link do cabeçalho: rola (com movimento reduzido, em salto), dá o foco ao formulário e
|
|
* cancela o clique. Quando ele não o trata (ainda não montou, por exemplo), este botão cancela a navegação, rola e dá
|
|
* o foco por conta própria. Em outra página, nada é cancelado: o Link navega para `/?tipo=…#contact`.
|
|
*/
|
|
export default function ContactOfferLink({
|
|
kind,
|
|
className,
|
|
children,
|
|
}: {
|
|
kind: ContactKind;
|
|
className?: string;
|
|
children: ReactNode;
|
|
}) {
|
|
const onClick = (event: MouseEvent<HTMLAnchorElement>) => {
|
|
window.dispatchEvent(
|
|
new CustomEvent<ContactKind>(CONTACT_KIND_EVENT, { detail: kind }),
|
|
);
|
|
if (window.location.pathname !== "/") return;
|
|
const form = elementForHash(CONTACT_HASH);
|
|
if (!form) return;
|
|
if (event.defaultPrevented) {
|
|
showContactInAddress();
|
|
return;
|
|
}
|
|
// aba nova, clique com tecla modificadora ou botão do meio: o navegador cuida, com o `?tipo=` do href
|
|
if (!isPlainPrimaryClick(event.nativeEvent)) return;
|
|
event.preventDefault();
|
|
showContactInAddress();
|
|
form.scrollIntoView({
|
|
behavior: prefersReducedMotion() ? "instant" : "smooth",
|
|
block: "start",
|
|
});
|
|
focusFragment(form);
|
|
};
|
|
|
|
return (
|
|
<HoverPrefetchLink
|
|
href={contactKindHref(kind)}
|
|
className={className}
|
|
onClick={onClick}
|
|
>
|
|
{children}
|
|
</HoverPrefetchLink>
|
|
);
|
|
}
|