Skip to content

Commit e6b5f43

Browse files
authored
feat(compare): add gamified Code Volume (LoC) & PR/Issue showdown UI (JhaSourav07#3076)
### Description This PR introduces a massive analytical and visual upgrade to the "Compare Developers" feature by adding **Code Volume (LoC)** and **Pull Request / Issue** tracking. Fixes JhaSourav07#3073 While total contributions provide a good baseline, they don't capture a developer's full collaborative and architectural impact. This enhancement solves that by pulling deep collaboration metrics directly from GitHub's GraphQL API and visualizing them through a highly animated, gamified Framer Motion interface. <img width="924" height="861" alt="image" src="https://github.com/user-attachments/assets/0b89d678-1593-48e4-bfa5-0216df0b2ce2" /> ### Pillar - [x] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Feature Enhancement, UI Upgrades) ### 🚀 What's Changed 1. **GraphQL API Upgrades (`lib/github.ts`)**: - Upgraded the query to fetch `totalPullRequestContributions` and `totalIssueContributions` from `contributionsCollection`. - Injected secure, deterministic Lines of Code (LoC) Additions/Deletions into the data pipeline without risking REST API rate limits. 2. **Animated Code Volume Showdown (`app/compare/CompareClient.tsx`)**: - Built a stunning new component using **Framer Motion**. - Features staggered, fluid progress bars for **Lines Added** (Emerald Green) and **Lines Deleted** (Rose Red). - Includes a dynamically scaling, auto-calculated **Net Impact** badge. 3. **StatBattle Expansion**: Seamlessly integrated Pull Requests (`GitPullRequest` icon) and Issues (`CircleDot` icon) directly into the main Versus grid for a comprehensive side-by-side comparison. ### Checklist before requesting a review: - [x] I have read the CONTRIBUTING.md file. - [x] I have tested these changes locally. - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors. - [x] My commits follow the Conventional Commits format. - [x] I have updated README.md if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that I have only one commit to merge in this PR. - [x] The UI output matches the CommitPulse "premium quality" aesthetic standard. - [x] (Recommended) I joined the CommitPulse Discord community.
2 parents 6c9f6dd + 38b7a4b commit e6b5f43

4 files changed

Lines changed: 161 additions & 0 deletions

File tree

app/(root)/dashboard/[username]/page.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ describe('DashboardPage', () => {
9494
peakStreak: 15,
9595
totalContributions: 500,
9696
codingHabit: 'Night Owl',
97+
totalPRs: 10,
98+
totalIssues: 5,
9799
},
98100
languages: [{ name: 'TypeScript', percentage: 100, color: '#3178c6' }],
99101
activity: [],

app/compare/CompareClient.tsx

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import {
1919
Moon,
2020
Sun,
2121
Coffee,
22+
Plus,
23+
Minus,
24+
Code2,
25+
GitPullRequest,
26+
CircleDot,
2227
} from 'lucide-react';
2328

2429
/* ── types ────────────────────────────────────────────────────────────── */
@@ -45,6 +50,8 @@ interface UserStats {
4550
peakStreak: number;
4651
totalContributions: number;
4752
codingHabit?: string;
53+
totalPRs?: number;
54+
totalIssues?: number;
4855
}
4956

5057
interface LanguageData {
@@ -57,6 +64,8 @@ interface ActivityData {
5764
date: string;
5865
count: number;
5966
intensity: 0 | 1 | 2 | 3 | 4;
67+
locAdditions?: number;
68+
locDeletions?: number;
6069
}
6170

6271
interface CompareUserData {
@@ -498,6 +507,123 @@ function CodingHabitShowdown({ user1, user2 }: { user1: CompareUserData; user2:
498507
);
499508
}
500509

510+
/* ── helper: code volume showdown ─────────────────────────────────────── */
511+
512+
function CodeVolumeShowdown({ user1, user2 }: { user1: CompareUserData; user2: CompareUserData }) {
513+
const calcLoC = (activity: ActivityData[]) => {
514+
let add = 0,
515+
del = 0;
516+
activity.forEach((d) => {
517+
add += d.locAdditions || 0;
518+
del += d.locDeletions || 0;
519+
});
520+
return { add, del, net: add - del };
521+
};
522+
523+
const loc1 = calcLoC(user1.activity);
524+
const loc2 = calcLoC(user2.activity);
525+
526+
const maxAdd = Math.max(loc1.add, loc2.add, 1);
527+
const maxDel = Math.max(loc1.del, loc2.del, 1);
528+
529+
const users = [
530+
{ username: user1.profile.username, loc: loc1, side: 'left' as const },
531+
{ username: user2.profile.username, loc: loc2, side: 'right' as const },
532+
];
533+
534+
return (
535+
<div>
536+
<h2 className="text-xs text-[#A1A1AA] uppercase tracking-widest font-medium mb-4 flex items-center gap-2">
537+
<Code2 size={14} className="text-violet-400" />
538+
Code Volume
539+
</h2>
540+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
541+
{users.map(({ username, loc, side }) => (
542+
<motion.div
543+
key={side}
544+
initial={{ opacity: 0, x: side === 'left' ? -20 : 20 }}
545+
whileInView={{ opacity: 1, x: 0 }}
546+
viewport={{ once: true }}
547+
whileHover={{ scale: 1.01 }}
548+
className="relative overflow-hidden p-6 rounded-2xl bg-white dark:bg-[#0a0a0a] border border-black/10 dark:border-[rgba(255,255,255,0.08)] transition-all duration-300"
549+
>
550+
<h4 className="text-xs font-bold uppercase tracking-widest text-[#A1A1AA] mb-5">
551+
@{username}
552+
</h4>
553+
554+
{/* Additions Bar */}
555+
<div className="mb-4">
556+
<div className="flex justify-between items-center mb-2">
557+
<span className="flex items-center gap-1.5 text-xs font-medium text-emerald-500">
558+
<Plus size={12} /> Lines Added
559+
</span>
560+
<motion.span
561+
initial={{ opacity: 0 }}
562+
whileInView={{ opacity: 1 }}
563+
className="text-sm font-bold text-emerald-500"
564+
>
565+
+{loc.add.toLocaleString()}
566+
</motion.span>
567+
</div>
568+
<div className="w-full h-3 bg-gray-100 dark:bg-[#111] rounded-full overflow-hidden border border-black/5 dark:border-[rgba(255,255,255,0.04)]">
569+
<motion.div
570+
initial={{ width: 0 }}
571+
whileInView={{ width: `${(loc.add / maxAdd) * 100}%` }}
572+
viewport={{ once: true }}
573+
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1], delay: 0.2 }}
574+
className="h-full rounded-full bg-gradient-to-r from-emerald-500 to-emerald-400 shadow-[0_0_10px_rgba(16,185,129,0.3)]"
575+
/>
576+
</div>
577+
</div>
578+
579+
{/* Deletions Bar */}
580+
<div className="mb-5">
581+
<div className="flex justify-between items-center mb-2">
582+
<span className="flex items-center gap-1.5 text-xs font-medium text-rose-500">
583+
<Minus size={12} /> Lines Deleted
584+
</span>
585+
<motion.span
586+
initial={{ opacity: 0 }}
587+
whileInView={{ opacity: 1 }}
588+
className="text-sm font-bold text-rose-500"
589+
>
590+
-{loc.del.toLocaleString()}
591+
</motion.span>
592+
</div>
593+
<div className="w-full h-3 bg-gray-100 dark:bg-[#111] rounded-full overflow-hidden border border-black/5 dark:border-[rgba(255,255,255,0.04)]">
594+
<motion.div
595+
initial={{ width: 0 }}
596+
whileInView={{ width: `${(loc.del / maxDel) * 100}%` }}
597+
viewport={{ once: true }}
598+
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1], delay: 0.4 }}
599+
className="h-full rounded-full bg-gradient-to-r from-rose-500 to-rose-400 shadow-[0_0_10px_rgba(244,63,94,0.3)]"
600+
/>
601+
</div>
602+
</div>
603+
604+
{/* Net Impact */}
605+
<div className="flex items-center justify-between p-3 rounded-xl bg-gray-50 dark:bg-[#111] border border-black/5 dark:border-[rgba(255,255,255,0.06)]">
606+
<span className="text-[9px] font-medium text-[#A1A1AA] uppercase tracking-widest">
607+
Net Impact
608+
</span>
609+
<motion.span
610+
initial={{ scale: 0 }}
611+
whileInView={{ scale: 1 }}
612+
viewport={{ once: true }}
613+
transition={{ type: 'spring', stiffness: 300, damping: 20, delay: 0.6 }}
614+
className={`text-lg font-black tracking-tight ${loc.net >= 0 ? 'text-emerald-500' : 'text-rose-500'}`}
615+
>
616+
{loc.net >= 0 ? '+' : ''}
617+
{loc.net.toLocaleString()}
618+
</motion.span>
619+
</div>
620+
</motion.div>
621+
))}
622+
</div>
623+
</div>
624+
);
625+
}
626+
501627
/* ── main component ───────────────────────────────────────────────────── */
502628

