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:
1 parent
de6af74a0c
commit
f8545cd442
44 files changed
+2286
-571
No files matched your search
+71
-14
@@ -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 ?? "")
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user