feat: set up basic project structure and initial styling

This commit initializes the project with a basic Next.js setup, including:
- Integration of `tailwindcss` and `tw-animate-css` for styling.
- Definition of a comprehensive color palette using CSS variables for both light and dark themes, supporting a theming system.
- Implementation of a custom `aurora` keyframe animation.
- Configuration of global CSS styles for `border` and `body` elements.
- Setup of `next.config.ts` with image optimization, performance enhancements, and security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy).
- Update of `package.json` to include new dependencies for UI components (`class-variance-authority`, `clsx`, `lucide-react`, `motion`, `tailwind-merge`, `tailwindcss-animate`) and development tools (`@biomejs/biome`, `@tailwindcss/postcss`, `tw-animate-css`).
- Refinement of `app/layout.tsx` to include comprehensive metadata for SEO (title, description, keywords, authors, Open Graph, Twitter cards, robots), preconnect/dns-prefetch links for external resources, favicon, manifest, and mobile app meta tags.
- Restructuring of `app/page.tsx` to use dynamic imports for various sections (Hero, About, Skills, Experience, Projects, Contact, WorkModalities) to improve performance, and integration of `Squares` component for background effects.

This foundational commit prepares the application for further development by establishing a robust and performant base.
This commit is contained in:
¨evandrodevbr¨ committed 2025-09-02 22:30:47 -03:00
1 parent ca9e70a53e
commit 90cf3c73ad
37 files changed
+3898 -118

No files matched your search

