feat(profile): add contribution heatmap to profile pages#21
feat(profile): add contribution heatmap to profile pages#21caiodomingues wants to merge 1 commit intoisabellaherman:mainfrom
Conversation
|
@caiodomingues is attempting to deploy a commit to the isabellaherman's projects Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Pull request overview
Adds a GitHub-style contribution heatmap to profile pages by querying GitHub’s GraphQL contributions calendar and rendering an interactive SVG grid on the user profile.
Changes:
- Add
GitHubService.getContributionCalendar(username)plusContribution*calendar interfaces. - Introduce
components/ContributionHeatmap.tsxclient component to render the calendar as an SVG heatmap with labels/tooltip and light/dark palettes. - Update
app/[username]/page.tsxto fetch the calendar server-side and display the heatmap beneath the existing profile layout.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
lib/github-service.ts |
Adds GraphQL fetch method and types for contribution calendar data. |
components/ContributionHeatmap.tsx |
New client component that renders the contribution calendar as a heatmap grid with tooltip and legend. |
app/[username]/page.tsx |
Fetches contribution calendar and renders the heatmap in a new card section on profiles. |
components/UserLinksDisplay.tsx |
Refactors link augmentation to use useCallback and reorders the empty-state return. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <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> |
There was a problem hiding this comment.
ContributionHeatmap returns null when calendar.weeks is empty, but this page always renders the surrounding card. On API errors / missing token this will produce an empty section with padding. Consider conditionally rendering the whole “Contribution Heatmap” card only when contributionCalendar.weeks.length > 0 (or render an explicit fallback message).
| <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} |
| import BadgeWall from '@/components/BadgeSystem'; | ||
| import UserLinksDisplay from '@/components/UserLinksDisplay'; | ||
| import Link from 'next/link'; | ||
| import { GitHubService } from '@/lib/github-service'; |
There was a problem hiding this comment.
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).
| import { GitHubService } from '@/lib/github-service'; | |
| import GitHubService from '@/lib/github-service'; |
| // Sort links by order | ||
| const sortedLinks = [...userLinks].sort((a, b) => a.order - b.order); | ||
|
|
||
| if (!userLinks || userLinks.length === 0) { | ||
| return null; | ||
| } | ||
|
|
There was a problem hiding this comment.
sortedLinks is computed before the empty-state guard. This does unnecessary work when userLinks is empty and can throw if this component is ever called with an undefined/null value at runtime. Move the early return above the sort (and the !userLinks part is redundant given the prop type).
| // 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); |
| totalXp: number; | ||
| } | ||
|
|
There was a problem hiding this comment.
totalXp is declared in ContributionHeatmapProps and passed from the profile page, but the component doesn’t use it. Either remove this prop to avoid confusion, or use it for the displayed totals so the component API matches the call site.
| totalXp: number; | |
| } | |
| } |
| <h4 className="text-sm font-medium text-muted-foreground text-center"> | ||
| {(calendar.totalContributions * 5).toLocaleString()} XP in the last year | ||
| </h4> |
There was a problem hiding this comment.
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.
Summary
Add a GitHub-style contribution heatmap to user profile pages, showing the last year of activity with XP values.
Changes
lib/github-service.ts: New getContributionCalendar(username) method that queries GitHub's GraphQL API for contributionsCollection.contributionCalendar (per-day contribution data for the last ~1 year). Added ContributionDay, ContributionWeek, and ContributionCalendar interfaces.app/[username]/page.tsx: Fetches contribution calendar server-side and renders the heatmap in a full-width card below the existing 2-column layout