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:
1 parent
ca9e70a53e
commit
90cf3c73ad
37 files changed
+3898
-118
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
link meu portifólio atual:https://evandro.dev.br/
|
||||
|
||||
minha imagem:./imagens
|
||||
|
||||
markdown linkedin:
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+134
@@ -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]: {}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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")],
|
||||
};
|
||||
Reference in new issue
Block a user