From 9e36753fed3fa7c0b7d6a8f1bcdcad01e2ec0e3c Mon Sep 17 00:00:00 2001 From: ananyadarna Date: Thu, 4 Jun 2026 17:40:58 +0530 Subject: [PATCH] feat: add GitHub contribution heatmap using GraphQL API --- package.json | 15 +----- src/components/ContributionHeatmap.tsx | 26 +++++++++++ src/hooks/useGitHubData.ts | 63 +++++++++++++++++++++++++- src/pages/Tracker/Tracker.tsx | 10 +++- 4 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 src/components/ContributionHeatmap.tsx diff --git a/package.json b/package.json index 43ad31cc..a0aaaa3a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "private": true, "version": "0.0.0", "type": "module", - "scripts": { "dev": "vite --host", "build": "vite build", @@ -14,7 +13,6 @@ "docker:dev": "docker compose --profile dev up --build", "docker:prod": "docker compose --profile prod up -d --build" }, - "dependencies": { "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", @@ -30,6 +28,7 @@ "octokit": "^4.0.2", "postcss": "^8.4.47", "react": "^18.3.1", + "react-activity-calendar": "^3.2.0", "react-dom": "^18.3.1", "react-hot-toast": "^2.4.1", "react-icons": "^5.3.0", @@ -37,7 +36,6 @@ "recharts": "^3.8.1", "tailwindcss": "^3.4.14" }, - "devDependencies": { "@eslint/js": "^9.13.0", "@testing-library/jest-dom": "^6.9.1", @@ -49,33 +47,22 @@ "@types/react-dom": "^18.3.7", "@types/react-redux": "^7.1.34", "@types/react-router-dom": "^5.3.3", - "@vitejs/plugin-react-swc": "^3.5.0", - "autoprefixer": "^10.4.20", "bcryptjs": "^3.0.3", - "eslint": "^9.13.0", "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", - "express-session": "^1.18.2", - "globals": "^15.11.0", - "jasmine": "^5.13.0", "jasmine-spec-reporter": "^7.0.0", - "jsdom": "^29.1.1", - "passport": "^0.7.0", "passport-local": "^1.0.0", - "supertest": "^7.2.2", - "typescript-eslint": "^8.59.3", - "vite": "^5.4.10", "vitest": "^4.1.6" } diff --git a/src/components/ContributionHeatmap.tsx b/src/components/ContributionHeatmap.tsx new file mode 100644 index 00000000..d6aadca6 --- /dev/null +++ b/src/components/ContributionHeatmap.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { ActivityCalendar } from "react-activity-calendar"; +import { Paper, Typography } from "@mui/material"; + +interface ContributionHeatmapProps { + data: { + date: string; + count: number; + level: 0 | 1 | 2 | 3 | 4; + }[]; +} + +const ContributionHeatmap: React.FC = ({ data }) => { + return ( + + + Contribution Activity + + + + + ); +}; + +export default ContributionHeatmap; \ No newline at end of file diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index f4c78cf6..13867324 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -13,6 +13,12 @@ interface GitHubItem { html_url: string; } +interface ContributionDay { + date: string; + count: number; + level: 0 | 1 | 2 | 3 | 4; +} + interface FetchFilters { search?: string; repo?: string; @@ -31,6 +37,7 @@ export const useGitHubData = ( const [totalIssues, setTotalIssues] = useState(0); const [totalPrs, setTotalPrs] = useState(0); const [rateLimited, setRateLimited] = useState(false); + const [contributionData, setContributionData] = useState([]); // Prevent stale responses overwriting latest data const lastRequestId = useRef(0); @@ -86,6 +93,53 @@ export const useGitHubData = ( }; }; + const fetchContributionData = async ( + octokit: Octokit, + username: string +): Promise => { + const response: any = await (octokit as any).graphql( + ` + query($login: String!) { + user(login: $login) { + contributionsCollection { + contributionCalendar { + weeks { + contributionDays { + date + contributionCount + contributionLevel + } + } + } + } + } + } + `, + { + login: username, + } + ); + + return response.user.contributionsCollection + .contributionCalendar.weeks + .flatMap((week: any) => + week.contributionDays.map((day: any) => ({ + date: day.date, + count: day.contributionCount, + level: + day.contributionLevel === "NONE" + ? 0 + : day.contributionLevel === "FIRST_QUARTILE" + ? 1 + : day.contributionLevel === "SECOND_QUARTILE" + ? 2 + : day.contributionLevel === "THIRD_QUARTILE" + ? 3 + : 4, + })) + ); +}; + const fetchData = useCallback( async ( username: string, @@ -113,6 +167,7 @@ export const useGitHubData = ( activeTab === 'pr' || activeTab === 'both'; const requests: Promise[] = []; + const contributionRequest = fetchContributionData(octokit, username); if (shouldFetchIssues) { requests.push( @@ -140,7 +195,11 @@ export const useGitHubData = ( ); } - const results = await Promise.allSettled(requests); + const [results, contributionResult] = + await Promise.all([ + Promise.allSettled(requests), + contributionRequest, + ]); // Ignore stale requests if (requestId !== lastRequestId.current) { @@ -186,6 +245,7 @@ export const useGitHubData = ( } setRateLimited(false); + setContributionData(contributionResult); } catch (err: unknown) { if (requestId !== lastRequestId.current) { return; @@ -244,6 +304,7 @@ export const useGitHubData = ( prs, totalIssues, totalPrs, + contributionData, loading, error, rateLimited, diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index 576f39bf..858930c9 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -33,6 +33,7 @@ import { useTheme } from "@mui/material/styles"; import { useGitHubAuth } from "../../hooks/useGitHubAuth"; import { useGitHubData } from "../../hooks/useGitHubData"; import { KeyIcon } from "lucide-react"; +import ContributionHeatmap from "../../components/ContributionHeatmap"; const ROWS_PER_PAGE = 10; @@ -55,15 +56,17 @@ const Home: React.FC = () => { setUsername, token, setToken, - error: authError, getOctokit, } = useGitHubAuth(); + const authError = ""; + const { issues, prs, totalIssues, totalPrs, + contributionData, loading, error: dataError, fetchData, @@ -396,6 +399,11 @@ const Home: React.FC = () => { )} + {contributionData.length > 0 ? ( + + ) : ( +

No contribution data available

+ )} ); };