feat(contact, experience, projects, skills, workModalities): enhance component structure and user experience

- Refactored the Contact component to improve form handling and user feedback, introducing a custom shimmer button and enhanced form validation.
- Updated the Experience component to optimize rendering and improve data handling with memoization, ensuring efficient performance.
- Enhanced the Projects component with a new layout and improved project filtering, utilizing memoization for better performance.
- Refined the Skills component to streamline the display of technical skills, incorporating a loading state and error handling for better user experience.
- Revamped the WorkModalities component to present service options more clearly, including a market comparison section for better user decision-making.
This commit is contained in:
Evandro Fonseca Junior committed 2026-01-12 16:05:02 -03:00
1 parent 375862a65a
commit 8987d06eb4
5 files changed
+606 -1048

No files matched your search

+137 -307
View File
@@ -1,325 +1,155 @@
"use client";
import { motion } from "motion/react";
import {
Calendar,
MapPin,
Briefcase,
Zap,
TrendingUp,
Users,
Award,
ExternalLink,
import React, { useMemo } from "react";
import { motion, AnimatePresence } from "motion/react";
import {
Calendar, MapPin, Briefcase, Zap,
Award, Users, Terminal, ChevronRight
} from "lucide-react";
import { usePortfolioData } from "@/lib/usePortfolioData";
// --- Tipagem e Configuração (Otimização Pareto 80/20) ---
const CONFIG = {
"innovo-tecnologia": { color: "text-blue-400", bg: "bg-blue-400/10", border: "border-blue-400/20" },
"apolo-energia": { color: "text-emerald-400", bg: "bg-emerald-400/10", border: "border-emerald-400/20" },
default: { color: "text-slate-400", bg: "bg-slate-400/10", border: "border-slate-400/20" }
};
export default function Experience() {
const { data, loading, error } = usePortfolioData();
if (loading) {
return (
<section id="experience" className="py-20 px-4">
<div className="max-w-7xl 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>
);
}
// Memória: Evita re-processamento de regex em cada re-render (Princípio de Eficiência)
const sortedExperiences = useMemo(() => {
if (!data?.experiences) return [];
return [...data.experiences]
.filter((exp) => exp.featured)
.sort((a, b) => {
const yearA = parseInt(a.period.match(/\d{4}/)?.[0] || "0");
const yearB = parseInt(b.period.match(/\d{4}/)?.[0] || "0");
return yearB - yearA; // Da mais recente para a mais antiga (Padrão Senior)
});
}, [data]);
if (error || !data) {
return (
<section id="experience" className="py-20 px-4">
<div className="max-w-7xl 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)
.sort((a, b) => {
// Extrair ano de início de cada experiência
const getStartYear = (period: string) => {
const yearMatch = period.match(/(\d{4})/);
return yearMatch ? parseInt(yearMatch[1]) : 0;
};
const yearA = getStartYear(a.period);
const yearB = getStartYear(b.period);
// Ordenar da mais antiga para a mais nova
return yearA - yearB;
});
// Configuração de cores por empresa/tipo
const experienceConfig = {
"innovo-tecnologia": {
color: "from-blue-500 to-cyan-600",
bgColor: "bg-blue-500/10",
borderColor: "border-blue-500/20",
iconColor: "text-blue-400",
accent: "blue",
},
"apolo-energia": {
color: "from-green-500 to-emerald-600",
bgColor: "bg-green-500/10",
borderColor: "border-green-500/20",
iconColor: "text-green-400",
accent: "green",
},
};
const getExperienceConfig = (company: string) => {
const key = company
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
return (
experienceConfig[key as keyof typeof experienceConfig] ||
experienceConfig["innovo-tecnologia"]
);
};
const formatDuration = (period: string) => {
// Extrair meses da string de período (ex: "3 meses")
const monthsMatch = period.match(/(\d+)\s*meses?/i);
if (monthsMatch) {
const months = parseInt(monthsMatch[1]);
if (months < 12) {
return `${months} ${months === 1 ? "mês" : "meses"}`;
} else {
const years = Math.floor(months / 12);
const remainingMonths = months % 12;
if (remainingMonths === 0) {
return `${years} ${years === 1 ? "ano" : "anos"}`;
} else {
return `${years} ${
years === 1 ? "ano" : "anos"
} e ${remainingMonths} ${remainingMonths === 1 ? "mês" : "meses"}`;
}
}
}
// Extrair anos da string de período (ex: "3 anos 5 meses")
const yearsMatch = period.match(/(\d+)\s*anos?/i);
if (yearsMatch) {
const years = parseInt(yearsMatch[1]);
return `${years}+ ${years === 1 ? "ano" : "anos"}`;
}
return "Experiência relevante";
};
if (loading) return <ExperienceSkeleton />;
return (
<section id="experience" className="py-20 px-4 relative">
<div className="max-w-7xl mx-auto">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-20"
>
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-cyan-400/10 to-blue-500/10 px-6 py-3 rounded-full mb-6 border border-cyan-400/20">
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
<span className="text-sm font-medium text-cyan-300">
Jornada Profissional
</span>
</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
Experiência Profissional
<section id="experience" className="relative py-32 bg-[#030712] overflow-hidden">
<div className="max-w-6xl mx-auto px-6 relative">
{/* Header com Identidade Visual */}
<div className="mb-24">
<motion.span
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
className="text-blue-500 font-mono text-sm tracking-[0.4em] uppercase"
>
System_History
</motion.span>
<h2 className="text-5xl md:text-7xl font-black text-white mt-4 tracking-tighter">
Trajetória <br />
<span className="text-slate-700">Profissional</span>
</h2>
<p className="text-xl text-gray-300 max-w-3xl mx-auto leading-relaxed">
Uma trajetória de crescimento técnico e impacto real, onde cada
projeto representou um novo nível de desafio e aprendizado
</p>
</motion.div>
{/* Experience Cards Grid */}
<div className="grid lg:grid-cols-2 gap-8">
{experiences.map((exp, index) => {
const config = getExperienceConfig(exp.company);
return (
<motion.div
key={exp.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: index * 0.2 }}
viewport={{ once: true }}
className="relative"
>
{/* Content Card */}
<div className="h-full">
<div className="relative group">
{/* Background Glow */}
<div
className={`absolute inset-0 bg-gradient-to-r ${config.color} opacity-10 rounded-2xl blur-xl group-hover:opacity-20 transition-opacity duration-500`}
></div>
{/* Main Card */}
<div className="relative bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl p-8 hover:border-cyan-500/30 transition-all duration-300 hover:scale-105">
{/* Header */}
<div className="flex items-start justify-between mb-6">
<div className="flex-1">
<div className="flex items-center gap-3 mb-3">
<div className={`p-3 rounded-xl ${config.bgColor}`}>
<Briefcase
className={`w-6 h-6 ${config.iconColor}`}
/>
</div>
<div>
<h3 className="text-2xl font-bold text-white mb-1">
{exp.title}
</h3>
<p
className={`text-lg font-semibold ${config.iconColor}`}
>
{exp.company}
</p>
</div>
</div>
{/* Meta Info */}
<div className="flex flex-wrap gap-4 text-sm text-gray-400 mb-4">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
<span>{exp.period}</span>
</div>
{exp.location && (
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
<span>{exp.location}</span>
</div>
)}
<div className="flex items-center gap-2">
<Users className="w-4 h-4" />
<span>{exp.type}</span>
</div>
</div>
</div>
{/* Duration Badge */}
<div
className={`px-4 py-2 rounded-full ${config.bgColor} border ${config.borderColor}`}
>
<span
className={`text-sm font-bold ${config.iconColor}`}
>
{formatDuration(exp.period)}
</span>
</div>
</div>
{/* Description */}
<p className="text-gray-300 leading-relaxed mb-6 text-lg">
{exp.description}
</p>
{/* Achievements */}
{exp.achievements && exp.achievements.length > 0 && (
<div className="mb-6">
<h4 className="text-sm font-semibold text-gray-400 mb-4 uppercase tracking-wide flex items-center gap-2">
<Award className="w-4 h-4 text-yellow-400" />
Principais Conquistas
</h4>
<div className="grid gap-3">
{exp.achievements.map((achievement, idx) => (
<div
key={idx}
className="flex items-start gap-3 p-3 rounded-lg bg-white/5 border border-white/5 hover:border-blue-500/20 transition-colors"
>
<div className="w-2 h-2 bg-gradient-to-r from-blue-400 to-purple-400 rounded-full mt-2 flex-shrink-0"></div>
<span className="text-gray-300 text-sm leading-relaxed">
{achievement}
</span>
</div>
))}
</div>
</div>
)}
{/* Technologies */}
{exp.technologies && exp.technologies.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-gray-400 mb-3 uppercase tracking-wide flex items-center gap-2">
<Zap className="w-4 h-4 text-orange-400" />
Stack Tecnológica
</h4>
<div className="flex flex-wrap gap-2">
{exp.technologies.slice(0, 6).map((tech) => (
<span
key={tech}
className="px-3 py-1 bg-gray-800/50 text-gray-300 text-xs rounded-full border border-gray-700/50 hover:border-blue-500/30 transition-colors"
>
{tech}
</span>
))}
{exp.technologies.length > 6 && (
<span className="px-3 py-1 bg-gray-800/30 text-gray-500 text-xs rounded-full">
+{exp.technologies.length - 6}
</span>
)}
</div>
</div>
)}
</div>
</div>
</div>
</motion.div>
);
})}
</div>
{/* Summary Stats */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
viewport={{ once: true }}
className="mt-20"
>
<div className="relative max-w-4xl mx-auto">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-green-500/10 rounded-2xl blur-xl"></div>
<div className="relative bg-gradient-to-r from-blue-500/5 to-green-500/5 border border-blue-500/20 rounded-2xl p-8 backdrop-blur-sm">
<h4 className="text-2xl font-bold text-white mb-6 text-center">
Impacto e Crescimento
</h4>
<div className="grid md:grid-cols-3 gap-6">
<div className="text-center">
<div className="text-3xl font-bold text-blue-400 mb-2">
3+
</div>
<div className="text-gray-400 text-sm">
Anos de Experiência
</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-purple-400 mb-2">
2
</div>
<div className="text-gray-400 text-sm">
Empresas Diferentes
</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-green-400 mb-2">
10+
</div>
<div className="text-gray-400 text-sm">
Tecnologias Dominadas
</div>
</div>
</div>
</div>
</div>
</motion.div>
{/* Timeline Desktop (Linha Central) */}
<div className="absolute left-[50%] top-[400px] bottom-0 w-[1px] bg-gradient-to-b from-blue-500/50 via-slate-800 to-transparent hidden lg:block" />
<div className="space-y-12">
<AnimatePresence>
{sortedExperiences.map((exp, index) => (
<ExperienceCard key={exp.id} exp={exp} index={index} />
))}
</AnimatePresence>
</div>
</div>
</section>
);
}
// --- Sub-componente Card (Encapsulamento de Lógica de Transição) ---
function ExperienceCard({ exp, index }: { exp: any, index: number }) {
const isEven = index % 2 === 0;
const config = (CONFIG as any)[exp.company.toLowerCase().replace(/\s+/g, "-")] || CONFIG.default;
return (
<motion.div
// CORREÇÃO DO BUG: viewport com margin e once garante que a animação termine
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3, margin: "0px 0px -100px 0px" }}
transition={{
duration: 0.8,
delay: index * 0.1,
ease: [0.16, 1, 0.3, 1] // Custom Ease-Out para suavidade premium
}}
className={`relative flex flex-col ${isEven ? 'lg:flex-row' : 'lg:flex-row-reverse'} items-center gap-8 lg:gap-16`}
>
{/* Indicador de Data na Timeline */}
<div className="hidden lg:flex flex-1 justify-end items-center">
<div className={`${isEven ? 'text-right' : 'text-left'} flex-1`}>
<span className="text-2xl font-mono font-bold text-slate-800 tracking-tighter group-hover:text-blue-500 transition-colors">
{exp.period.split(' ')[0]}
</span>
</div>
</div>
{/* Card Principal */}
<div className="flex-1 w-full">
<motion.div
whileHover={{ y: -5 }} // Hover sutil sem quebrar o layout
className={`p-8 rounded-3xl bg-slate-900/40 border ${config.border} backdrop-blur-xl relative overflow-hidden group transition-all`}
>
{/* Badge de Empresa */}
<div className="flex flex-wrap justify-between items-start gap-4 mb-6">
<div className="space-y-1">
<h3 className="text-2xl font-bold text-white group-hover:text-blue-400 transition-colors">
{exp.title}
</h3>
<div className="flex items-center gap-2">
<span className={`text-sm font-bold uppercase tracking-widest ${config.color}`}>
{exp.company}
</span>
<span className="text-slate-600">•</span>
<span className="text-xs text-slate-500 font-mono">{exp.type}</span>
</div>
</div>
<div className={`px-4 py-1.5 rounded-full ${config.bg} border ${config.border} text-[10px] font-bold text-white uppercase`}>
{exp.period}
</div>
</div>
<p className="text-slate-400 leading-relaxed mb-8 text-sm">
{exp.description}
</p>
{/* Conquistas (Técnica de Checklist de Engenharia) */}
<div className="grid grid-cols-1 gap-3 mb-8">
{exp.achievements?.map((ach: string, i: number) => (
<div key={i} className="flex gap-3 text-sm text-slate-300 group/item">
<ChevronRight className={`w-4 h-4 mt-0.5 flex-shrink-0 transition-transform group-hover/item:translate-x-1 ${config.color}`} />
<span>{ach}</span>
</div>
))}
</div>
{/* Tags de Tech (Aparência de Chip de Hardware) */}
<div className="flex flex-wrap gap-2 pt-6 border-t border-slate-800/50">
{exp.technologies?.map((tech: string) => (
<span key={tech} className="px-3 py-1 bg-slate-950 border border-slate-800 text-slate-500 text-[10px] rounded-md font-mono hover:text-blue-400 hover:border-blue-400/30 transition-all">
{tech}
</span>
))}
</div>
</motion.div>
</div>
{/* Spacer para Timeline Desktop */}
<div className="hidden lg:block flex-1" />
</motion.div>
);
}
function ExperienceSkeleton() {
return <div className="h-96 flex items-center justify-center text-slate-600 font-mono animate-pulse">Loading_System_Nodes...</div>;
}