feat: Introduce new AI skills, core application layout, and cinematic UI components for the portfolio.
This commit is contained in:
1 parent
04923ccb2f
commit
a3ffc2a25e
26 files changed
+5020
-34
No files matched your search
+15
-12
@@ -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>
|
||||
|
||||
@@ -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)",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user