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

@@ -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
);
}