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:
1 parent
375862a65a
commit
8987d06eb4
5 files changed
+606
-1048
No files matched your search
+137
-307
@@ -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>;
|
||||
}
|
||||
Reference in new issue
Block a user