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