feat: Add initial portfolio UI components and skill definitions.

This commit is contained in:
Evandro Fonseca Junior committed 2026-03-18 07:00:46 -03:00
1 parent e8d9ef4697
commit 04923ccb2f
15 files changed
+1419 -64

No files matched your search

+5 -5
View File
@@ -5,6 +5,7 @@ import { useState } from "react";
import { motion, type Variants, useReducedMotion } from "framer-motion";
import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
import MagneticButton from "./cinematic/MagneticButton";
const WHATSAPP_NUMBER = "5547992742365";
@@ -200,15 +201,14 @@ export default function Contact() {
/>
</div>
<motion.button
<MagneticButton
as="button"
type="submit"
className="ds-link ds-link-accent min-h-12 border-0"
whileHover={shouldReduceMotion ? undefined : { scale: 1.03 }}
whileTap={shouldReduceMotion ? undefined : { scale: 0.97 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
strength={0.2}
>
Enviar briefing pelo WhatsApp
</motion.button>
</MagneticButton>
</motion.form>
</motion.div>
</Section>
+23 -11
View File
@@ -3,6 +3,7 @@
import { motion } from "motion/react";
import { Mail, MessageCircle } from "lucide-react";
import { usePortfolioData } from "@/lib/usePortfolioData";
import MagneticButton from "./cinematic/MagneticButton";
interface ContactInfoProps {
onWhatsAppClick: () => void;
@@ -28,11 +29,14 @@ export function ContactInfo({ onWhatsAppClick }: ContactInfoProps) {
Entre em Contato
</h3>
<div className="space-y-4">
<a
<MagneticButton
as="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"
className="flex items-center gap-4 p-4 hover:bg-white/5 rounded-lg transition-colors w-full text-left"
wrapperClassName="block w-full"
strength={0.1}
>
<div className="p-3 rounded-full bg-blue-500/20">
<Mail className="w-6 h-6 text-blue-400" />
@@ -41,11 +45,13 @@ export function ContactInfo({ onWhatsAppClick }: ContactInfoProps) {
<p className="text-white font-medium">Email</p>
<p className="text-gray-400">{data.contact.email}</p>
</div>
</a>
</MagneticButton>
<button
<MagneticButton
onClick={onWhatsAppClick}
className="flex items-center gap-4 p-4 hover:bg-white/5 rounded-lg transition-colors w-full text-left"
wrapperClassName="block w-full"
strength={0.1}
>
<div className="p-3 rounded-full bg-green-500/20">
<MessageCircle className="w-6 h-6 text-green-400" />
@@ -54,13 +60,16 @@ export function ContactInfo({ onWhatsAppClick }: ContactInfoProps) {
<p className="text-white font-medium">WhatsApp</p>
<p className="text-gray-400">{data.contact.whatsapp}</p>
</div>
</button>
</MagneticButton>
<a
<MagneticButton
as="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"
className="flex items-center gap-4 p-4 hover:bg-white/5 rounded-lg transition-colors w-full text-left"
wrapperClassName="block w-full"
strength={0.1}
>
<div className="p-3 rounded-full bg-blue-500/20">
<svg
@@ -77,13 +86,16 @@ export function ContactInfo({ onWhatsAppClick }: ContactInfoProps) {
{data.contact.linkedin.replace("https://www.", "")}
</p>
</div>
</a>
</MagneticButton>
<a
<MagneticButton
as="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"
className="flex items-center gap-4 p-4 hover:bg-white/5 rounded-lg transition-colors w-full text-left"
wrapperClassName="block w-full"
strength={0.1}
>
<div className="p-3 rounded-full bg-gray-500/20">
<svg
@@ -100,7 +112,7 @@ export function ContactInfo({ onWhatsAppClick }: ContactInfoProps) {
{data.contact.github.replace("https://github.com/", "")}
</p>
</div>
</a>
</MagneticButton>
</div>
</div>
+4 -3
View File
@@ -1,10 +1,11 @@
"use client";
"use client";
import { useMemo } from "react";
import { motion, type Variants, useReducedMotion } from "framer-motion";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
import MagneticButton from "./cinematic/MagneticButton";
import { SectionLoading } from "./SectionLoading";
export default function Experience() {
@@ -107,9 +108,9 @@ export default function Experience() {
<ul className="ds-tags mt-sp-6">
{exp.technologies?.slice(0, 6).map((tech) => (
<li key={tech} className="ds-tag">
<MagneticButton key={tech} as="li" className="ds-tag" strength={0.15}>
{tech}
</li>
</MagneticButton>
))}
</ul>
</motion.article>
+14 -6
View File
@@ -36,6 +36,8 @@ const quickLinks = [
},
];
import MagneticButton from "./cinematic/MagneticButton";
export default function Footer() {
const currentYear = new Date().getFullYear();
@@ -60,12 +62,14 @@ export default function Footer() {
<p className="mb-2 text-[12px] tracking-[0.02em] text-[var(--meta)]">
E-mail direto
</p>
<a
<MagneticButton
as="a"
href="mailto:[email protected]"
className="inline-block text-[18px] font-bold text-[var(--ink)] transition-colors duration-200 hover:text-[var(--slash)]"
strength={0.2}
>
evandro@dev.br
</a>
</MagneticButton>
</div>
</div>
@@ -88,13 +92,15 @@ export default function Footer() {
<div className="mt-sp-6 flex flex-col gap-sp-4">
{socialLinks.map((link) => (
<a
<MagneticButton
key={link.name}
as="a"
href={link.href}
target="_blank"
rel="noopener noreferrer"
aria-label={link.ariaLabel}
className="group flex items-center justify-between rounded-[8px] border border-[var(--border)] bg-[rgba(10,10,10,0.02)] p-sp-4 transition-all duration-300 hover:border-[var(--slash)] hover:bg-[rgba(10,10,10,0.04)]"
strength={0.15}
>
<div className="flex items-center gap-sp-3">
<svg
@@ -112,7 +118,7 @@ export default function Footer() {
<span className="text-[12px] tracking-[0.08em] text-[var(--meta)] transition-colors group-hover:text-[var(--slash)]">
abrir
</span>
</a>
</MagneticButton>
))}
</div>
@@ -120,13 +126,15 @@ export default function Footer() {
<div className="ds-kicker">Explorar</div>
<div className="mt-sp-4 grid gap-sp-3 md:grid-cols-2">
{quickLinks.map((link) => (
<a
<MagneticButton
key={link.href}
as="a"
href={link.href}
className="rounded-[8px] border border-[var(--border)] bg-[rgba(10,10,10,0.02)] px-sp-4 py-sp-3 text-[13px] leading-6 text-[var(--ink-80)] transition-colors duration-200 hover:border-[var(--slash)] hover:text-[var(--ink)]"
strength={0.15}
>
{link.label}
</a>
</MagneticButton>
))}
</div>
</div>
+17 -8
View File
@@ -2,6 +2,7 @@
import { useState } from "react";
import { Logo } from "./Logo";
import MagneticButton from "./cinematic/MagneticButton";
const navItems = [
{ href: "#home", label: "Home" },
@@ -30,12 +31,14 @@ export function Header() {
<ul className="flex list-none items-center gap-sp-2">
{navItems.map((item) => (
<li key={item.href}>
<a
<MagneticButton
as="a"
href={item.href}
className="ds-link rounded-sm px-sp-3 py-sp-2 text-white/85 transition-colors hover:bg-white/10 hover:text-white"
strength={0.15}
>
{item.label}
</a>
</MagneticButton>
</li>
))}
</ul>
@@ -45,13 +48,15 @@ export function Header() {
<span className="ds-metric hidden rounded-full border border-white/10 px-[10px] py-1 text-white/40 lg:inline-flex">
v1.0 / 2026
</span>
<a
<MagneticButton
as="a"
href="/Profile.pdf"
download="Evandro_Fonseca_Junior_CV.pdf"
className="ds-link ds-link-accent hidden rounded-sm font-semibold transition-shadow hover:shadow-[0_0_0_1px_rgba(232,255,0,.8),0_8px_24px_rgba(232,255,0,.35)] lg:inline-flex"
strength={0.2}
>
Download CV
</a>
</MagneticButton>
<button
type="button"
className="flex h-8 w-8 flex-col items-center justify-center gap-sp-2 border-none bg-transparent lg:hidden"
@@ -80,23 +85,27 @@ export function Header() {
<ul className="flex list-none flex-col gap-sp-2">
{navItems.map((item) => (
<li key={item.href}>
<a
<MagneticButton
as="a"
href={item.href}
className="ds-link block rounded-sm py-sp-3 text-white/85 transition-all hover:pl-sp-2 hover:text-white"
onClick={() => setMobileMenuOpen(false)}
strength={0.15}
>
{item.label}
</a>
</MagneticButton>
</li>
))}
<li>
<a
<MagneticButton
as="a"
href="/Profile.pdf"
download="Evandro_Fonseca_Junior_CV.pdf"
className="ds-link ds-link-accent mt-sp-2 inline-flex rounded-sm font-semibold"
strength={0.15}
>
Download CV
</a>
</MagneticButton>
</li>
</ul>
</nav>
+16 -17
View File
@@ -4,6 +4,7 @@ import { useMemo } from "react";
import { motion, type Variants, useReducedMotion } from "framer-motion";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { Section } from "./Section";
import MagneticButton from "./cinematic/MagneticButton";
import { DrawLine } from "./animations/DrawLine";
import { SectionLoading } from "./SectionLoading";
@@ -91,45 +92,43 @@ export default function Projects() {
</p>
)}
</div>
<span className="ds-tag">{project.status}</span>
<MagneticButton as="span" className="ds-tag" strength={0.15}>{project.status}</MagneticButton>
</div>
<p className="ds-copy">{project.description}</p>
<ul className="ds-tags mt-sp-6">
{project.technologies.slice(0, 5).map((tech: string) => (
<li key={tech} className="ds-tag">
<MagneticButton key={tech} as="li" className="ds-tag" strength={0.15}>
{tech}
</li>
</MagneticButton>
))}
</ul>
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
{project.githubUrl && (
<motion.a
<MagneticButton
as="a"
href={project.githubUrl}
target="_blank"
rel="noreferrer"
className="ds-link ds-link-muted"
whileHover={shouldReduceMotion ? undefined : { scale: 1.03 }}
whileTap={shouldReduceMotion ? undefined : { scale: 0.97 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
strength={0.25}
>
Ver codigo
</motion.a>
</MagneticButton>
)}
{project.demoUrl && (
<motion.a
<MagneticButton
as="a"
href={project.demoUrl}
target="_blank"
rel="noreferrer"
className="ds-link ds-link-accent"
whileHover={shouldReduceMotion ? undefined : { scale: 1.03 }}
whileTap={shouldReduceMotion ? undefined : { scale: 0.97 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
strength={0.25}
>
Abrir projeto
</motion.a>
</MagneticButton>
)}
</div>
</motion.article>
@@ -137,12 +136,12 @@ export default function Projects() {
</motion.div>
<div className="mt-sp-8 flex flex-wrap gap-sp-3">
<a href="/cases" className="ds-link ds-link-accent">
<MagneticButton as="a" href="/cases" className="ds-link ds-link-accent" strength={0.2}>
Explorar todos os cases
</a>
<a href="/servicos" className="ds-link ds-link-muted">
</MagneticButton>
<MagneticButton as="a" href="/servicos" className="ds-link ds-link-muted" strength={0.2}>
Ver servicos relacionados
</a>
</MagneticButton>
</div>
</Section>
);
+4 -3
View File
@@ -1,8 +1,9 @@
"use client";
"use client";
import { motion, type Variants, useReducedMotion } from "framer-motion";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { Section } from "./Section";
import MagneticButton from "./cinematic/MagneticButton";
import { DrawLine } from "./animations/DrawLine";
import { SectionLoading } from "./SectionLoading";
@@ -82,9 +83,9 @@ export default function Skills() {
</p>
<ul className="ds-tags">
{category.skills.map((skill) => (
<li key={skill} className="ds-tag">
<MagneticButton key={skill} as="li" className="ds-tag" strength={0.15}>
{skill}
</li>
</MagneticButton>
))}
</ul>
</motion.article>
+7 -7
View File
@@ -5,6 +5,7 @@ import { motion, type Variants, useReducedMotion } from "framer-motion";
import { serviceHubLinks } from "@/lib/site-content";
import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
import MagneticButton from "./cinematic/MagneticButton";
interface Tier {
id: string;
@@ -147,17 +148,16 @@ function TierCard({ tier }: Readonly<{ tier: Tier }>) {
</div>
</div>
<motion.a
<MagneticButton
as="a"
href={tier.ctaHref}
className={`ds-link mt-sp-6 min-h-[50px] w-full justify-center px-sp-4 py-sp-3 text-center text-[11px] leading-none ${
tier.featured ? "ds-link-accent" : "ds-link-muted"
}`}
whileHover={shouldReduceMotion ? undefined : { scale: 1.02 }}
whileTap={shouldReduceMotion ? undefined : { scale: 0.98 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
strength={0.25}
>
{tier.ctaLabel}
</motion.a>
</MagneticButton>
</motion.article>
);
}
@@ -207,9 +207,9 @@ export function WorkModalities() {
aqui esta o caminho mais direto para a pagina certa.
</p>
</div>
<Link href="/servicos" className="ds-link ds-link-accent">
<MagneticButton as="a" href="/servicos" className="ds-link ds-link-accent" strength={0.2}>
Ver hub de servicos
</Link>
</MagneticButton>
</div>
<div className="mt-sp-6 grid gap-sp-3 md:grid-cols-2 xl:grid-cols-3">
+10 -4
View File
@@ -9,11 +9,13 @@ interface MagneticButtonProps {
href?: string;
onClick?: () => void;
strength?: number;
as?: "a" | "button";
as?: "a" | "button" | "div" | "span" | "li";
target?: string;
rel?: string;
download?: boolean;
download?: boolean | string;
"data-cursor"?: string;
type?: "button" | "submit" | "reset";
wrapperClassName?: string;
}
export default function MagneticButton({
@@ -27,6 +29,8 @@ export default function MagneticButton({
rel,
download,
"data-cursor": dataCursor,
type,
wrapperClassName = "",
}: MagneticButtonProps) {
const ref = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
@@ -49,15 +53,16 @@ export default function MagneticButton({
setPosition({ x: 0, y: 0 });
};
const Tag = as === "a" ? motion.a : motion.button;
const Tag = as === "a" ? motion.a : as === "button" ? motion.button : as === "div" ? motion.div : as === "span" ? motion.span : motion.li;
const linkProps = as === "a" ? { href, target, rel, download } : {};
const buttonProps = as === "button" ? { type } : {};
return (
<div
ref={ref}
onMouseMove={handleMouse}
onMouseLeave={handleMouseLeave}
className="inline-block"
className={`inline-block ${wrapperClassName}`.trim()}
>
<Tag
className={className}
@@ -74,6 +79,7 @@ export default function MagneticButton({
mass: 0.5,
}}
{...linkProps}
{...buttonProps}
>
{children}
</Tag>