Skip to content

Commit cd75fc9

Browse files
committed
feat: enhance website structure and animations
1 parent e666f4b commit cd75fc9

File tree

12 files changed

+731
-1091
lines changed

12 files changed

+731
-1091
lines changed

website/app/(home)/page.tsx

Lines changed: 29 additions & 726 deletions
Large diffs are not rendered by default.

website/app/global.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,21 @@
132132
}
133133
}
134134

135+
@keyframes fade-in {
136+
from { opacity: 0; transform: scale(0.95); }
137+
to { opacity: 1; transform: scale(1); }
138+
}
139+
140+
@keyframes progress-bar {
141+
from { transform: scaleX(0); }
142+
to { transform: scaleX(1); }
143+
}
144+
145+
@keyframes float-up {
146+
from { opacity: 0; transform: translateY(20px); }
147+
to { opacity: 1; transform: translateY(0); }
148+
}
149+
135150
@layer utilities {
136151
.animate-marquee {
137152
animation: marquee 40s linear infinite;
@@ -146,5 +161,14 @@
146161
background-size: 200% 200%;
147162
animation: gradient-x 3s ease infinite;
148163
}
164+
.animate-fade-in {
165+
animation: fade-in 0.5s ease-out forwards;
166+
}
167+
.animate-progress-bar {
168+
animation: progress-bar var(--duration, 5000ms) linear forwards;
169+
}
170+
.animate-float-up {
171+
animation: float-up 0.6s ease-out forwards;
172+
}
149173
}
150174

website/components/bento-grid.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
'use client';
2-
31
import { ReactNode } from 'react';
42
import { clsx } from 'clsx';
53
import { twMerge } from 'tailwind-merge';
6-
import { motion } from 'motion/react';
74

