added a git heatmap from my forgejo instance to the website
This commit is contained in:
parent
6f3333b6be
commit
c745cfc304
4 changed files with 193 additions and 1 deletions
19
src/app/api/heatmap/route.ts
Normal file
19
src/app/api/heatmap/route.ts
Normal file
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
121
src/app/components/ContributionHeatmap.tsx
Normal file
121
src/app/components/ContributionHeatmap.tsx
Normal file
|
|
@ -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<string, number>();
|
||||||
|
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 <div style={{ color: "var(--gray)", fontSize: "0.85rem" }}>Loading contributions...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div style={{ color: "var(--gray)", fontSize: "0.85rem" }}>Failed to load contributions.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "10px", width: "100%" }}>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<div style={{ fontSize: "0.85rem", color: "var(--fg)" }}>
|
||||||
|
<span style={{ color: "var(--gray)" }}>martin@git.dvorakmortonxd.dev</span>:~$ echo "<span style={{ color: "var(--green)" }}>{totalContribs}</span> contributions in the last year"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="heatmap-container">
|
||||||
|
{weeks.map((week, wIdx) => (
|
||||||
|
<div key={wIdx} className="heatmap-week">
|
||||||
|
{week.map((day, dIdx) => (
|
||||||
|
<div
|
||||||
|
key={dIdx}
|
||||||
|
className="heatmap-cell"
|
||||||
|
data-level={day.level >= 0 ? day.level : undefined}
|
||||||
|
style={{ visibility: day.level === -1 ? "hidden" : "visible" }}
|
||||||
|
title={day.level >= 0 ? `${day.contribs} contributions on ${day.date}` : undefined}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", justifyContent: "flex-end", alignItems: "center", gap: "4px", fontSize: "0.75rem", color: "var(--gray)" }}>
|
||||||
|
<span>Less</span>
|
||||||
|
<div className="heatmap-cell" data-level="0"></div>
|
||||||
|
<div className="heatmap-cell" data-level="1"></div>
|
||||||
|
<div className="heatmap-cell" data-level="2"></div>
|
||||||
|
<div className="heatmap-cell" data-level="3"></div>
|
||||||
|
<div className="heatmap-cell" data-level="4"></div>
|
||||||
|
<span>More</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -958,7 +958,7 @@ body {
|
||||||
|
|
||||||
.gym-grid {
|
.gym-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -1003,3 +1003,44 @@ body {
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
font-family: var(--font-custom-mono), monospace;
|
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); }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Metadata } from 'next';
|
||||||
import JokerLogo from "./components/JokerLogo";
|
import JokerLogo from "./components/JokerLogo";
|
||||||
import ServiceButton from "./components/ServiceButton";
|
import ServiceButton from "./components/ServiceButton";
|
||||||
import SlBackground from "./components/SlBackground";
|
import SlBackground from "./components/SlBackground";
|
||||||
|
import ContributionHeatmap from "./components/ContributionHeatmap";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'dvorakmortonxd.dev',
|
title: 'dvorakmortonxd.dev',
|
||||||
|
|
@ -339,6 +340,16 @@ export default function Home() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Commits Panel */}
|
||||||
|
<div className="commits-section" style={{ marginTop: '16px' }}>
|
||||||
|
<div className="tui-panel">
|
||||||
|
<span className="tui-panel-label" style={{ color: 'var(--green)' }}>├─ COMMITS.GIT</span>
|
||||||
|
<div className="commits-content" style={{ padding: '24px 16px 16px' }}>
|
||||||
|
<ContributionHeatmap />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Services Panel */}
|
{/* Services Panel */}
|
||||||
<div className="services-section">
|
<div className="services-section">
|
||||||
<div className="tui-panel">
|
<div className="tui-panel">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue