Initial: Gitea heatmap sidecar with private contributions
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
{{/*
|
||||
Drop this into $GITEA_CUSTOM/templates/user/profile.tmpl as part of a full
|
||||
profile.tmpl override. Place near the existing heatmap section (search for
|
||||
"heatmap" in the upstream profile.tmpl for your Gitea version).
|
||||
|
||||
The {{if eq .ContextUser.LowerName ...}} guard means only the named user
|
||||
gets the override; everyone else keeps the stock heatmap.
|
||||
|
||||
The HEATMAP_BASE_URL placeholder must be replaced at install time with the
|
||||
reverse-proxied URL of the sidecar service, e.g. https://heatmap.dideric.is
|
||||
*/}}
|
||||
|
||||
{{if eq .ContextUser.LowerName "didericis"}}
|
||||
<div class="ui attached segment private-heatmap-wrap">
|
||||
<h4 class="ui header">Contributions (incl. private)</h4>
|
||||
<div id="private-heatmap"
|
||||
data-user="{{.ContextUser.LowerName}}"
|
||||
data-source="HEATMAP_BASE_URL/heatmap/{{.ContextUser.LowerName}}.json">
|
||||
<noscript>JavaScript is required to render this heatmap.</noscript>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.private-heatmap-wrap #private-heatmap {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(53, 11px);
|
||||
grid-template-rows: repeat(7, 11px);
|
||||
gap: 2px;
|
||||
overflow-x: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.private-heatmap-wrap .cell {
|
||||
border-radius: 2px;
|
||||
background: var(--color-secondary, #161b22);
|
||||
}
|
||||
.private-heatmap-wrap .cell[data-level="1"] { background: #0e4429; }
|
||||
.private-heatmap-wrap .cell[data-level="2"] { background: #006d32; }
|
||||
.private-heatmap-wrap .cell[data-level="3"] { background: #26a641; }
|
||||
.private-heatmap-wrap .cell[data-level="4"] { background: #39d353; }
|
||||
.private-heatmap-wrap .cell.future { background: transparent; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(async () => {
|
||||
const el = document.getElementById('private-heatmap');
|
||||
if (!el) return;
|
||||
|
||||
let data = [];
|
||||
try {
|
||||
const res = await fetch(el.dataset.source, { credentials: 'omit' });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
data = await res.json();
|
||||
} catch (e) {
|
||||
el.textContent = 'Heatmap unavailable.';
|
||||
console.warn('private-heatmap fetch failed:', e);
|
||||
return;
|
||||
}
|
||||
|
||||
const byDate = Object.fromEntries(data.map(d => [d.date, d.count]));
|
||||
const bucket = c =>
|
||||
c === 0 ? 0 : c < 3 ? 1 : c < 6 ? 2 : c < 10 ? 3 : 4;
|
||||
|
||||
const today = new Date();
|
||||
today.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
// Anchor rightmost column to the most recent Saturday (>= today).
|
||||
const end = new Date(today);
|
||||
while (end.getUTCDay() !== 6) end.setUTCDate(end.getUTCDate() + 1);
|
||||
|
||||
const start = new Date(end);
|
||||
start.setUTCDate(start.getUTCDate() - (53 * 7 - 1));
|
||||
|
||||
const fmt = d => d.toISOString().slice(0, 10);
|
||||
const frag = document.createDocumentFragment();
|
||||
|
||||
for (let i = 0; i < 53 * 7; i++) {
|
||||
const d = new Date(start);
|
||||
d.setUTCDate(d.getUTCDate() + i);
|
||||
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'cell';
|
||||
cell.style.gridColumn = Math.floor(i / 7) + 1;
|
||||
cell.style.gridRow = (i % 7) + 1;
|
||||
|
||||
if (d > today) {
|
||||
cell.classList.add('future');
|
||||
} else {
|
||||
const c = byDate[fmt(d)] || 0;
|
||||
cell.dataset.level = String(bucket(c));
|
||||
// Native browser tooltip. For richer ones, swap in Tippy.js etc.
|
||||
cell.title = `${fmt(d)}: ${c} contribution${c === 1 ? '' : 's'}`;
|
||||
}
|
||||
frag.appendChild(cell);
|
||||
}
|
||||
el.appendChild(frag);
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user