From c745cfc3049c60f6fa5e3db6fe4d57db14e004e9 Mon Sep 17 00:00:00 2001 From: dvorakmortonxd Date: Fri, 15 May 2026 11:37:46 +0200 Subject: [PATCH] added a git heatmap from my forgejo instance to the website --- src/app/api/heatmap/route.ts | 19 ++++ src/app/components/ContributionHeatmap.tsx | 121 +++++++++++++++++++++ src/app/page.css | 43 +++++++- src/app/page.tsx | 11 ++ 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 src/app/api/heatmap/route.ts create mode 100644 src/app/components/ContributionHeatmap.tsx diff --git a/src/app/api/heatmap/route.ts b/src/app/api/heatmap/route.ts new file mode 100644 index 0000000..e1f0643 --- /dev/null +++ b/src/app/api/heatmap/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + try { + const res = await fetch("https://git.dvorakmortonxd.dev/api/v1/users/dvorakmortonxd/heatmap", { + next: { revalidate: 3600 }, // Cache for 1 hour + }); + + if (!res.ok) { + return NextResponse.json({ error: "Failed to fetch heatmap" }, { status: res.status }); + } + + const data = await res.json(); + return NextResponse.json(data); + } catch (err) { + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + diff --git a/src/app/components/ContributionHeatmap.tsx b/src/app/components/ContributionHeatmap.tsx new file mode 100644 index 0000000..44fc49a --- /dev/null +++ b/src/app/components/ContributionHeatmap.tsx @@ -0,0 +1,121 @@ +"use client"; + +import React, { useEffect, useState } from "react"; + +interface HeatmapData { + timestamp: number; + contributions: number; +} + +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); + + 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; + 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"); + map.set(dateStr, item.contributions); + 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 >= 3) level = 2; + if (contribs >= 6) level = 3; + if (contribs >= 10) level = 4; + + currentWeek.push({ date: dateStr, contribs, level }); + + if (currentWeek.length === 7) { + newWeeks.push(currentWeek); + currentWeek = []; + } + } + if (currentWeek.length > 0) { + while (currentWeek.length < 7) { + currentWeek.push({ date: "", contribs: -1, level: -1 }); + } + newWeeks.push(currentWeek); + } + + setWeeks(newWeeks); + setTotalContribs(total); + setLoading(false); + } catch (err) { + console.error("Failed to load heatmap:", err); + setError(true); + setLoading(false); + } + } + + fetchHeatmap(); + }, []); + + if (loading) { + return
Loading contributions...
; + } + + if (error) { + return
Failed to load contributions.
; + } + + return ( +
+
+
+ martin@git.dvorakmortonxd.dev:~$ echo "{totalContribs} contributions in the last year" +
+
+
+ {weeks.map((week, wIdx) => ( +
+ {week.map((day, dIdx) => ( +
= 0 ? day.level : undefined} + style={{ visibility: day.level === -1 ? "hidden" : "visible" }} + title={day.level >= 0 ? `${day.contribs} contributions on ${day.date}` : undefined} + > +
+ ))} +
+ ))} +
+
+ Less +
+
+
+
+
+ More +
+
+ ); +} diff --git a/src/app/page.css b/src/app/page.css index 3a19d0a..3b51da5 100644 --- a/src/app/page.css +++ b/src/app/page.css @@ -958,7 +958,7 @@ body { .gym-grid { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; padding: 24px; text-align: center; @@ -1003,3 +1003,44 @@ body { color: var(--fg); font-family: var(--font-custom-mono), monospace; } + +/* ============================================ + HEATMAP + ============================================ */ +.heatmap-container { + display: flex; + overflow-x: auto; + gap: 4px; + justify-content: space-between; + width: 100%; + scrollbar-width: thin; + scrollbar-color: var(--gray) transparent; +} + +.heatmap-week { + display: flex; + flex-direction: column; + gap: 4px; +} + +.heatmap-cell { + width: 12px; + height: 12px; + border-radius: 0; /* squares not rounded */ + background-color: var(--bg-hard); + transition: transform 0.1s ease; +} + +.heatmap-cell:hover { + transform: scale(1.2); + z-index: 10; + box-shadow: 0 0 6px rgba(184, 187, 38, 0.5); +} + +.heatmap-cell[data-level="1"] { background-color: rgba(184, 187, 38, 0.4); } +.heatmap-cell[data-level="2"] { background-color: rgba(184, 187, 38, 0.6); } +.heatmap-cell[data-level="3"] { background-color: rgba(184, 187, 38, 0.8); } +.heatmap-cell[data-level="4"] { background-color: rgba(184, 187, 38, 1); } + + + diff --git a/src/app/page.tsx b/src/app/page.tsx index ae05b4b..930cfa7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,6 +4,7 @@ import { Metadata } from 'next'; import JokerLogo from "./components/JokerLogo"; import ServiceButton from "./components/ServiceButton"; import SlBackground from "./components/SlBackground"; +import ContributionHeatmap from "./components/ContributionHeatmap"; export const metadata: Metadata = { title: 'dvorakmortonxd.dev', @@ -339,6 +340,16 @@ export default function Home() { + {/* Commits Panel */} +
+
+ ├─ COMMITS.GIT +
+ +
+
+
+ {/* Services Panel */}