feat: set up basic project structure and initial styling

This commit initializes the project with a basic Next.js setup, including:
- Integration of `tailwindcss` and `tw-animate-css` for styling.
- Definition of a comprehensive color palette using CSS variables for both light and dark themes, supporting a theming system.
- Implementation of a custom `aurora` keyframe animation.
- Configuration of global CSS styles for `border` and `body` elements.
- Setup of `next.config.ts` with image optimization, performance enhancements, and security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy).
- Update of `package.json` to include new dependencies for UI components (`class-variance-authority`, `clsx`, `lucide-react`, `motion`, `tailwind-merge`, `tailwindcss-animate`) and development tools (`@biomejs/biome`, `@tailwindcss/postcss`, `tw-animate-css`).
- Refinement of `app/layout.tsx` to include comprehensive metadata for SEO (title, description, keywords, authors, Open Graph, Twitter cards, robots), preconnect/dns-prefetch links for external resources, favicon, manifest, and mobile app meta tags.
- Restructuring of `app/page.tsx` to use dynamic imports for various sections (Hero, About, Skills, Experience, Projects, Contact, WorkModalities) to improve performance, and integration of `Squares` component for background effects.

This foundational commit prepares the application for further development by establishing a robust and performant base.
This commit is contained in:
¨evandrodevbr¨ committed 2025-09-02 22:30:47 -03:00
1 parent ca9e70a53e
commit 90cf3c73ad
37 files changed
+3898 -118

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
"use client";
import { motion } from "motion/react";
import { MagicCard } from "@/components/magicui/magic-card";
import { TextReveal } from "@/components/magicui/text-reveal";
export default function About() {
return (
<section id="about" className="py-20 px-4">
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-4xl md:text-5xl font-bold mb-6">Sobre Mim</h2>
</motion.div>
<div className="grid md:grid-cols-2 gap-12 items-center">
{/* Main Description */}
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
>
<div className="p-8 space-y-6">
<TextReveal>
Desenvolvedor Full-Stack com 3+ anos de experiência,
especializado em soluções web modernas e inovadoras. Cursando
Engenharia da Computação, com conhecimentos em computação em
nuvem e inteligência artificial. Busco constantemente novos
desafios onde possa aplicar minha paixão por tecnologia e
resolver problemas de forma criativa.
</TextReveal>
<TextReveal>
Meu objetivo é entregar soluções de alta qualidade, sempre
priorizando colaboração, ética e inovação. Acredito no poder da
tecnologia para fazer a diferença e estou comprometido com o
aprendizado contínuo e a excelência técnica.
</TextReveal>
</div>
</motion.div>
{/* Soft Skills */}
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
viewport={{ once: true }}
className="space-y-6"
>
<h3 className="text-2xl font-semibold mb-6 text-cyan-400">
Soft Skills
</h3>
<div className="space-y-4">
{[
{ skill: "Comunicação", level: 90 },
{ skill: "Trabalho em Equipe", level: 85 },
// { skill: "Resolução de Problemas", level: 95 },
{ skill: "Aprendizado Contínuo", level: 90 },
{ skill: "Adaptabilidade", level: 85 },
].map((item) => (
<div key={item.skill} className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-300">{item.skill}</span>
<span className="text-cyan-400">{item.level}%</span>
</div>
<div className="w-full bg-white/10 rounded-full h-2">
<div
className="bg-gradient-to-r from-cyan-400 to-blue-500 h-2 rounded-full transition-all duration-1000"
style={{ width: `${item.level}%` }}
/>
</div>
</div>
))}
</div>
<div className="mt-8">
<h4 className="text-lg font-semibold mb-4 text-blue-400">
Destaques
</h4>
<ul className="space-y-2 text-gray-300">
<li className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-400 rounded-full" />
3+ anos de experiência
</li>
<li className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-400 rounded-full" />
Engenheiro da Computação
</li>
<li className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-400 rounded-full" />
Conhecimento em Cloud e IA
</li>
<li className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-400 rounded-full" />
Ampla gama de conhecimentos
</li>
</ul>
</div>
</motion.div>
</div>
</div>
</section>
);
}
+90
View File
@@ -0,0 +1,90 @@
"use client";
import { motion } from "motion/react";
import { ShimmerButton } from "@/components/magicui/shimmer-button";
import { useRef, useState } from "react";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { ContactForm } from "./ContactForm"; // Importar o novo componente
import { ContactInfo } from "./ContactInfo"; // Importar o novo componente
export default function Contact() {
const { data } = usePortfolioData();
const contactSectionRef = useRef<HTMLElement>(null);
const [formType, setFormType] = useState<"simple" | "advanced">("simple"); // Estado para controlar o tipo de formulário
const handleWhatsAppClick = () => {
setFormType("simple"); // Redireciona para o formulário simples
if (contactSectionRef.current) {
contactSectionRef.current.scrollIntoView({ behavior: "smooth" });
}
};
const scrollToContact = () => {
if (contactSectionRef.current) {
contactSectionRef.current.scrollIntoView({ behavior: "smooth" });
}
};
if (!data) {
return null; // Ou um componente de carregamento/erro
}
return (
<section
id="contact"
ref={contactSectionRef}
className="py-20 px-4 bg-gradient-to-t from-black/20 to-transparent"
>
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-4xl md:text-5xl font-bold mb-6">
Vamos Conversar
</h2>
<p className="text-xl text-gray-300 max-w-2xl mx-auto">
Estou sempre interessado em novos desafios e oportunidades. Vamos
discutir como posso ajudar seu projeto?
</p>
</motion.div>
<div className="grid md:grid-cols-2 gap-8 items-start mt-16">
{/* Contact Info */}
<ContactInfo onWhatsAppClick={handleWhatsAppClick} />
{/* Contact Options and Map */}
<div className="flex flex-col gap-8">
<ContactForm formType={formType} setFormType={setFormType} />{" "}
{/* Usar o novo componente ContactForm */}
{/* Mapa */}
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
viewport={{ once: true }}
>
<h3 className="text-2xl font-semibold mb-4 text-blue-400">
Onde me encontrar
</h3>
<div className="overflow-hidden rounded-lg shadow-lg">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3557.1999999999996!2d-48.77891484804123!3d-26.087801635807004!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x0%3A0x0!2zMjbCsDA1JzE2LjEiUyA0OMKwNDYnNDQuMSJX!5e0!3m2!1spt-BR!2sbr!4v1700000000000!5m2!1spt-BR!2sbr"
width="100%"
height="450"
style={{ border: 0 }}
allowFullScreen={true}
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
></iframe>
</div>
</motion.div>
</div>
</div>
</div>
</section>
);
}
+769
View File
@@ -0,0 +1,769 @@
"use client";
import { useState, ChangeEvent, FormEvent, useEffect } from "react";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { motion } from "motion/react";
interface ContactFormProps {
formType: "simple" | "advanced";
setFormType: (type: "simple" | "advanced") => void;
}
export function ContactForm({ formType, setFormType }: ContactFormProps) {
const { data } = usePortfolioData();
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>("");
const [isDecisor, setIsDecisor] = useState<string>("");
const [projectApproved, setProjectApproved] = useState<string>("");
const [urgency, setUrgency] = useState<string>("");
const [preferredModality, setPreferredModality] = useState<string>("");
const [isExpanded, setIsExpanded] = useState<boolean>(false); // Novo estado para controlar a expansão do formulário avançado
const [nameError, setNameError] = useState<string | null>(null);
const [emailError, setEmailError] = useState<string | null>(null);
const [phoneError, setPhoneError] = useState<string | null>(null);
const [otherProjectTypeError, setOtherProjectTypeError] = useState<
string | null
>(null); // Novo estado de erro
const [honeypot, setHoneypot] = useState<string>(""); // Estado para o campo honeypot
const [startTime, setStartTime] = useState<number>(0); // Estado para o tempo de início do formulário
const [nameFocused, setNameFocused] = useState<boolean>(false);
const [emailFocused, setEmailFocused] = useState<boolean>(false);
const [phoneFocused, setPhoneFocused] = useState<boolean>(false);
useEffect(() => {
setStartTime(Date.now()); // Registra o tempo de carregamento do componente
}, []);
if (!data) {
return null; // Ou um componente de carregamento/erro
}
const getGreetingMessageWithUserName = () => {
const hour = new Date().getHours();
const baseMessage = (name: string) => {
if (hour >= 8 && hour < 12) {
// Manhã
return `Bom dia Evandro, meu nome é ${name}. Vi seu portfólio e estou interessado nos seus serviços. Podemos conversar?`;
} else if (hour >= 13 && hour < 18) {
// Tarde
return `Boa tarde Evandro, meu nome é ${name}. Vi seu portfólio e estou interessado nos seus serviços. Podemos conversar?`;
} else {
// Noite
return `Olá Evandro, meu nome é ${name}. Vi seu portfólio e estou interessado nos seus serviços. Podemos conversar quando estiver disponível?`;
}
};
return baseMessage(userName);
};
const generateAdvancedMessage = () => {
if (customMessage) return customMessage;
let message = `Olá Evandro,\n\n`;
message += `Meu nome é ${userName}`;
if (userCompany) {
message += ` e sou de ${userCompany}`;
}
if (howFound) {
message += `. Conheci seu trabalho via ${howFound}`;
}
message += `.\n\n`;
message += `Estou entrando em contato para discutir um projeto de `;
if (projectType === "outro" && otherProjectType) {
message += `${otherProjectType}`;
} else if (projectType) {
message += `${projectType}`;
} else {
message += `[Tipo de Projeto Não Especificado]`;
}
message += `.\n`;
if (desiredDeadline) {
message += `O prazo desejado é ${desiredDeadline}.\n`;
}
if (budgetValue) {
message += `O orçamento disponível é ${budgetValue}.\n`;
}
if (technicalComplexity) {
message += `A complexidade técnica estimada é ${technicalComplexity}.\n`;
}
if (hasInternalTeam) {
message += `Já possuímos uma equipe técnica ${hasInternalTeam}.\n`;
}
message += `\nMeus contatos:\n`;
if (userEmail) {
message += `Email: ${userEmail}\n`;
}
if (userPhone) {
message += `Telefone: ${userPhone}\n`;
}
message += `\nAguardamos seu retorno para discutir os próximos passos.`;
return message;
};
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
setUserName(e.target.value);
if (e.target.value.trim() !== "") setNameError(null);
};
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);
};
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()
);
const phone = data.contact.whatsapp.replace(/\D/g, ""); // Remove caracteres não numéricos
return `https://wa.me/${phone}?text=${message}`;
};
const generateEmailUrl = () => {
const subject = encodeURIComponent("Proposta de Projeto - Portfólio");
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()
}`
);
return `mailto:${data.contact.email}?subject=${subject}&body=${body}`;
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
let hasError = false;
if (userName.trim() === "") {
setNameError("Por favor, insira seu nome.");
hasError = true;
} else {
setNameError(null);
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userEmail)) {
setEmailError("Por favor, insira um email válido.");
hasError = true;
} else {
setEmailError(null);
}
if (userPhone.trim() === "") {
setPhoneError("Por favor, insira seu telefone.");
hasError = true;
} else {
setPhoneError(null);
}
if (projectType === "outro" && otherProjectType.trim() === "") {
setOtherProjectTypeError("Por favor, especifique o tipo de projeto.");
hasError = true;
} else {
setOtherProjectTypeError(null);
}
// Validação Honeypot
if (honeypot) {
console.log("Honeypot preenchido, provável bot.");
return; // Ignora a submissão
}
// Validação de tempo de preenchimento (mínimo de 3 segundos)
const timeElapsed = Date.now() - startTime;
if (timeElapsed < 3000) {
console.log("Formulário preenchido muito rápido, provável bot.");
return; // Ignora a submissão
}
if (hasError) return;
// 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;
}
if (contactMethod === "whatsapp") {
window.open(generateWhatsAppUrl(), "_blank");
} else {
window.open(generateEmailUrl(), "_blank");
}
};
const getCtaMessage = () => {
if (projectType === "consultoria") {
return "Agendar Consultoria Gratuita";
}
if (budgetValue && parseFloat(budgetValue.replace(/\D/g, "")) < 2000) {
return "Discutir Soluções de Baixo Custo";
}
if (projectType && desiredDeadline && budgetValue) {
return "Receber Proposta Personalizada";
}
return "Enviar Formulário";
};
const getEstimatedValue = () => {
let value = 0;
// Lógica de estimativa baseada nas respostas
switch (projectType) {
case "web-app":
value += 10000;
break;
case "landing-page":
value += 2000;
break;
case "mvp":
value += 5000;
break;
case "iot-app":
value += 15000;
break;
case "integracao":
value += 7000;
break;
case "manutencao":
value += 1000;
break;
case "consultoria":
value += 500;
break;
default:
break;
}
switch (desiredDeadline) {
case "1-3-weeks":
value *= 1.2; // Urgência aumenta o valor
break;
case "1-2-months":
value *= 1.1;
break;
case "6-plus-months":
value *= 0.9; // Prazo longo pode reduzir o valor
break;
default:
break;
}
switch (technicalComplexity) {
case "alta":
value *= 1.3;
break;
case "muito-alta":
value *= 1.5;
break;
default:
break;
}
if (hasInternalTeam === "nao") {
value *= 1.1; // Sem equipe interna pode indicar mais trabalho
}
// Ajustar pelo valor do orçamento, se aplicável
if (budgetValue) {
const parsedBudget = parseFloat(budgetValue.replace(/\D/g, ""));
if (!isNaN(parsedBudget)) {
// Se o valor estimado for maior que o orçamento fornecido, ajusta para o orçamento
value = Math.min(value, parsedBudget);
}
}
return value > 0 ? `Estimativa: R$ ${value.toFixed(2)}` : "Estimativa: N/A";
};
return (
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
viewport={{ once: true }}
className="space-y-4"
>
<h3 className="text-2xl font-semibold mb-6 text-blue-primary">
Preencha o Formulário
</h3>
<div className="flex justify-center gap-4 mb-8">
<button
type="button"
onClick={() => setFormType("simple")}
className={`py-2 px-4 rounded-lg font-semibold transition-all duration-300 ${
formType === "simple"
? "bg-blue-primary text-white shadow-lg shadow-blue-primary/25 border-2 border-blue-500"
: "bg-gray-700 text-gray-300 opacity-50 cursor-pointer"
}`}
>
Formulário Simples
</button>
<button
type="button"
onClick={() => setFormType("advanced")}
className={`py-2 px-4 rounded-lg font-semibold transition-all duration-300 ${
formType === "advanced"
? "bg-blue-primary text-white shadow-lg shadow-blue-primary/25 border-2 border-blue-500"
: "bg-gray-700 text-gray-300 opacity-50 cursor-pointer"
}`}
>
Formulário Avançado
</button>
</div>
{formType === "simple" ? (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Campo Honeypot */}
<div style={{ display: "none" }}>
<label htmlFor="honeypot">Não preencha este campo</label>
<input
type="text"
id="honeypot"
name="honeypot"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
/>
</div>
{/* Formulário Simples: Apenas Nome */}
<div>
<label
htmlFor="simple-user-name"
className="block text-sm font-medium text-gray-300 mb-2"
>
Seu Nome:
</label>
<input
type="text"
id="simple-user-name"
className={`w-full p-3 rounded-lg bg-white/5 border ${
nameError ? "border-red-500" : "border-white/10"
} text-white placeholder-gray-500 focus:ring-2 focus:ring-blue-primary focus:border-transparent`}
placeholder="Seu nome"
value={userName}
onChange={handleNameChange}
onFocus={handleNameFocus}
required
/>
{nameError && (
<p className="text-red-400 text-xs mt-1">{nameError}</p>
)}
</div>
{/* Botão de Contato para o formulário simples */}
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<button
type="submit"
className="w-full py-4 px-6 rounded-lg font-semibold text-lg transition-all duration-300 bg-green-600 hover:bg-green-700 text-white shadow-lg shadow-green-600/25"
onClick={() => window.open(generateWhatsAppUrl(), "_blank")}
>
Enviar Mensagem via WhatsApp
</button>
</motion.div>
</form>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Campo Honeypot */}
<div style={{ display: "none" }}>
<label htmlFor="honeypot">Não preencha este campo</label>
<input
type="text"
id="honeypot"
name="honeypot"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
/>
</div>
{/* Dados Básicos */}
<div>
<h4 className="text-xl font-semibold mb-4 text-gray-200">
Dados Básicos
</h4>
<div className="space-y-4">
<div>
<label
htmlFor="user-name"
className="block text-sm font-medium text-gray-300 mb-2"
>
Nome Completo:
</label>
<input
type="text"
id="user-name"
className={`w-full p-3 rounded-lg bg-white/5 border ${
nameError ? "border-red-500" : "border-white/10"
} text-white placeholder-gray-500 focus:ring-2 focus:ring-blue-primary focus:border-transparent`}
placeholder="Seu nome completo"
value={userName}
onChange={handleNameChange}
onFocus={handleNameFocus}
required
/>
{nameError && (
<p className="text-red-400 text-xs mt-1">{nameError}</p>
)}
</div>
<div>
<label
htmlFor="user-email"
className="block text-sm font-medium text-gray-300 mb-2"
>
Email Corporativo:
</label>
<input
type="email"
id="user-email"
className={`w-full p-3 rounded-lg bg-white/5 border ${
emailError ? "border-red-500" : "border-white/10"
} text-white placeholder-gray-500 focus:ring-2 focus:ring-blue-primary focus:border-transparent`}
placeholder="[email protected]"
value={userEmail}
onChange={handleEmailChange}
onFocus={handleEmailFocus}
required
/>
{emailError && (
<p className="text-red-400 text-xs mt-1">{emailError}</p>
)}
</div>
<div>
<label
htmlFor="user-phone"
className="block text-sm font-medium text-gray-300 mb-2"
>
WhatsApp/Telefone:
</label>
<input
type="tel"
id="user-phone"
className={`w-full p-3 rounded-lg bg-white/5 border ${
phoneError ? "border-red-500" : "border-white/10"
} text-white placeholder-gray-500 focus:ring-2 focus:ring-blue-primary focus:border-transparent`}
placeholder="(XX) XXXXX-XXXX"
value={userPhone}
onChange={handlePhoneChange}
onFocus={handlePhoneFocus}
required
/>
{phoneError && (
<p className="text-red-400 text-xs mt-1">{phoneError}</p>
)}
</div>
<div>
<label
htmlFor="user-company"
className="block text-sm font-medium text-gray-300 mb-2"
>
Empresa/Posição:
</label>
<input
type="text"
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-blue-primary focus:border-transparent"
placeholder="Nome da empresa, sua posição"
value={userCompany}
onChange={handleCompanyChange}
/>
</div>
<div>
<label
htmlFor="how-found"
className="block text-sm font-medium text-gray-300 mb-2"
>
Como conheceu meu trabalho?
</label>
<select
id="how-found"
className="w-full p-3 rounded-lg bg-white/5 border border-white/10 text-white focus:ring-2 focus:ring-blue-primary focus:border-transparent"
value={howFound}
onChange={handleHowFoundChange}
>
<option value="">Selecione uma opção</option>
<option value="linkedin">LinkedIn</option>
<option value="github">GitHub</option>
<option value="indicacao">Indicação</option>
<option value="pesquisa">Pesquisa Online</option>
<option value="outro">Outro</option>
</select>
</div>
</div>
</div>
{/* Tile de Expandir/Recolher */}
<div
onClick={() => setIsExpanded(!isExpanded)}
className="p-4 rounded-lg bg-white/5 border border-white/10 text-white cursor-pointer hover:bg-white/10 transition-colors duration-300 mb-6 flex justify-between items-center"
>
<h4 className="text-xl font-semibold text-gray-200">
{isExpanded
? "Detalhes do Projeto (Recolher)"
: "Detalhes do Projeto (Expandir)"}
</h4>
<svg
className={`w-6 h-6 transition-transform duration-300 ${
isExpanded ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 9l-7 7-7-7"
></path>
</svg>
</div>
<div
className={`overflow-hidden transition-all duration-500 ease-in-out ${
isExpanded ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0"
} space-y-6`}
>
{/* Conteúdo do Formulário Avançado */}
{/* Sobre o Projeto */}
<div>
<h4 className="text-xl font-semibold mb-4 text-gray-200">
Sobre o Projeto
</h4>
<div className="space-y-4">
<div>
<label
htmlFor="project-type"
className="block text-sm font-medium text-gray-300 mb-2"
>
Tipo de Projeto:
</label>
<select
id="project-type"
className="w-full p-3 rounded-lg bg-white/5 border border-white/10 text-white focus:ring-2 focus:ring-blue-primary focus:border-transparent"
value={projectType}
onChange={handleProjectTypeChange}
>
<option value="">Selecione o tipo</option>
<option value="web-app">Aplicação Web Completa</option>
<option value="landing-page">Landing Page</option>
<option value="mvp">MVP (Produto Mínimo Viável)</option>
<option value="iot-app">Aplicação IoT</option>
<option value="integracao">Integração de Sistemas</option>
<option value="manutencao">Manutenção/Ajustes</option>
<option value="consultoria">Consultoria/Mentoria</option>
<option value="outro">Outro</option>
</select>
</div>
{projectType === "outro" && (
<div>
<label
htmlFor="other-project-type"
className="block text-sm font-medium text-gray-300 mb-2"
>
Qual outro tipo de projeto?
</label>
<input
type="text"
id="other-project-type"
className={`w-full p-3 rounded-lg bg-white/5 border ${
otherProjectTypeError
? "border-red-500"
: "border-white/10"
} text-white placeholder-gray-500 focus:ring-2 focus:ring-blue-primary 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);
}}
required
/>
{otherProjectTypeError && (
<p className="text-red-400 text-xs mt-1">
{otherProjectTypeError}
</p>
)}
</div>
)}
<div>
<label
htmlFor="desired-deadline"
className="block text-sm font-medium text-gray-300 mb-2"
>
Prazo Desejado:
</label>
<select
id="desired-deadline"
className="w-full p-3 rounded-lg bg-white/5 border border-white/10 text-white focus:ring-2 focus:ring-blue-primary focus:border-transparent"
value={desiredDeadline}
onChange={handleDesiredDeadlineChange}
>
<option value="">Selecione o prazo</option>
<option value="1-3-weeks">1-3 Semanas</option>
<option value="1-2-months">1-2 Meses</option>
<option value="2-4-months">2-4 Meses</option>
<option value="4-6-months">4-6 Meses</option>
<option value="6-plus-months">Mais de 6 Meses</option>
</select>
</div>
<div>
<label
htmlFor="budget-value"
className="block text-sm font-medium text-gray-300 mb-2"
>
Orçamento Disponível (R$):
</label>
<input
type="text"
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-blue-primary focus:border-transparent"
placeholder="Ex: 5000, 10000, A Combinar"
value={budgetValue}
onChange={handleBudgetValueChange}
/>
</div>
<div>
<label
htmlFor="technical-complexity"
className="block text-sm font-medium text-gray-300 mb-2"
>
Complexidade Técnica Estimada:
</label>
<select
id="technical-complexity"
className="w-full p-3 rounded-lg bg-white/5 border border-white/10 text-white focus:ring-2 focus:ring-blue-primary focus:border-transparent"
value={technicalComplexity}
onChange={handleTechnicalComplexityChange}
>
<option value="">Selecione a complexidade</option>
<option value="baixa">Baixa</option>
<option value="media">Média</option>
<option value="alta">Alta</option>
<option value="muito-alta">Muito Alta</option>
</select>
</div>
<div>
<label
htmlFor="has-internal-team"
className="block text-sm font-medium text-gray-300 mb-2"
>
Já tem equipe técnica interna?
</label>
<select
id="has-internal-team"
className="w-full p-3 rounded-lg bg-white/5 border border-white/10 text-white focus:ring-2 focus:ring-blue-primary focus:border-transparent"
value={hasInternalTeam}
onChange={handleHasInternalTeamChange}
>
<option value="">Selecione</option>
<option value="sim">Sim</option>
<option value="nao">Não</option>
<option value="parcial">Parcialmente</option>
</select>
</div>
</div>
</div>
{/* Custom Message Input */}
<div className="mb-6">
<label
htmlFor="custom-message"
className="block text-sm font-medium text-gray-300 mb-2"
>
Mensagem Adicional (Opcional):
</label>
<textarea
id="custom-message"
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-blue-500 focus:border-transparent"
placeholder="Descreva seu projeto ou suas necessidades aqui..."
value={customMessage}
onChange={handleMessageChange}
></textarea>
</div>
</div>
{/* Contact Button */}
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<button
type="submit"
className="w-full py-4 px-6 rounded-lg font-semibold text-lg transition-all duration-300 bg-blue-primary hover:bg-blue-primary/90 text-white shadow-lg shadow-blue-primary/25 border-2 border-blue-500"
>
{getCtaMessage()}
</button>
</motion.div>
{projectType && desiredDeadline && budgetValue && (
<p className="text-center text-lg font-semibold text-blue-400 mt-4">
{getEstimatedValue()}
</p>
)}
</form>
)}
</motion.div>
);
}
+128
View File
@@ -0,0 +1,128 @@
"use client";
import { motion } from "motion/react";
import { Mail, MessageCircle } from "lucide-react";
import { usePortfolioData } from "@/lib/usePortfolioData";
interface ContactInfoProps {
onWhatsAppClick: () => void;
}
export function ContactInfo({ onWhatsAppClick }: ContactInfoProps) {
const { data } = usePortfolioData();
if (!data) {
return null; // Ou um componente de carregamento/erro
}
return (
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
className="space-y-4"
>
<div>
<h3 className="text-2xl font-semibold mb-6 text-blue-400">
Entre em Contato
</h3>
<div className="space-y-4">
<a
href={`mailto:${data.contact.email}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-4 p-4 hover:bg-white/5 rounded-lg transition-colors"
>
<div className="p-3 rounded-full bg-blue-500/20">
<Mail className="w-6 h-6 text-blue-400" />
</div>
<div>
<p className="text-white font-medium">Email</p>
<p className="text-gray-400">{data.contact.email}</p>
</div>
</a>
<button
onClick={onWhatsAppClick}
className="flex items-center gap-4 p-4 hover:bg-white/5 rounded-lg transition-colors w-full text-left"
>
<div className="p-3 rounded-full bg-green-500/20">
<MessageCircle className="w-6 h-6 text-green-400" />
</div>
<div>
<p className="text-white font-medium">WhatsApp</p>
<p className="text-gray-400">{data.contact.whatsapp}</p>
</div>
</button>
<a
href={data.contact.linkedin}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-4 p-4 hover:bg-white/5 rounded-lg transition-colors"
>
<div className="p-3 rounded-full bg-blue-500/20">
<svg
className="w-6 h-6 text-blue-400"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
</div>
<div>
<p className="text-white font-medium">LinkedIn</p>
<p className="text-gray-400">
{data.contact.linkedin.replace("https://www.", "")}
</p>
</div>
</a>
<a
href={data.contact.github}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-4 p-4 hover:bg-white/5 rounded-lg transition-colors"
>
<div className="p-3 rounded-full bg-gray-500/20">
<svg
className="w-6 h-6 text-gray-400"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</div>
<div>
<p className="text-white font-medium">GitHub</p>
<p className="text-gray-400">
{data.contact.github.replace("https://github.com/", "")}
</p>
</div>
</a>
</div>
</div>
<div>
<h3 className="text-2xl font-semibold mb-4 text-blue-400">
Localização
</h3>
<p className="text-gray-300">📍 {data.contact.location}</p>
<p className="text-gray-400 mt-2">
Disponível para projetos remotos e trabalho presencial na região.
</p>
</div>
{/* Timeline de Resposta (Placeholder) */}
<div>
<h3 className="text-2xl font-semibold mb-4 text-blue-400">
Timeline de Resposta
</h3>
<p className="text-gray-300">
Em breve, uma timeline detalhada do processo de contato e resposta.
</p>
</div>
</motion.div>
);
}
+123
View File
@@ -0,0 +1,123 @@
"use client";
import { motion } from "motion/react";
import { BentoGrid, BentoGridItem } from "@/components/magicui/bento-grid";
import { Calendar, MapPin, Briefcase } from "lucide-react";
import { usePortfolioData } from "@/lib/usePortfolioData";
export default function Experience() {
const { data, loading, error } = usePortfolioData();
if (loading) {
return (
<section id="experience" className="py-20 px-4">
<div className="max-w-6xl mx-auto text-center">
<div className="animate-pulse">
<div className="h-8 bg-gray-700 rounded w-1/3 mx-auto mb-4"></div>
<div className="h-4 bg-gray-700 rounded w-1/2 mx-auto"></div>
</div>
</div>
</section>
);
}
if (error || !data) {
return (
<section id="experience" className="py-20 px-4">
<div className="max-w-6xl mx-auto text-center">
<p className="text-red-400">Erro ao carregar dados de experiência.</p>
</div>
</section>
);
}
const experiences = data.experiences.filter((exp) => exp.featured);
return (
<section
id="experience"
className="py-20 px-4 bg-gradient-to-b from-transparent to-black/20"
>
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-4xl md:text-5xl font-bold mb-6">
Experiência Profissional
</h2>
<p className="text-xl text-gray-300 max-w-2xl mx-auto">
Minha jornada profissional em desenvolvimento de software e soluções
tecnológicas
</p>
</motion.div>
<BentoGrid className="gap-6">
{experiences.map((exp, index) => (
<BentoGridItem
key={exp.id}
colSpan={index === 0 ? 2 : 1}
className="group hover:scale-[1.02] transition-all duration-300 hover:shadow-2xl hover:shadow-blue-500/10"
>
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-xl font-semibold text-white mb-1">
{exp.title}
</h3>
<p className="text-blue-400 font-medium">{exp.company}</p>
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-400 mt-2">
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
{exp.period}
</div>
{exp.location && (
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
{exp.location}
</div>
)}
<div className="flex items-center gap-1">
<Briefcase className="w-4 h-4" />
{exp.type}
</div>
</div>
</div>
</div>
{/* Description */}
<p className="text-gray-300 leading-relaxed">
{exp.description}
</p>
{/* Achievements */}
{exp.achievements.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-blue-400 mb-2">
Principais Conquistas:
</h4>
<ul className="space-y-1">
{exp.achievements.map((achievement, idx) => (
<li
key={idx}
className="text-sm text-gray-400 flex items-center gap-2"
>
<div className="w-1.5 h-1.5 bg-blue-400 rounded-full" />
{achievement}
</li>
))}
</ul>
</div>
)}
</div>
</BentoGridItem>
))}
</BentoGrid>
</div>
</section>
);
}
+151
View File
@@ -0,0 +1,151 @@
"use client";
import { motion } from "motion/react";
import { CalendarIcon, HomeIcon, MailIcon, PencilIcon } from "lucide-react";
import Link from "next/link";
import React from "react";
import { cn } from "@/lib/utils";
import { Dock, DockIcon } from "@/components/magicui/dock";
export type IconProps = React.HTMLAttributes<SVGElement>;
const Icons = {
calendar: (props: IconProps) => <CalendarIcon {...props} />,
email: (props: IconProps) => <MailIcon {...props} />,
linkedin: (props: IconProps) => (
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
<title>LinkedIn</title>
<path
fill="currentColor"
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
/>
</svg>
),
x: (props: IconProps) => (
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
<title>X</title>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
),
youtube: (props: IconProps) => (
<svg
width="32px"
height="32px"
viewBox="0 0 32 32"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>youtube</title>
<path d="M29.41,9.26a3.5,3.5,0,0,0-2.47-2.47C24.76,6.2,16,6.2,16,6.2s-8.76,0-10.94.59A3.5,3.5,0,0,0,2.59,9.26,36.13,36.13,0,0,0,2,16a36.13,36.13,0,0,0,.59,6.74,3.5,3.5,0,0,0,2.47,2.47C7.24,25.8,16,25.8,16,25.8s8.76,0,10.94-.59a3.5,3.5,0,0,0,2.47-2.47A36.13,36.13,0,0,0,30,16,36.13,36.13,0,0,0,29.41,9.26ZM13.2,20.2V11.8L20.47,16Z" />
</svg>
),
github: (props: IconProps) => (
<svg viewBox="0 0 438.549 438.549" {...props}>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
),
};
const DATA = {
navbar: [
{ href: "#", icon: HomeIcon, label: "Home" },
{ href: "#", icon: PencilIcon, label: "Blog" },
],
contact: {
social: {
GitHub: {
name: "GitHub",
url: "#",
icon: Icons.github,
},
LinkedIn: {
name: "LinkedIn",
url: "#",
icon: Icons.linkedin,
},
X: {
name: "X",
url: "#",
icon: Icons.x,
},
email: {
name: "Send Email",
url: "#",
icon: Icons.email,
},
},
},
};
export function Header() {
return (
<motion.header
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="fixed top-0 w-full z-[100] bg-black/60 backdrop-blur-md"
>
<nav className="max-w-6xl mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="text-xl font-bold"
>
<span className="bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
Evandro.dev
</span>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
className="flex items-center space-x-8"
>
{[
{ href: "#", label: "Home" },
{ href: "#about", label: "Sobre" },
{ href: "#skills", label: "Habilidades" },
{ href: "#experience", label: "Experiência" },
{ href: "#projects", label: "Projetos" },
{ href: "#contact", label: "Contato" },
].map((item, index) => (
<motion.a
key={item.href}
href={item.href}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 + index * 0.1 }}
className="text-gray-300 hover:text-white transition-all duration-300 hover:scale-105 relative group"
>
{item.label}
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-cyan-400 to-blue-500 group-hover:w-full transition-all duration-300"></span>
</motion.a>
))}
<motion.a
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 1 }}
href="/cv.pdf"
className="hidden bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white px-4 py-2 rounded-lg transition-all duration-300 hover:scale-105 hover:shadow-lg hover:shadow-blue-500/25"
download
>
Download CV
</motion.a>
</motion.div>
</div>
</nav>
</motion.header>
);
}
+203
View File
@@ -0,0 +1,203 @@
"use client";
import { motion } from "motion/react";
import Image from "next/image";
import { AuroraText } from "@/components/magicui/aurora-text";
import { AnimatedGradientText } from "@/components/magicui/animated-gradient-text";
import { useEffect } from "react";
export default function Hero() {
const getGreetingMessage = () => {
const hour = new Date().getHours();
if (hour >= 8 && hour < 12) {
// Manhã
return "Olá! Vi seu portfólio e estou interessado nos seus serviços. Podemos conversar?";
} else if (hour >= 13 && hour < 18) {
// Tarde
return "Boa tarde! Vi seu portfólio e estou interessado nos seus serviços. Podemos conversar?";
} else {
// Noite
return "Olá! Vi seu portfólio e estou interessado nos seus serviços. Podemos conversar quando estiver disponível?";
}
};
const generateWhatsAppUrl = () => {
const message = encodeURIComponent(getGreetingMessage());
const phone = "5547992742365"; // Número sem formatação
return `https://wa.me/${phone}?text=${message}`;
};
const scrollToContact = () => {
const contactSection = document.getElementById("contact");
if (contactSection) {
contactSection.scrollIntoView({ behavior: "smooth" });
}
};
return (
<section className="relative min-h-screen flex items-center justify-center px-4">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-20"></div>
<div className="relative z-10 text-center max-w-4xl mx-auto">
{/* Profile Image */}
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8 }}
className="mb-8"
>
<div className="relative w-48 h-48 mx-auto mb-8 mt-10">
<Image
src="/edev.jpeg"
alt="Evandro Fonseca Junior - Desenvolvedor Full-Stack"
fill
className="rounded-lg object-cover"
priority
sizes="(max-width: 768px) 192px, 192px"
quality={85}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R+IRjWjBqO6O2mhP//Z"
/>
</div>
</motion.div>
{/* Name and Title */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="mb-8"
>
<h1 className="text-3xl md:text-5xl lg:text-6xl font-black mb-6 leading-tight">
<AuroraText>Evandro Fonseca Junior</AuroraText>
</h1>
<AnimatedGradientText className="text-xl md:text-2xl lg:text-3xl font-bold">
Desenvolvedor Full-Stack
</AnimatedGradientText>
</motion.div>
{/* Description */}
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
className="text-xl md:text-2xl lg:text-3xl text-gray-300 max-w-3xl mx-auto mb-12 leading-relaxed"
>
Especialista em soluções web modernas e inovadoras. Formado em
Engenharia da Computação, com conhecimentos sólidos em computação em
nuvem e inteligência artificial.
</motion.p>
{/* Location */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
className="text-gray-400 mb-8"
>
📍 Garuva, Santa Catarina, Brasil
</motion.div>
{/* Social Icons */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.8 }}
className="flex justify-center space-x-6 mb-8"
>
<a
href="https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-blue-400 transition-colors"
aria-label="LinkedIn"
>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
</a>
<a
href="https://github.com/evandrodevbr"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
aria-label="GitHub"
>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</a>
<a
href="#contact"
onClick={(e) => {
e.preventDefault();
scrollToContact();
}}
className="text-gray-400 hover:text-green-400 transition-colors"
aria-label="WhatsApp"
>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<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 0020.885 3.488" />
</svg>
</a>
<a
href="#contact"
onClick={(e) => {
e.preventDefault();
scrollToContact();
}}
className="text-gray-400 hover:text-blue-400 transition-colors"
aria-label="Email"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
</a>
</motion.div>
{/* CTA Button */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1 }}
className="flex justify-center mb-8"
>
<a
href="#contact"
className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg font-semibold transition-colors"
>
Vamos Trabalhar Juntos
</a>
</motion.div>
</div>
{/* Scroll Indicator */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 1.2 }}
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
>
<div className="w-6 h-10 border-2 border-white/30 rounded-full flex justify-center">
<motion.div
animate={{ y: [0, 12, 0] }}
transition={{ duration: 1.5, repeat: Infinity }}
className="w-1 h-3 bg-white/50 rounded-full mt-2"
/>
</div>
</motion.div>
</section>
);
}
+134
View File
@@ -0,0 +1,134 @@
"use client";
import { motion } from "motion/react";
import { BentoGrid, BentoGridItem } from "@/components/magicui/bento-grid";
import { ExternalLink, Github } from "lucide-react";
import { usePortfolioData } from "@/lib/usePortfolioData";
export default function Projects() {
const { data, loading, error } = usePortfolioData();
if (loading) {
return (
<section id="projects" className="py-20 px-4">
<div className="max-w-6xl mx-auto text-center">
<div className="animate-pulse">
<div className="h-8 bg-gray-700 rounded w-1/3 mx-auto mb-4"></div>
<div className="h-4 bg-gray-700 rounded w-1/2 mx-auto"></div>
</div>
</div>
</section>
);
}
if (error || !data) {
return (
<section id="projects" className="py-20 px-4">
<div className="max-w-6xl mx-auto text-center">
<p className="text-red-400">Erro ao carregar dados de projetos.</p>
</div>
</section>
);
}
const projects = data.projects.filter((project) => project.featured);
return (
<section id="projects" className="py-20 px-4">
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-4xl md:text-5xl font-bold mb-6">
Projetos em Destaque
</h2>
<p className="text-xl text-gray-300 max-w-2xl mx-auto">
Alguns dos projetos que desenvolvi, demonstrando minhas habilidades
e paixão por tecnologia
</p>
</motion.div>
<BentoGrid className="gap-6">
{projects.map((project, index) => (
<BentoGridItem
key={project.id}
colSpan={index === 0 ? 2 : 1}
className="group hover:scale-[1.02] transition-all duration-300 hover:shadow-2xl hover:shadow-blue-500/10"
>
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-xl font-semibold text-white mb-2">
{project.title}
</h3>
<div className="flex flex-wrap gap-2 mb-3">
{project.technologies.map((tech) => (
<span
key={tech}
className="px-2 py-1 bg-blue-500/20 text-blue-400 text-xs rounded-full border border-blue-500/30"
>
{tech}
</span>
))}
</div>
</div>
<div className="flex gap-2">
<a
href={project.demoUrl}
className="p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
aria-label="Ver demo"
>
<ExternalLink className="w-4 h-4" />
</a>
<a
href={project.githubUrl}
className="p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
aria-label="Ver código"
>
<Github className="w-4 h-4" />
</a>
</div>
</div>
{/* Description */}
<p className="text-gray-300 leading-relaxed">
{project.description}
</p>
{/* Features */}
<div>
<h4 className="text-sm font-semibold text-blue-400 mb-2">
Funcionalidades:
</h4>
<ul className="space-y-1">
{project.features.map((feature, idx) => (
<li
key={idx}
className="text-sm text-gray-400 flex items-center gap-2"
>
<div className="w-1.5 h-1.5 bg-blue-400 rounded-full" />
{feature}
</li>
))}
</ul>
</div>
{/* Status */}
<div className="pt-2 border-t border-white/10">
<span className="text-xs text-blue-400 font-medium">
{project.status}
</span>
</div>
</div>
</BentoGridItem>
))}
</BentoGrid>
</div>
</section>
);
}
+135
View File
@@ -0,0 +1,135 @@
"use client";
import { motion } from "motion/react";
import { MagicCard } from "@/components/magicui/magic-card";
import {
Code,
Database,
Cloud,
Cpu,
Smartphone,
Zap,
GitBranch,
TestTube,
} from "lucide-react";
import { usePortfolioData } from "@/lib/usePortfolioData";
// Mapa de ícones para as categorias
const iconMap = {
Code: Code,
Database: Database,
Cloud: Cloud,
Cpu: Cpu,
Smartphone: Smartphone,
Zap: Zap,
GitBranch: GitBranch,
TestTube: TestTube,
};
// Mapa de cores para garantir que o Tailwind inclua as classes no bundle
const colorMap = {
"text-blue-440": "bg-blue-400",
"text-blue-400": "bg-blue-400",
"text-green-400": "bg-blue-400",
"text-purple-400": "bg-blue-400",
"text-red-400": "bg-blue-400",
"text-yellow-400": "bg-blue-400",
"text-pink-400": "bg-blue-400",
"text-indigo-400": "bg-blue-400",
};
export default function Skills() {
const { data, loading, error } = usePortfolioData();
if (loading) {
return (
<section id="skills" className="py-20 px-4">
<div className="max-w-6xl mx-auto text-center">
<div className="animate-pulse">
<div className="h-8 bg-gray-700 rounded w-1/3 mx-auto mb-4"></div>
<div className="h-4 bg-gray-700 rounded w-1/2 mx-auto"></div>
</div>
</div>
</section>
);
}
if (error || !data) {
return (
<section id="skills" className="py-20 px-4">
<div className="max-w-6xl mx-auto text-center">
<p className="text-red-400">Erro ao carregar dados de habilidades.</p>
</div>
</section>
);
}
const skillCategories = data.skills.technical;
return (
<section id="skills" className="py-20 px-4">
<div className="max-w-6xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-4xl md:text-5xl font-bold mb-6">
Habilidades Técnicas
</h2>
<p className="text-xl text-gray-300 max-w-2xl mx-auto">
Tecnologias e ferramentas que utilizo para criar soluções inovadoras
</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{skillCategories.map((category, index) => {
const IconComponent =
iconMap[category.icon as keyof typeof iconMap] || Code;
return (
<motion.div
key={category.category}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: index * 0.1 }}
viewport={{ once: true }}
>
<div className="p-6 space-y-4">
{/* Header */}
<div className="flex items-center gap-3">
<div className={`p-3 rounded-lg ${category.bgColor}`}>
<IconComponent className={`w-6 h-6 ${category.color}`} />
</div>
<h3 className="text-xl font-semibold">
{category.category}
</h3>
</div>
{/* Skills List */}
<div className="space-y-2">
{category.skills.map((skill, skillIndex) => (
<div
key={skillIndex}
className="flex items-center gap-2 text-gray-300"
>
<div
className={`w-2 h-2 rounded-full ${
colorMap[category.color as keyof typeof colorMap] ||
"bg-gray-400"
}`}
/>
<span className="text-sm">{skill}</span>
</div>
))}
</div>
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
}
+126
View File
@@ -0,0 +1,126 @@
"use client";
import { motion } from "motion/react";
import { Briefcase, Clock, DollarSign, HardHat, Lightbulb } from "lucide-react";
const WorkModalitiesData = [
{
title: "Desenvolvimento por Demanda",
value: "A partir de R$ 50/hora",
idealFor:
"Empresas que buscam otimização rápida, startups com necessidades urgentes ou projetos que exigem intervenção especializada e focada.",
description:
"Soluções ágeis e focadas para desafios específicos. Ideal para otimizar recursos e obter resultados rápidos sem compromissos de longo prazo. Sua visão, minha expertise, entregas pontuais.",
icon: Clock,
color: "blue",
},
{
title: "Retainer Mensal",
value: "Sob consulta (pacotes personalizados)",
idealFor:
"Negócios que valorizam estabilidade, crescimento planejado e acesso contínuo a um especialista dedicado para impulsionar sua inovação.",
description:
"Parceria estratégica para evolução contínua e suporte prioritário. Garanta acesso exclusivo à minha expertise para manutenção, novas funcionalidades e consultoria estratégica, com um compromisso de longo prazo que otimiza seu investimento.",
icon: Briefcase,
color: "green",
},
{
title: "Consultoria Especializada",
value: "Sob consulta (projetos de alto impacto)",
idealFor:
"Líderes de tecnologia e empresas visionárias que enfrentam gargalos técnicos ou buscam validação e aprimoramento de suas estratégias de desenvolvimento.",
description:
"Direcionamento estratégico e técnico para desafios complexos. Minha experiência em arquitetura de sistemas, IoT e otimização de performance é o diferencial para transformar suas ideias em soluções robustas e escaláveis. Ideal para quem busca excelência e inovação.",
icon: Lightbulb,
color: "purple",
},
{
title: "Desenvolvimento Completo",
value: "A partir de R$ 5.000 (investimento inicial)",
idealFor:
"Empreendedores e empresas que precisam de um parceiro para construir soluções digitais do zero, com a garantia de um produto de alta qualidade e impacto.",
description:
"Da concepção à entrega, seu projeto ganha vida com uma solução completa e personalizada. Garanto um processo transparente, com foco em performance, segurança e escalabilidade, para que você tenha um produto final que realmente se destaque no mercado.",
icon: HardHat,
color: "orange",
},
{
title: "Projetos Fechados",
value: "A partir de R$ 5.000 (soluções sob medida)",
idealFor:
"Clientes com necessidades claras e orçamentos específicos, que buscam uma entrega eficiente e sem surpresas para projetos de médio porte.",
description:
"Soluções com escopo e prazo definidos para resultados previsíveis. Ideal para quem busca agilidade e clareza no investimento, com a certeza de uma entrega que atende exatamente às suas expectativas e impulsiona seus objetivos.",
icon: DollarSign,
color: "red",
},
];
export function WorkModalities() {
return (
<section className="py-16 px-4">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-4xl md:text-5xl font-bold mb-4">
Minhas Modalidades de Trabalho
</h2>
<p className="text-xl text-gray-300 max-w-2xl mx-auto">
Escolha a opção que melhor se encaixa nas suas necessidades.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
{WorkModalitiesData.map((modality, index) => (
<motion.div
key={modality.title}
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
className={`bg-white/5 p-6 rounded-lg shadow-lg border border-white/10 ${
modality.color === "blue" ? "hover:border-blue-500" : ""
} ${modality.color === "green" ? "hover:border-green-500" : ""} ${
modality.color === "purple" ? "hover:border-purple-500" : ""
} ${modality.color === "orange" ? "hover:border-orange-500" : ""} ${
modality.color === "red" ? "hover:border-red-500" : ""
} transition-all duration-300 cursor-pointer`}
>
<div
className={`p-3 rounded-full ${
modality.color === "blue" ? "bg-blue-500/20" : ""
} ${modality.color === "green" ? "bg-green-500/20" : ""} ${
modality.color === "purple" ? "bg-purple-500/20" : ""
} ${modality.color === "orange" ? "bg-orange-500/20" : ""} ${
modality.color === "red" ? "bg-red-500/20" : ""
} inline-block mb-4`}
>
<modality.icon
className={`w-8 h-8 ${
modality.color === "blue" ? "text-blue-400" : ""
} ${modality.color === "green" ? "text-green-400" : ""} ${
modality.color === "purple" ? "text-purple-400" : ""
} ${modality.color === "orange" ? "text-orange-400" : ""} ${
modality.color === "red" ? "text-red-400" : ""
}`}
/>
</div>
<h3 className="text-2xl font-semibold mb-2">{modality.title}</h3>
<p className="text-lg font-medium text-gray-200 mb-2">
{modality.value}
</p>
<p className="text-gray-400 mb-4">{modality.description}</p>
<p className="text-sm text-gray-500">
<span className="font-semibold">Ideal para:</span>{" "}
{modality.idealFor}
</p>
</motion.div>
))}
</div>
</section>
);
}
+155 -14
View File
@@ -1,26 +1,167 @@
@import "tailwindcss";
@import "tw-animate-css";
:root {
--background: #ffffff;
--foreground: #171717;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--animate-aurora: aurora 8s ease-in-out infinite alternate;
@keyframes aurora {
0% {
background-position: 0% 50%;
transform: rotate(-5deg) scale(0.9);
}
25% {
background-position: 50% 100%;
transform: rotate(5deg) scale(1.1);
}
50% {
background-position: 100% 50%;
transform: rotate(-3deg) scale(0.95);
}
75% {
background-position: 50% 0%;
transform: rotate(3deg) scale(1.05);
}
100% {
background-position: 0% 50%;
transform: rotate(-5deg) scale(0.9);
}
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--blue-primary: #4facfe; /* Nova cor azul primária */
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--blue-primary: #4facfe; /* Nova cor azul primária */
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
.animate-shimmer {
animation: shimmer 2s linear infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
}
html {
scroll-behavior: smooth; /* Adicionar rolagem suave */
scroll-padding-top: 64px; /* Altura do cabeçalho fixo */
}
+98 -3
View File
@@ -13,8 +13,59 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Evandro Fonseca Junior - Desenvolvedor Full-Stack",
description:
"Desenvolvedor Full-Stack especializado em soluções web modernas com Angular, React, Node.js e computação em nuvem. Experiência em energia renovável e IoT.",
keywords: [
"desenvolvedor",
"full-stack",
"angular",
"react",
"node.js",
"typescript",
"computação em nuvem",
"iot",
"energia renovável",
],
authors: [{ name: "Evandro Fonseca Junior" }],
creator: "Evandro Fonseca Junior",
publisher: "Evandro Fonseca Junior",
formatDetection: {
email: false,
address: false,
telephone: false,
},
metadataBase: new URL("https://evandrofonsecajunior.dev"),
alternates: {
canonical: "/",
},
openGraph: {
title: "Evandro Fonseca Junior - Desenvolvedor Full-Stack",
description:
"Desenvolvedor Full-Stack especializado em soluções web modernas com Angular, React, Node.js e computação em nuvem.",
url: "https://evandrofonsecajunior.dev",
siteName: "Evandro Fonseca Junior",
locale: "pt_BR",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Evandro Fonseca Junior - Desenvolvedor Full-Stack",
description:
"Desenvolvedor Full-Stack especializado em soluções web modernas com Angular, React, Node.js e computação em nuvem.",
creator: "@evandrodevbr",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
export default function RootLayout({
@@ -23,7 +74,51 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<html lang="pt-BR">
<head>
{/* Preconnect to external origins for better performance */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
<link rel="preconnect" href="https://wa.me" />
<link rel="preconnect" href="https://www.linkedin.com" />
<link rel="preconnect" href="https://github.com" />
{/* DNS prefetch for additional performance */}
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
<link rel="dns-prefetch" href="//wa.me" />
<link rel="dns-prefetch" href="//www.linkedin.com" />
<link rel="dns-prefetch" href="//github.com" />
{/* Favicon and icons */}
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
{/* SEO and Crawling */}
<link rel="canonical" href="https://evandrofonsecajunior.dev" />
{/* Theme color for mobile browsers */}
<meta name="theme-color" content="#000000" />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<meta name="apple-mobile-web-app-title" content="Evandro.dev" />
{/* Performance hints */}
<link rel="preload" href="/edev.jpeg" as="image" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=5"
/>
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
+71 -96
View File
@@ -1,103 +1,78 @@
import Image from "next/image";
import dynamic from "next/dynamic";
import { Header } from "./components/Header";
import { Squares } from "@/components/magicui/squares";
import { WorkModalities } from "./components/WorkModalities"; // Importar WorkModalities
// Lazy load components for better performance
const Hero = dynamic(() => import("./components/Hero"), {
loading: () => (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-pulse text-white">Carregando...</div>
</div>
),
});
const About = dynamic(() => import("./components/About"), {
loading: () => (
<div className="py-20 px-4 text-center">
<div className="animate-pulse text-white">Carregando seção...</div>
</div>
),
});
const Skills = dynamic(() => import("./components/Skills"), {
loading: () => (
<div className="py-20 px-4 text-center">
<div className="animate-pulse text-white">Carregando habilidades...</div>
</div>
),
});
const Experience = dynamic(() => import("./components/Experience"), {
loading: () => (
<div className="py-20 px-4 text-center">
<div className="animate-pulse text-white">Carregando experiência...</div>
</div>
),
});
const Projects = dynamic(() => import("./components/Projects"), {
loading: () => (
<div className="py-20 px-4 text-center">
<div className="animate-pulse text-white">Carregando projetos...</div>
</div>
),
});
const Contact = dynamic(() => import("./components/Contact"), {
loading: () => (
<div className="py-20 px-4 text-center">
<div className="animate-pulse text-white">Carregando contato...</div>
</div>
),
});
export default function Home() {
return (
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
<div className="min-h-screen bg-black text-white relative">
<Squares
direction="diagonal"
speed={0.5}
borderColor="#374151"
hoverFillColor="#1e40af"
squareSize={30}
className="absolute inset-0 opacity-30"
/>
<Header />
<main className="relative z-10">
<Hero />
<About />
<Skills />
<Experience />
<Projects />
<WorkModalities /> {/* Adicionar WorkModalities aqui */}
<Contact />
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
);
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
@@ -0,0 +1,36 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface AnimatedGradientTextProps {
children: React.ReactNode;
className?: string;
}
export function AnimatedGradientText({
children,
className,
}: AnimatedGradientTextProps) {
return (
<motion.span
className={cn(
"bg-gradient-to-r from-cyan-400 via-blue-500 to-blue-600 bg-clip-text text-transparent font-bold",
className
)}
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
}}
style={{
backgroundSize: "200% 200%",
}}
>
{children}
</motion.span>
);
}
+33
View File
@@ -0,0 +1,33 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface AuroraTextProps {
children: React.ReactNode;
className?: string;
}
export function AuroraText({ children, className }: AuroraTextProps) {
return (
<motion.span
className={cn(
"bg-gradient-to-r from-blue-400 via-cyan-500 to-blue-500 bg-clip-text text-transparent",
className
)}
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "linear",
}}
style={{
backgroundSize: "200% 200%",
}}
>
{children}
</motion.span>
);
}
+54
View File
@@ -0,0 +1,54 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface BentoGridProps {
children: React.ReactNode;
className?: string;
}
interface BentoGridItemProps {
children: React.ReactNode;
className?: string;
colSpan?: number;
rowSpan?: number;
}
export function BentoGrid({ children, className }: BentoGridProps) {
return (
<div
className={cn(
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl mx-auto",
className
)}
>
{children}
</div>
);
}
export function BentoGridItem({
children,
className,
colSpan = 1,
rowSpan = 1,
}: BentoGridItemProps) {
return (
<motion.div
className={cn(
"relative overflow-hidden rounded-lg border border-white/20 bg-white/5 backdrop-blur-sm p-6",
`col-span-${colSpan} row-span-${rowSpan}`,
className
)}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
whileHover={{ scale: 1.02 }}
>
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 via-blue-400/10 to-cyan-500/10 opacity-0 hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10">{children}</div>
</motion.div>
);
}
+153
View File
@@ -0,0 +1,153 @@
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import {
motion,
MotionValue,
useMotionValue,
useSpring,
useTransform,
} from "motion/react";
import type { MotionProps } from "motion/react";
import React, { PropsWithChildren, useRef } from "react";
import { cn } from "@/lib/utils";
export interface DockProps extends VariantProps<typeof dockVariants> {
className?: string;
iconSize?: number;
iconMagnification?: number;
disableMagnification?: boolean;
iconDistance?: number;
direction?: "top" | "middle" | "bottom";
children: React.ReactNode;
}
const DEFAULT_SIZE = 40;
const DEFAULT_MAGNIFICATION = 60;
const DEFAULT_DISTANCE = 140;
const DEFAULT_DISABLEMAGNIFICATION = false;
const dockVariants = cva(
"supports-backdrop-blur:bg-white/10 supports-backdrop-blur:dark:bg-black/10 mx-auto mt-8 flex h-[58px] w-max items-center justify-center gap-2 rounded-2xl border p-2 backdrop-blur-md",
);
const Dock = React.forwardRef<HTMLDivElement, DockProps>(
(
{
className,
children,
iconSize = DEFAULT_SIZE,
iconMagnification = DEFAULT_MAGNIFICATION,
disableMagnification = DEFAULT_DISABLEMAGNIFICATION,
iconDistance = DEFAULT_DISTANCE,
direction = "middle",
...props
},
ref,
) => {
const mouseX = useMotionValue(Infinity);
const renderChildren = () => {
return React.Children.map(children, (child) => {
if (
React.isValidElement<DockIconProps>(child) &&
child.type === DockIcon
) {
return React.cloneElement(child, {
...child.props,
mouseX: mouseX,
size: iconSize,
magnification: iconMagnification,
disableMagnification: disableMagnification,
distance: iconDistance,
});
}
return child;
});
};
return (
<motion.div
ref={ref}
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
{...props}
className={cn(dockVariants({ className }), {
"items-start": direction === "top",
"items-center": direction === "middle",
"items-end": direction === "bottom",
})}
>
{renderChildren()}
</motion.div>
);
},
);
Dock.displayName = "Dock";
export interface DockIconProps
extends Omit<MotionProps & React.HTMLAttributes<HTMLDivElement>, "children"> {
size?: number;
magnification?: number;
disableMagnification?: boolean;
distance?: number;
mouseX?: MotionValue<number>;
className?: string;
children?: React.ReactNode;
props?: PropsWithChildren;
}
const DockIcon = ({
size = DEFAULT_SIZE,
magnification = DEFAULT_MAGNIFICATION,
disableMagnification,
distance = DEFAULT_DISTANCE,
mouseX,
className,
children,
...props
}: DockIconProps) => {
const ref = useRef<HTMLDivElement>(null);
const padding = Math.max(6, size * 0.2);
const defaultMouseX = useMotionValue(Infinity);
const distanceCalc = useTransform(mouseX ?? defaultMouseX, (val: number) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});
const targetSize = disableMagnification ? size : magnification;
const sizeTransform = useTransform(
distanceCalc,
[-distance, 0, distance],
[size, targetSize, size],
);
const scaleSize = useSpring(sizeTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
return (
<motion.div
ref={ref}
style={{ width: scaleSize, height: scaleSize, padding }}
className={cn(
"flex aspect-square cursor-pointer items-center justify-center rounded-full",
disableMagnification && "transition-colors hover:bg-muted-foreground",
className,
)}
{...props}
>
<div>{children}</div>
</motion.div>
);
};
DockIcon.displayName = "DockIcon";
export { Dock, DockIcon, dockVariants };
+158
View File
@@ -0,0 +1,158 @@
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import React, { useEffect, useId, useRef, useState } from "react";
/**
* DotPattern Component Props
*
* @param {number} [width=16] - The horizontal spacing between dots
* @param {number} [height=16] - The vertical spacing between dots
* @param {number} [x=0] - The x-offset of the entire pattern
* @param {number} [y=0] - The y-offset of the entire pattern
* @param {number} [cx=1] - The x-offset of individual dots
* @param {number} [cy=1] - The y-offset of individual dots
* @param {number} [cr=1] - The radius of each dot
* @param {string} [className] - Additional CSS classes to apply to the SVG container
* @param {boolean} [glow=false] - Whether dots should have a glowing animation effect
*/
interface DotPatternProps extends React.SVGProps<SVGSVGElement> {
width?: number;
height?: number;
x?: number;
y?: number;
cx?: number;
cy?: number;
cr?: number;
className?: string;
glow?: boolean;
[key: string]: unknown;
}
/**
* DotPattern Component
*
* A React component that creates an animated or static dot pattern background using SVG.
* The pattern automatically adjusts to fill its container and can optionally display glowing dots.
*
* @component
*
* @see DotPatternProps for the props interface.
*
* @example
* // Basic usage
* <DotPattern />
*
* // With glowing effect and custom spacing
* <DotPattern
* width={20}
* height={20}
* glow={true}
* className="opacity-50"
* />
*
* @notes
* - The component is client-side only ("use client")
* - Automatically responds to container size changes
* - When glow is enabled, dots will animate with random delays and durations
* - Uses Motion for animations
* - Dots color can be controlled via the text color utility classes
*/
export function DotPattern({
width = 16,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
className,
glow = false,
...props
}: DotPatternProps) {
const id = useId();
const containerRef = useRef<SVGSVGElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
const { width, height } = containerRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
};
updateDimensions();
window.addEventListener("resize", updateDimensions);
return () => window.removeEventListener("resize", updateDimensions);
}, []);
const dots = Array.from(
{
length:
Math.ceil(dimensions.width / width) *
Math.ceil(dimensions.height / height),
},
(_, i) => {
const col = i % Math.ceil(dimensions.width / width);
const row = Math.floor(i / Math.ceil(dimensions.width / width));
return {
x: col * width + cx,
y: row * height + cy,
delay: Math.random() * 5,
duration: Math.random() * 3 + 2,
};
},
);
return (
<svg
ref={containerRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full",
className,
)}
{...props}
>
<defs>
<radialGradient id={`${id}-gradient`}>
<stop offset="0%" stopColor="currentColor" stopOpacity="1" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</radialGradient>
</defs>
{dots.map((dot, index) => (
<motion.circle
key={`${dot.x}-${dot.y}`}
cx={dot.x}
cy={dot.y}
r={cr}
fill={glow ? `url(#${id}-gradient)` : "currentColor"}
className="text-neutral-400/80"
initial={glow ? { opacity: 0.4, scale: 1 } : {}}
animate={
glow
? {
opacity: [0.4, 1, 0.4],
scale: [1, 1.5, 1],
}
: {}
}
transition={
glow
? {
duration: dot.duration,
repeat: Infinity,
repeatType: "reverse",
delay: dot.delay,
ease: "easeInOut",
}
: {}
}
/>
))}
</svg>
);
}
+128
View File
@@ -0,0 +1,128 @@
"use client";
import createGlobe, { COBEOptions } from "cobe";
import { useMotionValue, useSpring } from "motion/react";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
const MOVEMENT_DAMPING = 1400;
const GLOBE_CONFIG: COBEOptions = {
width: 800,
height: 800,
onRender: () => {},
devicePixelRatio: 2,
phi: 0,
theta: 0.3,
dark: 0,
diffuse: 0.4,
mapSamples: 16000,
mapBrightness: 1.2,
baseColor: [1, 1, 1],
markerColor: [251 / 255, 100 / 255, 21 / 255],
glowColor: [1, 1, 1],
markers: [
{ location: [14.5995, 120.9842], size: 0.03 },
{ location: [19.076, 72.8777], size: 0.1 },
{ location: [23.8103, 90.4125], size: 0.05 },
{ location: [30.0444, 31.2357], size: 0.07 },
{ location: [39.9042, 116.4074], size: 0.08 },
{ location: [-23.5505, -46.6333], size: 0.1 },
{ location: [19.4326, -99.1332], size: 0.1 },
{ location: [40.7128, -74.006], size: 0.1 },
{ location: [34.6937, 135.5022], size: 0.05 },
{ location: [41.0082, 28.9784], size: 0.06 },
],
};
export function Globe({
className,
config = GLOBE_CONFIG,
}: {
className?: string;
config?: COBEOptions;
}) {
let phi = 0;
let width = 0;
const canvasRef = useRef<HTMLCanvasElement>(null);
const pointerInteracting = useRef<number | null>(null);
const pointerInteractionMovement = useRef(0);
const r = useMotionValue(0);
const rs = useSpring(r, {
mass: 1,
damping: 30,
stiffness: 100,
});
const updatePointerInteraction = (value: number | null) => {
pointerInteracting.current = value;
if (canvasRef.current) {
canvasRef.current.style.cursor = value !== null ? "grabbing" : "grab";
}
};
const updateMovement = (clientX: number) => {
if (pointerInteracting.current !== null) {
const delta = clientX - pointerInteracting.current;
pointerInteractionMovement.current = delta;
r.set(r.get() + delta / MOVEMENT_DAMPING);
}
};
useEffect(() => {
const onResize = () => {
if (canvasRef.current) {
width = canvasRef.current.offsetWidth;
}
};
window.addEventListener("resize", onResize);
onResize();
const globe = createGlobe(canvasRef.current!, {
...config,
width: width * 2,
height: width * 2,
onRender: (state) => {
if (!pointerInteracting.current) phi += 0.005;
state.phi = phi + rs.get();
state.width = width * 2;
state.height = width * 2;
},
});
setTimeout(() => (canvasRef.current!.style.opacity = "1"), 0);
return () => {
globe.destroy();
window.removeEventListener("resize", onResize);
};
}, [rs, config]);
return (
<div
className={cn(
"absolute inset-0 mx-auto aspect-[1/1] w-full max-w-[600px]",
className,
)}
>
<canvas
className={cn(
"size-full opacity-0 transition-opacity duration-500 [contain:layout_paint_size]",
)}
ref={canvasRef}
onPointerDown={(e) => {
pointerInteracting.current = e.clientX;
updatePointerInteraction(e.clientX);
}}
onPointerUp={() => updatePointerInteraction(null)}
onPointerOut={() => updatePointerInteraction(null)}
onMouseMove={(e) => updateMovement(e.clientX)}
onTouchMove={(e) =>
e.touches[0] && updateMovement(e.touches[0].clientX)
}
/>
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface MagicCardProps {
children: React.ReactNode;
className?: string;
}
export function MagicCard({ children, className }: MagicCardProps) {
return (
<motion.div
className={cn(
"relative overflow-hidden rounded-lg border border-white/20 bg-white/5 backdrop-blur-sm",
className
)}
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full animate-shimmer" />
{children}
</motion.div>
);
}
+31
View File
@@ -0,0 +1,31 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface ShimmerButtonProps {
children: React.ReactNode;
className?: string;
onClick?: () => void;
}
export function ShimmerButton({
children,
className,
onClick,
}: ShimmerButtonProps) {
return (
<motion.button
className={cn(
"relative overflow-hidden rounded-lg bg-gradient-to-r from-cyan-500 to-blue-600 px-6 py-3 font-semibold text-white shadow-lg",
className
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={onClick}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full animate-shimmer" />
<span className="relative z-10">{children}</span>
</motion.button>
);
}
+181
View File
@@ -0,0 +1,181 @@
"use client";
import { useRef, useEffect, useState } from "react";
interface SquaresProps {
direction?: "right" | "left" | "up" | "down" | "diagonal";
speed?: number;
borderColor?: string;
squareSize?: number;
hoverFillColor?: string;
className?: string;
}
export function Squares({
direction = "right",
speed = 1,
borderColor = "#333",
squareSize = 40,
hoverFillColor = "#222",
className,
}: SquaresProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const requestRef = useRef<number | undefined>(undefined);
const numSquaresX = useRef<number | undefined>(undefined);
const numSquaresY = useRef<number | undefined>(undefined);
const gridOffset = useRef({ x: 0, y: 0 });
const [hoveredSquare, setHoveredSquare] = useState<{
x: number;
y: number;
} | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Set canvas background
canvas.style.background = "#060606";
const resizeCanvas = () => {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
numSquaresX.current = Math.ceil(canvas.width / squareSize) + 1;
numSquaresY.current = Math.ceil(canvas.height / squareSize) + 1;
};
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
const drawGrid = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;
ctx.lineWidth = 0.5;
for (let x = startX; x < canvas.width + squareSize; x += squareSize) {
for (let y = startY; y < canvas.height + squareSize; y += squareSize) {
const squareX = x - (gridOffset.current.x % squareSize);
const squareY = y - (gridOffset.current.y % squareSize);
if (
hoveredSquare &&
Math.floor((x - startX) / squareSize) === hoveredSquare.x &&
Math.floor((y - startY) / squareSize) === hoveredSquare.y
) {
ctx.fillStyle = hoverFillColor;
ctx.fillRect(squareX, squareY, squareSize, squareSize);
}
ctx.strokeStyle = borderColor;
ctx.strokeRect(squareX, squareY, squareSize, squareSize);
}
}
const gradient = ctx.createRadialGradient(
canvas.width / 2,
canvas.height / 2,
0,
canvas.width / 2,
canvas.height / 2,
Math.sqrt(Math.pow(canvas.width, 2) + Math.pow(canvas.height, 2)) / 2
);
gradient.addColorStop(0, "rgba(6, 6, 6, 0)");
gradient.addColorStop(1, "#060606");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
const updateAnimation = () => {
const effectiveSpeed = Math.max(speed, 0.1);
switch (direction) {
case "right":
gridOffset.current.x =
(gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
break;
case "left":
gridOffset.current.x =
(gridOffset.current.x + effectiveSpeed + squareSize) % squareSize;
break;
case "up":
gridOffset.current.y =
(gridOffset.current.y + effectiveSpeed + squareSize) % squareSize;
break;
case "down":
gridOffset.current.y =
(gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
break;
case "diagonal":
gridOffset.current.x =
(gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
gridOffset.current.y =
(gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
break;
}
drawGrid();
requestRef.current = requestAnimationFrame(updateAnimation);
};
const handleMouseMove = (event: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;
const hoveredSquareX = Math.floor(
(mouseX + gridOffset.current.x - startX) / squareSize
);
const hoveredSquareY = Math.floor(
(mouseY + gridOffset.current.y - startY) / squareSize
);
setHoveredSquare({ x: hoveredSquareX, y: hoveredSquareY });
};
const handleMouseLeave = () => {
setHoveredSquare(null);
};
// Event listeners
window.addEventListener("resize", resizeCanvas);
canvas.addEventListener("mousemove", handleMouseMove);
canvas.addEventListener("mouseleave", handleMouseLeave);
// Initial setup
resizeCanvas();
requestRef.current = requestAnimationFrame(updateAnimation);
// Cleanup
return () => {
window.removeEventListener("resize", resizeCanvas);
canvas.removeEventListener("mousemove", handleMouseMove);
canvas.removeEventListener("mouseleave", handleMouseLeave);
if (requestRef.current) {
cancelAnimationFrame(requestRef.current);
}
};
}, [
direction,
speed,
borderColor,
hoverFillColor,
hoveredSquare,
squareSize,
]);
return (
<canvas
ref={canvasRef}
className={`w-full h-full border-none block ${className}`}
/>
);
}
+23
View File
@@ -0,0 +1,23 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface TextRevealProps {
children: React.ReactNode;
className?: string;
}
export function TextReveal({ children, className }: TextRevealProps) {
return (
<motion.p
className={cn("text-gray-300 leading-relaxed", className)}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
{children}
</motion.p>
);
}
+5
View File
@@ -0,0 +1,5 @@
link meu portifólio atual:https://evandro.dev.br/
minha imagem:./imagens
markdown linkedin:
+93
View File
@@ -0,0 +1,93 @@
import { useState, useEffect } from "react";
interface PortfolioData {
personal: {
name: string;
title: string;
location: string;
email: string;
phone: string;
linkedin: string;
github: string;
summary: string;
};
experiences: Array<{
id: string;
title: string;
company: string;
period: string;
location: string;
type: string;
description: string;
achievements: string[];
technologies: string[];
featured: boolean;
}>;
projects: Array<{
id: string;
title: string;
description: string;
longDescription: string;
technologies: string[];
features: string[];
status: string;
demoUrl: string;
githubUrl: string;
imageUrl: string;
featured: boolean;
category: string;
}>;
skills: {
technical: Array<{
category: string;
icon: string;
color: string;
bgColor: string;
skills: string[];
}>;
soft: Array<{
skill: string;
level: number;
}>;
};
contact: {
email: string;
phone: string;
whatsapp: string;
linkedin: string;
github: string;
location: string;
};
social: Array<{
name: string;
url: string;
icon: string;
}>;
}
export function usePortfolioData() {
const [data, setData] = useState<PortfolioData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadData = async () => {
try {
const response = await fetch("/portfolio-data.json");
if (!response.ok) {
throw new Error("Failed to load portfolio data");
}
const portfolioData: PortfolioData = await response.json();
setData(portfolioData);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
};
loadData();
}, []);
return { data, loading, error };
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+175
View File
@@ -0,0 +1,175 @@
# Evandro Fonseca Junior
**Desenvolvedor Full-Stack Angular**
📍 Garuva, Santa Catarina, Brasil
🔗 [LinkedIn](https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/)
---
## Sobre Mim
Desenvolvedor Full-Stack com 3+ anos de experiência, especializado em soluções web modernas e inovadoras. Formado em Engenharia da Computação, com conhecimentos sólidos em computação em nuvem e inteligência artificial. Busco constantemente novos desafios onde possa aplicar minha paixão por tecnologia e resolver problemas de forma criativa.
Meu objetivo é entregar soluções de alta qualidade que superem expectativas, sempre priorizando colaboração, ética e inovação. Acredito no poder da tecnologia para fazer a diferença e estou comprometido com o aprendizado contínuo e a excelência técnica.
---
## Experiência Profissional
### **Desenvolvedor Full Stack | Innovo Tecnologia Da Informação Ltda.**
_Dezembro 2024 - Fevereiro 2025 (3 meses) | Tempo Integral_
### **Desenvolvedor Full Stack | Julia Beauty Lashes**
_Julho 2024 - Presente (1 ano 3 meses) | Freelance | Balneário Camboriú, SC_
- **Liderança Técnica**: Arquitetei e desenvolvi sistema web completo para estúdio de beleza especializado em extensão de cílios
- **Tecnologias**: Next.js, TypeScript, Tailwind CSS, Firebase, Jest, React Testing Library
- **Resultados Mensuráveis**:
- Redução de 40% no tempo de marcação de horários com sistema de agendamento automatizado
- Aumento de 25% na taxa de conversão através de interface responsiva otimizada
- Pontuação 90+ no Google Lighthouse para SEO e performance
- Cobertura de testes acima de 80%
- **Integração e Automação**: Implementei pipeline CI/CD com GitHub Actions, deploy automatizado na Vercel, e integração com APIs de pagamento e WhatsApp
- **Segurança**: Desenvolvimento de sistema robusto de autenticação e autorização com Firebase Authentication
### **Desenvolvedor Full Stack | Apolo Energia**
_Junho 2022 - Presente (3 anos 4 meses) | Autônomo | Curitiba, PR_
**Responsabilidades Principais**:
- Desenvolvimento de aplicações web e mobile responsivas e escaláveis
- Criação e implementação de APIs RESTful eficientes
- Otimização de performance de aplicações e bancos de dados
- Implementação de práticas avançadas de segurança de dados
- Integração com sistemas terceiros e APIs externas
- Colaboração com equipes multidisciplinares (UX/UI, dados, IoT)
- Code reviews e garantia de qualidade de código
**Especialização em Energia Renovável**:
- Desenvolvimento de soluções para geração, armazenamento e gestão de energia
- Criação de sistemas de automação para otimização de eficiência industrial
- Implementação de soluções IoT para comunicação entre dispositivos
- Participação ativa no planejamento e arquitetura de projetos
---
## Principais Competências Técnicas
### **Frontend**
- React.js & Next.js
- Angular
- TypeScript & JavaScript
- Tailwind CSS
- HTML5 & CSS3
- UI/UX Design Responsivo
### **Backend**
- Node.js
- PHP
- APIs RESTful
- Firebase
- MySQL
- Arquitetura de microsserviços
### **DevOps & Ferramentas**
- GitHub Actions (CI/CD)
- Vercel
- Git & GitHub
- Jest & React Testing Library
- SEO e Performance Optimization
### **Especialidades**
- Desenvolvimento de software personalizado
- Computação em nuvem
- Inteligência artificial
- IoT e conectividade
- Energias renováveis
- Automação e controle de sistemas
---
## Serviços Oferecidos
- **Desenvolvimento Web**: Aplicações modernas e responsivas
- **Desenvolvimento de Software Personalizado**: Soluções sob medida
- **Consultoria de TI**: Arquitetura e planejamento de projetos
- **Suporte Técnico**: Redes, recuperação de dados e manutenção
- **Integração de Sistemas**: Conectividade entre plataformas
---
## Projetos em Destaque
### **Heaven Time Tracker**
_Ferramenta moderna de gestão de tempo e produtividade_
- **Tecnologias**: Next.js 14, TypeScript, Tailwind CSS
- **Funcionalidades**: Dashboard personalizado, análises detalhadas, sistema de metas, relatórios avançados, integração com calendário
- **Características**: Interface intuitiva com suporte a tema claro/escuro, performance otimizada
- **Status**: Versão 1.0.0 em produção
### **MinGW Downloader and Installer**
_Aplicativo Python para automação de instalação de ambiente de desenvolvimento_
- **Tecnologias**: Python, Tkinter, Requests, Threading, Logging
- **Funcionalidades**: Download automatizado, verificação de compatibilidade, integração com PATH do sistema
- **Impacto**: Simplificação do processo de configuração para desenvolvedores Windows
---
## Diferenciais Profissionais
### **Inovação e Criatividade**
- Desenvolvimento de soluções criativas e personalizadas
- Foco em experiência do usuário e interface intuitiva
- Implementação de tecnologias de ponta
### **Eficiência e Performance**
- Otimização constante de processos e aplicações
- Práticas de Clean Code e TDD
- Arquiteturas escaláveis e maintíveis
### **Sustentabilidade e Impacto**
- Especialização em soluções de energia renovável
- Desenvolvimento consciente com foco em redução de impacto ambiental
- Soluções que geram valor real para o negócio
### **Comunicação e Colaboração**
- Habilidade de traduzir conceitos técnicos para stakeholders não técnicos
- Trabalho efetivo em equipes multidisciplinares
- Mentoria e transferência de conhecimento
---
## Status Atual
**🔍 Em busca de oportunidades como:**
- Engenheiro de Software
- Desenvolvedor Full-Stack
- Desenvolvedor Web
**📊 Engajamento LinkedIn:**
- 40 conexões profissionais
- 41 seguidores
- Publicações técnicas regulares sobre projetos e inovações
---
_"Acredito no poder de levar ideias adiante e fazer a diferença de forma significativa através da tecnologia."_
+58 -1
View File
@@ -1,7 +1,64 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
// Otimização de imagens
images: {
formats: ["image/webp", "image/avif"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 60,
},
// Otimizações de performance
experimental: {
scrollRestoration: true,
},
// Headers de cache e segurança
async headers(): Promise<
Array<{ source: string; headers: Array<{ key: string; value: string }> }>
> {
return [
{
source: "/(.*)",
headers: [
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "origin-when-cross-origin",
},
],
},
{
source: "/static/(.*)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
{
source: "/_next/image(.*)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
];
},
// Compressão
compress: true,
};
export default nextConfig;
+12 -4
View File
@@ -10,17 +10,25 @@
"format": "biome format --write"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cobe": "^0.6.4",
"lucide-react": "^0.542.0",
"motion": "^12.23.12",
"next": "15.5.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.5.2"
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"typescript": "^5",
"@biomejs/biome": "2.2.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"@biomejs/biome": "2.2.0"
"tw-animate-css": "^1.3.7",
"typescript": "^5"
}
}
+134
View File
@@ -8,6 +8,21 @@ importers:
.:
dependencies:
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
cobe:
specifier: ^0.6.4
version: 0.6.4
lucide-react:
specifier: ^0.542.0
version: 0.542.0([email protected])
motion:
specifier: ^12.23.12
version: 12.23.12([email protected]([email protected]))([email protected])
next:
specifier: 15.5.2
version: 15.5.2([email protected]([email protected]))([email protected])
@@ -17,6 +32,12 @@ importers:
react-dom:
specifier: 19.1.0
version: 19.1.0([email protected])
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7([email protected])
devDependencies:
'@biomejs/biome':
specifier: 2.2.0
@@ -36,6 +57,9 @@ importers:
tailwindcss:
specifier: ^4
version: 4.1.12
tw-animate-css:
specifier: ^1.3.7
version: 1.3.7
typescript:
specifier: ^5
version: 5.9.2
@@ -404,9 +428,19 @@ packages:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
[email protected]:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
[email protected]:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-huuGFnDoXLy/tsCZYYa/H35BBRs9cxsS0XKJ3BXjRp699cQKuoEVrvKlAQMx0DKXG7+VUv4jsHVrS7yPbkLSkQ==}
[email protected]:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -432,6 +466,20 @@ packages:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
[email protected]:
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
[email protected]:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -506,6 +554,11 @@ packages:
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
engines: {node: '>= 12.0.0'}
[email protected]:
resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
[email protected]:
resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==}
@@ -522,6 +575,26 @@ packages:
engines: {node: '>=10'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
[email protected]:
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
[email protected]:
resolution: {integrity: sha512-8jCD8uW5GD1csOoqh1WhH1A6j5APHVE15nuBkFeRiMzYBdRwyAHmSP/oXSuW0WJPZRXTFdBoG4hY9TFWNhhwng==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
[email protected]:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -548,6 +621,9 @@ packages:
sass:
optional: true
[email protected]:
resolution: {integrity: sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A==}
[email protected]:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -600,6 +676,14 @@ packages:
babel-plugin-macros:
optional: true
[email protected]:
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
[email protected]:
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders'
[email protected]:
resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==}
@@ -614,6 +698,9 @@ packages:
[email protected]:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
[email protected]:
resolution: {integrity: sha512-lvLb3hTIpB5oGsk8JmLoAjeCHV58nKa2zHYn8yWOoG5JJusH3bhJlF2DLAZ/5NmJ+jyH3ssiAx/2KmbhavJy/A==}
[email protected]:
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
engines: {node: '>=14.17'}
@@ -897,8 +984,18 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
clsx: 2.1.1
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
phenomenon: 1.6.0
[email protected]:
dependencies:
color-name: 1.1.4
@@ -928,6 +1025,15 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.2.3
[email protected]([email protected]([email protected]))([email protected]):
dependencies:
motion-dom: 12.23.12
motion-utils: 12.23.6
tslib: 2.8.1
optionalDependencies:
react: 19.1.0
react-dom: 19.1.0([email protected])
[email protected]: {}
[email protected]:
@@ -980,6 +1086,10 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.30.1
lightningcss-win32-x64-msvc: 1.30.1
[email protected]([email protected]):
dependencies:
react: 19.1.0
[email protected]:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -992,6 +1102,20 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
motion-utils: 12.23.6
[email protected]: {}
[email protected]([email protected]([email protected]))([email protected]):
dependencies:
framer-motion: 12.23.12([email protected]([email protected]))([email protected])
tslib: 2.8.1
optionalDependencies:
react: 19.1.0
react-dom: 19.1.0([email protected])
[email protected]: {}
[email protected]([email protected]([email protected]))([email protected]):
@@ -1017,6 +1141,8 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -1085,6 +1211,12 @@ snapshots:
client-only: 0.0.1
react: 19.1.0
[email protected]: {}
[email protected]([email protected]):
dependencies:
tailwindcss: 4.1.12
[email protected]: {}
[email protected]: {}
@@ -1100,6 +1232,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

+20
View File
@@ -0,0 +1,20 @@
{
"name": "Evandro Fonseca Junior - Portfólio",
"short_name": "Evandro.dev",
"description": "Portfólio profissional de Evandro Fonseca Junior, Desenvolvedor Full-Stack",
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#000000",
"orientation": "portrait-primary",
"categories": ["portfolio", "developer", "technology"],
"lang": "pt-BR",
"dir": "ltr",
"icons": [
{
"src": "/favicon.ico",
"sizes": "any",
"type": "image/x-icon"
}
]
}
+169
View File
@@ -0,0 +1,169 @@
{
"personal": {
"name": "Evandro Fonseca Junior",
"title": "Desenvolvedor Full-Stack",
"location": "Garuva, Santa Catarina, Brasil",
"email": "[email protected]",
"phone": "+55 47 99274-2365",
"linkedin": "https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
"github": "https://github.com/evandrodevbr",
"summary": "Desenvolvedor Full-Stack com 3+ anos de experiência, especializado em soluções web modernas e inovadoras. Formado em Engenharia da Computação, com conhecimentos sólidos em computação em nuvem e inteligência artificial. Busco constantemente novos desafios onde possa aplicar minha paixão por tecnologia e resolver problemas de forma criativa."
},
"experiences": [
{
"id": "julia-beauty",
"title": "Desenvolvedor Full Stack",
"company": "Julia Beauty Lashes",
"period": "Julho 2024 - Presente",
"location": "Balneário Camboriú, SC",
"type": "Freelance",
"description": "Sistema web completo para estúdio de beleza especializado em extensão de cílios. Tecnologias: Next.js, TypeScript, Tailwind CSS, Firebase.",
"achievements": [
"Redução de 40% no tempo de marcação de horários",
"Aumento de 25% na taxa de conversão"
],
"technologies": ["Next.js", "TypeScript", "Tailwind CSS", "Firebase"],
"featured": true
},
{
"id": "apolo-energia",
"title": "Desenvolvedor Full Stack",
"company": "Apolo Energia",
"period": "Junho 2022 - Presente",
"location": "Curitiba, PR",
"type": "Autônomo",
"description": "Soluções em energia renovável e IoT. Desenvolvimento web/mobile responsivo.",
"achievements": ["Foco em sustentabilidade e automação industrial"],
"technologies": ["React", "Node.js", "IoT", "Cloud"],
"featured": true
},
{
"id": "innovo-tecnologia",
"title": "Desenvolvedor Fullstack",
"company": "Innovo Tecnologia",
"period": "Dezembro 2024 - Fevereiro 2025",
"location": "",
"type": "Tempo Integral",
"description": "Desenvolvimento de soluções empresariais.",
"achievements": [],
"technologies": ["Angular", "PHP", "MySQL"],
"featured": false
}
],
"projects": [
{
"id": "heaven-time-tracker",
"title": "Heaven Time Tracker",
"description": "Ferramenta moderna de gestão de tempo e produtividade com dashboard personalizado, análises detalhadas e sistema de metas.",
"longDescription": "Uma aplicação completa para rastreamento de tempo com recursos avançados de análise e relatórios. Inclui dashboard interativo, sistema de metas personalizáveis e integração com ferramentas de produtividade.",
"technologies": ["Next.js 14", "TypeScript", "Tailwind CSS"],
"features": [
"Dashboard personalizado",
"Análises detalhadas",
"Sistema de metas",
"Relatórios avançados",
"Interface claro/escuro"
],
"status": "Versão 1.0.0 em produção",
"demoUrl": "#",
"githubUrl": "#",
"imageUrl": "/projects/heaven-time-tracker.jpg",
"featured": true,
"category": "produtividade"
},
{
"id": "mingw-downloader",
"title": "MinGW Downloader and Installer",
"description": "Aplicativo Python para automação de instalação de ambiente de desenvolvimento Windows.",
"longDescription": "Ferramenta automatizada para desenvolvedores Windows que simplifica a instalação e configuração do ambiente MinGW. Inclui verificação de compatibilidade e integração automática com o PATH do sistema.",
"technologies": ["Python", "Tkinter", "Requests", "Threading"],
"features": [
"Download automatizado",
"Verificação de compatibilidade",
"Integração com PATH",
"Interface gráfica intuitiva"
],
"status": "Concluído",
"demoUrl": "#",
"githubUrl": "#",
"imageUrl": "/projects/mingw-installer.jpg",
"featured": true,
"category": "ferramenta"
}
],
"skills": {
"technical": [
{
"category": "Frontend",
"icon": "Code",
"color": "text-blue-400",
"bgColor": "bg-blue-500/20",
"skills": [
"React.js & Next.js",
"Angular",
"TypeScript & JavaScript",
"Tailwind CSS",
"HTML5 & CSS3"
]
},
{
"category": "Backend",
"icon": "Database",
"color": "text-blue-400",
"bgColor": "bg-blue-500/20",
"skills": ["Node.js", "PHP", "MySQL", "APIs RESTful", "Firebase"]
},
{
"category": "DevOps & Tools",
"icon": "GitBranch",
"color": "text-blue-400",
"bgColor": "bg-blue-500/20",
"skills": ["GitHub Actions", "Git", "Jest & Testing", "Vercel", "CI/CD"]
},
{
"category": "Especialidades",
"icon": "Cpu",
"color": "text-blue-400",
"bgColor": "bg-blue-500/20",
"skills": [
"Cloud Computing",
"Inteligência Artificial",
"IoT",
"Energia Renovável"
]
}
],
"soft": [
{ "skill": "Comunicação", "level": 90 },
{ "skill": "Trabalho em Equipe", "level": 85 },
{ "skill": "Resolução de Problemas", "level": 95 },
{ "skill": "Aprendizado Contínuo", "level": 90 },
{ "skill": "Adaptabilidade", "level": 85 }
]
},
"contact": {
"email": "[email protected]",
"phone": "+55 47 99274-2365",
"whatsapp": "+55 47 99274-2365",
"linkedin": "https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
"github": "https://github.com/evandrodevbr",
"location": "Garuva, Santa Catarina, Brasil"
},
"social": [
{
"name": "LinkedIn",
"url": "https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
"icon": "linkedin"
},
{
"name": "GitHub",
"url": "https://github.com/evandrodevbr",
"icon": "github"
},
{
"name": "Email",
"url": "mailto:[email protected]",
"icon": "mail"
}
]
}
+10
View File
@@ -0,0 +1,10 @@
User-agent: *
Allow: /
# Block access to sensitive files
Disallow: /api/
Disallow: /_next/
Disallow: /admin/
# Sitemap
Sitemap: https://evandrofonsecajunior.dev/sitemap.xml
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://evandrofonsecajunior.dev</loc>
<lastmod>2025-01-09</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://evandrofonsecajunior.dev/#about</loc>
<lastmod>2025-01-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://evandrofonsecajunior.dev/#skills</loc>
<lastmod>2025-01-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://evandrofonsecajunior.dev/#experience</loc>
<lastmod>2025-01-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://evandrofonsecajunior.dev/#projects</loc>
<lastmod>2025-01-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://evandrofonsecajunior.dev/#contact</loc>
<lastmod>2025-01-09</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
+40
View File
@@ -0,0 +1,40 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
"blue-primary": "#4facfe", // Cor azul personalizada
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
};