Skip to content

Commit b145b11

Browse files
feat(compare): add animated Developer Skills Radar Chart using recharts
1 parent 38b7a4b commit b145b11

3 files changed

Lines changed: 443 additions & 4 deletions

File tree

app/compare/CompareClient.tsx

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
import { useState, useEffect, useCallback } from 'react';
44
import { motion, AnimatePresence } from 'framer-motion';
5+
import {
6+
Radar,
7+
RadarChart,
8+
PolarGrid,
9+
PolarAngleAxis,
10+
PolarRadiusAxis,
11+
ResponsiveContainer,
12+
Tooltip,
13+
} from 'recharts';
514
import { useSearchParams, useRouter } from 'next/navigation';
615
import {
716
Search,
@@ -624,6 +633,156 @@ function CodeVolumeShowdown({ user1, user2 }: { user1: CompareUserData; user2: C
624633
);
625634
}
626635

636+
/* ── helper: developer skills radar chart ──────────────────────────────── */
637+
638+
function normalizeSkill(value: number, max: number): number {
639+
if (max <= 0) return 0;
640+
return Math.min(Math.round((value / max) * 100), 100);
641+
}
642+
643+
function DeveloperSkillsRadar({
644+
user1,
645+
user2,
646+
}: {
647+
user1: CompareUserData;
648+
user2: CompareUserData;
649+
}) {
650+
// Compute raw values for each skill dimension
651+
const calcLoC = (activity: ActivityData[]) => {
652+
let add = 0;
653+
activity.forEach((d) => {
654+
add += d.locAdditions || 0;
655+
});
656+
return add;
657+
};
658+
659+
const raw1 = {
660+
volume: user1.stats.totalContributions + calcLoC(user1.activity),
661+
consistency: user1.stats.currentStreak * 2 + user1.stats.peakStreak,
662+
impact: user1.profile.stats.stars * 3 + user1.profile.stats.followers,
663+
collaboration: (user1.stats.totalPRs || 0) * 2 + (user1.stats.totalIssues || 0),
664+
versatility: user1.languages.length * 20,
665+
};
666+
667+
const raw2 = {
668+
volume: user2.stats.totalContributions + calcLoC(user2.activity),
669+
consistency: user2.stats.currentStreak * 2 + user2.stats.peakStreak,
670+
impact: user2.profile.stats.stars * 3 + user2.profile.stats.followers,
671+
collaboration: (user2.stats.totalPRs || 0) * 2 + (user2.stats.totalIssues || 0),
672+
versatility: user2.languages.length * 20,
673+
};
674+
675+
// Normalize against combined max for fair comparison
676+
const maxVolume = Math.max(raw1.volume, raw2.volume, 1);
677+
const maxConsistency = Math.max(raw1.consistency, raw2.consistency, 1);
678+
const maxImpact = Math.max(raw1.impact, raw2.impact, 1);
679+
const maxCollaboration = Math.max(raw1.collaboration, raw2.collaboration, 1);
680+
const maxVersatility = Math.max(raw1.versatility, raw2.versatility, 1);
681+
682+
const radarData = [
683+
{
684+
skill: 'Volume',
685+
user1: normalizeSkill(raw1.volume, maxVolume),
686+
user2: normalizeSkill(raw2.volume, maxVolume),
687+
},
688+
{
689+
skill: 'Consistency',
690+
user1: normalizeSkill(raw1.consistency, maxConsistency),
691+
user2: normalizeSkill(raw2.consistency, maxConsistency),
692+
},
693+
{
694+
skill: 'Impact',
695+
user1: normalizeSkill(raw1.impact, maxImpact),
696+
user2: normalizeSkill(raw2.impact, maxImpact),
697+
},
698+
{
699+
skill: 'Collaboration',
700+
user1: normalizeSkill(raw1.collaboration, maxCollaboration),
701+
user2: normalizeSkill(raw2.collaboration, maxCollaboration),
702+
},
703+
{
704+
skill: 'Versatility',
705+
user1: normalizeSkill(raw1.versatility, maxVersatility),
706+
user2: normalizeSkill(raw2.versatility, maxVersatility),
707+
},
708+
];
709+
710+
return (
711+
<motion.div
712+
initial={{ opacity: 0, y: 20 }}
713+
whileInView={{ opacity: 1, y: 0 }}
714+
viewport={{ once: true }}
715+
transition={{ duration: 0.6 }}
716+
>
717+
<h2 className="text-xs text-[#A1A1AA] uppercase tracking-widest font-medium mb-4 flex items-center gap-2">
718+
<Trophy size={14} className="text-amber-400" />
719+
Developer Skills Radar
720+
</h2>
721+
<div className="p-6 rounded-xl bg-white dark:bg-[#0a0a0a] border border-black/10 dark:border-[rgba(255,255,255,0.08)]">
722+
{/* Legend */}
723+
<div className="flex justify-center gap-6 mb-4">
724+
<div className="flex items-center gap-2">
725+
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: '#8B5CF6' }} />
726+
<span className="text-xs text-[#A1A1AA] font-medium">@{user1.profile.username}</span>
727+
</div>
728+
<div className="flex items-center gap-2">
729+
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: '#06B6D4' }} />
730+
<span className="text-xs text-[#A1A1AA] font-medium">@{user2.profile.username}</span>
731+
</div>
732+
</div>
733+
734+
<ResponsiveContainer width="100%" height={380}>
735+
<RadarChart cx="50%" cy="50%" outerRadius="75%" data={radarData}>
736+
<PolarGrid stroke="rgba(161,161,170,0.15)" strokeDasharray="3 3" />
737+
<PolarAngleAxis
738+
dataKey="skill"
739+
tick={{
740+
fill: '#A1A1AA',
741+
fontSize: 11,
742+
fontWeight: 600,
743+
}}
744+
/>
745+
<PolarRadiusAxis angle={90} domain={[0, 100]} tick={false} axisLine={false} />
746+
<Radar
747+
name={user1.profile.username}
748+
dataKey="user1"
749+
stroke="#8B5CF6"
750+
fill="#8B5CF6"
751+
fillOpacity={0.25}
752+
strokeWidth={2}
753+
dot={{ r: 4, fill: '#8B5CF6', strokeWidth: 0 }}
754+
animationDuration={1200}
755+
animationEasing="ease-out"
756+
/>
757+
<Radar
758+
name={user2.profile.username}
759+
dataKey="user2"
760+
stroke="#06B6D4"
761+
fill="#06B6D4"
762+
fillOpacity={0.25}
763+
strokeWidth={2}
764+
dot={{ r: 4, fill: '#06B6D4', strokeWidth: 0 }}
765+
animationDuration={1200}
766+
animationEasing="ease-out"
767+
/>
768+
<Tooltip
769+
contentStyle={{
770+
backgroundColor: '#0a0a0a',
771+
border: '1px solid rgba(255,255,255,0.1)',
772+
borderRadius: '10px',
773+
padding: '10px 14px',
774+
fontSize: '12px',
775+
color: '#fff',
776+
}}
777+
itemStyle={{ color: '#e4e4e7', fontSize: '11px' }}
778+
/>
779+
</RadarChart>
780+
</ResponsiveContainer>
781+
</div>
782+
</motion.div>
783+
);
784+
}
785+
627786
/* ── main component ───────────────────────────────────────────────────── */
628787

629788
export default function CompareClient() {
@@ -915,6 +1074,9 @@ export default function CompareClient() {
9151074
{/* Code Volume Showdown */}
9161075
<CodeVolumeShowdown user1={d1} user2={d2} />
9171076

1077+
{/* Developer Skills Radar */}
1078+
<DeveloperSkillsRadar user1={d1} user2={d2} />
1079+
9181080
{/* Language Comparison */}
9191081
<LanguageComparison
9201082
langsA={d1.languages}

0 commit comments

Comments
 (0)