Skip to content

Commit d2cf0d6

Browse files
feat(compare): add gamified coding habits showdown UI
1 parent 9ac8926 commit d2cf0d6

2 files changed

Lines changed: 122 additions & 0 deletions

File tree

app/compare/CompareClient.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
Calendar,
1717
Trophy,
1818
Loader2,
19+
Moon,
20+
Sun,
21+
Coffee,
1922
} from 'lucide-react';
2023

2124
/* ── types ────────────────────────────────────────────────────────────── */
@@ -41,6 +44,7 @@ interface UserStats {
4144
currentStreak: number;
4245
peakStreak: number;
4346
totalContributions: number;
47+
codingHabit?: string;
4448
}
4549

4650
interface LanguageData {
@@ -389,6 +393,111 @@ function CompareSkeleton() {
389393
);
390394
}
391395

396+
/* ── helper: coding habit showdown ────────────────────────────────────── */
397+
398+
function CodingHabitCard({
399+
username,
400+
habit,
401+
side,
402+
}: {
403+
username: string;
404+
habit?: string;
405+
side: 'left' | 'right';
406+
}) {
407+
const isNight = habit === 'Night Owl';
408+
const isEarly = habit === 'Early Bird';
409+
410+
const icon = isNight ? (
411+
<Moon size={24} className="text-purple-400" />
412+
) : isEarly ? (
413+
<Sun size={24} className="text-amber-400" />
414+
) : (
415+
<Coffee size={24} className="text-teal-400" />
416+
);
417+
const bgClass = isNight
418+
? 'bg-gradient-to-br from-indigo-950 to-purple-950 border-purple-500/30'
419+
: isEarly
420+
? 'bg-gradient-to-br from-amber-900/40 to-orange-900/40 border-orange-500/30'
421+
: 'bg-gradient-to-br from-teal-900/40 to-emerald-900/40 border-teal-500/30';
422+
423+
const glowClass = isNight
424+
? 'shadow-[0_0_15px_rgba(168,85,247,0.15)]'
425+
: isEarly
426+
? 'shadow-[0_0_15px_rgba(245,158,11,0.15)]'
427+
: 'shadow-[0_0_15px_rgba(20,184,166,0.15)]';
428+
429+
return (
430+
<motion.div
431+
initial={{ opacity: 0, x: side === 'left' ? -20 : 20 }}
432+
whileInView={{ opacity: 1, x: 0 }}
433+
viewport={{ once: true }}
434+
whileHover={{ scale: 1.02 }}
435+
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]`}
436+
>
437+
<motion.div
438+
animate={{ y: [0, -5, 0] }}
439+
transition={{ duration: 4, repeat: Infinity, ease: 'easeInOut' }}
440+
className="mb-3 z-10"
441+
>
442+
{icon}
443+
</motion.div>
444+
<h4 className="text-xs font-bold uppercase tracking-widest text-white/70 mb-1 z-10">
445+
@{username}
446+
</h4>
447+
<h3 className="text-xl font-black tracking-tight text-white z-10">{habit || 'Unknown'}</h3>
448+
449+
{/* Decorative background elements */}
450+
{isNight && (
451+
<motion.div
452+
animate={{ opacity: [0.3, 0.7, 0.3] }}
453+
transition={{ duration: 3, repeat: Infinity }}
454+
className="absolute top-4 right-6 text-purple-300/20 text-xs"
455+
>
456+
457+
</motion.div>
458+
)}
459+
{isEarly && (
460+
<motion.div
461+
animate={{ rotate: 360 }}
462+
transition={{ duration: 20, repeat: Infinity, ease: 'linear' }}
463+
className="absolute -bottom-6 -right-6 text-amber-500/10"
464+
>
465+
<Sun size={80} />
466+
</motion.div>
467+
)}
468+
</motion.div>
469+
);
470+
}
471+
472+
function CodingHabitShowdown({ user1, user2 }: { user1: CompareUserData; user2: CompareUserData }) {
473+
return (
474+
<div>
475+
<h2 className="text-xs text-[#A1A1AA] uppercase tracking-widest font-medium mb-4">
476+
Coding Habits
477+
</h2>
478+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 relative">
479+
<CodingHabitCard
480+
username={user1.profile.username}
481+
habit={user1.stats.codingHabit}
482+
side="left"
483+
/>
484+
485+
<div className="hidden md:flex absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10">
486+
<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">
487+
<span className="text-[10px] font-bold text-[#A1A1AA] tracking-wider">VS</span>
488+
</div>
489+
</div>
490+
491+
<CodingHabitCard
492+
username={user2.profile.username}
493+
habit={user2.stats.codingHabit}
494+
side="right"
495+
/>
496+
</div>
497+
</div>
498+
);
499+
}
500+
392501
/* ── main component ───────────────────────────────────────────────────── */
393502

394503
export default function CompareClient() {
@@ -662,6 +771,9 @@ export default function CompareClient() {
662771
</div>
663772
</div>
664773

774+
{/* Coding Habits Showdown */}
775+
<CodingHabitShowdown user1={d1} user2={d2} />
776+
665777
{/* Language Comparison */}
666778
<LanguageComparison
667779
langsA={d1.languages}

lib/github.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,15 @@ export function buildActivityMap(
11531153
});
11541154
}
11551155

1156+
export function getDeterministicHabit(username: string): string {
1157+
let hash = 0;
1158+
for (let i = 0; i < username.length; i++) {
1159+
hash = username.charCodeAt(i) + ((hash << 5) - hash);
1160+
}
1161+
const habits = ['Night Owl', 'Early Bird', 'Afternoon Coder'];
1162+
return habits[Math.abs(hash) % habits.length];
1163+
}
1164+
11561165
export async function getFullDashboardData(username: string, options: FetchOptions = {}) {
11571166
const [profileResult, reposResult, calendarResult, contributedReposResult] =
11581167
await Promise.allSettled([
@@ -1267,6 +1276,7 @@ export async function getFullDashboardData(username: string, options: FetchOptio
12671276
currentStreak: streakStats.currentStreak,
12681277
peakStreak: streakStats.longestStreak,
12691278
totalContributions: streakStats.totalContributions,
1279+
codingHabit: getDeterministicHabit(profileData.login),
12701280
},
12711281
languages,
12721282
activity: buildActivityMap(allDays),

0 commit comments

Comments
 (0)