"use client"; import React, { useEffect, useState } from "react"; interface HeatmapData { timestamp: number; contributions: number; } interface Activity { op_type: string; content?: string | null; created: string; repo?: { full_name?: string; }; } interface ActivityPayload { Commits?: Array<{ Message?: string; }>; HeadCommit?: { Message?: string; }; } export default function ContributionHeatmap() { const [weeks, setWeeks] = useState<{ date: string; contribs: number; level: number }[][]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [totalContribs, setTotalContribs] = useState(0); const [activities, setActivities] = useState([]); const [hoveredCell, setHoveredCell] = useState<{ date: string; contribs: number } | null>(null); useEffect(() => { async function fetchHeatmap() { try { const res = await fetch("/api/heatmap"); if (!res.ok) { throw new Error("Failed to fetch"); } const data: HeatmapData[] = await res.json(); const map = new Map(); let total = 0; let maxContribs = 0; data.forEach((item) => { const d = new Date(item.timestamp * 1000); const dateStr = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); const currentDayTotal = (map.get(dateStr) || 0) + item.contributions; map.set(dateStr, currentDayTotal); if (currentDayTotal > maxContribs) maxContribs = currentDayTotal; total += item.contributions; }); const endDate = new Date(); endDate.setHours(0, 0, 0, 0); const startDate = new Date(endDate); startDate.setFullYear(startDate.getFullYear() - 1); startDate.setDate(startDate.getDate() - startDate.getDay()); const newWeeks = []; let currentWeek = []; for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { const dateStr = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); const contribs = map.get(dateStr) || 0; let level = 0; if (contribs > 0) { level = 1; if (contribs > maxContribs * 0.25) level = 2; if (contribs > maxContribs * 0.5) level = 3; if (contribs > maxContribs * 0.75) level = 4; } currentWeek.push({ date: dateStr, contribs, level }); if (currentWeek.length === 7) { newWeeks.push(currentWeek); currentWeek = []; } } if (currentWeek.length > 0) { newWeeks.push(currentWeek); } setWeeks(newWeeks); setTotalContribs(total); setLoading(false); } catch (err) { console.error("Failed to load heatmap:", err); setError(true); setLoading(false); } } async function fetchActivities() { try { const res = await fetch("/api/activities"); if (res.ok) { const data: Activity[] = await res.json(); // Filter to just standard push/commit events const valid = data.filter((d) => d.op_type === "commit_repo" || d.op_type === "push_repo").slice(0, 3); setActivities(valid); } } catch (err) { console.error("Failed to load activities:", err); } } fetchHeatmap(); fetchActivities(); }, []); if (loading) { return
Loading contributions...
; } if (error) { return
Failed to load contributions.
; } return (
martin@git.dvorakmortonxd.dev:~$ echo "{totalContribs} contributions in the last year"
{totalContribs} contributions in the last year
{weeks.map((week, wIdx) => (
{week.map((day, dIdx) => (
= 0 ? day.level : undefined} title={day.level >= 0 ? `${day.contribs} contributions on ${day.date}` : undefined} onMouseEnter={() => day.level >= 0 && setHoveredCell({ date: day.date, contribs: day.contribs })} onMouseLeave={() => setHoveredCell(null)} >
))}
))}
{hoveredCell ? ( {hoveredCell.contribs} contributions on {hoveredCell.date} ) : ( Hover over a day to see contributions )}
Less
More
{activities.length > 0 && (
martin@git.dvorakmortonxd.dev:~$ git log -n 3 #displays my 3 latest contributions
{activities.map((act, idx) => { let msg = "No message"; try { if (act.content) { const parsed = JSON.parse(act.content) as ActivityPayload; if (parsed.Commits && parsed.Commits.length > 0) { msg = parsed.Commits[0].Message?.split("\n")[0] ?? msg; } else if (parsed.HeadCommit?.Message) { msg = parsed.HeadCommit.Message.split("\n")[0]; } } } catch {} const created = new Date(act.created); return (
repo: {act.repo?.full_name || "unknown"} {created.toLocaleDateString()}-{created.toLocaleTimeString()}
{msg}
); })}
)}
); }