diff --git a/app/[username]/page.tsx b/app/[username]/page.tsx
index 1a0b453..361aab3 100644
--- a/app/[username]/page.tsx
+++ b/app/[username]/page.tsx
@@ -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 (
@@ -204,6 +209,13 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
+
+ {/* Contribution Heatmap */}
+
);
diff --git a/components/ContributionHeatmap.tsx b/components/ContributionHeatmap.tsx
new file mode 100644
index 0000000..325fdbf
--- /dev/null
+++ b/components/ContributionHeatmap.tsx
@@ -0,0 +1,211 @@
+'use client';
+
+import { useState } from 'react';
+import type { ContributionCalendar } from '@/lib/github-service';
+
+interface ContributionHeatmapProps {
+ calendar: ContributionCalendar;
+ totalXp: number;
+}
+
+const DAY_LABELS = ['Mon', '', 'Wed', '', 'Fri', '', ''];
+
+const LIGHT_FILLS = ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'];
+const DARK_FILLS = ['#161b22', '#0e4429', '#006d32', '#26a641', '#39d353'];
+
+function getIntensityLevel(count: number): number {
+ if (count === 0) return 0;
+ if (count <= 3) return 1;
+ if (count <= 6) return 2;
+ if (count <= 9) return 3;
+ return 4;
+}
+
+function formatDate(dateStr: string): string {
+ const date = new Date(dateStr + 'T00:00:00');
+ return date.toLocaleDateString('en-US', {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ });
+}
+
+export default function ContributionHeatmap({ calendar }: ContributionHeatmapProps) {
+ const [tooltip, setTooltip] = useState<{
+ text: string;
+ x: number;
+ y: number;
+ } | null>(null);
+
+ if (!calendar.weeks.length) return null;
+
+ // Build month labels from first day of each week
+ const monthLabels: { label: string; colIndex: number }[] = [];
+ let lastMonth = -1;
+ calendar.weeks.forEach((week, i) => {
+ const firstDay = week.contributionDays[0];
+ if (!firstDay) return;
+ const month = new Date(firstDay.date + 'T00:00:00').getMonth();
+ if (month !== lastMonth) {
+ monthLabels.push({
+ label: new Date(firstDay.date + 'T00:00:00').toLocaleDateString('en-US', {
+ month: 'short',
+ }),
+ colIndex: i,
+ });
+ lastMonth = month;
+ }
+ });
+
+ const cellSize = 12;
+ const cellGap = 2;
+ const step = cellSize + cellGap;
+ const dayLabelWidth = 32;
+ const headerHeight = 20;
+
+ return (
+
+
+ {(calendar.totalContributions * 5).toLocaleString()} XP in the last year
+
+
+
+
+
+
+ {/* Legend */}
+
+
Less
+
+ {LIGHT_FILLS.map((_, i) => (
+
+
+
+ ))}
+
+
More
+
+
+ {/* Tooltip */}
+ {tooltip && (
+
+ {tooltip.text}
+
+ )}
+
+ );
+}
diff --git a/components/UserLinksDisplay.tsx b/components/UserLinksDisplay.tsx
index b9f2818..83402be 100644
--- a/components/UserLinksDisplay.tsx
+++ b/components/UserLinksDisplay.tsx
@@ -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;
+ }
+
return (
{
+ try {
+ const query = `
+ query($username: String!) {
+ user(login: $username) {
+ contributionsCollection {
+ contributionCalendar {
+ totalContributions
+ weeks {
+ contributionDays {
+ date
+ contributionCount
+ color
+ }
+ }
+ }
+ }
+ }
+ }
+ `;
+
+ const response: Record = await this.graphqlWithAuth(query, { username });
+ const user = response.user as Record | undefined;
+
+ if (!user) {
+ throw new Error(`User ${username} not found`);
+ }
+
+ const collection = user.contributionsCollection as Record;
+ const calendar = collection.contributionCalendar as ContributionCalendar;
+
+ return calendar;
+ } catch (error) {
+ console.error('Error fetching contribution calendar:', error);
+ return { totalContributions: 0, weeks: [] };
+ }
+ }
+
async getRateLimit() {
try {
const { data } = await this.octokit.rest.rateLimit.get();