-
Notifications
You must be signed in to change notification settings - Fork 20
feat(profile): add contribution heatmap to profile pages #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,8 @@ import UserStats from '@/components/UserStats'; | |||||||||||||||||||||||||
| import BadgeWall from '@/components/BadgeSystem'; | ||||||||||||||||||||||||||
| import UserLinksDisplay from '@/components/UserLinksDisplay'; | ||||||||||||||||||||||||||
| import Link from 'next/link'; | ||||||||||||||||||||||||||
| import { GitHubService } from '@/lib/github-service'; | ||||||||||||||||||||||||||
| import ContributionHeatmap from '@/components/ContributionHeatmap'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| interface ProfilePageProps { | ||||||||||||||||||||||||||
| params: Promise<{ | ||||||||||||||||||||||||||
|
|
@@ -59,6 +61,9 @@ export default async function ProfilePage({ params }: ProfilePageProps) { | |||||||||||||||||||||||||
| checkAndUpdateContributorStatus(user.githubUsername).catch(console.error); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const githubService = new GitHubService(process.env.GITHUB_TOKEN); | ||||||||||||||||||||||||||
| const contributionCalendar = await githubService.getContributionCalendar(user.githubUsername); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| <main className="min-h-screen bg-background"> | ||||||||||||||||||||||||||
| <div className="container mx-auto px-4 py-8"> | ||||||||||||||||||||||||||
|
|
@@ -204,6 +209,13 @@ export default async function ProfilePage({ params }: ProfilePageProps) { | |||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {/* Contribution Heatmap */} | ||||||||||||||||||||||||||
| <div className="max-w-6xl mx-auto mt-8"> | ||||||||||||||||||||||||||
| <div className="bg-card rounded-xl p-6"> | ||||||||||||||||||||||||||
| <ContributionHeatmap calendar={contributionCalendar} totalXp={user.xp} /> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
|
Comment on lines
+214
to
+218
|
||||||||||||||||||||||||||
| <div className="max-w-6xl mx-auto mt-8"> | |
| <div className="bg-card rounded-xl p-6"> | |
| <ContributionHeatmap calendar={contributionCalendar} totalXp={user.xp} /> | |
| </div> | |
| </div> | |
| {contributionCalendar?.weeks?.length ? ( | |
| <div className="max-w-6xl mx-auto mt-8"> | |
| <div className="bg-card rounded-xl p-6"> | |
| <ContributionHeatmap calendar={contributionCalendar} totalXp={user.xp} /> | |
| </div> | |
| </div> | |
| ) : null} |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,211 @@ | ||||||||
| 'use client'; | ||||||||
|
|
||||||||
| import { useState } from 'react'; | ||||||||
| import type { ContributionCalendar } from '@/lib/github-service'; | ||||||||
|
|
||||||||
| interface ContributionHeatmapProps { | ||||||||
| calendar: ContributionCalendar; | ||||||||
| totalXp: number; | ||||||||
| } | ||||||||
|
|
||||||||
|
Comment on lines
+8
to
+10
|
||||||||
| totalXp: number; | |
| } | |
| } |
Copilot
AI
Mar 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The heatmap labels/tooltip treat contributionCount and totalContributions as XP by multiplying by 5, but in this codebase XP is calculated with different weights (e.g., PRs=40, issues=10, reviews=15 in GitHubService.getWeeklyXp, and lifetime XP in lib/xp-calculator.ts). Since contributionCalendar is an aggregate count across contribution types, this will display incorrect XP values; consider either showing “contributions” (not XP) or extending the GraphQL query to fetch per-type counts per day and compute XP consistently.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import { usePathname } from "next/navigation"; | ||||||||||||||||||||||||||
| import { useCallback } from "react"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| interface UserLink { | ||||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||||
|
|
@@ -16,10 +17,6 @@ interface UserLinksDisplayProps { | |||||||||||||||||||||||||
| export default function UserLinksDisplay({ userLinks }: UserLinksDisplayProps) { | ||||||||||||||||||||||||||
| const pathname = usePathname() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!userLinks || userLinks.length === 0) { | ||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Build an augmented URL with UTM params and a `ref` identifying the profile | ||||||||||||||||||||||||||
| // This runs only when the user clicks (client-side) to avoid SSR/window access. | ||||||||||||||||||||||||||
| const buildAugmentedUrl = useCallback((originalUrl: string) => { | ||||||||||||||||||||||||||
|
|
@@ -120,6 +117,10 @@ export default function UserLinksDisplay({ userLinks }: UserLinksDisplayProps) { | |||||||||||||||||||||||||
| // Sort links by order | ||||||||||||||||||||||||||
| const sortedLinks = [...userLinks].sort((a, b) => a.order - b.order); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!userLinks || userLinks.length === 0) { | ||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
Comment on lines
117
to
+123
|
||||||||||||||||||||||||||
| // Sort links by order | |
| const sortedLinks = [...userLinks].sort((a, b) => a.order - b.order); | |
| if (!userLinks || userLinks.length === 0) { | |
| return null; | |
| } | |
| if (userLinks.length === 0) { | |
| return null; | |
| } | |
| // Sort links by order | |
| const sortedLinks = [...userLinks].sort((a, b) => a.order - b.order); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most other call sites import this service as the default export (
import GitHubService from '@/lib/github-service'). To keep imports consistent across the codebase, consider switching this to a default import (or standardize on named imports everywhere).