Skip to content

Commit 2d8c80e

Browse files
Merge remote-tracking branch 'upstream/main' into feat/contribution
2 parents 0dac9fb + 5fe2d64 commit 2d8c80e

7 files changed

Lines changed: 68 additions & 15 deletions

File tree

app/components/CopyRepoButton.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { Copy } from 'lucide-react';
5+
6+
export default function CopyRepoButton() {
7+
const [copied, setCopied] = useState(false);
8+
9+
const repoUrl = 'https://github.com/JhaSourav07/commitpulse';
10+
11+
const handleCopy = async () => {
12+
await navigator.clipboard.writeText(repoUrl);
13+
14+
setCopied(true);
15+
16+
setTimeout(() => {
17+
setCopied(false);
18+
}, 2000);
19+
};
20+
21+
return (
22+
<button
23+
onClick={handleCopy}
24+
className="inline-flex items-center gap-2 rounded-2xl border border-black/10 bg-white/60 px-8 py-4 font-semibold transition-all duration-300 hover:scale-105 dark:border-white/10 dark:bg-white/5"
25+
>
26+
<Copy className="h-5 w-5" />
27+
{copied ? 'Copied!' : 'Copy URL'}
28+
</button>
29+
);
30+
}

app/contributors/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import Link from 'next/link';
22
import { Globe, Sparkles, Users, GitPullRequest, ArrowRight } from 'lucide-react';
3-
43
import BrandParticles from '@/components/BrandParticles';
54
import { Footer } from '@/app/components/Footer';
65
import ContributorsSearch from './ContributorsSearch';
76
import Leaderboard from '@/components/Leaderboard';
7+
import CopyRepoButton from '@/app/components/CopyRepoButton';
88