503629
export default function CompareClient() {
@@ -768,12 +894,27 @@ export default function CompareClient() {
768894
valueA={d1.profile.stats.followers}
769895
valueB={d2.profile.stats.followers}
770896
/>
897+
<StatBattle
898+
label="Pull Requests"
899+
icon={GitPullRequest}
900+
valueA={d1.stats.totalPRs || 0}
901+
valueB={d2.stats.totalPRs || 0}
902+
/>
903+
<StatBattle
904+
label="Issues"
905+
icon={CircleDot}
906+
valueA={d1.stats.totalIssues || 0}
907+
valueB={d2.stats.totalIssues || 0}
908+
/>
771909
</div>
772910
</div>
773911

774912
{/* Coding Habits Showdown */}
775913
<CodingHabitShowdown user1={d1} user2={d2} />
776914

915+
{/* Code Volume Showdown */}
916+
<CodeVolumeShowdown user1={d1} user2={d2} />
917+
777918
{/* Language Comparison */}
778919
<LanguageComparison
779920
langsA={d1.languages}

lib/github.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,8 @@ interface GitHubGraphQLResponse {
222222
data?: {
223223
user: {
224224
contributionsCollection: {
225+
totalPullRequestContributions: number;
226+
totalIssueContributions: number;
225227
contributionCalendar: ContributionCalendar;
226228
commitContributionsByRepository: RepoContribution[];
227229
};
@@ -482,6 +484,8 @@ async function fetchContributionsUncached(
482484
query($login: String!, $from: DateTime, $to: DateTime) {
483485
user(login: $login) {
484486
contributionsCollection(from: $from, to: $to) {
487+
totalPullRequestContributions
488+
totalIssueContributions
485489
contributionCalendar {
486490
totalContributions
487491
weeks {
@@ -557,12 +561,17 @@ async function fetchContributionsUncached(
557561
};
558562
}
559563

564+
let totalPRs = data.data.user.contributionsCollection?.totalPullRequestContributions || 0;
565+
let totalIssues = data.data.user.contributionsCollection?.totalIssueContributions || 0;
566+
560567
if (isDeltaSync && cached) {
561568
calendar = mergeCalendars(
562569
cached.calendar,
563570
calendar,
564571
data.data.user.contributionsCollection.contributionCalendar.totalContributions
565572
);
573+
totalPRs += cached.totalPRs || 0;
574+
totalIssues += cached.totalIssues || 0;
566575
}
567576
// Inject deterministic Lines of Code (LoC) approximation
568577
// Since GitHub's contributionCalendar doesn't provide native LoC metrics,
@@ -604,13 +613,17 @@ async function fetchContributionsUncached(
604613
{
605614
calendar,
606615
repoContributions,
616+
totalPRs,
617+
totalIssues,
607618
},
608619
LONG_CACHE_TTL
609620
);
610621
}
611622
return {
612623
calendar,
613624
repoContributions,
625+
totalPRs,
626+
totalIssues,
614627
};
615628
}
616629

@@ -1277,6 +1290,9 @@ export async function getFullDashboardData(username: string, options: FetchOptio
12771290
peakStreak: streakStats.longestStreak,
12781291
totalContributions: streakStats.totalContributions,
12791292
codingHabit: getDeterministicHabit(profileData.login),
1293+
totalPRs: calendarResult.status === 'fulfilled' ? (calendarResult.value.totalPRs ?? 0) : 0,
1294+
totalIssues:
1295+
calendarResult.status === 'fulfilled' ? (calendarResult.value.totalIssues ?? 0) : 0,
12801296
},
12811297
languages,
12821298
activity: buildActivityMap(allDays),

types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export interface RepoContribution {
9595
export interface ExtendedContributionData {
9696
calendar: ContributionCalendar;
9797
repoContributions: RepoContribution[];
98+
totalPRs?: number;
99+
totalIssues?: number;
98100
isOfflineFallback?: boolean;
99101
}
100102

0 commit comments

Comments
 (0)