Skip to content

Commit 7738a2a

Browse files
authored
Feat/discord button animation with GSAP & Framer motion (JhaSourav07#1735)
## Description Added an animated `DiscordButton` component to the hero section with GSAP magnetic 3D tilt effect and Framer Motion floating aura animation. Fixes JhaSourav07#1721 ## Pillar - [x] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview The button features: - GSAP magnetic hover effect with 3D tilt (rotationX/Y) - Framer Motion continuous floating animation - Rotating gradient glow ring on hover - Discord-branded sweep highlight animation - Pulsing live status dot ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [ ] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started the repo. (This means a lot to us ❤️) - [x] I have made sure that i have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts) - [x] (Recommended) I joined the CommitPulse Discord server for faster collaboration, mentorship, and PR support.
2 parents d285f67 + 6e40733 commit 7738a2a

6 files changed

Lines changed: 650 additions & 76 deletions

File tree

app/page.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,31 @@ vi.mock('next/link', () => ({
2929
),
3030
}));
3131

32+
// Mock GSAP so FeatureCards don't break in JSDOM
33+
vi.mock('gsap', () => {
34+
const tween = { kill: vi.fn() };
35+
const timeline = {
36+
to: vi.fn().mockReturnThis(),
37+
fromTo: vi.fn().mockReturnThis(),
38+
set: vi.fn().mockReturnThis(),
39+
kill: vi.fn(),
40+
};
41+
return {
42+
default: {
43+
registerPlugin: vi.fn(),
44+
set: vi.fn(),
45+
to: vi.fn().mockReturnValue(tween),
46+
fromTo: vi.fn().mockReturnValue(tween),
47+
timeline: vi.fn().mockReturnValue(timeline),
48+
context: vi.fn((_fn: any) => ({ revert: vi.fn() })),
49+
},
50+
};
51+
});
52+
53+
vi.mock('gsap/ScrollTrigger', () => ({
54+
ScrollTrigger: {},
55+
}));
56+
3257
// Mock framer-motion
3358
vi.mock('framer-motion', () => ({
3459
motion: {

app/page.tsx

Lines changed: 15 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { CustomizeCTA } from './components/CustomizeCTA';
1111
import { useRecentSearches } from '@/hooks/useRecentSearches';
1212
import { useDebounce } from '@/hooks/useDebounce';
1313
import { Footer } from '@/app/components/Footer';
14+
import InteractiveViewer from '@/components/InteractiveViewer';
15+
import { FeatureCard, FeatureCardsSection } from '@/components/FeatureCards';
16+
import { DiscordButton } from '@/components/DiscordButton';
1417

1518
const Icons = {
1619
Github: () => (
@@ -146,45 +149,7 @@ export default function LandingPage() {
146149

147150
<main className="relative z-10 mx-auto max-w-6xl px-6 mt-32">
148151
<div className="mb-16 text-center">
149-
<motion.a
150-
href="https://discord.gg/Cb73bS79j"
151-
target="_blank"
152-
rel="noopener noreferrer"
153-
initial={{ opacity: 0, y: -10 }}
154-
animate={{ opacity: 1, y: 0 }}
155-
transition={{ duration: 0.5, delay: 0.1 }}
156-
whileHover={{ scale: 1.04 }}
157-
whileTap={{ scale: 0.97 }}
158-
className="mb-8 inline-flex items-center gap-2.5 rounded-full border border-black/10 bg-white px-4 py-1.5 text-xs font-medium text-gray-700 shadow-sm backdrop-blur-sm transition-colors duration-200 hover:bg-black/[0.03] hover:border-black/20 hover:text-black dark:border-white/10 dark:bg-white/[0.04] dark:text-white/50 dark:hover:bg-white/[0.07] dark:hover:border-white/20 dark:hover:text-white/80"
159-
>
160-
<span className="relative flex h-1.5 w-1.5">
161-
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-black/40 dark:bg-white/50" />
162-
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-black/70 dark:bg-white/70" />
163-
</span>
164-
<svg
165-
width="13"
166-
height="13"
167-
viewBox="0 0 24 24"
168-
fill="currentColor"
169-
className="opacity-60"
170-
>
171-
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3333-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3333-.946 2.4189-2.1568 2.4189Z" />
172-
</svg>
173-
Join the community on Discord
174-
<svg
175-
width="10"
176-
height="10"
177-
viewBox="0 0 24 24"
178-
fill="none"
179-
stroke="currentColor"
180-
strokeWidth="2.5"
181-
strokeLinecap="round"
182-
strokeLinejoin="round"
183-
className="opacity-40"
184-
>
185-
<path d="M7 17L17 7M17 7H7M17 7v10" />
186-
</svg>
187-
</motion.a>
152+
<DiscordButton />
188153

189154
<motion.div
190155
initial={{ opacity: 0, y: 20 }}
@@ -423,63 +388,38 @@ export default function LandingPage() {
423388

424389
<CustomizeCTA />
425390

426-
<div className="grid gap-6 md:grid-cols-3">
391+
<FeatureCardsSection>
427392
<FeatureCard
428393
icon={<Icons.Zap />}
429-
accent="text-black dark:text-white"
394+
accent="text-white"
395+
accentColor="#10b981"
396+
index={0}
430397
title="Real-time Sync"
431398
desc="Pulled directly from GitHub GraphQL API. Your streak updates as fast as your code pushes."
432399
/>
433400
<FeatureCard
434401
icon={<Icons.Copy />}
435-
accent="text-black dark:text-white"
402+
accent="text-white"
403+
accentColor="#8b5cf6"
404+
index={1}
436405
title="Theme Engine"
437406
desc="Switch between Neon, Dracula, or custom HEX modes via simple URL management."
438407
/>
439408
<FeatureCard
440409
icon={<Icons.Box />}
441-
accent="text-black dark:text-white"
410+
accent="text-white"
411+
accentColor="#06b6d4"
412+
index={2}
442413
title="Isometric Math"
443414
desc="Sophisticated 3D projection formulas turn 2D data into digital architecture."
444415
/>
445-
</div>
416+
</FeatureCardsSection>
446417
<Footer />
447418
</main>
448419
</div>
449420
);
450421
}
451422

452-
function FeatureCard({
453-
icon,
454-
title,
455-
desc,
456-
accent,
457-
}: {
458-
icon: ReactNode;
459-
title: string;
460-
desc: string;
461-
accent: string;
462-
}) {
463-
return (
464-
<motion.div
465-
whileHover={{ y: -5, scale: 1.02 }}
466-
transition={{ type: 'spring', stiffness: 300 }}
467-
className="group relative overflow-hidden rounded-3xl border border-black/5 bg-white/60 p-8 shadow-xl shadow-black/5 hover:border-emerald-500/30 dark:border-white/10 dark:bg-[#0a0a0a]/80 dark:shadow-2xl dark:shadow-black/50 dark:hover:border-emerald-500/40 dark:hover:bg-[#111] transition-all duration-300 backdrop-blur-xl"
468-
>
469-
<div className="absolute -right-20 -top-20 h-40 w-40 rounded-full bg-emerald-500/10 blur-3xl transition-all duration-500 group-hover:bg-emerald-500/20" />
470-
<div
471-
className={`mb-6 w-fit rounded-2xl bg-gradient-to-br from-gray-100 to-gray-50 p-3.5 text-black shadow-sm ring-1 ring-black/5 dark:from-white/10 dark:to-white/5 dark:text-white dark:ring-white/10 ${accent}`}
472-
>
473-
{icon}
474-
</div>
475-
<h3 className="mb-3 text-lg font-bold text-gray-900 dark:text-white tracking-tight">
476-
{title}
477-
</h3>
478-
<p className="text-sm leading-relaxed text-gray-600 dark:text-white/65">{desc}</p>
479-
</motion.div>
480-
);
481-
}
482-
483423
const STEPS = [
484424
{
485425
n: '01',

components/DiscordButton.tsx

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
'use client';
2+
3+
import { useRef, useState, useEffect } from 'react';
4+
import { motion, useAnimation } from 'framer-motion';
5+
import gsap from 'gsap';
6+
7+
export function DiscordButton() {
8+
const buttonRef = useRef<HTMLAnchorElement>(null);
9+
const [isHovered, setIsHovered] = useState(false);
10+
11+
// Magnetic hover effect using GSAP for smooth mouse tracking
12+
useEffect(() => {
13+
const button = buttonRef.current;
14+
if (!button) return;
15+
16+
const handleMouseMove = (e: MouseEvent) => {
17+
if (!isHovered) return;
18+
const rect = button.getBoundingClientRect();
19+
const x = e.clientX - rect.left - rect.width / 2;
20+
const y = e.clientY - rect.top - rect.height / 2;
21+
22+
gsap.to(button, {
23+
x: x * 0.2, // move 20% towards the mouse
24+
y: y * 0.2,
25+
rotationX: -y * 0.1,
26+
rotationY: x * 0.1,
27+
duration: 0.4,
28+
ease: 'power2.out',
29+
transformPerspective: 500,
30+
});
31+
};
32+
33+
const handleMouseLeave = () => {
34+
setIsHovered(false);
35+
gsap.to(button, {
36+
x: 0,
37+
y: 0,
38+
rotationX: 0,
39+
rotationY: 0,
40+
duration: 0.7,
41+
ease: 'elastic.out(1, 0.3)',
42+
});
43+
};
44+
45+
button.addEventListener('mousemove', handleMouseMove);
46+
button.addEventListener('mouseleave', handleMouseLeave);
47+
48+
return () => {
49+
button.removeEventListener('mousemove', handleMouseMove);
50+
button.removeEventListener('mouseleave', handleMouseLeave);
51+
};
52+
}, [isHovered]);
53+
54+
return (
55+
<div className="relative inline-block perspective-1000">
56+
{/* Background Glow */}
57+
<motion.div
58+
className="absolute -inset-1 rounded-full opacity-30 blur-md bg-gradient-to-r from-[#5865F2]/50 via-purple-500/50 to-[#5865F2]/50 pointer-events-none"
59+
animate={{
60+
scale: isHovered ? 1.2 : 1,
61+
opacity: isHovered ? 0.8 : 0.4,
62+
rotate: isHovered ? 180 : 0,
63+
}}
64+
transition={{
65+
duration: isHovered ? 0.4 : 3,
66+
rotate: {
67+
duration: 8,
68+
repeat: Infinity,
69+
ease: 'linear',
70+
},
71+
}}
72+
/>
73+
74+
<motion.div
75+
animate={{ y: [0, -8, 0] }}
76+
transition={{ duration: 3, repeat: Infinity, ease: 'easeInOut' }}
77+
className="relative z-10"
78+
>
79+
<motion.a
80+
ref={buttonRef}
81+
href="https://discord.gg/Cb73bS79j"
82+
target="_blank"
83+
rel="noopener noreferrer"
84+
onMouseEnter={() => setIsHovered(true)}
85+
initial={{ opacity: 0, y: -20, scale: 0.9 }}
86+
animate={{ opacity: 1, y: 0, scale: 1 }}
87+
transition={{
88+
type: 'spring',
89+
stiffness: 260,
90+
damping: 20,
91+
delay: 0.1,
92+
}}
93+
whileTap={{ scale: 0.95 }}
94+
className="relative mb-8 inline-flex items-center gap-3 rounded-full border border-black/10 bg-white/80 backdrop-blur-md px-5 py-2 text-sm font-semibold text-gray-700 shadow-[0_8px_30px_rgb(0,0,0,0.08)] transition-all hover:text-black dark:border-white/10 dark:bg-[#0a0a0a]/80 dark:text-white/80 dark:hover:text-white overflow-hidden group"
95+
style={{ transformStyle: 'preserve-3d' }}
96+
>
97+
{/* Animated Sweep effect on hover */}
98+
<div className="absolute inset-0 translate-y-[100%] bg-gradient-to-r from-[#5865F2]/10 via-[#5865F2]/20 to-[#5865F2]/10 transition-transform duration-500 ease-out group-hover:translate-y-0" />
99+
100+
{/* Pulsing Dot */}
101+
<span className="relative flex h-2 w-2 z-10">
102+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#5865F2] opacity-75" />
103+
<span className="relative inline-flex h-2 w-2 rounded-full bg-[#5865F2] shadow-[0_0_8px_#5865F2]" />
104+
</span>
105+
106+
{/* Discord Icon */}
107+
<svg
108+
width="16"
109+
height="16"
110+
viewBox="0 0 24 24"
111+
fill="currentColor"
112+
className="z-10 text-[#5865F2] transition-transform duration-300 group-hover:scale-110 group-hover:rotate-[-5deg]"
113+
>
114+
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3333-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3333-.946 2.4189-2.1568 2.4189Z" />
115+
</svg>
116+
117+
<span className="z-10 bg-gradient-to-br from-gray-900 to-gray-500 bg-clip-text text-transparent dark:from-white dark:to-gray-400 group-hover:text-black dark:group-hover:text-white transition-colors duration-300">
118+
Join the community on Discord
119+
</span>
120+
121+
<svg
122+
width="14"
123+
height="14"
124+
viewBox="0 0 24 24"
125+
fill="none"
126+
stroke="currentColor"
127+
strokeWidth="2.5"
128+
strokeLinecap="round"
129+
strokeLinejoin="round"
130+
className="z-10 opacity-40 transition-all duration-300 group-hover:opacity-100 group-hover:translate-x-1 group-hover:-translate-y-1 group-hover:text-[#5865F2]"
131+
>
132+
<path d="M7 17L17 7M17 7H7M17 7v10" />
133+
</svg>
134+
</motion.a>
135+
</motion.div>
136+
</div>
137+
);
138+
}

0 commit comments

Comments
 (0)