99
interface Contributor {
1010
id: number;
@@ -231,6 +231,8 @@ export default async function ContributorsPage() {
231231
View Repository
232232
</Link>
233233

234+
<CopyRepoButton />
235+
234236
<Link
235237
href="https://github.com/JhaSourav07/commitpulse/issues"
236238
target="_blank"

app/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use client';
22
import { trackUser } from '@/utils/tracking';
3-
import InteractiveViewer from '@/components/InteractiveViewer';
43
import Link from 'next/link';
54
import { useRef, useState, useEffect } from 'react';
65
import { AnimatePresence, motion } from 'framer-motion';

components/FeatureCards.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import { useRef, useState, useEffect, type ReactNode } from 'react';
44
import gsap from 'gsap';
55
import { ScrollTrigger } from 'gsap/ScrollTrigger';
66

7-
if (typeof window !== 'undefined') {
8-
gsap.registerPlugin(ScrollTrigger);
9-
}
7+
let gsapRegistered = false;
108

119
/* ─── Types ─── */
1210
interface FeatureCardProps {
@@ -44,7 +42,7 @@ function FloatingParticle({
4442
tl.to(ref.current, {
4543
opacity: 0.6,
4644
y: -60,
47-
x: `random(-30, 30)`,
45+
x: Math.random() * 60 - 30,
4846
duration: duration * 0.4,
4947
ease: 'power2.out',
5048
})
@@ -119,8 +117,6 @@ function AnimatedBorder({ color, isHovered }: { color: string; isHovered: boolea
119117
style={{
120118
opacity: 0,
121119
background: `conic-gradient(from 0deg, transparent, ${color}, transparent, ${color}44, transparent)`,
122-
mask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
123-
maskComposite: 'exclude',
124120
WebkitMaskComposite: 'xor',
125121
padding: '1.5px',
126122
}}
@@ -417,6 +413,13 @@ export function FeatureCard({ icon, title, desc, accent, index, accentColor }: F
417413

418414
/* ─── Section Wrapper (optional heading + background glow) ─── */
419415
export function FeatureCardsSection({ children }: { children: ReactNode }) {
416+
useEffect(() => {
417+
if (!gsapRegistered) {
418+
gsap.registerPlugin(ScrollTrigger);
419+
gsapRegistered = true;
420+
}
421+
}, []);
422+
420423
const sectionRef = useRef<HTMLDivElement>(null);
421424
const headingRef = useRef<HTMLHeadingElement>(null);
422425

lib/svg/animations.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ describe('getTowerAnimationCSS', () => {
3030
expect(css).not.toContain('@keyframes');
3131
});
3232

33+
it('scales transform-origin for rise animation when scale factor is provided', () => {
34+
const cssScale045 = getTowerAnimationCSS('rise', 0.45);
35+
expect(cssScale045).toContain('transform-origin: 0 4.5px');
36+
37+
const cssScale08 = getTowerAnimationCSS('rise', 0.8);
38+
expect(cssScale08).toContain('transform-origin: 0 8px');
39+
});
40+
41+
it('scales translateY translation for slide animation when scale factor is provided', () => {
42+
const cssScale045 = getTowerAnimationCSS('slide', 0.45);
43+
expect(cssScale045).toContain('transform: translateY(-9px)');
44+
45+
const cssScale08 = getTowerAnimationCSS('slide', 0.8);
46+
expect(cssScale08).toContain('transform: translateY(-16px)');
47+
});
48+
3349
it('includes accessibility support for prefers-reduced-motion', () => {
3450
const css = getTowerAnimationCSS('rise');
3551
expect(css).toContain('prefers-reduced-motion');

lib/svg/animations.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
const TOWER_BASE_Y = 10;
2222

2323
export function getTowerAnimationCSS(
24-
entrance: 'rise' | 'fade' | 'slide' | 'none' = 'rise'
24+
entrance: 'rise' | 'fade' | 'slide' | 'none' = 'rise',
25+
scale = 1.0
2526
): string {
2627
if (entrance === 'none') {
2728
return `
@@ -33,9 +34,10 @@ export function getTowerAnimationCSS(
3334
let keyframes = '';
3435

3536
if (entrance === 'rise') {
37+
const baseY = Math.round(TOWER_BASE_Y * scale * 100) / 100;
3638
baseStyles = `
3739
transform: scaleY(0);
38-
transform-origin: 0 ${TOWER_BASE_Y}px;
40+
transform-origin: 0 ${baseY}px;
3941
animation: grow-up 1.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
4042
`;
4143
keyframes = `
@@ -56,14 +58,15 @@ export function getTowerAnimationCSS(
5658
}
5759
`;
5860
} else if (entrance === 'slide') {
61+
const slideOffset = Math.round(-20 * scale * 100) / 100;
5962
baseStyles = `
6063
opacity: 0;
61-
transform: translateY(-20px);
64+
transform: translateY(${slideOffset}px);
6265
animation: slide-down 1.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
6366
`;
6467
keyframes = `
6568
@keyframes slide-down {
66-
from { opacity: 0; transform: translateY(-20px); }
69+
from { opacity: 0; transform: translateY(${slideOffset}px); }
6770
to { opacity: 1; transform: translateY(0); }
6871
}
6972
`;

lib/svg/generator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ function renderStyle(
221221
<style>
222222
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&amp;family=JetBrains+Mono&amp;family=Roboto&amp;family=Syncopate:wght@400;700&amp;family=Space+Grotesk:wght@400;500;600;700&amp;display=swap');
223223
${googleFontsImport}
224-
${getTowerAnimationCSS(entrance)}
224+
${getTowerAnimationCSS(entrance, sf)}
225225
.scan-line {
226226
animation: scan-sweep var(--scan-speed, 8s) linear infinite;
227227
transform-box: fill-box;
@@ -617,7 +617,7 @@ function generateAutoThemeSVG(
617617
:root { --cp-bg: #${light.bg}; --cp-text: #${light.text}; --cp-accent: #${light.accent}; --cp-label-fill: ${lightLabelFill}; --cp-label-opacity: ${lightLabelOpacity}; }
618618
@media (prefers-color-scheme: dark) { :root { --cp-bg: #${dark.bg}; --cp-text: #${dark.text}; --cp-accent: #${dark.accent}; --cp-label-fill: ${darkLabelFill}; --cp-label-opacity: ${darkLabelOpacity}; } }
619619
.cp-bg-fill { fill: var(--cp-bg); } .cp-text-fill { fill: var(--cp-text); color: var(--cp-text); } .cp-accent-fill { fill: var(--cp-accent); color: var(--cp-accent); }
620-
${getTowerAnimationCSS(params.entrance || 'rise')}
620+
${getTowerAnimationCSS(params.entrance || 'rise', sf)}
621621
.scan-line {
622622
animation: scan-sweep var(--scan-speed, 8s) linear infinite;
623623
transform-box: fill-box;
@@ -2014,7 +2014,7 @@ function generateAutoThemeVersusSVG(
20142014
:root { --cp-bg: #${light.bg}; --cp-text: #${light.text}; --cp-accent: #${light.accent}; --cp-label-fill: ${lightLabelFill}; --cp-label-opacity: ${lightLabelOpacity}; }
20152015
@media (prefers-color-scheme: dark) { :root { --cp-bg: #${dark.bg}; --cp-text: #${dark.text}; --cp-accent: #${dark.accent}; --cp-label-fill: ${darkLabelFill}; --cp-label-opacity: ${darkLabelOpacity}; } }
20162016
.cp-bg-fill { fill: var(--cp-bg); } .cp-text-fill { fill: var(--cp-text); color: var(--cp-text); } .cp-accent-fill { fill: var(--cp-accent); color: var(--cp-accent); }
2017-
${getTowerAnimationCSS(params.entrance || 'rise')}
2017+
${getTowerAnimationCSS(params.entrance || 'rise', sf)}
20182018
.scan-line {
20192019
animation: scan-sweep var(--scan-speed, 8s) linear infinite;
20202020
transform-box: fill-box;

0 commit comments

Comments
 (0)