@@ -0,0 +1,36 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface AnimatedGradientTextProps {
children: React.ReactNode;
className?: string;
}
export function AnimatedGradientText({
children,
className,
}: AnimatedGradientTextProps) {
return (
<motion.span
className={cn(
"bg-gradient-to-r from-cyan-400 via-blue-500 to-blue-600 bg-clip-text text-transparent font-bold",
className
)}
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
}}
style={{
backgroundSize: "200% 200%",
}}
>
{children}
</motion.span>
);
}
+33
View File
@@ -0,0 +1,33 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface AuroraTextProps {
children: React.ReactNode;
className?: string;
}
export function AuroraText({ children, className }: AuroraTextProps) {
return (
<motion.span
className={cn(
"bg-gradient-to-r from-blue-400 via-cyan-500 to-blue-500 bg-clip-text text-transparent",
className
)}
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "linear",
}}
style={{
backgroundSize: "200% 200%",
}}
>
{children}
</motion.span>
);
}
+54
View File
@@ -0,0 +1,54 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface BentoGridProps {
children: React.ReactNode;
className?: string;
}
interface BentoGridItemProps {
children: React.ReactNode;
className?: string;
colSpan?: number;
rowSpan?: number;
}
export function BentoGrid({ children, className }: BentoGridProps) {
return (
<div
className={cn(
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl mx-auto",
className
)}
>
{children}
</div>
);
}
export function BentoGridItem({
children,
className,
colSpan = 1,
rowSpan = 1,
}: BentoGridItemProps) {
return (
<motion.div
className={cn(
"relative overflow-hidden rounded-lg border border-white/20 bg-white/5 backdrop-blur-sm p-6",
`col-span-${colSpan} row-span-${rowSpan}`,
className
)}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
whileHover={{ scale: 1.02 }}
>
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 via-blue-400/10 to-cyan-500/10 opacity-0 hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10">{children}</div>
</motion.div>
);
}
+153
View File
@@ -0,0 +1,153 @@
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import {
motion,
MotionValue,
useMotionValue,
useSpring,
useTransform,
} from "motion/react";
import type { MotionProps } from "motion/react";
import React, { PropsWithChildren, useRef } from "react";
import { cn } from "@/lib/utils";
export interface DockProps extends VariantProps<typeof dockVariants> {
className?: string;
iconSize?: number;
iconMagnification?: number;
disableMagnification?: boolean;
iconDistance?: number;
direction?: "top" | "middle" | "bottom";
children: React.ReactNode;
}
const DEFAULT_SIZE = 40;
const DEFAULT_MAGNIFICATION = 60;
const DEFAULT_DISTANCE = 140;
const DEFAULT_DISABLEMAGNIFICATION = false;
const dockVariants = cva(
"supports-backdrop-blur:bg-white/10 supports-backdrop-blur:dark:bg-black/10 mx-auto mt-8 flex h-[58px] w-max items-center justify-center gap-2 rounded-2xl border p-2 backdrop-blur-md",
);
const Dock = React.forwardRef<HTMLDivElement, DockProps>(
(
{
className,
children,
iconSize = DEFAULT_SIZE,
iconMagnification = DEFAULT_MAGNIFICATION,
disableMagnification = DEFAULT_DISABLEMAGNIFICATION,
iconDistance = DEFAULT_DISTANCE,
direction = "middle",
...props
},
ref,
) => {
const mouseX = useMotionValue(Infinity);
const renderChildren = () => {
return React.Children.map(children, (child) => {
if (
React.isValidElement<DockIconProps>(child) &&
child.type === DockIcon
) {
return React.cloneElement(child, {
...child.props,
mouseX: mouseX,
size: iconSize,
magnification: iconMagnification,
disableMagnification: disableMagnification,
distance: iconDistance,
});
}
return child;
});
};
return (
<motion.div
ref={ref}
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
{...props}
className={cn(dockVariants({ className }), {
"items-start": direction === "top",
"items-center": direction === "middle",
"items-end": direction === "bottom",
})}
>
{renderChildren()}
</motion.div>
);
},
);
Dock.displayName = "Dock";
export interface DockIconProps
extends Omit<MotionProps & React.HTMLAttributes<HTMLDivElement>, "children"> {
size?: number;
magnification?: number;
disableMagnification?: boolean;
distance?: number;
mouseX?: MotionValue<number>;
className?: string;
children?: React.ReactNode;
props?: PropsWithChildren;
}
const DockIcon = ({
size = DEFAULT_SIZE,
magnification = DEFAULT_MAGNIFICATION,
disableMagnification,
distance = DEFAULT_DISTANCE,
mouseX,
className,
children,
...props
}: DockIconProps) => {
const ref = useRef<HTMLDivElement>(null);
const padding = Math.max(6, size * 0.2);
const defaultMouseX = useMotionValue(Infinity);
const distanceCalc = useTransform(mouseX ?? defaultMouseX, (val: number) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});
const targetSize = disableMagnification ? size : magnification;
const sizeTransform = useTransform(
distanceCalc,
[-distance, 0, distance],
[size, targetSize, size],
);
const scaleSize = useSpring(sizeTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
return (
<motion.div
ref={ref}
style={{ width: scaleSize, height: scaleSize, padding }}
className={cn(
"flex aspect-square cursor-pointer items-center justify-center rounded-full",
disableMagnification && "transition-colors hover:bg-muted-foreground",
className,
)}
{...props}
>
<div>{children}</div>
</motion.div>
);
};
DockIcon.displayName = "DockIcon";
export { Dock, DockIcon, dockVariants };
+158
View File
@@ -0,0 +1,158 @@
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import React, { useEffect, useId, useRef, useState } from "react";
/**
* DotPattern Component Props
*
* @param {number} [width=16] - The horizontal spacing between dots
* @param {number} [height=16] - The vertical spacing between dots
* @param {number} [x=0] - The x-offset of the entire pattern
* @param {number} [y=0] - The y-offset of the entire pattern
* @param {number} [cx=1] - The x-offset of individual dots
* @param {number} [cy=1] - The y-offset of individual dots
* @param {number} [cr=1] - The radius of each dot
* @param {string} [className] - Additional CSS classes to apply to the SVG container
* @param {boolean} [glow=false] - Whether dots should have a glowing animation effect
*/
interface DotPatternProps extends React.SVGProps<SVGSVGElement> {
width?: number;
height?: number;
x?: number;
y?: number;
cx?: number;
cy?: number;
cr?: number;
className?: string;
glow?: boolean;
[key: string]: unknown;
}
/**
* DotPattern Component
*
* A React component that creates an animated or static dot pattern background using SVG.
* The pattern automatically adjusts to fill its container and can optionally display glowing dots.
*
* @component
*
* @see DotPatternProps for the props interface.
*
* @example
* // Basic usage
* <DotPattern />
*
* // With glowing effect and custom spacing
* <DotPattern
* width={20}
* height={20}
* glow={true}
* className="opacity-50"
* />
*
* @notes
* - The component is client-side only ("use client")
* - Automatically responds to container size changes
* - When glow is enabled, dots will animate with random delays and durations
* - Uses Motion for animations
* - Dots color can be controlled via the text color utility classes
*/
export function DotPattern({
width = 16,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
className,
glow = false,
...props
}: DotPatternProps) {
const id = useId();
const containerRef = useRef<SVGSVGElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
const { width, height } = containerRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
};
updateDimensions();
window.addEventListener("resize", updateDimensions);
return () => window.removeEventListener("resize", updateDimensions);
}, []);
const dots = Array.from(
{
length:
Math.ceil(dimensions.width / width) *
Math.ceil(dimensions.height / height),
},
(_, i) => {
const col = i % Math.ceil(dimensions.width / width);
const row = Math.floor(i / Math.ceil(dimensions.width / width));
return {
x: col * width + cx,
y: row * height + cy,
delay: Math.random() * 5,
duration: Math.random() * 3 + 2,
};
},
);
return (
<svg
ref={containerRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full",
className,
)}
{...props}
>
<defs>
<radialGradient id={`${id}-gradient`}>
<stop offset="0%" stopColor="currentColor" stopOpacity="1" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</radialGradient>
</defs>
{dots.map((dot, index) => (
<motion.circle
key={`${dot.x}-${dot.y}`}
cx={dot.x}
cy={dot.y}
r={cr}
fill={glow ? `url(#${id}-gradient)` : "currentColor"}
className="text-neutral-400/80"
initial={glow ? { opacity: 0.4, scale: 1 } : {}}
animate={
glow
? {
opacity: [0.4, 1, 0.4],
scale: [1, 1.5, 1],
}
: {}
}
transition={
glow
? {
duration: dot.duration,
repeat: Infinity,
repeatType: "reverse",
delay: dot.delay,
ease: "easeInOut",
}
: {}
}
/>
))}
</svg>
);
}
+128
View File
@@ -0,0 +1,128 @@
"use client";
import createGlobe, { COBEOptions } from "cobe";
import { useMotionValue, useSpring } from "motion/react";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
const MOVEMENT_DAMPING = 1400;
const GLOBE_CONFIG: COBEOptions = {
width: 800,
height: 800,
onRender: () => {},
devicePixelRatio: 2,
phi: 0,
theta: 0.3,
dark: 0,
diffuse: 0.4,
mapSamples: 16000,
mapBrightness: 1.2,
baseColor: [1, 1, 1],
markerColor: [251 / 255, 100 / 255, 21 / 255],
glowColor: [1, 1, 1],
markers: [
{ location: [14.5995, 120.9842], size: 0.03 },
{ location: [19.076, 72.8777], size: 0.1 },
{ location: [23.8103, 90.4125], size: 0.05 },
{ location: [30.0444, 31.2357], size: 0.07 },
{ location: [39.9042, 116.4074], size: 0.08 },
{ location: [-23.5505, -46.6333], size: 0.1 },
{ location: [19.4326, -99.1332], size: 0.1 },
{ location: [40.7128, -74.006], size: 0.1 },
{ location: [34.6937, 135.5022], size: 0.05 },
{ location: [41.0082, 28.9784], size: 0.06 },
],
};
export function Globe({
className,
config = GLOBE_CONFIG,
}: {
className?: string;
config?: COBEOptions;
}) {
let phi = 0;
let width = 0;
const canvasRef = useRef<HTMLCanvasElement>(null);
const pointerInteracting = useRef<number | null>(null);
const pointerInteractionMovement = useRef(0);
const r = useMotionValue(0);
const rs = useSpring(r, {
mass: 1,
damping: 30,
stiffness: 100,
});
const updatePointerInteraction = (value: number | null) => {
pointerInteracting.current = value;
if (canvasRef.current) {
canvasRef.current.style.cursor = value !== null ? "grabbing" : "grab";
}
};
const updateMovement = (clientX: number) => {
if (pointerInteracting.current !== null) {
const delta = clientX - pointerInteracting.current;
pointerInteractionMovement.current = delta;
r.set(r.get() + delta / MOVEMENT_DAMPING);
}
};
useEffect(() => {
const onResize = () => {
if (canvasRef.current) {
width = canvasRef.current.offsetWidth;
}
};
window.addEventListener("resize", onResize);
onResize();
const globe = createGlobe(canvasRef.current!, {
...config,
width: width * 2,
height: width * 2,
onRender: (state) => {
if (!pointerInteracting.current) phi += 0.005;
state.phi = phi + rs.get();
state.width = width * 2;
state.height = width * 2;
},
});
setTimeout(() => (canvasRef.current!.style.opacity = "1"), 0);
return () => {
globe.destroy();
window.removeEventListener("resize", onResize);
};
}, [rs, config]);
return (
<div
className={cn(
"absolute inset-0 mx-auto aspect-[1/1] w-full max-w-[600px]",
className,
)}
>
<canvas
className={cn(
"size-full opacity-0 transition-opacity duration-500 [contain:layout_paint_size]",
)}
ref={canvasRef}
onPointerDown={(e) => {
pointerInteracting.current = e.clientX;
updatePointerInteraction(e.clientX);
}}
onPointerUp={() => updatePointerInteraction(null)}
onPointerOut={() => updatePointerInteraction(null)}
onMouseMove={(e) => updateMovement(e.clientX)}
onTouchMove={(e) =>
e.touches[0] && updateMovement(e.touches[0].clientX)
}
/>
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface MagicCardProps {
children: React.ReactNode;
className?: string;
}
export function MagicCard({ children, className }: MagicCardProps) {
return (
<motion.div
className={cn(
"relative overflow-hidden rounded-lg border border-white/20 bg-white/5 backdrop-blur-sm",
className
)}
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full animate-shimmer" />
{children}
</motion.div>
);
}
+31
View File
@@ -0,0 +1,31 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface ShimmerButtonProps {
children: React.ReactNode;
className?: string;
onClick?: () => void;
}
export function ShimmerButton({
children,
className,
onClick,
}: ShimmerButtonProps) {
return (
<motion.button
className={cn(
"relative overflow-hidden rounded-lg bg-gradient-to-r from-cyan-500 to-blue-600 px-6 py-3 font-semibold text-white shadow-lg",
className
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={onClick}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full animate-shimmer" />
<span className="relative z-10">{children}</span>
</motion.button>
);
}
+181
View File
@@ -0,0 +1,181 @@
"use client";
import { useRef, useEffect, useState } from "react";
interface SquaresProps {
direction?: "right" | "left" | "up" | "down" | "diagonal";
speed?: number;
borderColor?: string;
squareSize?: number;
hoverFillColor?: string;
className?: string;
}
export function Squares({
direction = "right",
speed = 1,
borderColor = "#333",
squareSize = 40,
hoverFillColor = "#222",
className,
}: SquaresProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const requestRef = useRef<number | undefined>(undefined);
const numSquaresX = useRef<number | undefined>(undefined);
const numSquaresY = useRef<number | undefined>(undefined);
const gridOffset = useRef({ x: 0, y: 0 });
const [hoveredSquare, setHoveredSquare] = useState<{
x: number;
y: number;
} | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Set canvas background
canvas.style.background = "#060606";
const resizeCanvas = () => {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
numSquaresX.current = Math.ceil(canvas.width / squareSize) + 1;
numSquaresY.current = Math.ceil(canvas.height / squareSize) + 1;
};
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
const drawGrid = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;
ctx.lineWidth = 0.5;
for (let x = startX; x < canvas.width + squareSize; x += squareSize) {
for (let y = startY; y < canvas.height + squareSize; y += squareSize) {
const squareX = x - (gridOffset.current.x % squareSize);
const squareY = y - (gridOffset.current.y % squareSize);
if (
hoveredSquare &&
Math.floor((x - startX) / squareSize) === hoveredSquare.x &&
Math.floor((y - startY) / squareSize) === hoveredSquare.y
) {
ctx.fillStyle = hoverFillColor;
ctx.fillRect(squareX, squareY, squareSize, squareSize);
}
ctx.strokeStyle = borderColor;
ctx.strokeRect(squareX, squareY, squareSize, squareSize);
}
}
const gradient = ctx.createRadialGradient(
canvas.width / 2,
canvas.height / 2,
0,
canvas.width / 2,
canvas.height / 2,
Math.sqrt(Math.pow(canvas.width, 2) + Math.pow(canvas.height, 2)) / 2
);
gradient.addColorStop(0, "rgba(6, 6, 6, 0)");
gradient.addColorStop(1, "#060606");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
const updateAnimation = () => {
const effectiveSpeed = Math.max(speed, 0.1);
switch (direction) {
case "right":
gridOffset.current.x =
(gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
break;
case "left":
gridOffset.current.x =
(gridOffset.current.x + effectiveSpeed + squareSize) % squareSize;
break;
case "up":
gridOffset.current.y =
(gridOffset.current.y + effectiveSpeed + squareSize) % squareSize;
break;
case "down":
gridOffset.current.y =
(gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
break;
case "diagonal":
gridOffset.current.x =
(gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
gridOffset.current.y =
(gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
break;
}
drawGrid();
requestRef.current = requestAnimationFrame(updateAnimation);
};
const handleMouseMove = (event: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;
const hoveredSquareX = Math.floor(
(mouseX + gridOffset.current.x - startX) / squareSize
);
const hoveredSquareY = Math.floor(
(mouseY + gridOffset.current.y - startY) / squareSize
);
setHoveredSquare({ x: hoveredSquareX, y: hoveredSquareY });
};
const handleMouseLeave = () => {
setHoveredSquare(null);
};
// Event listeners
window.addEventListener("resize", resizeCanvas);
canvas.addEventListener("mousemove", handleMouseMove);
canvas.addEventListener("mouseleave", handleMouseLeave);
// Initial setup
resizeCanvas();
requestRef.current = requestAnimationFrame(updateAnimation);
// Cleanup
return () => {
window.removeEventListener("resize", resizeCanvas);
canvas.removeEventListener("mousemove", handleMouseMove);
canvas.removeEventListener("mouseleave", handleMouseLeave);
if (requestRef.current) {
cancelAnimationFrame(requestRef.current);
}
};
}, [
direction,
speed,
borderColor,
hoverFillColor,
hoveredSquare,
squareSize,
]);
return (
<canvas
ref={canvasRef}
className={`w-full h-full border-none block ${className}`}
/>
);
}
+23
View File
@@ -0,0 +1,23 @@
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface TextRevealProps {
children: React.ReactNode;
className?: string;
}
export function TextReveal({ children, className }: TextRevealProps) {
return (
<motion.p
className={cn("text-gray-300 leading-relaxed", className)}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
{children}
</motion.p>
);
}