(function () { 'use strict'; var MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; var I18N = { en: { summary: function(t, g, h) { return t + ' commits in the last year \xb7 ' + g + ' from gitolite \xb7 ' + h + ' from GitHub'; }, tooltip_gitolite: 'gitolite', tooltip_github: 'GitHub', tooltip_total: 'total', }, it: { summary: function(t, g, h) { return t + ' commit nell’ultimo anno \xb7 ' + g + ' da gitolite \xb7 ' + h + ' da GitHub'; }, tooltip_gitolite: 'gitolite', tooltip_github: 'GitHub', tooltip_total: 'totale', }, }; function level(count) { if (count === 0) return 0; if (count <= 3) return 0.35; if (count <= 9) return 0.60; if (count <= 19) return 0.80; return 1.0; } function hexWithOpacity(hex, opacity) { hex = hex.replace('#', '').trim(); if (hex.length === 3) { hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; } var r = parseInt(hex.substring(0, 2), 16); var g = parseInt(hex.substring(2, 4), 16); var b = parseInt(hex.substring(4, 6), 16); return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')'; } function buildCellStyle(gitoliteCount, githubCount, rootStyle) { var gLevel = level(gitoliteCount); var hLevel = level(githubCount); if (gLevel === 0 && hLevel === 0) return ''; var accent2 = rootStyle.getPropertyValue('--accent2').trim() || '#00ff88'; var accent = rootStyle.getPropertyValue('--accent').trim() || '#a855f7'; var border = rootStyle.getPropertyValue('--border').trim() || '#182840'; var leftColor = gLevel === 0 ? border : hexWithOpacity(accent2, gLevel); var rightColor = hLevel === 0 ? border : hexWithOpacity(accent, hLevel); return 'linear-gradient(90deg, ' + leftColor + ' 50%, ' + rightColor + ' 50%)'; } function formatDate(dateStr) { // dateStr: "YYYY-MM-DD" var parts = dateStr.split('-'); var d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; var day = d.getDate(); return months[d.getMonth()] + ' ' + day + ', ' + d.getFullYear(); } function buildGraph(container, data) { var cellSize = parseInt(container.dataset.cellSize, 10) || 8; var lang = container.dataset.lang || 'en'; var ariaLabel = container.dataset.ariaLabel || 'Contribution graph'; var i18n = I18N[lang] || I18N.en; var gap = cellSize <= 6 ? 1 : 2; var rootStyle = getComputedStyle(document.documentElement); // Sort dates oldest → newest var dates = Object.keys(data).sort(); // Build 53-col × 7-row grid // Find the day-of-week of the oldest date (Sun=0) var firstParts = dates[0].split('-'); var firstDate = new Date(parseInt(firstParts[0]), parseInt(firstParts[1]) - 1, parseInt(firstParts[2])); var startDow = firstDate.getDay(); // 0=Sun // Assign each date to (col, row) // col 0 starts at row=startDow var cells = []; // { col, row, date, gitolite, github } for (var i = 0; i < dates.length; i++) { var slot = startDow + i; var col = Math.floor(slot / 7); var row = slot % 7; cells.push({ col: col, row: row, date: dates[i], gitolite: data[dates[i]].gitolite || 0, github: data[dates[i]].github || 0 }); } var totalCols = cells[cells.length - 1].col + 1; // Month labels: track first col per month var monthCols = {}; // "YYYY-MM" → col index for (var j = 0; j < cells.length; j++) { var c = cells[j]; var ym = c.date.substring(0, 7); if (!(ym in monthCols)) monthCols[ym] = c.col; } // Build figure var figure = document.createElement('figure'); figure.setAttribute('role', 'img'); figure.setAttribute('aria-label', ariaLabel); var wrap = document.createElement('div'); wrap.className = 'contrib-graph-wrap'; // Month label row var monthRow = document.createElement('div'); monthRow.className = 'contrib-month-row'; monthRow.style.setProperty('--contrib-cell-size', cellSize + 'px'); monthRow.style.setProperty('--contrib-gap', gap + 'px'); monthRow.style.gridTemplateColumns = 'repeat(' + totalCols + ', ' + cellSize + 'px)'; var monthKeys = Object.keys(monthCols).sort(); for (var m = 0; m < monthKeys.length; m++) { var ym2 = monthKeys[m]; var colIdx = monthCols[ym2]; var monthNum = parseInt(ym2.split('-')[1], 10) - 1; var span = document.createElement('span'); span.className = 'contrib-month-label'; span.textContent = MONTHS[monthNum]; span.style.gridColumn = (colIdx + 1).toString(); monthRow.appendChild(span); } // Graph grid var grid = document.createElement('div'); grid.className = 'contrib-grid'; grid.style.setProperty('--contrib-cell-size', cellSize + 'px'); grid.style.setProperty('--contrib-gap', gap + 'px'); grid.style.gridTemplateColumns = 'repeat(' + totalCols + ', ' + cellSize + 'px)'; // Build a map of col+row → cell data for quick lookup var cellMap = {}; for (var k = 0; k < cells.length; k++) { cellMap[cells[k].col + '_' + cells[k].row] = cells[k]; } // Fill grid col by col, row by row for (var col2 = 0; col2 < totalCols; col2++) { for (var row2 = 0; row2 < 7; row2++) { var div = document.createElement('div'); div.className = 'contrib-cell'; var key = col2 + '_' + row2; var cellData = cellMap[key]; if (cellData) { var bg = buildCellStyle(cellData.gitolite, cellData.github, rootStyle); if (bg) div.style.background = bg; var label = formatDate(cellData.date) + ': ' + cellData.gitolite + ' ' + i18n.tooltip_gitolite + ', ' + cellData.github + ' ' + i18n.tooltip_github; div.setAttribute('aria-label', label); div.dataset.gitolite = cellData.gitolite; div.dataset.github = cellData.github; div.dataset.date = cellData.date; } grid.appendChild(div); } } wrap.appendChild(monthRow); wrap.appendChild(grid); figure.appendChild(wrap); // Totals for summary var totalCommits = 0, totalGitolite = 0, totalGithub = 0; for (var n = 0; n < cells.length; n++) { totalGitolite += cells[n].gitolite; totalGithub += cells[n].github; } totalCommits = totalGitolite + totalGithub; // Summary figcaption var caption = document.createElement('figcaption'); caption.className = 'contrib-summary'; caption.textContent = i18n.summary(totalCommits, totalGitolite, totalGithub); figure.appendChild(caption); container.appendChild(figure); container.style.display = ''; // Tooltip (inject once) var tooltip = document.querySelector('.contrib-tooltip'); if (!tooltip) { tooltip = document.createElement('div'); tooltip.className = 'contrib-tooltip'; tooltip.setAttribute('aria-hidden', 'true'); document.body.appendChild(tooltip); } // Event delegation on grid grid.addEventListener('mouseover', function(e) { var cell = e.target.closest('.contrib-cell'); if (!cell || !cell.dataset.date) return; var g = parseInt(cell.dataset.gitolite, 10); var h = parseInt(cell.dataset.github, 10); tooltip.innerHTML = formatDate(cell.dataset.date) + '
' + i18n.tooltip_gitolite + ': ' + g + '
' + i18n.tooltip_github + ': ' + h + '
' + i18n.tooltip_total + ': ' + (g + h); tooltip.classList.add('visible'); }); grid.addEventListener('mousemove', function(e) { var x = e.clientX + 12; var y = e.clientY + 12; var tw = tooltip.offsetWidth; var th = tooltip.offsetHeight; if (x + tw > window.innerWidth - 8) x = e.clientX - tw - 12; if (y + th > window.innerHeight - 8) y = e.clientY - th - 12; tooltip.style.left = x + 'px'; tooltip.style.top = y + 'px'; }); grid.addEventListener('mouseout', function(e) { if (!e.relatedTarget || !grid.contains(e.relatedTarget)) { tooltip.classList.remove('visible'); } }); } document.addEventListener('DOMContentLoaded', function() { var containers = document.querySelectorAll('.contrib-container'); if (!containers.length) return; fetch('https://danix.xyz/contributions.json') .then(function(res) { if (!res.ok) throw new Error('fetch failed'); return res.json(); }) .then(function(data) { containers.forEach(function(container) { buildGraph(container, data); }); }) .catch(function() { // hide silently — containers already display:none }); }); }());