85
function cn(...inputs: (string | undefined | null | false)[]) {
96
return twMerge(clsx(inputs));
@@ -20,7 +17,7 @@ export function BentoGrid({
2017
<div
2118
className={cn(
2219
'grid grid-cols-1 md:grid-cols-6 lg:grid-cols-3 gap-4 lg:gap-8 max-w-7xl mx-auto',
23-
className,
20+
className
2421
)}
2522
>
2623
{children}
@@ -46,27 +43,27 @@ export function BentoCard({
4643
cta: string;
4744
}) {
4845
return (
49-
<motion.div
50-
whileHover={{ y: -8, scale: 1.02 }}
51-
transition={{ type: "tween", duration: 0.1, ease: "easeOut" }}
46+
<div
5247
key={name}
5348
className={cn(
5449
'group relative col-span-3 flex flex-col justify-between overflow-hidden rounded-3xl',
5550
'bg-white/40 dark:bg-white/[0.02] border border-slate-200/60 dark:border-white/10',
5651
'shadow-sm hover:shadow-lg hover:shadow-blue-500/5 transition-all duration-150 backdrop-blur-md',
57-
className,
52+
// CSS hover animation instead of Framer Motion
53+
'hover:-translate-y-2 hover:scale-[1.02]',
54+
className
5855
)}
5956
>
6057
<div className="absolute inset-0 z-0 transition-transform duration-200 group-hover:scale-105 opacity-60">
6158
{background}
6259
</div>
63-
60+
6461
{/* Subtle Glass Gradient Overlay */}
6562
<div className="absolute inset-0 z-10 bg-gradient-to-t from-white/80 via-white/20 to-transparent dark:from-black/80 dark:via-black/20 dark:to-transparent pointer-events-none" />
6663

6764
<div className="pointer-events-none z-20 flex flex-col gap-2 p-6 mt-auto">
6865
<div className="w-10 h-10 rounded-xl bg-white/60 dark:bg-white/10 flex items-center justify-center backdrop-blur-md mb-2 shadow-sm border border-white/20 dark:border-white/10">
69-
<Icon className="h-5 w-5 text-slate-700 dark:text-slate-200" />
66+
<Icon className="h-5 w-5 text-slate-700 dark:text-slate-200" />
7067
</div>
7168
<h3 className="text-lg font-bold font-heading text-slate-900 dark:text-neutral-100">
7269
{name}
@@ -78,7 +75,7 @@ export function BentoCard({
7875

7976
<div
8077
className={cn(
81-
'pointer-events-none absolute bottom-6 right-6 z-30 opacity-0 -translate-x-2 transition-all duration-300 group-hover:opacity-100 group-hover:translate-x-0',
78+
'pointer-events-none absolute bottom-6 right-6 z-30 opacity-0 -translate-x-2 transition-all duration-300 group-hover:opacity-100 group-hover:translate-x-0'
8279
)}
8380
>
8481
<div className="pointer-events-auto">
@@ -104,6 +101,6 @@ export function BentoCard({
104101
</div>
105102
</div>
106103
<div className="pointer-events-none absolute inset-0 transition-all duration-150 group-hover:bg-blue-500/[0.02] dark:group-hover:bg-blue-500/[0.05]" />
107-
</motion.div>
104+
</div>
108105
);
109106
}
Lines changed: 143 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,157 @@
11
'use client';
22

3-
import { motion } from 'motion/react';
4-
import { useEffect, useState, useMemo } from 'react';
5-
import { GRID_SIZE } from './grid-background';
3+
import { useEffect, useRef } from 'react';
4+
import { useIsMobile, useReducedMotion } from '@/hooks/use-mobile';
5+
6+
const GRID_SIZE = 40;
7+
const LARGE_GRID_SIZE = 200;
8+
9+
interface Cell {
10+
r: number;
11+
c: number;
12+
phase: number;
13+
speed: number;
14+
}
615

716
export function BlinkingGridBackground() {
8-
const [mounted, setMounted] = useState(false);
17+
const canvasRef = useRef<HTMLCanvasElement>(null);
18+
const isMobile = useIsMobile();
19+
const prefersReducedMotion = useReducedMotion();
920

1021
useEffect(() => {
11-
setMounted(true);
12-
}, []);
13-
14-
// Generate enough cells to cover large viewports
15-
const COLS = 50; // Wide enough for 2000px+ screens
16-
const ROWS = 30; // Tall enough for 1200px+ screens
17-
18-
const cells = useMemo(() => {
19-
const items = [];
20-
for (let r = 0; r < ROWS; r++) {
21-
for (let c = 0; c < COLS; c++) {
22-
// Sparsity: 12% of cells blink for a subtle effect
23-
if (Math.random() > 0.12) continue;
24-
items.push({ r, c, delay: Math.random() * 8 });
22+
const canvas = canvasRef.current;
23+
if (!canvas) return;
24+
25+
const ctx = canvas.getContext('2d');
26+
if (!ctx) return;
27+
28+
// Generate blinking cells (skip only if reduced motion preferred)
29+
const cells: Cell[] = [];
30+
if (!prefersReducedMotion) {
31+
const COLS = Math.ceil(2000 / GRID_SIZE);
32+
const ROWS = Math.ceil(1200 / GRID_SIZE);
33+
// Fewer cells on mobile for performance
34+
const cellDensity = isMobile ? 0.06 : 0.1;
35+
36+
for (let r = 0; r < ROWS; r++) {
37+
for (let c = 0; c < COLS; c++) {
38+
if (Math.random() > cellDensity) continue;
39+
cells.push({
40+
r,
41+
c,
42+
phase: Math.random() * Math.PI * 2,
43+
speed: 0.3 + Math.random() * 0.5,
44+
});
45+
}
2546
}
2647
}
27-
return items;
28-
}, []);
2948

30-
if (!mounted) return null;
49+
let animationId: number;
50+
const startTime = performance.now();
51+
52+
const drawGrid = (isDark: boolean, width: number, height: number) => {
53+
54+
// Draw small grid lines
55+
ctx.beginPath();
56+
ctx.strokeStyle = isDark
57+
? 'rgba(37, 99, 235, 0.08)' // blue-600 very subtle
58+
: 'rgba(148, 163, 184, 0.15)'; // slate-400 subtle
59+
ctx.lineWidth = 1;
60+
61+
// Vertical lines
62+
for (let x = 0; x <= width; x += GRID_SIZE) {
63+
ctx.moveTo(x, 0);
64+
ctx.lineTo(x, height);
65+
}
66+
// Horizontal lines
67+
for (let y = 0; y <= height; y += GRID_SIZE) {
68+
ctx.moveTo(0, y);
69+
ctx.lineTo(width, y);
70+
}
71+
ctx.stroke();
72+
73+
// Draw large grid lines
74+
ctx.beginPath();
75+
ctx.strokeStyle = isDark
76+
? 'rgba(37, 99, 235, 0.05)' // blue-600 very faint
77+
: 'rgba(148, 163, 184, 0.10)'; // slate-400 faint
78+
ctx.lineWidth = 1;
79+
80+
// Vertical lines
81+
for (let x = 0; x <= width; x += LARGE_GRID_SIZE) {
82+
ctx.moveTo(x, 0);
83+
ctx.lineTo(x, height);
84+
}
85+
// Horizontal lines
86+
for (let y = 0; y <= height; y += LARGE_GRID_SIZE) {
87+
ctx.moveTo(0, y);
88+
ctx.lineTo(width, y);
89+
}
90+
ctx.stroke();
91+
};
92+
93+
const animate = (time: number) => {
94+
const elapsed = (time - startTime) / 1000;
95+
96+
// Resize canvas to match display size
97+
const rect = canvas.getBoundingClientRect();
98+
const dpr = window.devicePixelRatio || 1;
99+
const width = rect.width * dpr;
100+
const height = rect.height * dpr;
101+
102+
if (canvas.width !== width || canvas.height !== height) {
103+
canvas.width = width;
104+
canvas.height = height;
105+
ctx.scale(dpr, dpr);
106+
}
107+
108+
// Clear
109+
ctx.clearRect(0, 0, rect.width, rect.height);
110+
111+
// Check for dark mode
112+
const isDark = document.documentElement.classList.contains('dark');
113+
114+
// Draw static grid lines first
115+
drawGrid(isDark, rect.width, rect.height);
116+
117+
// Draw blinking cells (skip only if reduced motion preferred)
118+
if (!prefersReducedMotion) {
119+
cells.forEach((cell) => {
120+
// Max opacity reduced for subtlety
121+
const maxOpacity = isDark ? 0.1 : 0.18;
122+
const opacity = ((Math.sin(elapsed * cell.speed + cell.phase) + 1) / 2) * maxOpacity;
123+
if (opacity < 0.02) return; // Skip nearly invisible cells
124+
125+
ctx.fillStyle = isDark
126+
? `rgba(96, 165, 250, ${opacity})` // blue-400
127+
: `rgba(59, 130, 246, ${opacity})`; // blue-500
128+
129+
ctx.fillRect(
130+
cell.c * GRID_SIZE,
131+
cell.r * GRID_SIZE,
132+
GRID_SIZE - 1, // Slight inset to not overlap grid lines
133+
GRID_SIZE - 1
134+
);
135+
});
136+
}
137+
138+
animationId = requestAnimationFrame(animate);
139+
};
140+
141+
animationId = requestAnimationFrame(animate);
142+
143+
return () => {
144+
cancelAnimationFrame(animationId);
145+
};
146+
}, [isMobile, prefersReducedMotion]);
31147

32148
return (
33-
// Absolute positioning within parent, with radial fade mask
34-
<div className="absolute inset-0 overflow-hidden pointer-events-none [mask-image:radial-gradient(ellipse_80%_70%_at_50%_40%,black_10%,transparent_70%)]">
35-
<svg className="w-full h-full" width="100%" height="100%">
36-
{cells.map((cell, i) => (
37-
<motion.rect
38-
key={i}
39-
// Coordinates match the grid pattern exactly
40-
x={cell.c * GRID_SIZE}
41-
y={cell.r * GRID_SIZE}
42-
width={GRID_SIZE}
43-
height={GRID_SIZE}
44-
className="fill-blue-400/30 dark:fill-blue-400/25"
45-
initial={{ opacity: 0 }}
46-
animate={{ opacity: [0, 0.5, 0] }}
47-
transition={{
48-
duration: 3 + Math.random() * 4,
49-
repeat: Infinity,
50-
delay: cell.delay,
51-
ease: 'easeInOut',
52-
}}
53-
/>
54-
))}
55-
</svg>
149+
<div className="absolute inset-0 overflow-hidden pointer-events-none">
150+
<canvas
151+
ref={canvasRef}
152+
className="w-full h-full"
153+
style={{ width: '100%', height: '100%' }}
154+
/>
56155
</div>
57156
);
58157
}

website/components/floating-orbs.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { motion } from 'motion/react';
44
import { useMemo } from 'react';
5+
import { useIsMobile, useReducedMotion } from '@/hooks/use-mobile';
56

67
interface Orb {
78
id: number;
@@ -14,7 +15,16 @@ interface Orb {
1415
}
1516

1617
export function FloatingOrbs() {
18+
const isMobile = useIsMobile();
19+
const prefersReducedMotion = useReducedMotion();
20+
21+
// Disable animations on mobile or when reduced motion is preferred
22+
const shouldAnimate = !isMobile && !prefersReducedMotion;
23+
1724
const orbs = useMemo<Orb[]>(() => {
25+
// Return fewer orbs on mobile, or none if animations disabled
26+
if (!shouldAnimate) return [];
27+
1828
const colors = [
1929
'bg-blue-400/20 dark:bg-blue-500/15',
2030
'bg-purple-400/20 dark:bg-purple-500/15',
@@ -31,7 +41,9 @@ export function FloatingOrbs() {
3141
delay: Math.random() * 5,
3242
color: colors[i % colors.length],
3343
}));
34-
}, []);
44+
}, [shouldAnimate]);
45+
46+
if (!shouldAnimate) return null;
3547

3648
return (
3749
<div className="absolute inset-0 overflow-hidden pointer-events-none">

website/components/grid-background.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
'use client';
2-
31
const GRID_SIZE = 40;
42
const LARGE_GRID_SIZE = 200;
53

@@ -14,7 +12,7 @@ export function GridBackground({
1412
}) {
1513
const opacity = prominent
1614
? 'opacity-[0.35] dark:opacity-[0.25]'
17-
: 'opacity-[0.25] dark:opacity-[0.18]';
15+
: 'opacity-[0.25] dark:opacity-[0.15]';
1816

1917
return (
2018
<div className={`absolute inset-0 pointer-events-none ${className}`}>

0 commit comments

Comments
 (0)