Skip to content

Commit b2e4b3f

Browse files
INQTRcursoragent
andcommitted
feat: add animated feature cards to features page
- Created `feature-animations.tsx` with 8 custom React animations - Updated `features-content.tsx` bento grid to use `grid-flow-dense` - Separated text content from animations to prevent overlapping - Re-centered and scaled animations for double-width cards Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b57fc11 commit b2e4b3f

File tree

2 files changed

+437
-14
lines changed

2 files changed

+437
-14
lines changed
Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { Check } from "lucide-react";
5+
6+
export function RealtimeVotingAnimation() {
7+
const [hovered, setHovered] = useState<number | null>(null);
8+
const [selected, setSelected] = useState<number | null>(null);
9+
10+
useEffect(() => {
11+
let isMounted = true;
12+
const play = async () => {
13+
while (isMounted) {
14+
setHovered(null);
15+
setSelected(null);
16+
await new Promise((r) => setTimeout(r, 1000));
17+
if (!isMounted) break;
18+
19+
setHovered(2); // Card '5'
20+
await new Promise((r) => setTimeout(r, 400));
21+
if (!isMounted) break;
22+
23+
setSelected(2);
24+
await new Promise((r) => setTimeout(r, 2500));
25+
}
26+
};
27+
play();
28+
return () => {
29+
isMounted = false;
30+
};
31+
}, []);
32+
33+
const cards = [1, 3, 5, 8];
34+
35+
return (
36+
<div className="absolute inset-0 flex items-end justify-center pr-0 pb-4 sm:pb-16 pointer-events-none">
37+
<div className="absolute right-1/4 bottom-0 w-96 h-96 bg-primary/5 rounded-full blur-3xl"></div>
38+
<div className="flex gap-4 sm:gap-8 z-10 translate-y-2 sm:translate-y-4 opacity-90">
39+
{cards.map((c, i) => (
40+
<div
41+
key={c}
42+
className={`w-16 h-24 sm:w-28 sm:h-40 rounded-2xl flex items-center justify-center font-bold text-2xl sm:text-5xl transition-all duration-300
43+
${
44+
selected === i
45+
? "bg-primary text-primary-foreground -translate-y-8 shadow-2xl scale-110"
46+
: hovered === i
47+
? "bg-white dark:bg-zinc-800 text-primary -translate-y-4 shadow-xl border border-primary/30"
48+
: "bg-white dark:bg-zinc-800 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-zinc-700 shadow-md"
49+
}
50+
`}
51+
>
52+
{c}
53+
</div>
54+
))}
55+
</div>
56+
</div>
57+
);
58+
}
59+
60+
export function AnalyticsAnimation() {
61+
const [revealed, setRevealed] = useState(false);
62+
63+
useEffect(() => {
64+
let isMounted = true;
65+
const play = async () => {
66+
while (isMounted) {
67+
setRevealed(false);
68+
await new Promise((r) => setTimeout(r, 1000));
69+
if (!isMounted) break;
70+
71+
setRevealed(true);
72+
await new Promise((r) => setTimeout(r, 3500));
73+
}
74+
};
75+
play();
76+
return () => {
77+
isMounted = false;
78+
};
79+
}, []);
80+
81+
const bars = [
82+
{ height: "h-6", target: "h-12", value: "3" },
83+
{ height: "h-6", target: "h-24", value: "5", active: true },
84+
{ height: "h-6", target: "h-16", value: "8" },
85+
{ height: "h-6", target: "h-8", value: "13" },
86+
];
87+
88+
return (
89+
<div className="absolute inset-0 flex items-end justify-center pr-0 pb-4 sm:pb-10 pointer-events-none">
90+
<div className="absolute right-1/4 bottom-0 w-80 h-80 bg-emerald-500/5 rounded-full blur-3xl"></div>
91+
<div className="flex items-end gap-8 sm:gap-16 z-10 opacity-90 translate-y-4 w-full px-12 sm:px-24 justify-center">
92+
{bars.map((bar, i) => (
93+
<div key={i} className="flex flex-col items-center gap-3">
94+
<div className="w-12 sm:w-20 bg-white dark:bg-zinc-800 rounded-t-xl border-t border-x border-gray-200 dark:border-zinc-700 flex flex-col justify-end overflow-hidden shadow-sm">
95+
<div
96+
className={`w-full transition-all duration-1000 ease-out ${
97+
revealed ? bar.target : bar.height
98+
} ${
99+
bar.active
100+
? "bg-emerald-500"
101+
: "bg-gray-200 dark:bg-zinc-700"
102+
}`}
103+
></div>
104+
</div>
105+
<span className="text-sm font-bold text-gray-500">{bar.value}</span>
106+
</div>
107+
))}
108+
</div>
109+
</div>
110+
);
111+
}
112+
113+
export function TimerAnimation() {
114+
const [time, setTime] = useState(60);
115+
116+
useEffect(() => {
117+
let isMounted = true;
118+
let interval: NodeJS.Timeout;
119+
120+
const play = async () => {
121+
setTime(60);
122+
interval = setInterval(() => {
123+
if (!isMounted) return;
124+
setTime((t) => (t > 0 ? t - 1 : 60));
125+
}, 50); // Fast countdown for effect
126+
};
127+
128+
play();
129+
return () => {
130+
isMounted = false;
131+
clearInterval(interval);
132+
};
133+
}, []);
134+
135+
return (
136+
<div className="absolute inset-0 flex items-end justify-center pb-4 sm:pb-8 pointer-events-none">
137+
<div className="absolute left-1/2 -translate-x-1/2 bottom-0 w-48 h-48 bg-fuchsia-500/5 rounded-full blur-3xl"></div>
138+
<div className="w-24 h-24 sm:w-28 sm:h-28 rounded-full border-4 border-fuchsia-100 dark:border-fuchsia-900/30 flex items-center justify-center relative z-10 bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm shadow-md translate-y-2">
139+
<svg className="absolute inset-0 w-full h-full -rotate-90" viewBox="0 0 100 100">
140+
<circle
141+
cx="50"
142+
cy="50"
143+
r="46"
144+
fill="none"
145+
stroke="currentColor"
146+
strokeWidth="8"
147+
className="text-fuchsia-500 transition-all duration-75"
148+
strokeDasharray="289"
149+
strokeDashoffset={289 - (time / 60) * 289}
150+
/>
151+
</svg>
152+
<span className="text-xl sm:text-2xl font-bold font-mono text-fuchsia-600 dark:text-fuchsia-400">
153+
00:{time.toString().padStart(2, '0')}
154+
</span>
155+
</div>
156+
</div>
157+
);
158+
}
159+
160+
export function CanvasAnimation() {
161+
const [pos, setPos] = useState({ x: 0, y: 0 });
162+
163+
useEffect(() => {
164+
let isMounted = true;
165+
const play = async () => {
166+
while (isMounted) {
167+
setPos({ x: 0, y: 0 });
168+
await new Promise((r) => setTimeout(r, 1000));
169+
if (!isMounted) break;
170+
171+
setPos({ x: -20, y: -30 });
172+
await new Promise((r) => setTimeout(r, 800));
173+
if (!isMounted) break;
174+
175+
setPos({ x: -40, y: -10 });
176+
await new Promise((r) => setTimeout(r, 1500));
177+
}
178+
};
179+
play();
180+
return () => {
181+
isMounted = false;
182+
};
183+
}, []);
184+
185+
return (
186+
<div className="absolute inset-0 flex items-end justify-center sm:justify-end pr-0 sm:pr-8 pb-4 sm:pb-8 pointer-events-none">
187+
<div className="absolute right-0 bottom-0 w-64 h-64 bg-cyan-500/5 rounded-full blur-3xl"></div>
188+
<div
189+
className="w-32 h-20 bg-white dark:bg-zinc-800 rounded-xl shadow-lg border border-cyan-200 dark:border-cyan-900/50 flex flex-col justify-between p-3 transition-transform duration-700 ease-in-out z-10"
190+
style={{ transform: `translate(${pos.x}px, ${pos.y}px)` }}
191+
>
192+
<div className="h-2 w-12 bg-cyan-100 dark:bg-cyan-900/50 rounded"></div>
193+
<div className="flex justify-between items-end">
194+
<div className="w-6 h-6 rounded-full bg-cyan-500 text-[10px] text-white flex items-center justify-center font-bold">5</div>
195+
<div className="w-4 h-4 rounded-full bg-gray-200 dark:bg-zinc-700"></div>
196+
</div>
197+
</div>
198+
199+
{/* Background dots grid */}
200+
<div className="absolute inset-0 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] dark:bg-[radial-gradient(#27272a_1px,transparent_1px)] [background-size:16px_16px] opacity-30"></div>
201+
</div>
202+
);
203+
}
204+
205+
export function ScalesAnimation() {
206+
const [scaleIdx, setScaleIdx] = useState(0);
207+
208+
useEffect(() => {
209+
let isMounted = true;
210+
const play = async () => {
211+
while (isMounted) {
212+
await new Promise((r) => setTimeout(r, 2000));
213+
if (!isMounted) break;
214+
setScaleIdx((prev) => (prev + 1) % 3);
215+
}
216+
};
217+
play();
218+
return () => {
219+
isMounted = false;
220+
};
221+
}, []);
222+
223+
const scales = [
224+
["1", "2", "3", "5", "8"],
225+
["XS", "S", "M", "L", "XL"],
226+
["1", "2", "4", "8", "16"],
227+
];
228+
229+
return (
230+
<div className="absolute inset-0 flex items-center justify-center sm:items-end sm:justify-end pr-0 sm:pr-8 pb-0 sm:pb-8 pointer-events-none">
231+
<div className="absolute right-0 bottom-0 w-48 h-48 bg-orange-500/5 rounded-full blur-3xl"></div>
232+
<div className="flex gap-2 z-10 opacity-90">
233+
{scales[scaleIdx].map((val, i) => (
234+
<div
235+
key={i + val} // Key changes to force animation
236+
className="w-10 h-14 bg-white dark:bg-zinc-800 rounded-lg flex items-center justify-center font-bold text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-zinc-700 shadow-sm animate-in zoom-in fade-in duration-300"
237+
style={{ animationDelay: `${i * 50}ms` }}
238+
>
239+
{val}
240+
</div>
241+
))}
242+
</div>
243+
</div>
244+
);
245+
}
246+
247+
export function PlayerManagementAnimation() {
248+
const [avatars, setAvatars] = useState<number[]>([]);
249+
250+
useEffect(() => {
251+
let isMounted = true;
252+
const play = async () => {
253+
while (isMounted) {
254+
setAvatars([]);
255+
await new Promise((r) => setTimeout(r, 1000));
256+
if (!isMounted) break;
257+
258+
setAvatars([1]);
259+
await new Promise((r) => setTimeout(r, 500));
260+
if (!isMounted) break;
261+
262+
setAvatars([1, 2]);
263+
await new Promise((r) => setTimeout(r, 700));
264+
if (!isMounted) break;
265+
266+
setAvatars([1, 2, 3]);
267+
await new Promise((r) => setTimeout(r, 2500));
268+
}
269+
};
270+
play();
271+
return () => {
272+
isMounted = false;
273+
};
274+
}, []);
275+
276+
const colors = [
277+
"bg-indigo-500 text-indigo-50",
278+
"bg-violet-500 text-violet-50",
279+
"bg-purple-500 text-purple-50",
280+
];
281+
const initials = ["JD", "AB", "RW"];
282+
283+
return (
284+
<div className="absolute inset-0 flex items-end justify-center sm:justify-end pr-0 sm:pr-10 pb-4 sm:pb-8 pointer-events-none overflow-hidden">
285+
<div className="absolute right-0 bottom-0 w-48 h-48 bg-indigo-500/5 rounded-full blur-3xl"></div>
286+
<div className="flex -space-x-4 z-10">
287+
{avatars.map((a, i) => (
288+
<div
289+
key={a}
290+
className={`w-14 h-14 rounded-full ${colors[i]} border-4 border-white dark:border-zinc-900 flex items-center justify-center shadow-lg animate-in fade-in zoom-in slide-in-from-right-4 duration-300`}
291+
style={{ zIndex: 10 - a }}
292+
>
293+
<span className="text-sm font-bold">{initials[i]}</span>
294+
</div>
295+
))}
296+
</div>
297+
</div>
298+
);
299+
}
300+
301+
export function IssuesAnimation() {
302+
const [active, setActive] = useState(0);
303+
304+
useEffect(() => {
305+
let isMounted = true;
306+
const play = async () => {
307+
while (isMounted) {
308+
await new Promise((r) => setTimeout(r, 1500));
309+
if (!isMounted) break;
310+
setActive((prev) => (prev + 1) % 3);
311+
}
312+
};
313+
play();
314+
return () => {
315+
isMounted = false;
316+
};
317+
}, []);
318+
319+
const issues = ["PROJ-123", "PROJ-124", "PROJ-125"];
320+
321+
return (
322+
<div className="absolute inset-0 flex items-end justify-center sm:justify-end pr-0 sm:pr-8 pb-4 sm:pb-8 pointer-events-none">
323+
<div className="absolute right-0 bottom-0 w-48 h-48 bg-sky-500/5 rounded-full blur-3xl"></div>
324+
<div className="flex flex-col gap-2 z-10 w-48">
325+
{issues.map((issue, i) => (
326+
<div
327+
key={issue}
328+
className={`h-10 rounded-lg flex items-center px-3 gap-3 transition-all duration-500 border shadow-sm
329+
${
330+
active === i
331+
? "bg-white dark:bg-zinc-800 border-sky-200 dark:border-sky-900/50 scale-105"
332+
: "bg-white/60 dark:bg-zinc-800/60 border-gray-100 dark:border-zinc-800/50 scale-100 opacity-60"
333+
}
334+
`}
335+
>
336+
<div className={`w-4 h-4 rounded-full border flex items-center justify-center ${active === i ? "border-sky-500 bg-sky-500 text-white" : "border-gray-300 dark:border-zinc-600"}`}>
337+
{active === i && <Check className="w-3 h-3" />}
338+
</div>
339+
<div className="h-2 w-16 bg-gray-200 dark:bg-zinc-700 rounded"></div>
340+
</div>
341+
))}
342+
</div>
343+
</div>
344+
);
345+
}
346+
347+
export function AutoCompleteAnimation() {
348+
const [count, setCount] = useState(3);
349+
const [revealed, setRevealed] = useState(false);
350+
351+
useEffect(() => {
352+
let isMounted = true;
353+
const play = async () => {
354+
while (isMounted) {
355+
setCount(3);
356+
setRevealed(false);
357+
await new Promise((r) => setTimeout(r, 1000));
358+
if (!isMounted) break;
359+
360+
setCount(2);
361+
await new Promise((r) => setTimeout(r, 1000));
362+
if (!isMounted) break;
363+
364+
setCount(1);
365+
await new Promise((r) => setTimeout(r, 1000));
366+
if (!isMounted) break;
367+
368+
setRevealed(true);
369+
await new Promise((r) => setTimeout(r, 2500));
370+
}
371+
};
372+
play();
373+
return () => {
374+
isMounted = false;
375+
};
376+
}, []);
377+
378+
return (
379+
<div className="absolute inset-0 flex items-end justify-center sm:justify-end pr-0 sm:pr-8 pb-4 sm:pb-8 pointer-events-none">
380+
<div className="absolute right-0 bottom-0 w-48 h-48 bg-rose-500/5 rounded-full blur-3xl"></div>
381+
<div className="flex flex-col items-center gap-4 z-10">
382+
{!revealed ? (
383+
<div className="w-16 h-16 rounded-full bg-white dark:bg-zinc-800 border-4 border-rose-100 dark:border-rose-900/30 flex items-center justify-center shadow-lg animate-in zoom-in duration-300">
384+
<span className="text-3xl font-bold text-rose-500">{count}</span>
385+
</div>
386+
) : (
387+
<div className="flex gap-2 animate-in slide-in-from-bottom-4 fade-in duration-500">
388+
{[5, 5, 8].map((v, i) => (
389+
<div key={i} className="w-10 h-14 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg flex items-center justify-center font-bold text-lg text-gray-900 dark:text-white shadow-md">
390+
{v}
391+
</div>
392+
))}
393+
</div>
394+
)}
395+
</div>
396+
</div>
397+
);
398+
}

0 commit comments

Comments
 (0)