feat(home): ofertas que vendem o título, WhatsApp no hero e menu do celular acessível

Três ofertas (site que traz pedidos pelo WhatsApp, sistema para tirar a operação da planilha, revisão de sistema existente) com miniatura e formulário com o tipo escolhido; botão principal do hero abre o WhatsApp; moldura da SEV no hero do celular; menu fecha ao navegar, Esc, foco preso incluindo a barra de cookies e espaço para ela; CinematicHome como componente de servidor com ilha cliente; /ia com H1 de oferta e cartão do que você recebe; botão flutuante em aside com transform; AVIF antes de WebP; copy sem repetir contato direto.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 17:01:36 -03:00
1 parent de6af74a0c
commit f8545cd442
44 files changed
+2286 -571

No files matched your search

+71 -14
View File
@@ -30,32 +30,89 @@ export interface WhatsAppContexts {
/** Entra na frase "Quero conversar sobre {assunto}." da mensagem. */
export const AI_REVIEW_SUBJECT = "a revisão de um aplicativo feito com IA";
/** Nome pela chave; nome vazio ou só com espaços vale como ausente, para a mensagem cair no assunto padrão em vez de "sobre .". */
function lookup(table: Record<string, string> | undefined, key: string) {
const name = table && Object.hasOwn(table, key) ? table[key].trim() : "";
/**
* Assuntos das páginas sem slug. Cabeçalho, botão flutuante e corpo da mesma página usam a mesma frase;
* /cases e /servicos repetem a que os botões do corpo dessas páginas já usam.
*/
export const HOME_SUBJECT = "um site ou sistema para a minha empresa";
export const CASES_SUBJECT = "um projeto parecido com os cases do portfólio";
export const SERVICES_SUBJECT = "Sites, sistemas e aplicativos";
export const RESUME_SUBJECT = "um projeto, depois de ver o seu currículo";
/** Frase padrão para quem quer algo como um projeto publicado (case de cliente ou de laboratório). */
export function similarProjectSubject(title: string) {
return `um projeto parecido com o ${title}`;
}
/** Assunto de cada página sem parâmetro, pelo caminho sem a barra final (só a home mantém a barra). */
const INDEX_SUBJECTS: Readonly<Record<string, string>> = {
"/": HOME_SUBJECT,
"/ia": AI_REVIEW_SUBJECT,
"/cases": CASES_SUBJECT,
"/servicos": SERVICES_SUBJECT,
"/curriculo": RESUME_SUBJECT,
};
/** Páginas com um parâmetro no caminho: em que tabela de nomes ele se procura e como o nome vira assunto. */
const DETAIL_ROUTES = [
{
pattern: /^\/servicos\/([^/]+)$/,
table: "services",
subject: (name: string) => name,
},
{
pattern: /^\/cases\/([^/]+)$/,
table: "cases",
subject: similarProjectSubject,
},
] as const;
/**
* Nome pela chave; chave ausente, nome vazio ou só com espaços valem como ausente, para a mensagem cair no assunto
* padrão em vez de "sobre .".
*/
function lookup(
table: Record<string, string> | undefined,
key: string | undefined,
) {
const name =
table && key !== undefined && Object.hasOwn(table, key)
? table[key].trim()
: "";
return name || undefined;
}
/**
* Assunto da conversa de WhatsApp para a rota atual. Em /servicos/<slug> cita o serviço, em /cases/<slug> o case
* e em /ia a revisão de aplicativo; nas demais rotas devolve undefined e a mensagem fala de "um projeto".
* Assunto da conversa de WhatsApp para a rota atual. Em /servicos/<slug> cita o serviço, em /cases/<slug> o case,
* em /ia a revisão de aplicativo e nas páginas-índice (home, /cases, /servicos, /curriculo) o assunto da própria
* página; nas demais rotas devolve undefined e a mensagem fala de "um projeto".
*/
export function whatsAppSubjectForPath(
pathname: string | null | undefined,
contexts?: WhatsAppContexts,
): string | undefined {
const path = pathname?.replace(/\/$/, "") ?? "";
if (path === "/ia") return AI_REVIEW_SUBJECT;
const path = pathname?.replace(/(.)\/$/, "$1") ?? "";
if (Object.hasOwn(INDEX_SUBJECTS, path)) return INDEX_SUBJECTS[path];
const slug = path.match(/^\/servicos\/([^/]+)$/)?.[1];
if (slug) return lookup(contexts?.services, slug);
const caseId = path.match(/^\/cases\/([^/]+)$/)?.[1];
const title = caseId ? lookup(contexts?.cases, caseId) : undefined;
return title ? `um projeto parecido com o da ${title}` : undefined;
for (const { pattern, table, subject } of DETAIL_ROUTES) {
const name = lookup(contexts?.[table], pattern.exec(path)?.[1]);
if (name) return subject(name);
}
return undefined;
}
/** Páginas de serviço e de case: nelas o botão do cabeçalho abre o WhatsApp em vez de ir ao formulário da home. */
/** Páginas de serviço e de case. */
export function isServiceOrCasePath(pathname: string | null | undefined) {
return /^\/(servicos|cases)\/[^/]+\/?$/.test(pathname ?? "");
}
/**
* Páginas em que o botão do cabeçalho abre o WhatsApp com o assunto da página, em vez de ir ao formulário da home:
* serviço e case, e também /ia, /cases, /servicos e /curriculo. Na home e nas demais rotas ele leva ao formulário.
*/
export function headerOpensWhatsApp(pathname: string | null | undefined) {
return (
isServiceOrCasePath(pathname) ||
/^\/(ia|cases|servicos|curriculo)\/?$/.test(pathname ?? "")
);
}