From b167d5b943818e0b3aa5ca346c884f5f50c7a449 Mon Sep 17 00:00:00 2001 From: Diksha Dabhole Date: Tue, 19 May 2026 22:06:43 +0530 Subject: [PATCH 1/2] feat: add contributor activity timeline to public profiles --- src/app/[handle]/page.tsx | 39 +++++++++- src/components/activity-heatmap.tsx | 115 ++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/components/activity-heatmap.tsx diff --git a/src/app/[handle]/page.tsx b/src/app/[handle]/page.tsx index 1c75591..9e7710e 100644 --- a/src/app/[handle]/page.tsx +++ b/src/app/[handle]/page.tsx @@ -4,6 +4,7 @@ import { cacheGet, cacheSet } from '@/lib/cache'; import Link from 'next/link'; import { ExternalLink, ArrowLeft } from 'lucide-react'; import { CopyButton } from '@/components/copy-button'; +import { ActivityHeatmap } from '@/components/activity-heatmap'; export const revalidate = 300; @@ -59,6 +60,11 @@ type ActiveTask = { difficulty: string | null; }; +type ActivityDay = { + date: string; + count: number; +}; + type ProfileData = { profileId: string; githubHandle: string; @@ -74,10 +80,11 @@ type ProfileData = { timeline: TimelineEvent[]; orgs: OrgEntry[]; activeTasks: ActiveTask[]; + activityHistory: ActivityDay[]; }; async function loadProfileData(handle: string): Promise { - const cacheKey = `profile:v2:${handle}`; + const cacheKey = `profile:v3:${handle}`; const cached = await cacheGet(cacheKey); if (cached) { const { getPublicStreak } = await import('@/app/actions/streak'); @@ -95,6 +102,10 @@ async function loadProfileData(handle: string): Promise { if (!profile) return null; + const ninetyDaysAgo = new Date(); + ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90); + ninetyDaysAgo.setHours(0, 0, 0, 0); + // Fetch all data in parallel const [ prsResult, @@ -103,6 +114,7 @@ async function loadProfileData(handle: string): Promise { claimedRecsResult, recentPRsResult, recentRecsResult, + activityResult, ] = await Promise.all([ // Merged PRs count service @@ -148,6 +160,14 @@ async function loadProfileData(handle: string): Promise { .in('status', ['claimed', 'completed']) .order('claimed_at', { ascending: false }) .limit(5), + + // Public activity from xp_events for the past 90 days + service + .from('xp_events') + .select('created_at') + .eq('user_id', profile.id) + .gte('created_at', ninetyDaysAgo.toISOString()) + .in('source', ['recommended_merge', 'unrecommended_merge', 'help_review']), ]); const prsMerged = prsResult.count ?? 0; @@ -249,6 +269,17 @@ async function loadProfileData(handle: string): Promise { const { getPublicStreak } = await import('@/app/actions/streak'); const { days: streakDays } = await getPublicStreak(profile.id); + // Group events by day in UTC + const activityMap: Record = {}; + for (const event of activityResult.data ?? []) { + const dateStr = new Date(event.created_at).toISOString().slice(0, 10); + activityMap[dateStr] = (activityMap[dateStr] || 0) + 1; + } + const activityHistory = Object.entries(activityMap).map(([date, count]) => ({ + date, + count, + })); + const data: ProfileData = { profileId: profile.id, githubHandle: profile.github_handle, @@ -264,6 +295,7 @@ async function loadProfileData(handle: string): Promise { timeline, orgs, activeTasks, + activityHistory, }; await cacheSet(cacheKey, data, 300); @@ -487,6 +519,11 @@ export default async function PublicProfile({ params }: { params: { handle: stri + + {/* Activity Heatmap */} +
+ +
{/* Right: Orgs + Active Tasks */} diff --git a/src/components/activity-heatmap.tsx b/src/components/activity-heatmap.tsx new file mode 100644 index 0000000..da56fb9 --- /dev/null +++ b/src/components/activity-heatmap.tsx @@ -0,0 +1,115 @@ +'use client'; + +type ActivityDay = { + date: string; + count: number; +}; + +interface ActivityHeatmapProps { + activityHistory: ActivityDay[]; +} + +export function ActivityHeatmap({ activityHistory }: ActivityHeatmapProps) { + // Convert history array to a lookup map + const activityMap = new Map(); + for (const item of activityHistory) { + activityMap.set(item.date, item.count); + } + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // Display exactly 13 columns (weeks), each with 7 rows (Sunday to Saturday). + // Find the Sunday of the week that was 12 weeks ago. + const currentDayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, etc. + const startOfCurrentWeek = new Date(today); + startOfCurrentWeek.setDate(today.getDate() - currentDayOfWeek); + + const startDate = new Date(startOfCurrentWeek); + startDate.setDate(startOfCurrentWeek.getDate() - 12 * 7); // 12 weeks ago Sunday + + // Generate 91 days (13 weeks) + const days: { dateStr: string; count: number; isFuture: boolean; label: string }[] = []; + const runningDate = new Date(startDate); + + for (let i = 0; i < 91; i++) { + const ymd = runningDate.toISOString().slice(0, 10); + const count = activityMap.get(ymd) || 0; + const isFuture = runningDate > today; + const formattedDate = runningDate.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + + days.push({ + dateStr: ymd, + count, + isFuture, + label: isFuture ? '' : `${count} contribution${count === 1 ? '' : 's'} on ${formattedDate}`, + }); + + runningDate.setDate(runningDate.getDate() + 1); + } + + // Calculate stats for the summary + const totalContributions = activityHistory.reduce((sum, item) => sum + item.count, 0); + + function getColorClass(count: number, isFuture: boolean) { + if (isFuture) return 'bg-transparent cursor-default'; + if (count === 0) return 'bg-[#161b22] border border-[#21262d] hover:border-zinc-500'; + if (count === 1) + return 'bg-emerald-900/60 border border-emerald-800/40 hover:border-emerald-600'; + if (count <= 3) return 'bg-emerald-800 border border-emerald-700/60 hover:border-emerald-500'; + if (count <= 5) return 'bg-emerald-600 border border-emerald-500/80 hover:border-emerald-400'; + return 'bg-emerald-400 border border-emerald-300 hover:border-white'; + } + + return ( +
+
+
+

+ Activity Timeline (Last 90 Days) +

+

+ {totalContributions} Contributions +

+
+
+ Less +
+
+
+
+
+ More +
+
+ +
+ {/* Weekday labels column */} +
+
+
Mon
+
+
Wed
+
+
Fri
+
+
+ + {/* Heatmap Grid */} +
+ {days.map((day) => ( +
+ ))} +
+
+
+ ); +} From 289afdbede824258ca1e7c8ee79c142c5d40c5aa Mon Sep 17 00:00:00 2001 From: Diksha Dabhole Date: Sat, 23 May 2026 21:51:20 +0530 Subject: [PATCH 2/2] feat: extend activity heatmap to full year --- src/app/[handle]/page.tsx | 10 +++++----- src/components/activity-heatmap.tsx | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/[handle]/page.tsx b/src/app/[handle]/page.tsx index 9e7710e..a9ad1a0 100644 --- a/src/app/[handle]/page.tsx +++ b/src/app/[handle]/page.tsx @@ -102,9 +102,9 @@ async function loadProfileData(handle: string): Promise { if (!profile) return null; - const ninetyDaysAgo = new Date(); - ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90); - ninetyDaysAgo.setHours(0, 0, 0, 0); + const oneYearAgo = new Date(); + oneYearAgo.setDate(oneYearAgo.getDate() - 365); + oneYearAgo.setHours(0, 0, 0, 0); // Fetch all data in parallel const [ @@ -161,12 +161,12 @@ async function loadProfileData(handle: string): Promise { .order('claimed_at', { ascending: false }) .limit(5), - // Public activity from xp_events for the past 90 days + // Public activity from xp_events for the past year service .from('xp_events') .select('created_at') .eq('user_id', profile.id) - .gte('created_at', ninetyDaysAgo.toISOString()) + .gte('created_at', oneYearAgo.toISOString()) .in('source', ['recommended_merge', 'unrecommended_merge', 'help_review']), ]); diff --git a/src/components/activity-heatmap.tsx b/src/components/activity-heatmap.tsx index da56fb9..d67f84f 100644 --- a/src/components/activity-heatmap.tsx +++ b/src/components/activity-heatmap.tsx @@ -19,20 +19,20 @@ export function ActivityHeatmap({ activityHistory }: ActivityHeatmapProps) { const today = new Date(); today.setHours(0, 0, 0, 0); - // Display exactly 13 columns (weeks), each with 7 rows (Sunday to Saturday). - // Find the Sunday of the week that was 12 weeks ago. + // Display exactly 53 columns (weeks), each with 7 rows (Sunday to Saturday). + // Find the Sunday of the week that was 52 weeks ago. const currentDayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, etc. const startOfCurrentWeek = new Date(today); startOfCurrentWeek.setDate(today.getDate() - currentDayOfWeek); const startDate = new Date(startOfCurrentWeek); - startDate.setDate(startOfCurrentWeek.getDate() - 12 * 7); // 12 weeks ago Sunday + startDate.setDate(startOfCurrentWeek.getDate() - 52 * 7); // 52 weeks ago Sunday - // Generate 91 days (13 weeks) + // Generate 371 days (53 weeks) const days: { dateStr: string; count: number; isFuture: boolean; label: string }[] = []; const runningDate = new Date(startDate); - for (let i = 0; i < 91; i++) { + for (let i = 0; i < 371; i++) { const ymd = runningDate.toISOString().slice(0, 10); const count = activityMap.get(ymd) || 0; const isFuture = runningDate > today; @@ -70,7 +70,7 @@ export function ActivityHeatmap({ activityHistory }: ActivityHeatmapProps) {

- Activity Timeline (Last 90 Days) + Activity Timeline (Last Year)

{totalContributions} Contributions