feat: Implement initial portfolio website structure with core components and a new post creator tool.

This commit is contained in:
Evandro Fonseca Junior committed 2026-03-07 06:50:56 -03:00
1 parent 036363b521
commit aa07d6faeb
15 files changed
+1337 -1855

No files matched your search

+73 -194
View File
@@ -1,207 +1,86 @@
"use client";
"use client";
import React, { useMemo } from "react";
import { motion } from "motion/react";
import { BentoGrid, BentoGridItem } from "@/components/magicui/bento-grid";
import {
ExternalLink,
Github,
Star,
Zap,
Globe,
Cpu,
Code2,
Terminal,
} from "lucide-react";
import { useMemo } from "react";
import { usePortfolioData } from "@/lib/usePortfolioData";
// --- Configuração de Cores (Harmonizado com a seção anterior) ---
const CATEGORY_THEMES: Record<string, any> = {
"full-stack": { color: "text-blue-400", bg: "bg-blue-500/10", border: "border-blue-500/20", icon: Globe },
iot: { color: "text-purple-400", bg: "bg-purple-500/10", border: "border-purple-500/20", icon: Cpu },
api: { color: "text-orange-400", bg: "bg-orange-500/10", border: "border-orange-500/20", icon: Code2 },
web: { color: "text-green-400", bg: "bg-green-500/10", border: "border-green-500/20", icon: Globe },
desktop: { color: "text-emerald-400", bg: "bg-emerald-500/10", border: "border-emerald-500/20", icon: Terminal },
};
// Funções auxiliares para Schema
function getApplicationCategory(category: string): string {
const map: Record<string, string> = {
"full-stack": "WebApplication",
"iot": "DesktopApplication",
"api": "WebApplication",
"web": "WebApplication",
"desktop": "DesktopApplication"
};
return map[category.toLowerCase()] || "WebApplication";
}
function getOperatingSystem(category: string): string {
if (category.toLowerCase() === "desktop") return "Windows, Linux";
return "Web Browser";
}
import { Section } from "./Section";
export default function Projects() {
const { data, loading, error } = usePortfolioData();
const projects = useMemo(() =>
data?.projects.filter((p) => p.featured) || [],
[data]);
const projectsJsonLd = useMemo(() => {
if (!projects || projects.length === 0) return null;
return {
"@context": "https://schema.org",
"@type": "ItemList",
"itemListElement": projects.map((project, index) => ({
"@type": "ListItem",
"position": index + 1,
"item": {
"@type": "SoftwareApplication",
"name": project.title,
"description": project.description,
"applicationCategory": getApplicationCategory(project.category),
"operatingSystem": getOperatingSystem(project.category),
"url": project.demoUrl || project.githubUrl || `https://evandro.dev.br/#projects`,
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "BRL"
},
"softwareVersion": "1.0.0",
"author": {
"@type": "Person",
"name": "Evandro Fonseca Junior",
"url": "https://evandro.dev.br"
}
}
}))
};
}, [projects]);
if (loading) return <div className="py-20 text-center animate-pulse text-slate-500 font-mono">Loading_Assets...</div>;
if (error) return <div className="py-20 text-center text-red-500">System_Error: Failed to sync projects.</div>;
return (
<section id="projects" className="py-32 px-6 relative">
{/* Background Decor */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-blue-500/5 via-transparent to-transparent pointer-events-none" />
<div className="max-w-7xl mx-auto relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-left mb-20"
>
<span className="text-blue-500 font-mono text-sm tracking-[0.3em] uppercase block mb-4">Produção_Intelectual</span>
<h2 className="text-5xl md:text-7xl font-black text-white tracking-tighter">
Projetos <span className="text-slate-700 italic">Principais</span>
</h2>
</motion.div>
<BentoGrid className="grid-cols-1 md:grid-cols-3 gap-4">
{projects.map((project, index) => {
const theme = CATEGORY_THEMES[project.category.toLowerCase()] || CATEGORY_THEMES.web;
// Lógica de Grid Inteligente:
// Index 0: NutriScan (Destaque - 2 colunas)
// Index 1: Twins Lab (Complemento - 1 coluna)
// Demais: 1 coluna cada (Preenche a linha 2 perfeitamente)
const colSpan = index === 0 ? "md:col-span-2" : "md:col-span-1";
return (
<BentoGridItem
key={project.id}
className={`${colSpan} group relative flex flex-col justify-between overflow-hidden rounded-3xl border border-slate-800 bg-slate-900/40 backdrop-blur-xl p-8 transition-all duration-500 hover:border-blue-500/30 min-h-[22rem]`}
>
{/* Header: Icon & Links */}
<div className="relative z-10">
<div className="flex items-start justify-between mb-8">
<div className="flex items-center gap-4">
<div className={`p-4 rounded-2xl ${theme.bg} border ${theme.border} group-hover:scale-110 transition-transform duration-500`}>
<theme.icon className={`w-6 h-6 ${theme.color}`} />
</div>
<div>
<h3 className="text-2xl font-bold text-white tracking-tight">{project.title}</h3>
<p className="text-sm text-slate-500 font-medium">{project.subtitle}</p>
</div>
</div>
<div className="flex gap-2">
{project.githubUrl && <ProjectLink href={project.githubUrl} icon={Github} />}
{project.demoUrl && <ProjectLink href={project.demoUrl} icon={ExternalLink} highlight />}
</div>
</div>
{/* Badges de Tecnologias */}
<div className="flex flex-wrap gap-2 mb-6">
{project.technologies.slice(0, 5).map((tech: string) => (
<span key={tech} className="px-2 py-1 text-[10px] font-mono font-bold bg-slate-950 text-slate-400 border border-slate-800 rounded-md">
{tech}
</span>
))}
</div>
<p className="text-slate-400 text-sm leading-relaxed mb-6 line-clamp-3 group-hover:line-clamp-none transition-all duration-500">
{project.description}
</p>
</div>
{/* Footer: Metrics & Status */}
<div className="relative z-10 pt-6 border-t border-slate-800/50 mt-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-[10px] uppercase tracking-widest text-slate-600 font-bold">Complexity</span>
<ComplexityStars rating={project.metrics?.complexity || 3} />
</div>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full animate-pulse ${project.status === 'Em Produção' ? 'bg-emerald-500' : 'bg-blue-500'}`} />
<span className="text-[10px] font-bold text-slate-400 uppercase">{project.status}</span>
</div>
</div>
{/* Background Hover Glow */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
</BentoGridItem>
);
})}
</BentoGrid>
{/* JSON-LD Schema for Projects */}
{projectsJsonLd && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(projectsJsonLd) }}
/>
)}
</div>
</section>
const projects = useMemo(
() => data?.projects.filter((p) => p.featured) || [],
[data],
);
}
// --- Subcomponentes Auxiliares ---
if (loading)
return <div className="ds-loading">Loading featured projects</div>;
if (error)
return <div className="ds-error">Error loading featured projects</div>;
function ProjectLink({ href, icon: Icon, highlight }: { href: string; icon: any; highlight?: boolean }) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={`p-2 rounded-xl transition-all duration-300 hover:scale-110 ${highlight ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/20' : 'bg-slate-900 border border-slate-800 text-slate-500 hover:text-white'
}`}
<Section
id="projects"
eyebrow="PRODUCAO_INTELECTUAL"
title="Projetos com recorte técnico claro e impacto observável"
description="Seleção de produtos e ferramentas onde performance, confiabilidade e clareza de arquitetura foram requisitos reais."
dark
>
<Icon className="w-4 h-4" />
</a>
<div className="ds-grid-2">
{projects.map((project) => (
<article key={project.id} className="ds-card">
<div className="flex items-start justify-between gap-sp-6">
<div>
<div className="ds-kicker">{project.category}</div>
<h3 className="ds-card-title">{project.title}</h3>
<p className="mt-sp-2 text-[11px] uppercase tracking-[0.1em] text-[rgba(255,255,255,.36)]">
{project.subtitle}
</p>
</div>
<span className="ds-tag">{project.status}</span>
</div>
<p className="ds-copy">{project.description}</p>
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
{project.githubUrl && (
<a
href={project.githubUrl}
target="_blank"
rel="noreferrer"
className="ds-link ds-link-muted"
>
GitHub
</a>
)}
{project.demoUrl && (
<a
href={project.demoUrl}
target="_blank"
rel="noreferrer"
className="ds-link ds-link-accent"
>
Live demo
</a>
)}
</div>
<ul className="ds-tags mt-sp-6">
{project.technologies.slice(0, 6).map((tech: string) => (
<li key={tech} className="ds-tag">
{tech}
</li>
))}
</ul>
<div className="mt-sp-6 flex flex-wrap gap-sp-4">
<span className="ds-metric">
Complexity {project.metrics?.complexity || 3}/5
</span>
{project.metrics?.teamSize && (
<span className="ds-metric">
Team {project.metrics.teamSize}
</span>
)}
</div>
</article>
))}
</div>
</Section>
);
}
function ComplexityStars({ rating }: { rating: number }) {
return (
<div className="flex gap-1">
{[...Array(5)].map((_, i) => (
<div key={i} className={`w-1.5 h-1.5 rounded-full ${i < rating ? 'bg-yellow-500' : 'bg-slate-800'}`} />
))}
</div>
);
}