feat: Implement initial portfolio website structure with core components and a new post creator tool.
This commit is contained in:
1 parent
036363b521
commit
aa07d6faeb
15 files changed
+1337
-1855
No files matched your search
+73
-194
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user