|
2 | 2 |
|
3 | 3 | import { useState, useEffect, useCallback } from 'react'; |
4 | 4 | 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'; |
5 | 14 | import { useSearchParams, useRouter } from 'next/navigation'; |
6 | 15 | import { |
7 | 16 | Search, |
@@ -624,6 +633,156 @@ function CodeVolumeShowdown({ user1, user2 }: { user1: CompareUserData; user2: C |
624 | 633 | ); |
625 | 634 | } |
626 | 635 |
|
| 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 | + |
627 | 786 | /* ── main component ───────────────────────────────────────────────────── */ |
628 | 787 |
|
629 | 788 | export default function CompareClient() { |
@@ -915,6 +1074,9 @@ export default function CompareClient() { |
915 | 1074 | {/* Code Volume Showdown */} |
916 | 1075 | <CodeVolumeShowdown user1={d1} user2={d2} /> |
917 | 1076 |
|
| 1077 | + {/* Developer Skills Radar */} |
| 1078 | + <DeveloperSkillsRadar user1={d1} user2={d2} /> |
| 1079 | + |
918 | 1080 | {/* Language Comparison */} |
919 | 1081 | <LanguageComparison |
920 | 1082 | langsA={d1.languages} |
|
0 commit comments