feat: add animated components for enhanced UI/UX

- Introduced DrawLine component for drawing animations.
- Added LazyRevealImage component for lazy loading images with reveal animations.
- Implemented TextDiffuseReveal for animated text reveals with staggered effects.
- Created TopProgressBar for a dynamic loading indicator.
- Added PointCloud mock for testing animations.
- Developed useReducedMotion hook to handle user preferences for reduced motion.
- Centralized animation configurations in config.ts for easier management.
- Defined animation variants in variants.ts for consistent animation effects.
- Established AnimationProvider to supply global animation settings.
- Created tests for AnimationProvider and useReducedMotion hook.
- Added SVG assets for branding and UI elements.
- Documented frontend review plan and hero circuit board animation plan.
This commit is contained in:
Evandro Fonseca Junior committed 2026-03-15 21:06:43 -03:00
1 parent 0ab6930f92
commit 08dd046887
76 files changed
+21682 -1197

No files matched your search

+128 -140
View File
@@ -14,18 +14,23 @@ export function ContactForm({ formType, setFormType }: ContactFormProps) {
const [contactMethod, setContactMethod] = useState<"whatsapp" | "email">(
"whatsapp",
);
const [userName, setUserName] = useState<string>("");
const [userEmail, setUserEmail] = useState<string>("");
const [userPhone, setUserPhone] = useState<string>("");
const [userCompany, setUserCompany] = useState<string>("");
const [howFound, setHowFound] = useState<string>("");
const [projectType, setProjectType] = useState<string>("");
const [otherProjectType, setOtherProjectType] = useState<string>(""); // Novo estado para "Outro" tipo de projeto
const [desiredDeadline, setDesiredDeadline] = useState<string>("");
const [budgetValue, setBudgetValue] = useState<string>(""); // Novo estado para o valor do orçamento
const [technicalComplexity, setTechnicalComplexity] = useState<string>("");
const [hasInternalTeam, setHasInternalTeam] = useState<string>("");
const [customMessage, setCustomMessage] = useState<string>("");
// MENTORIA: Agrupamento de estado para reduzir complexidade e re-renders
const [formData, setFormData] = useState({
userName: "",
userEmail: "",
userPhone: "",
userCompany: "",
howFound: "",
projectType: "",
otherProjectType: "",
desiredDeadline: "",
budgetValue: "",
technicalComplexity: "",
hasInternalTeam: "",
customMessage: "",
});
const [isDecisor, setIsDecisor] = useState<string>("");
const [projectApproved, setProjectApproved] = useState<string>("");
const [urgency, setUrgency] = useState<string>("");
@@ -62,31 +67,31 @@ export function ContactForm({ formType, setFormType }: ContactFormProps) {
return `Olá Evandro, espero que esta ${dayPart} esteja sendo produtiva.
Meu nome é ${userName} e entrei em contato pois vi seu portfólio e identifiquei que suas habilidades como Desenvolvedor Full-Stack podem agregar muito ao projeto que tenho em mente.
Meu nome é ${formData.userName} e entrei em contato pois vi seu portfólio e identifiquei que suas habilidades como Desenvolvedor Full-Stack podem agregar muito ao projeto que tenho em mente.
Gostaria de agendar uma conversa para discutirmos detalhes e entender como podemos trabalhar juntos de forma eficiente e produtiva.
Fico no aguardo do seu contato para marcarmos um horário que seja conveniente para ambos.
Atenciosamente,
${userName}`;
${formData.userName}`;
};
const generateAdvancedMessage = () => {
if (customMessage) return customMessage;
if (formData.customMessage) return formData.customMessage;
// Saudação profissional
let message = `Prezado Evandro,\n\n`;
// Informações do contato
message += `Meu nome é ${userName}`;
if (userCompany) {
message += `, represento a empresa ${userCompany}`;
message += `Meu nome é ${formData.userName}`;
if (formData.userCompany) {
message += `, represento a empresa ${formData.userCompany}`;
}
message += `.\n`;
if (howFound) {
message += `Conheci seu trabalho através de ${howFound}.\n`;
if (formData.howFound) {
message += `Conheci seu trabalho através de ${formData.howFound}.\n`;
}
message += `\n`;
@@ -94,31 +99,31 @@ ${userName}`;
// Detalhes do projeto
message += `=== DETALHES DO PROJETO ===\n`;
message += `Tipo de projeto: `;
if (projectType === "outro" && otherProjectType) {
message += `${otherProjectType}\n`;
} else if (projectType) {
message += `${projectType}\n`;
if (formData.projectType === "outro" && formData.otherProjectType) {
message += `${formData.otherProjectType}\n`;
} else if (formData.projectType) {
message += `${formData.projectType}\n`;
} else {
message += `Não especificado\n`;
}
if (desiredDeadline) {
message += `Prazo desejado: ${desiredDeadline}\n`;
if (formData.desiredDeadline) {
message += `Prazo desejado: ${formData.desiredDeadline}\n`;
}
if (budgetValue) {
message += `Orçamento disponível: ${budgetValue}\n`;
if (formData.budgetValue) {
message += `Orçamento disponível: ${formData.budgetValue}\n`;
}
if (technicalComplexity) {
message += `Complexidade técnica estimada: ${technicalComplexity}\n`;
if (formData.technicalComplexity) {
message += `Complexidade técnica estimada: ${formData.technicalComplexity}\n`;
}
if (hasInternalTeam) {
if (formData.hasInternalTeam) {
message += `Equipe técnica interna: ${
hasInternalTeam === "sim"
formData.hasInternalTeam === "sim"
? "Sim"
: hasInternalTeam === "nao"
: formData.hasInternalTeam === "nao"
? "Não"
: "Parcialmente"
}\n`;
@@ -128,11 +133,11 @@ ${userName}`;
// Contatos
message += `=== INFORMAÇÕES PARA CONTATO ===\n`;
if (userEmail) {
message += `📧 Email: ${userEmail}\n`;
if (formData.userEmail) {
message += `📧 Email: ${formData.userEmail}\n`;
}
if (userPhone) {
message += `📱 Telefone/WhatsApp: ${userPhone}\n`;
if (formData.userPhone) {
message += `📱 Telefone/WhatsApp: ${formData.userPhone}\n`;
}
message += `\n`;
@@ -141,69 +146,38 @@ ${userName}`;
message += `Estou entrando em contato pois acredito que seus conhecimentos como Desenvolvedor Full-Stack podem agregar muito ao projeto que tenho em mente.\n\n`;
message += `Fico no aguardo do seu contato para discutirmos detalhes e definirmos os próximos passos.\n\n`;
message += `Atenciosamente,\n`;
message += `${userName}`;
message += `${formData.userName}`;
return message;
};
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
setUserName(e.target.value);
if (e.target.value.trim() !== "") setNameError(null);
};
const handleInputChange =
(field: keyof typeof formData) =>
(
e: ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>,
) => {
setFormData((prev) => ({ ...prev, [field]: e.target.value }));
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
setUserEmail(e.target.value);
if (e.target.value.trim() !== "") setEmailError(null);
};
const handlePhoneChange = (e: ChangeEvent<HTMLInputElement>) => {
setUserPhone(e.target.value);
if (e.target.value.trim() !== "") setPhoneError(null);
};
// Clear specific errors if the user starts typing
if (e.target.value.trim() !== "") {
if (field === "userName") setNameError(null);
if (field === "userEmail") setEmailError(null);
if (field === "userPhone") setPhoneError(null);
if (field === "otherProjectType") setOtherProjectTypeError(null);
}
};
const handleNameFocus = () => setNameFocused(true);
const handleEmailFocus = () => setEmailFocused(true);
const handlePhoneFocus = () => setPhoneFocused(true);
const handleCompanyChange = (e: ChangeEvent<HTMLInputElement>) => {
setUserCompany(e.target.value);
};
const handleHowFoundChange = (e: ChangeEvent<HTMLSelectElement>) => {
setHowFound(e.target.value);
};
const handleProjectTypeChange = (e: ChangeEvent<HTMLSelectElement>) => {
setProjectType(e.target.value);
};
const handleDesiredDeadlineChange = (e: ChangeEvent<HTMLSelectElement>) => {
setDesiredDeadline(e.target.value);
};
const handleBudgetValueChange = (e: ChangeEvent<HTMLInputElement>) => {
setBudgetValue(e.target.value);
};
const handleTechnicalComplexityChange = (
e: ChangeEvent<HTMLSelectElement>,
) => {
setTechnicalComplexity(e.target.value);
};
const handleHasInternalTeamChange = (e: ChangeEvent<HTMLSelectElement>) => {
setHasInternalTeam(e.target.value);
};
const handleMessageChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setCustomMessage(e.target.value);
};
const generateWhatsAppUrl = () => {
const message = encodeURIComponent(
formType === "advanced"
? generateAdvancedMessage()
: customMessage || getGreetingMessageWithUserName(),
: formData.customMessage || getGreetingMessageWithUserName(),
);
const phone = data.contact.whatsapp.replace(/\D/g, ""); // Remove caracteres não numéricos
return `https://wa.me/${phone}?text=${message}`;
@@ -214,8 +188,8 @@ ${userName}`;
const body = encodeURIComponent(
formType === "advanced"
? generateAdvancedMessage()
: `Nome: ${userName}\nEmail: ${userEmail}\nTelefone: ${userPhone}\nEmpresa/Posição: ${userCompany}\nComo conheceu: ${howFound}\n\nTipo de Projeto: ${projectType}\nPrazo Desejado: ${desiredDeadline}\nOrçamento Disponível: ${budgetValue}\nComplexidade Técnica: ${technicalComplexity}\nTem equipe interna: ${hasInternalTeam}\n\nMensagem: ${
customMessage || getGreetingMessageWithUserName()
: `Nome: ${formData.userName}\nEmail: ${formData.userEmail}\nTelefone: ${formData.userPhone}\nEmpresa/Posição: ${formData.userCompany}\nComo conheceu: ${formData.howFound}\n\nTipo de Projeto: ${formData.projectType}\nPrazo Desejado: ${formData.desiredDeadline}\nOrçamento Disponível: ${formData.budgetValue}\nComplexidade Técnica: ${formData.technicalComplexity}\nTem equipe interna: ${formData.hasInternalTeam}\n\nMensagem: ${
formData.customMessage || getGreetingMessageWithUserName()
}`,
);
return `mailto:${data.contact.email}?subject=${subject}&body=${body}`;
@@ -225,7 +199,7 @@ ${userName}`;
e.preventDefault();
let hasError = false;
if (userName.trim() === "") {
if (formData.userName.trim() === "") {
setNameError("Por favor, insira seu nome.");
hasError = true;
} else {
@@ -233,21 +207,24 @@ ${userName}`;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userEmail)) {
if (!emailRegex.test(formData.userEmail)) {
setEmailError("Por favor, insira um email válido.");
hasError = true;
} else {
setEmailError(null);
}
if (userPhone.trim() === "") {
if (formData.userPhone.trim() === "") {
setPhoneError("Por favor, insira seu telefone.");
hasError = true;
} else {
setPhoneError(null);
}
if (projectType === "outro" && otherProjectType.trim() === "") {
if (
formData.projectType === "outro" &&
formData.otherProjectType.trim() === ""
) {
setOtherProjectTypeError("Por favor, especifique o tipo de projeto.");
hasError = true;
} else {
@@ -260,10 +237,13 @@ ${userName}`;
return; // Ignora a submissão
}
// Validação de tempo de preenchimento (mínimo de 3 segundos)
// UX FIX: Validação de tempo com feedback ou limite menor
const timeElapsed = Date.now() - startTime;
if (timeElapsed < 3000) {
// Reduzido para 1000ms para evitar falsos positivos com autofill
if (timeElapsed < 1000) {
console.log("Formulário preenchido muito rápido, provável bot.");
// Opcional: Mostrar erro para humano real que digita muito rápido
// alert("Por favor, verifique os dados antes de enviar.");
return; // Ignora a submissão
}
@@ -271,9 +251,11 @@ ${userName}`;
// Verificação de interação mínima
if (!nameFocused || !emailFocused || !phoneFocused) {
console.log("Campos principais não foram focados, provável bot.");
// Opcional: exibir uma mensagem de erro para o usuário
return;
// MENTORIA: Isso pode bloquear usuários de Screen Readers ou Autofill agressivo.
// Considere remover ou tornar apenas um aviso no console.
console.warn(
"Campos principais não foram focados, verificando integridade.",
);
}
if (contactMethod === "whatsapp") {
@@ -284,13 +266,20 @@ ${userName}`;
};
const getCtaMessage = () => {
if (projectType === "consultoria") {
if (formData.projectType === "consultoria") {
return "Agendar Consultoria Gratuita";
}
if (budgetValue && parseFloat(budgetValue.replace(/\D/g, "")) < 2000) {
if (
formData.budgetValue &&
parseFloat(formData.budgetValue.replace(/\D/g, "")) < 2000
) {
return "Discutir Soluções de Baixo Custo";
}
if (projectType && desiredDeadline && budgetValue) {
if (
formData.projectType &&
formData.desiredDeadline &&
formData.budgetValue
) {
return "Receber Proposta Personalizada";
}
return "Enviar Formulário";
@@ -301,7 +290,7 @@ ${userName}`;
let hourlyRate = 55; // Taxa base R$ 55/h
// Estimativa de horas baseada no tipo de projeto
switch (projectType) {
switch (formData.projectType) {
case "web-app":
hours += 80; // Aplicação web completa
break;
@@ -328,7 +317,7 @@ ${userName}`;
}
// Ajuste de horas baseado no prazo
switch (desiredDeadline) {
switch (formData.desiredDeadline) {
case "1-3-weeks":
hours *= 1.3; // Urgência aumenta horas
break;
@@ -343,7 +332,7 @@ ${userName}`;
}
// Ajuste de horas baseado na complexidade
switch (technicalComplexity) {
switch (formData.technicalComplexity) {
case "alta":
hours *= 1.4;
hourlyRate = 65; // Taxa maior para alta complexidade
@@ -359,7 +348,7 @@ ${userName}`;
break;
}
if (hasInternalTeam === "nao") {
if (formData.hasInternalTeam === "nao") {
hours *= 1.2; // Sem equipe interna = mais horas necessárias
}
@@ -440,8 +429,8 @@ ${userName}`;
nameError ? "border-red-500" : "border-white/10"
} text-white placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent`}
placeholder="Seu nome"
value={userName}
onChange={handleNameChange}
value={formData.userName}
onChange={handleInputChange("userName")}
onFocus={handleNameFocus}
required
/>
@@ -495,8 +484,8 @@ ${userName}`;
nameError ? "border-red-500" : "border-white/10"
} text-white placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent`}
placeholder="Seu nome completo"
value={userName}
onChange={handleNameChange}
value={formData.userName}
onChange={handleInputChange("userName")}
onFocus={handleNameFocus}
required
/>
@@ -518,8 +507,8 @@ ${userName}`;
emailError ? "border-red-500" : "border-white/10"
} text-white placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent`}
placeholder="[email protected]"
value={userEmail}
onChange={handleEmailChange}
value={formData.userEmail}
onChange={handleInputChange("userEmail")}
onFocus={handleEmailFocus}
required
/>
@@ -541,8 +530,8 @@ ${userName}`;
phoneError ? "border-red-500" : "border-white/10"
} text-white placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent`}
placeholder="(XX) XXXXX-XXXX"
value={userPhone}
onChange={handlePhoneChange}
value={formData.userPhone}
onChange={handleInputChange("userPhone")}
onFocus={handlePhoneFocus}
required
/>
@@ -562,8 +551,8 @@ ${userName}`;
id="user-company"
className="w-full p-3 rounded-lg bg-white/5 border border-white/10 text-white placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
placeholder="Nome da empresa, sua posição"
value={userCompany}
onChange={handleCompanyChange}
value={formData.userCompany}
onChange={handleInputChange("userCompany")}
/>
</div>
<div>
@@ -582,9 +571,10 @@ ${userName}`;
backgroundRepeat: "no-repeat",
backgroundSize: "16px",
}}
value={howFound}
onChange={handleHowFoundChange}
value={formData.howFound}
onChange={handleInputChange("howFound")}
>
{" "}
<option value="" className="bg-gray-800 text-white">
Selecione uma opção
</option>
@@ -664,8 +654,8 @@ ${userName}`;
backgroundRepeat: "no-repeat",
backgroundSize: "16px",
}}
value={projectType}
onChange={handleProjectTypeChange}
value={formData.projectType}
onChange={handleInputChange("projectType")}
>
<option value="" className="bg-gray-800 text-white">
Selecione o tipo
@@ -708,7 +698,7 @@ ${userName}`;
</option>
</select>
</div>
{projectType === "outro" && (
{formData.projectType === "outro" && (
<div>
<label
htmlFor="other-project-type"
@@ -725,12 +715,8 @@ ${userName}`;
: "border-white/10"
} text-white placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent`}
placeholder="Ex: Desenvolvimento de Plugin, Automação de Processos"
value={otherProjectType}
onChange={(e) => {
setOtherProjectType(e.target.value);
if (e.target.value.trim() !== "")
setOtherProjectTypeError(null);
}}
value={formData.otherProjectType}
onChange={handleInputChange("otherProjectType")}
required
/>
{otherProjectTypeError && (
@@ -756,8 +742,8 @@ ${userName}`;
backgroundRepeat: "no-repeat",
backgroundSize: "16px",
}}
value={desiredDeadline}
onChange={handleDesiredDeadlineChange}
value={formData.desiredDeadline}
onChange={handleInputChange("desiredDeadline")}
>
<option value="" className="bg-gray-800 text-white">
Selecione o prazo
@@ -806,8 +792,8 @@ ${userName}`;
id="budget-value"
className="w-full p-3 rounded-lg bg-white/5 border border-white/10 text-white placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
placeholder="Ex: 5000, 10000, A Combinar"
value={budgetValue}
onChange={handleBudgetValueChange}
value={formData.budgetValue}
onChange={handleInputChange("budgetValue")}
/>
</div>
<div>
@@ -826,8 +812,8 @@ ${userName}`;
backgroundRepeat: "no-repeat",
backgroundSize: "16px",
}}
value={technicalComplexity}
onChange={handleTechnicalComplexityChange}
value={formData.technicalComplexity}
onChange={handleInputChange("technicalComplexity")}
>
<option value="" className="bg-gray-800 text-white">
Selecione a complexidade
@@ -865,8 +851,8 @@ ${userName}`;
backgroundRepeat: "no-repeat",
backgroundSize: "16px",
}}
value={hasInternalTeam}
onChange={handleHasInternalTeamChange}
value={formData.hasInternalTeam}
onChange={handleInputChange("hasInternalTeam")}
>
<option value="" className="bg-gray-800 text-white">
Selecione
@@ -898,8 +884,8 @@ ${userName}`;
rows={4}
className="w-full p-3 rounded-lg bg-white/5 border border-white/10 text-white placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
placeholder="Descreva seu projeto ou suas necessidades aqui..."
value={customMessage}
onChange={handleMessageChange}
value={formData.customMessage}
onChange={handleInputChange("customMessage")}
></textarea>
</div>
</div>
@@ -913,11 +899,13 @@ ${userName}`;
{getCtaMessage()}
</button>
</motion.div>
{projectType && desiredDeadline && budgetValue && (
<p className="text-center text-lg font-semibold text-cyan-400 mt-4">
{getEstimatedValue()}
</p>
)}
{formData.projectType &&
formData.desiredDeadline &&
formData.budgetValue && (
<p className="text-center text-lg font-semibold text-cyan-400 mt-4">
{getEstimatedValue()}
</p>
)}
</form>
)}
</motion.div>