Hero e movimento - cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py): marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao cursor e, parada no topo, passa sozinha pelos estados com legenda - três níveis de aparelho (full, lite, static); sem WebGL fica o pôster - título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha." - faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções - rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa compartilhada entre a lista de cases e o case Ofertas - atendimento no WhatsApp com IA e nota fiscal para a reforma tributária (páginas de serviço, cartões da home, formulário e aviso com data de validade) - página /ia atualizada, com fontes - peças 3D em loop nos cartões e topos de página (3d/pecas.py) Design - hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta - um destino por rótulo: "Falar sobre meu projeto" leva ao formulário, "Chamar no WhatsApp" abre o WhatsApp - logo montado em blocos, 404 redesenhado, grades sem célula vazia - ícones do PWA e manifesto novos Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
137 lines
5.1 KiB
TypeScript
137 lines
5.1 KiB
TypeScript
import { offerLabels } from "./offer-labels";
|
|
|
|
const COUNTRY = ["5", "5"];
|
|
const AREA = ["4", "7"];
|
|
const NINE = ["9"];
|
|
const FIRST = ["9", "2", "7", "4"];
|
|
const LAST = ["2", "3", "6", "5"];
|
|
|
|
export function getWhatsAppNumber() {
|
|
return [...COUNTRY, ...AREA, ...NINE, ...FIRST, ...LAST].join("");
|
|
}
|
|
|
|
export function getWhatsAppDisplayNumber() {
|
|
return `+${COUNTRY.join("")} ${AREA.join("")} ${NINE.join("")}${FIRST.join("")}-${LAST.join("")}`;
|
|
}
|
|
|
|
export function buildWhatsAppUrl(message: string) {
|
|
return `https://wa.me/${getWhatsAppNumber()}?text=${encodeURIComponent(message)}`;
|
|
}
|
|
|
|
/**
|
|
* Assuntos dos serviços e nomes dos cases, montados no servidor (app/layout.tsx) a partir de lib/site-content.ts:
|
|
* esse arquivo reúne o texto de todas as páginas e não deve ir inteiro para o navegador.
|
|
*/
|
|
export interface WhatsAppContexts {
|
|
/**
|
|
* slug do serviço -> `whatsappSubject` dele: uma frase em minúsculas que entra em "Quero conversar sobre {assunto}."
|
|
* (e não o nome do serviço, que daria "sobre Sites profissionais.").
|
|
*/
|
|
services: Record<string, string>;
|
|
/** id do case -> nome do projeto ou do cliente */
|
|
cases: Record<string, string>;
|
|
}
|
|
|
|
/** Entra na frase "Quero conversar sobre {assunto}." da mensagem. */
|
|
export const AI_REVIEW_SUBJECT = "a revisão de um aplicativo feito com IA";
|
|
|
|
/**
|
|
* 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";
|
|
|
|
/**
|
|
* Assunto de cada oferta, para o contato direto de WhatsApp do formulário quando o tipo de projeto já está
|
|
* escolhido ("Quero conversar sobre um site que traz pedidos pelo WhatsApp."). Os nomes das ofertas, em minúsculas e com artigo,
|
|
* que vêm de `offerLabels` (lib/offer-labels.ts), a mesma fonte do nome da oferta e da página dela.
|
|
*/
|
|
export const OFFER_SUBJECTS = {
|
|
site: offerLabels.site.whatsappSubject,
|
|
sistema: offerLabels.sistema.whatsappSubject,
|
|
whatsapp: offerLabels.whatsapp.whatsappSubject,
|
|
revisao: offerLabels.revisao.whatsappSubject,
|
|
fiscal: offerLabels.fiscal.whatsappSubject,
|
|
} as const;
|
|
|
|
/** Frase padrão para quem quer algo como um projeto publicado (case de cliente ou de laboratório). "O case" concorda com qualquer título: sem ele, saía "o Lavação 047" e "o SEV Moda Íntima". */
|
|
export function similarProjectSubject(title: string) {
|
|
return `um projeto parecido com o case ${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,
|
|
* 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(/(.)\/$/, "$1") ?? "";
|
|
if (Object.hasOwn(INDEX_SUBJECTS, path)) return INDEX_SUBJECTS[path];
|
|
|
|
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. */
|
|
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 ?? "")
|
|
);
|
|
}
|