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
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user