@@ -16,6 +16,14 @@ import {
1616 Calendar ,
1717 Trophy ,
1818 Loader2 ,
19+ Moon ,
20+ Sun ,
21+ Coffee ,
22+ Plus ,
23+ Minus ,
24+ Code2 ,
25+ GitPullRequest ,
26+ CircleDot ,
1927} from 'lucide-react' ;
2028
2129/* ── types ────────────────────────────────────────────────────────────── */
@@ -41,6 +49,9 @@ interface UserStats {
4149 currentStreak : number ;
4250 peakStreak : number ;
4351 totalContributions : number ;
52+ codingHabit ?: string ;
53+ totalPRs ?: number ;
54+ totalIssues ?: number ;
4455}
4556
4657interface LanguageData {
@@ -53,6 +64,8 @@ interface ActivityData {
5364 date : string ;
5465 count : number ;
5566 intensity : 0 | 1 | 2 | 3 | 4 ;
67+ locAdditions ?: number ;
68+ locDeletions ?: number ;
5669}
5770
5871interface CompareUserData {
@@ -389,6 +402,228 @@ function CompareSkeleton() {
389402 ) ;
390403}
391404
405+ /* ── helper: coding habit showdown ────────────────────────────────────── */
406+
407+ function CodingHabitCard ( {
408+ username,
409+ habit,
410+ side,
411+ } : {
412+ username : string ;
413+ habit ?: string ;
414+ side : 'left' | 'right' ;
415+ } ) {
416+ const isNight = habit === 'Night Owl' ;
417+ const isEarly = habit === 'Early Bird' ;
418+
419+ const icon = isNight ? (
420+ < Moon size = { 24 } className = "text-purple-400" />
421+ ) : isEarly ? (
422+ < Sun size = { 24 } className = "text-amber-400" />
423+ ) : (
424+ < Coffee size = { 24 } className = "text-teal-400" />
425+ ) ;
426+ const bgClass = isNight
427+ ? 'bg-gradient-to-br from-indigo-950 to-purple-950 border-purple-500/30'
428+ : isEarly
429+ ? 'bg-gradient-to-br from-amber-900/40 to-orange-900/40 border-orange-500/30'
430+ : 'bg-gradient-to-br from-teal-900/40 to-emerald-900/40 border-teal-500/30' ;
431+
432+ const glowClass = isNight
433+ ? 'shadow-[0_0_15px_rgba(168,85,247,0.15)]'
434+ : isEarly
435+ ? 'shadow-[0_0_15px_rgba(245,158,11,0.15)]'
436+ : 'shadow-[0_0_15px_rgba(20,184,166,0.15)]' ;
437+
438+ return (
439+ < motion . div
440+ initial = { { opacity : 0 , x : side === 'left' ? - 20 : 20 } }
441+ whileInView = { { opacity : 1 , x : 0 } }
442+ viewport = { { once : true } }
443+ whileHover = { { scale : 1.02 } }
444+ className = { `relative overflow-hidden p-6 rounded-2xl border ${ bgClass } ${ glowClass } transition-all duration-300 flex flex-col items-center justify-center text-center h-full min-h-[140px]` }
445+ >
446+ < motion . div
447+ animate = { { y : [ 0 , - 5 , 0 ] } }
448+ transition = { { duration : 4 , repeat : Infinity , ease : 'easeInOut' } }
449+ className = "mb-3 z-10"
450+ >
451+ { icon }
452+ </ motion . div >
453+ < h4 className = "text-xs font-bold uppercase tracking-widest text-white/70 mb-1 z-10" >
454+ @{ username }
455+ </ h4 >
456+ < h3 className = "text-xl font-black tracking-tight text-white z-10" > { habit || 'Unknown' } </ h3 >
457+
458+ { /* Decorative background elements */ }
459+ { isNight && (
460+ < motion . div
461+ animate = { { opacity : [ 0.3 , 0.7 , 0.3 ] } }
462+ transition = { { duration : 3 , repeat : Infinity } }
463+ className = "absolute top-4 right-6 text-purple-300/20 text-xs"
464+ >
465+ ★
466+ </ motion . div >
467+ ) }
468+ { isEarly && (
469+ < motion . div
470+ animate = { { rotate : 360 } }
471+ transition = { { duration : 20 , repeat : Infinity , ease : 'linear' } }
472+ className = "absolute -bottom-6 -right-6 text-amber-500/10"
473+ >
474+ < Sun size = { 80 } />
475+ </ motion . div >
476+ ) }
477+ </ motion . div >
478+ ) ;
479+ }
480+
481+ function CodingHabitShowdown ( { user1, user2 } : { user1 : CompareUserData ; user2 : CompareUserData } ) {
482+ return (
483+ < div >
484+ < h2 className = "text-xs text-[#A1A1AA] uppercase tracking-widest font-medium mb-4" >
485+ Coding Habits
486+ </ h2 >
487+ < div className = "grid grid-cols-1 md:grid-cols-2 gap-6 relative" >
488+ < CodingHabitCard
489+ username = { user1 . profile . username }
490+ habit = { user1 . stats . codingHabit }
491+ side = "left"
492+ />
493+
494+ < div className = "hidden md:flex absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10" >
495+ < div className = "w-8 h-8 rounded-full bg-white dark:bg-[#0a0a0a] border-2 border-black/10 dark:border-[rgba(255,255,255,0.08)] flex items-center justify-center shadow-xl" >
496+ < span className = "text-[10px] font-bold text-[#A1A1AA] tracking-wider" > VS</ span >
497+ </ div >
498+ </ div >
499+
500+ < CodingHabitCard
501+ username = { user2 . profile . username }
502+ habit = { user2 . stats . codingHabit }
503+ side = "right"
504+ />
505+ </ div >
506+ </ div >
507+ ) ;
508+ }
509+
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+
392627/* ── main component ───────────────────────────────────────────────────── */
393628
394629export default function CompareClient ( ) {
@@ -659,9 +894,27 @@ export default function CompareClient() {
659894 valueA = { d1 . profile . stats . followers }
660895 valueB = { d2 . profile . stats . followers }
661896 />
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+ />
662909 </ div >
663910 </ div >
664911
912+ { /* Coding Habits Showdown */ }
913+ < CodingHabitShowdown user1 = { d1 } user2 = { d2 } />
914+
915+ { /* Code Volume Showdown */ }
916+ < CodeVolumeShowdown user1 = { d1 } user2 = { d2 } />
917+
665918 { /* Language Comparison */ }
666919 < LanguageComparison
667920 langsA = { d1 . languages }
0 commit comments