Skip to content

feat(profile): add contribution heatmap to profile pages#21

Open
caiodomingues wants to merge 1 commit intoisabellaherman:mainfrom
caiodomingues:main
Open

feat(profile): add contribution heatmap to profile pages#21
caiodomingues wants to merge 1 commit intoisabellaherman:mainfrom
caiodomingues:main

Conversation

@caiodomingues
Copy link
Copy Markdown
Contributor

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.
  • components/ContributionHeatmap.tsx: New client component rendering a GitHub-style heatmap grid:
    • SVG-based grid (columns = weeks, rows = days Mon–Sun)
    • Green intensity palette with light/dark mode support
    • Tooltip on hover showing XP and date
    • Month labels along the top, day labels on the left
    • Horizontally scrollable on mobile
    • Total XP label
  • app/[username]/page.tsx: Fetches contribution calendar server-side and renders the heatmap in a full-width card below the existing 2-column layout

Copilot AI review requested due to automatic review settings March 7, 2026 01:56
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 7, 2026

@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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) plus Contribution* calendar interfaces.
  • Introduce components/ContributionHeatmap.tsx client component to render the calendar as an SVG heatmap with labels/tooltip and light/dark palettes.
  • Update app/[username]/page.tsx to 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.

Comment thread app/[username]/page.tsx
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>
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
<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}

Copilot uses AI. Check for mistakes.
Comment thread app/[username]/page.tsx
import BadgeWall from '@/components/BadgeSystem';
import UserLinksDisplay from '@/components/UserLinksDisplay';
import Link from 'next/link';
import { GitHubService } from '@/lib/github-service';
Copy link

Copilot AI Mar 7, 2026

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).

Suggested change
import { GitHubService } from '@/lib/github-service';
import GitHubService from '@/lib/github-service';

Copilot uses AI. Check for mistakes.
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;
}

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
// 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);

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +10
totalXp: number;
}

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
totalXp: number;
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +71
<h4 className="text-sm font-medium text-muted-foreground text-center">
{(calendar.totalContributions * 5).toLocaleString()} XP in the last year
</h4>
Copy link

Copilot AI Mar 7, 2026

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants