在 GitHub profile 页面以年份展示自用户创建以来所有的 contributions
// ==UserScript==
// @name Show all contributions by year in the GitHub profile
// @name:zh-CN 在 GitHub profile 页面以年份展示用户所有的贡献
// @namespace https://github.com/kang8
// @version 0.0.5
// @description Show all contributions by year since the user was created in the GitHub profile page
// @description:zh-CN 在 GitHub profile 页面以年份展示自用户创建以来所有的 contributions
// @author kang8
// @match https://github.com/*
// @grant none
// @run-at document-end
// @license MIT
// @homepage https://github.com/kang8/.dotfiles/tree/master/tampermonkey-scripts#show-all-contributions-by-year-in-github-profile
// @icon https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
// ==/UserScript==
(async function () {
'use strict';
const contributionsLastYearSelector =
'div.js-yearly-contributions > div:nth-child(1)';
await waitForElement(contributionsLastYearSelector);
const pathArr = location.pathname.split('/');
/**
* match:
* - https://github.com/kang8
* - https://github.com/kang8/
*/
if (
pathArr.length === 2 ||
(pathArr.length === 3 && pathArr[2] === '') && pathArr[1] !== ''
) {
const userId = pathArr[1];
const contributionsLastYear = document.querySelector(
contributionsLastYearSelector,
);
const contributionsAllYearsDiv = document.createElement('div');
contributionsAllYearsDiv.className = 'position-relative border';
contributionsAllYearsDiv.id = 'contributions-all-years';
contributionsLastYear.after(contributionsAllYearsDiv);
const details = document.createElement('details');
details.className = 'py-3';
details.innerHTML =
'<summary class="px-3">All contributions by year</summary>';
contributionsAllYearsDiv.append(details);
const totalYearsCreated =
document.querySelectorAll('div.js-profile-timeline-year-list > ul > li')
.length;
const thisYear = new Date().getFullYear();
for (let index = 0; index < totalYearsCreated; index++) {
const currentYear = thisYear - index;
fetch(
`/users/${userId}/contributions?from=${currentYear}-1-01&to=${currentYear}-12-31`,
{
method: 'GET',
},
)
.then((response) => response.text())
.then((data) => {
const contributionsDiv = document.createRange()
.createContextualFragment(data);
const contributionInfo =
contributionsDiv.querySelector('div > div:nth-child(1) > h2')
.textContent;
const svgCalendarGraph = contributionsDiv.querySelector(
'div > div:nth-child(1) > div > div',
);
svgCalendarGraph.querySelector('div > div.float-left').innerHTML =
contributionInfo;
if (currentYear !== thisYear) {
svgCalendarGraph.classList.add('border-top');
}
insertAndOrderToDetails(svgCalendarGraph, details, currentYear);
});
}
}
})();
/**
* @param {Element} graph
* @param {HTMLDetailsElement} details
* @param {number} currentYear
*/
function insertAndOrderToDetails(graph, details, currentYear) {
const contributionsCalendarAllYear = Array.from(
details.querySelectorAll('div.js-calendar-graph'),
);
if (contributionsCalendarAllYear.length === 0) {
details.append(graph);
} else if (contributionsCalendarAllYear.length === 1) {
const firstCalendarYear = getYearByContributionsCalendar(
contributionsCalendarAllYear[0],
);
if (currentYear > firstCalendarYear) {
contributionsCalendarAllYear[0].before(graph);
} else {
contributionsCalendarAllYear[0].after(graph);
}
} else {
for (
let index = 0;
index < contributionsCalendarAllYear.length - 1;
index++
) {
const currentYearInCalendar = getYearByContributionsCalendar(
contributionsCalendarAllYear[index],
);
const nextYearInCalendar = getYearByContributionsCalendar(
contributionsCalendarAllYear[index + 1],
);
if (currentYear > currentYearInCalendar) {
contributionsCalendarAllYear[index].before(graph);
break;
} else if (currentYear > nextYearInCalendar) {
contributionsCalendarAllYear[index].after(graph);
break;
} else if (
index === contributionsCalendarAllYear.length - 2 &&
currentYear < nextYearInCalendar
) {
contributionsCalendarAllYear[contributionsCalendarAllYear.length - 1]
.after(graph);
}
}
}
}
/**
* @param {number|Element} childNode
* @returns {number}
*/
function getYearByContributionsCalendar(childNode) {
return childNode.querySelector('div.float-left').textContent.replace(
/\s/g,
'',
).slice(-4);
}
/**
* @param {string} selector
*/
function waitForElement(selector) {
return new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
}