feat: Introduce new AI skills, core application layout, and cinematic UI components for the portfolio.

This commit is contained in:
Evandro Fonseca Junior committed 2026-03-18 07:15:39 -03:00
1 parent 04923ccb2f
commit a3ffc2a25e
26 files changed
+5020 -34

No files matched your search

+15 -12
View File
@@ -1,6 +1,8 @@
import type { Metadata } from "next";
import Link from "next/link";
import { caseStudies, siteName, siteUrl } from "@/lib/site-content";
import HeroBackground from "@/app/components/cinematic/HeroBackground";
import MagneticButton from "@/app/components/cinematic/MagneticButton";
export const metadata: Metadata = {
title: `Cases de Engenharia de Software, Next.js e IoT | ${siteName}`,
@@ -50,8 +52,9 @@ export default function CasesPage() {
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section className="ds-section ds-section-dark">
<div className="ds-section-shell">
<section className="ds-section ds-section-dark relative min-h-[340px] overflow-hidden">
<HeroBackground />
<div className="ds-section-shell relative z-10">
<div className="max-w-[860px]">
<p className="ds-eyebrow">Selected work</p>
<h1 className="ds-section-title max-w-[14ch] text-balance text-white">
@@ -78,21 +81,21 @@ export default function CasesPage() {
</p>
<ul className="ds-tags mt-sp-6">
{caseStudy.stack.map((item) => (
<li key={item} className="ds-tag">
<MagneticButton key={item} as="li" className="ds-tag" strength={0.15}>
{item}
</li>
</MagneticButton>
))}
</ul>
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
{caseStudy.demoUrl ? (
<a href={caseStudy.demoUrl} className="ds-link ds-link-accent" target="_blank" rel="noreferrer">
<MagneticButton as="a" href={caseStudy.demoUrl} className="ds-link ds-link-accent" target="_blank" rel="noreferrer" strength={0.25}>
Abrir projeto
</a>
</MagneticButton>
) : null}
{caseStudy.githubUrl ? (
<a href={caseStudy.githubUrl} className="ds-link ds-link-muted" target="_blank" rel="noreferrer">
<MagneticButton as="a" href={caseStudy.githubUrl} className="ds-link ds-link-muted" target="_blank" rel="noreferrer" strength={0.2}>
Ver codigo
</a>
</MagneticButton>
) : null}
</div>
</article>
@@ -100,12 +103,12 @@ export default function CasesPage() {
</div>
<div className="mt-sp-10 flex flex-wrap gap-sp-3">
<Link href="/servicos" className="ds-link ds-link-accent">
<MagneticButton as="a" href="/servicos" className="ds-link ds-link-accent" strength={0.25}>
Ver servicos
</Link>
<Link href="/#contact" className="ds-link ds-link-muted">
</MagneticButton>
<MagneticButton as="a" href="/#contact" className="ds-link ds-link-muted" strength={0.2}>
Ir para contato
</Link>
</MagneticButton>
</div>
</div>
</section>
+2 -1
View File
@@ -130,7 +130,8 @@ export default function Footer() {
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)]"
className="block 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)]"
wrapperClassName="block"
strength={0.15}
>
{link.label}
@@ -6,6 +6,7 @@ const CustomCursor = dynamic(() => import("./CustomCursor"), { ssr: false });
const FilmGrain = dynamic(() => import("./FilmGrain"), { ssr: false });
const PageIntro = dynamic(() => import("./PageIntro"), { ssr: false });
const ScrollProgress = dynamic(() => import("./ScrollProgress"), { ssr: false });
const HomeButton = dynamic(() => import("./HomeButton"), { ssr: false });
export default function CinematicOverlays() {
return (
@@ -14,6 +15,7 @@ export default function CinematicOverlays() {
<CustomCursor />
<FilmGrain />
<ScrollProgress />
<HomeButton />
</>
);
}
@@ -0,0 +1,38 @@
"use client";
import dynamic from "next/dynamic";
import { useReducedMotion } from "framer-motion";
const HeroScene = dynamic(() => import("./HeroScene"), { ssr: false });
/**
* Reusable 3D animated background for internal hero sections.
* Drop this inside any `relative overflow-hidden` dark section.
*/
export default function HeroBackground() {
const shouldReduceMotion = useReducedMotion();
if (shouldReduceMotion) return null;
return (
<>
{/* 3D particle scene */}
<HeroScene />
{/* Cinematic radial vignette */}
<div className="pointer-events-none absolute inset-0 z-[1] bg-[radial-gradient(ellipse_80%_60%_at_50%_40%,transparent_0%,rgba(10,10,10,0.5)_55%,var(--ink)_100%)]" />
{/* Gradient fade to bottom */}
<div className="pointer-events-none absolute inset-0 z-[1] bg-gradient-to-b from-transparent via-transparent to-[var(--ink)]" />
{/* Scanlines */}
<div
className="pointer-events-none absolute inset-0 z-[2] opacity-[0.012]"
style={{
backgroundImage:
"repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.03) 2px, rgba(255,255,255,0.03) 4px)",
}}
/>
</>
);
}
+66
View File
@@ -0,0 +1,66 @@
"use client";
import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import MagneticButton from "./MagneticButton";
export default function HomeButton() {
const pathname = usePathname();
const isHome = pathname === "/";
return (
<AnimatePresence>
{!isHome && (
<motion.div
className="fixed left-6 top-6 z-[9000]"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1], delay: 0.1 }}
>
<MagneticButton
as="a"
href="/"
strength={0.3}
aria-label="Voltar para o início"
className="group relative inline-flex items-center gap-2.5 overflow-hidden rounded-full border border-[rgba(255,255,255,0.1)] bg-[rgba(10,10,10,0.72)] px-4 py-2.5 text-[11px] font-mono uppercase tracking-[0.1em] text-[rgba(244,242,237,0.76)] backdrop-blur-[16px] transition-all duration-300 hover:border-[rgba(204,255,0,0.5)] hover:text-[var(--slash)]"
>
{/* Glow effect */}
<span
className="pointer-events-none absolute inset-0 rounded-full opacity-0 transition-opacity duration-300 group-hover:opacity-100"
style={{
boxShadow: "inset 0 0 20px rgba(204,255,0,0.06), 0 0 24px rgba(204,255,0,0.15)",
}}
/>
{/* Arrow left icon */}
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="relative z-10 -translate-x-0 transition-transform duration-300 group-hover:-translate-x-0.5"
>
<path d="M7.5 1.5 L2.5 6 L7.5 10.5" />
</svg>
<span className="relative z-10">Home</span>
{/* Animated neon dot */}
<motion.span
className="relative z-10 ml-0.5 h-1.5 w-1.5 rounded-full bg-[var(--slash)]"
animate={{ opacity: [1, 0.3, 1], scale: [1, 0.8, 1] }}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
/>
</MagneticButton>
</motion.div>
)}
</AnimatePresence>
);
}
@@ -16,6 +16,7 @@ interface MagneticButtonProps {
"data-cursor"?: string;
type?: "button" | "submit" | "reset";
wrapperClassName?: string;
"aria-label"?: string;
}
export default function MagneticButton({
@@ -31,6 +32,7 @@ export default function MagneticButton({
"data-cursor": dataCursor,
type,
wrapperClassName = "",
"aria-label": ariaLabel,
}: MagneticButtonProps) {
const ref = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
@@ -80,6 +82,7 @@ export default function MagneticButton({
}}
{...linkProps}
{...buttonProps}
aria-label={ariaLabel}
>
{children}
</Tag>
+20 -4
View File
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { createPortal } from "react-dom";
import { motion, AnimatePresence } from "framer-motion";
const INTRO_LINES = [
@@ -12,8 +13,18 @@ const INTRO_LINES = [
export default function PageIntro() {
const [isVisible, setIsVisible] = useState(true);
const [counter, setCounter] = useState(0);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
// Remove the immediate blocking overlay now that the animated loader is ready
const blocker = document.getElementById("__loading-blocker");
if (blocker) blocker.remove();
}, []);
useEffect(() => {
if (!mounted) return;
// Animate counter from 0 to 100
const duration = 2000;
const start = performance.now();
@@ -34,13 +45,16 @@ export default function PageIntro() {
};
requestAnimationFrame(tick);
}, []);
}, [mounted]);
return (
if (!mounted) return null;
return createPortal(
<AnimatePresence>
{isVisible && (
<motion.div
className="fixed inset-0 z-[10000] flex items-center justify-center bg-[#0a0a0a]"
className="fixed inset-0 z-[99999] flex items-center justify-center bg-[#0a0a0a]"
style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, width: "100vw", height: "100vh" }}
exit={{
clipPath: "inset(0 0 100% 0)",
transition: { duration: 1, ease: [0.76, 0, 0.24, 1] },
@@ -156,6 +170,8 @@ export default function PageIntro() {
</div>
</motion.div>
)}
</AnimatePresence>
</AnimatePresence>,
document.body
);
}
+45
View File
@@ -244,6 +244,20 @@ textarea {
white-space: normal;
word-break: normal;
overflow-wrap: break-word;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
transform 0.2s ease;
}
.ds-tag:hover {
border-color: rgba(10, 10, 10, 0.3);
box-shadow: 0 2px 12px rgba(10, 10, 10, 0.1);
}
.ds-section-dark .ds-tag:hover {
border-color: rgba(204, 255, 0, 0.3);
box-shadow: 0 2px 16px rgba(204, 255, 0, 0.1);
}
.ds-metric {
@@ -278,11 +292,42 @@ textarea {
color: var(--ink);
background: var(--slash);
padding: 12px 18px;
transition:
transform 0.25s ease,
box-shadow 0.25s ease,
background 0.25s ease;
}
.ds-link-accent:hover {
background: #ffffff;
box-shadow:
0 0 20px rgba(204, 255, 0, 0.45),
0 0 48px rgba(204, 255, 0, 0.18);
}
.ds-section-dark .ds-link-accent:hover {
box-shadow:
0 0 24px rgba(204, 255, 0, 0.55),
0 0 60px rgba(204, 255, 0, 0.22);
}
.ds-link-muted {
padding: 12px 18px;
border: 1px solid var(--border);
transition:
transform 0.25s ease,
box-shadow 0.25s ease,
border-color 0.25s ease;
}
.ds-link-muted:hover {
border-color: var(--ink-40);
box-shadow: 0 4px 20px rgba(10, 10, 10, 0.12);
}
.ds-section-dark .ds-link-muted:hover {
border-color: rgba(204, 255, 0, 0.35);
box-shadow: 0 4px 20px rgba(204, 255, 0, 0.08);
}
.ds-list {
+13
View File
@@ -242,6 +242,19 @@ export default function RootLayout({ children }: Readonly<RootLayoutProps>) {
>
<head>
<link rel="canonical" href={siteUrl} />
{/* Blocking script: creates immediate black overlay BEFORE any paint.
This prevents the flash of real content before React hydrates
and PageIntro mounts. PageIntro removes the #__loading-blocker div. */}
<script
dangerouslySetInnerHTML={{
__html: `(function(){
var el=document.createElement('div');
el.id='__loading-blocker';
el.style.cssText='position:fixed;inset:0;background:#0a0a0a;z-index:99998;pointer-events:none;';
document.documentElement.appendChild(el);
})();`,
}}
/>
</head>
<body>
<SmoothScrollProvider>
+16 -13
View File
@@ -8,6 +8,8 @@ import {
siteName,
siteUrl,
} from "@/lib/site-content";
import HeroBackground from "@/app/components/cinematic/HeroBackground";
import MagneticButton from "@/app/components/cinematic/MagneticButton";
interface ServicePageProps {
params: Promise<{
@@ -124,8 +126,9 @@ export default async function ServicePage({ params }: ServicePageProps) {
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section className="ds-section ds-section-dark">
<div className="ds-section-shell">
<section className="ds-section ds-section-dark relative min-h-[340px] overflow-hidden">
<HeroBackground />
<div className="ds-section-shell relative z-10">
<div className="max-w-[860px]">
<p className="ds-eyebrow">{service.eyebrow}</p>
<h1 className="ds-section-title max-w-[14ch] text-balance text-white">
@@ -135,12 +138,12 @@ export default async function ServicePage({ params }: ServicePageProps) {
{service.intro}
</p>
<div className="mt-sp-8 flex flex-wrap gap-sp-3">
<a href={service.ctaHref} className="ds-link ds-link-accent">
<MagneticButton as="a" href={service.ctaHref} className="ds-link ds-link-accent" strength={0.25}>
{service.ctaLabel}
</a>
<Link href="/cases" className="ds-link ds-link-muted">
</MagneticButton>
<MagneticButton as="a" href="/cases" className="ds-link ds-link-muted" strength={0.2}>
Ver casos relacionados
</Link>
</MagneticButton>
</div>
</div>
</div>
@@ -178,7 +181,7 @@ export default async function ServicePage({ params }: ServicePageProps) {
<article className="ds-card">
<div className="ds-kicker">Como eu estruturo</div>
<h2 className="ds-card-title">Processo de trabalho</h2>
<ol className="mt-sp-5 space-y-sp-4 text-[14px] leading-8 text-[var(--ink-80)]">
<ol className="list-none mt-sp-5 space-y-sp-4 text-[14px] leading-8 text-[var(--ink-80)]">
{service.process.map((step, index) => (
<li key={step} className="border-t border-[var(--border)] pt-sp-4 first:border-t-0 first:pt-0">
<span className="mr-sp-3 inline-flex h-7 min-w-7 items-center justify-center rounded-full border border-[var(--border)] text-[11px] font-semibold text-[var(--meta)]">
@@ -199,9 +202,9 @@ export default async function ServicePage({ params }: ServicePageProps) {
</p>
<ul className="ds-tags">
{service.stack.map((item) => (
<li key={item} className="ds-tag">
<MagneticButton key={item} as="li" className="ds-tag" strength={0.15}>
{item}
</li>
</MagneticButton>
))}
</ul>
</article>
@@ -267,12 +270,12 @@ export default async function ServicePage({ params }: ServicePageProps) {
indico o caminho mais seguro para avancar.
</p>
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
<a href={service.ctaHref} className="ds-link ds-link-accent">
<MagneticButton as="a" href={service.ctaHref} className="ds-link ds-link-accent" strength={0.25}>
{service.ctaLabel}
</a>
<Link href="/servicos" className="ds-link ds-link-muted">
</MagneticButton>
<MagneticButton as="a" href="/servicos" className="ds-link ds-link-muted" strength={0.2}>
Ver todos os servicos
</Link>
</MagneticButton>
</div>
</div>
</div>
+7 -4
View File
@@ -1,6 +1,8 @@
import Link from "next/link";
import type { Metadata } from "next";
import { serviceHubLinks, servicePages, siteName, siteUrl } from "@/lib/site-content";
import HeroBackground from "@/app/components/cinematic/HeroBackground";
import MagneticButton from "@/app/components/cinematic/MagneticButton";
export const metadata: Metadata = {
title: `Servicos de Engenharia de Software e Consultoria Tecnica | ${siteName}`,
@@ -50,8 +52,9 @@ export default function ServicesIndexPage() {
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section className="ds-section ds-section-dark">
<div className="ds-section-shell">
<section className="ds-section ds-section-dark relative min-h-[340px] overflow-hidden">
<HeroBackground />
<div className="ds-section-shell relative z-10">
<div className="max-w-[860px]">
<p className="ds-eyebrow">Hub de servicos</p>
<h1 className="ds-section-title max-w-[14ch] text-balance text-white">
@@ -74,9 +77,9 @@ export default function ServicesIndexPage() {
<h2 className="ds-card-title">{service.label}</h2>
<p className="ds-copy">{service.description}</p>
<div className="mt-sp-6">
<Link href={service.href} className="ds-link ds-link-accent">
<MagneticButton as="a" href={service.href} className="ds-link ds-link-accent" strength={0.25}>
Abrir pagina
</Link>
</MagneticButton>
</div>
</article>
))}