feat(home): prova no topo, contato sem atrito e conversão medida
Home mais curta (hero, vitrine de clientes, sobre, serviços, IA resumida, depoimentos e contato), faixa de clientes e telas reais no hero, sem números do GitHub. Barra de cookies fina, WhatsApp fixo no celular, formulário com e-mail opcional e alternativa por e-mail, página /ia, eventos de e-mail, redes e currículo e conversão do Ads opcional por variável. Rodapé em 3 colunas. Remove componentes órfãos (LighthouseMetrics, Skills, Experience, contagem do GitHub).
This commit is contained in:
1 parent
6e1022b58c
commit
8d07b8c6bb
60 files changed
+4392
-2124
No files matched your search
+113
-38
@@ -3,23 +3,52 @@
|
||||
import { ArrowUpRight, Mail, MapPin } from "lucide-react";
|
||||
import type { FormEvent } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { trackWhatsAppOpen } from "@/lib/analytics";
|
||||
import { trackEmailClick, trackWhatsAppOpen } from "@/lib/analytics";
|
||||
import { CONTACT_EMAIL } from "@/lib/site-navigation";
|
||||
import { buildWhatsAppUrl, getWhatsAppDisplayNumber } from "@/lib/whatsapp";
|
||||
import "./conversion.css";
|
||||
import WhatsAppLink from "./WhatsAppLink";
|
||||
import { WhatsAppMark } from "./WhatsAppMark";
|
||||
|
||||
// WhatsApp brand silhouette from Simple Icons (simple-icons/simple-icons).
|
||||
function WhatsAppMark({ className }: { className: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z" />
|
||||
</svg>
|
||||
);
|
||||
type ContactField = "name" | "email" | "message";
|
||||
type FieldErrors = Partial<Record<ContactField, string>>;
|
||||
|
||||
const FIELD_ERRORS: Record<ContactField, string> = {
|
||||
name: "Informe seu nome para eu saber com quem falo.",
|
||||
email: "Confira o e-mail: ele precisa ter o formato [email protected].",
|
||||
message: "Conte em poucas palavras o que você precisa resolver.",
|
||||
};
|
||||
|
||||
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const FIELD_ORDER: ContactField[] = ["name", "email", "message"];
|
||||
|
||||
/** E-mail é opcional: só é erro se vier preenchido e fora do formato. */
|
||||
function validate(data: {
|
||||
name: string;
|
||||
email: string;
|
||||
message: string;
|
||||
}): FieldErrors {
|
||||
const errors: FieldErrors = {};
|
||||
if (!data.name.trim()) errors.name = FIELD_ERRORS.name;
|
||||
const email = data.email.trim();
|
||||
if (email && !EMAIL_PATTERN.test(email)) errors.email = FIELD_ERRORS.email;
|
||||
if (!data.message.trim()) errors.message = FIELD_ERRORS.message;
|
||||
return errors;
|
||||
}
|
||||
|
||||
/** Mesmo texto para o WhatsApp e para o e-mail; a linha do e-mail só entra se a pessoa informou um. */
|
||||
function buildProjectMessage(data: {
|
||||
name: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
}) {
|
||||
const email = data.email.trim();
|
||||
return `Olá, Evandro! Quero conversar sobre um projeto.\n\nNome: ${data.name.trim()}\n${email ? `E-mail: ${email}\n` : ""}Tipo de projeto: ${data.subject}\n\nContexto, objetivo e prazo:\n${data.message.trim()}\n\nContato iniciado pelo site evandro.dev.br.`;
|
||||
}
|
||||
|
||||
function buildMailtoUrl(message: string) {
|
||||
return `mailto:${CONTACT_EMAIL}?subject=${encodeURIComponent("Projeto pelo site")}&body=${encodeURIComponent(message)}`;
|
||||
}
|
||||
|
||||
export default function Contact() {
|
||||
@@ -29,21 +58,37 @@ export default function Contact() {
|
||||
}, []);
|
||||
|
||||
const fieldClassName =
|
||||
"mt-sp-2 w-full min-w-0 rounded-lg border border-white/15 bg-black/25 px-sp-4 py-sp-3 font-display text-base leading-6 text-white placeholder:text-white/40 focus-visible:border-[var(--slash)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]";
|
||||
"mt-sp-2 w-full min-w-0 rounded-lg border border-white/15 bg-black/25 px-sp-4 py-sp-3 !font-display text-base leading-6 text-white placeholder:text-white/40 focus-visible:border-[var(--slash)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)] aria-[invalid=true]:border-[#ff8a80]";
|
||||
const labelClassName = "block font-display text-sm font-medium text-white/85";
|
||||
const errorClassName =
|
||||
"mt-sp-2 font-display text-sm leading-6 text-[#ff9d94]";
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
subject: "Sistemas web sob medida",
|
||||
subject: "Site para empresa",
|
||||
message: "",
|
||||
});
|
||||
const [errors, setErrors] = useState<FieldErrors>({});
|
||||
|
||||
const updateField = (field: ContactField | "subject", value: string) => {
|
||||
setFormData((current) => ({ ...current, [field]: value }));
|
||||
if (field !== "subject" && errors[field]) {
|
||||
setErrors((current) => ({ ...current, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!e.currentTarget.reportValidity()) return;
|
||||
const messageTemplate = `Olá, Evandro! Quero conversar sobre um projeto.\n\nNome: ${formData.name}\nE-mail: ${formData.email}\nTipo de projeto: ${formData.subject}\n\nContexto, objetivo e prazo:\n${formData.message}\n\nContato iniciado pelo site evandro.dev.br.`;
|
||||
const form = e.currentTarget;
|
||||
const found = validate(formData);
|
||||
setErrors(found);
|
||||
const firstInvalid = FIELD_ORDER.find((field) => found[field]);
|
||||
if (firstInvalid) {
|
||||
form.querySelector<HTMLElement>(`[name="${firstInvalid}"]`)?.focus();
|
||||
return;
|
||||
}
|
||||
window.open(
|
||||
buildWhatsAppUrl(messageTemplate),
|
||||
buildWhatsAppUrl(buildProjectMessage(formData)),
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
@@ -81,7 +126,6 @@ export default function Contact() {
|
||||
</div>
|
||||
<div className="mt-sp-8 divide-y divide-white/10 border-y border-white/10">
|
||||
<WhatsAppLink
|
||||
service="Contato"
|
||||
placement="contact_direct"
|
||||
className="group flex min-h-20 items-center gap-sp-4 rounded-sm py-sp-5 text-white no-underline"
|
||||
>
|
||||
@@ -102,12 +146,13 @@ export default function Contact() {
|
||||
/>
|
||||
</WhatsAppLink>
|
||||
<a
|
||||
href="mailto:[email protected]"
|
||||
href={`mailto:${CONTACT_EMAIL}`}
|
||||
onClick={() => trackEmailClick({ placement: "contact_direct" })}
|
||||
className="group flex min-h-16 items-center gap-sp-4 rounded-sm py-sp-5 text-white/65 no-underline hover:text-white focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--slash)]"
|
||||
>
|
||||
<Mail className="h-5 w-5 shrink-0" aria-hidden="true" />
|
||||
<span className="min-w-0 break-all font-display text-sm">
|
||||
evandrofonsecajunior@gmail.com
|
||||
{CONTACT_EMAIL}
|
||||
</span>
|
||||
<ArrowUpRight
|
||||
className="ml-auto h-4 w-4 shrink-0"
|
||||
@@ -123,6 +168,7 @@ export default function Contact() {
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
aria-labelledby="contact-form-title"
|
||||
className="min-w-0 rounded-2xl border border-white/12 bg-white/[0.035] p-sp-5 sm:p-sp-8"
|
||||
>
|
||||
@@ -155,30 +201,43 @@ export default function Contact() {
|
||||
type="text"
|
||||
autoComplete="name"
|
||||
placeholder="Como posso te chamar?"
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, name: e.target.value })
|
||||
aria-invalid={errors.name ? true : undefined}
|
||||
aria-describedby={
|
||||
errors.name ? "contact-name-error" : undefined
|
||||
}
|
||||
value={formData.name}
|
||||
onChange={(e) => updateField("name", e.target.value)}
|
||||
className={fieldClassName}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p id="contact-name-error" className={errorClassName}>
|
||||
{errors.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="contact-email" className={labelClassName}>
|
||||
E-mail
|
||||
E-mail (opcional)
|
||||
</label>
|
||||
<input
|
||||
id="contact-email"
|
||||
name="email"
|
||||
required
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder="[email protected]"
|
||||
value={formData.email}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, email: e.target.value })
|
||||
aria-invalid={errors.email ? true : undefined}
|
||||
aria-describedby={
|
||||
errors.email ? "contact-email-error" : undefined
|
||||
}
|
||||
value={formData.email}
|
||||
onChange={(e) => updateField("email", e.target.value)}
|
||||
className={fieldClassName}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p id="contact-email-error" className={errorClassName}>
|
||||
{errors.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -189,11 +248,10 @@ export default function Contact() {
|
||||
id="contact-subject"
|
||||
name="subject"
|
||||
value={formData.subject}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, subject: e.target.value })
|
||||
}
|
||||
onChange={(e) => updateField("subject", e.target.value)}
|
||||
className={`${fieldClassName} [color-scheme:dark]`}
|
||||
>
|
||||
<option value="Site para empresa">Site para empresa</option>
|
||||
<option value="Sistemas web sob medida">
|
||||
Sistemas web sob medida
|
||||
</option>
|
||||
@@ -220,22 +278,39 @@ export default function Contact() {
|
||||
rows={4}
|
||||
autoComplete="off"
|
||||
placeholder="O que você precisa resolver? Qual resultado espera e existe um prazo?"
|
||||
aria-describedby="contact-privacy-note"
|
||||
value={formData.message}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, message: e.target.value })
|
||||
aria-invalid={errors.message ? true : undefined}
|
||||
aria-describedby={
|
||||
errors.message
|
||||
? "contact-message-error contact-privacy-note"
|
||||
: "contact-privacy-note"
|
||||
}
|
||||
value={formData.message}
|
||||
onChange={(e) => updateField("message", e.target.value)}
|
||||
className={`${fieldClassName} min-h-32 resize-y`}
|
||||
/>
|
||||
{errors.message && (
|
||||
<p id="contact-message-error" className={errorClassName}>
|
||||
{errors.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex min-h-14 w-full items-center justify-center gap-sp-3 rounded-lg bg-[#25d366] px-sp-4 py-sp-4 font-display text-base font-semibold leading-6 text-[#071b10] transition-colors hover:bg-[#54e58b] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#25d366] motion-reduce:transition-none"
|
||||
className="cv-btn flex min-h-14 w-full items-center justify-center gap-sp-3 rounded-lg bg-[#25d366] px-sp-4 py-sp-4 text-[#071b10] transition-colors hover:bg-[#54e58b] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#25d366] motion-reduce:transition-none"
|
||||
>
|
||||
<WhatsAppMark className="h-6 w-6 shrink-0" />
|
||||
Continuar no WhatsApp
|
||||
<ArrowUpRight className="h-5 w-5 shrink-0" aria-hidden="true" />
|
||||
</button>
|
||||
<p className="text-center font-display text-[15px] leading-6 text-white/70">
|
||||
<a
|
||||
href={buildMailtoUrl(buildProjectMessage(formData))}
|
||||
onClick={() => trackEmailClick({ placement: "contact_form" })}
|
||||
className="inline-flex min-h-11 items-center underline underline-offset-4 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--slash)]"
|
||||
>
|
||||
Prefere e-mail? Enviar por e-mail
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
id="contact-privacy-note"
|
||||
className="font-display text-xs leading-6 text-white/55"
|
||||
|
||||
Reference in new issue
Block a user