GitHub Join Date

Displays user's join date/time/age.

Per 27-03-2025. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         GitHub Join Date
// @description  Displays user's join date/time/age.
// @icon         https://github.githubassets.com/favicons/favicon-dark.svg
// @version      1.0
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/misc-scripts/
// @supportURL   https://github.com/afkarxyz/misc-scripts/issues
// @license      MIT
// @match        https://github.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const ELEMENT_ID = 'userscript-join-date-display';
    const CACHE_KEY = 'githubUserJoinDatesCache_v1';
    let isProcessing = false;
    let observerDebounceTimeout = null;

    const svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" fill="currentColor" style="width: 1em; height: 1em; vertical-align: middle; margin-right: 0.25em; position: relative; top: -0.08em;" aria-hidden="true"><path d="M128 0c13.3 0 24 10.7 24 24l0 40 144 0 0-40c0-13.3 10.7-24 24-24s24 10.7 24 24l0 40 40 0c35.3 0 64 28.7 64 64l0 16 0 48-16 0-32 0-112 0L48 192l0 256c0 8.8 7.2 16 16 16l220.5 0c12.3 18.8 28 35.1 46.3 48L64 512c-35.3 0-64-28.7-64-64L0 192l0-48 0-16C0 92.7 28.7 64 64 64l40 0 0-40c0-13.3 10.7-24 24-24zM288 368a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-80c-8.8 0-16 7.2-16 16l0 64c0 8.8 7.2 16 16 16l48 0c8.8 0 16-7.2 16-16s-7.2-16-16-16l-32 0 0-48c0-8.8-7.2-16-16-16z"/></svg>`;

    function readCache() {
        try {
            const cachedData = localStorage.getItem(CACHE_KEY);
            return cachedData ? JSON.parse(cachedData) : {};
        } catch (e) {
            return {};
        }
    }

    function writeCache(cacheData) {
        try {
            localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
        } catch (e) {

        }
    }

    function getRelativeTime(dateString) {
        const joinDate = new Date(dateString); const now = new Date(); const diffInSeconds = Math.round((now - joinDate) / 1000); const minute = 60, hour = 3600, day = 86400, month = 2592000, year = 31536000; if (diffInSeconds < minute) return `less than a minute ago`; if (diffInSeconds < hour) { const m = Math.floor(diffInSeconds / minute); return `${m} ${m === 1 ? 'minute' : 'minutes'} ago`; } if (diffInSeconds < day) { const h = Math.floor(diffInSeconds / hour); return `${h} ${h === 1 ? 'hour' : 'hours'} ago`; } if (diffInSeconds < month) { const d = Math.floor(diffInSeconds / day); return `${d} ${d === 1 ? 'day' : 'days'} ago`; } if (diffInSeconds < year) { const mo = Math.floor(diffInSeconds / month); return `${mo} ${mo === 1 ? 'month' : 'months'} ago`; } const y = Math.floor(diffInSeconds / year); return `${y} ${y === 1 ? 'year' : 'years'} ago`;
    }

    async function getGitHubJoinDate(user) {
         const apiUrl = `https://api.github.com/users/${user}`; try { const response = await fetch(apiUrl); if (!response.ok) { return null; } const userData = await response.json(); return userData.created_at; } catch (error) { return null; }
    }

    function removeExistingElement() {
        const existingElement = document.getElementById(ELEMENT_ID);
        if (existingElement) {
            existingElement.remove();
        }
    }

    async function addOrUpdateJoinDateElement() {
        if (document.getElementById(ELEMENT_ID) && !isProcessing) { return; }
        if (isProcessing) { return; }

        const pathParts = window.location.pathname.split('/').filter(part => part);
        if (pathParts.length < 1 || pathParts.length > 2 || (pathParts.length === 2 && !['sponsors', 'followers', 'following'].includes(pathParts[1]))) {
             removeExistingElement(); return;
        }
        const profileSidebar = document.querySelector('.vcard');
        const mainContentArea = document.querySelector('div[itemtype="http://schema.org/Person"]');
         if (!profileSidebar && !mainContentArea) {
             removeExistingElement(); return;
        }
        const username = pathParts[0].toLowerCase();

        isProcessing = true;
        let joinElement = document.getElementById(ELEMENT_ID);
        let createdAtISO = null;
        let fromCache = false;

        try {
            const cache = readCache();
            if (cache[username]) {
                createdAtISO = cache[username];
                fromCache = true;
            }

             if (!joinElement) {
                joinElement = document.createElement('div');
                joinElement.id = ELEMENT_ID;
                joinElement.innerHTML = fromCache ? `${svgIcon} ...` : `${svgIcon} Loading...`;
                joinElement.style.marginTop = '8px'; joinElement.style.marginBottom = '8px'; joinElement.style.color = 'var(--color-fg-muted)'; joinElement.style.fontSize = '14px';

                const editableArea = mainContentArea?.querySelector('.js-profile-editable-area') || profileSidebar?.querySelector('.js-profile-editable-area');
                if (!editableArea) {
                     const detailsList = profileSidebar?.querySelector('ul.vcard-details');
                     if (detailsList) { const listItem = document.createElement('li'); listItem.classList.add('vcard-detail', 'pt-1'); listItem.appendChild(joinElement); joinElement.style.marginTop = '0'; joinElement.style.marginBottom = '0'; detailsList.appendChild(listItem); }
                     else { isProcessing = false; return; }
                } else {
                    const originalBioElement = editableArea.querySelector('.user-profile-bio'); const editButtonContainer = editableArea.querySelector('div.mb-3:has(> button.js-profile-editable-edit-button)'); if (originalBioElement && originalBioElement.offsetParent !== null) { originalBioElement.insertAdjacentElement('afterend', joinElement); } else if (editButtonContainer) { editButtonContainer.insertAdjacentElement('beforebegin', joinElement); } else { editableArea.prepend(joinElement); }
                }
            }

            if (!fromCache) {
                createdAtISO = await getGitHubJoinDate(username);
                joinElement = document.getElementById(ELEMENT_ID);
                 if (!joinElement) { return; }
                if (createdAtISO) {
                    const currentCache = readCache();
                    currentCache[username] = createdAtISO;
                    writeCache(currentCache);
                } else {
                    removeExistingElement(); return;
                }
            }

             if (createdAtISO && joinElement) {
                const joinDate = new Date(createdAtISO);
                const dateOptions = { year: 'numeric', month: 'long', day: 'numeric' };
                const formattedDate = joinDate.toLocaleDateString('en-US', dateOptions);
                const hours = joinDate.getHours().toString().padStart(2, '0');
                const minutes = joinDate.getMinutes().toString().padStart(2, '0');
                const formattedTime = `${hours}:${minutes}`;
                const relativeTimeString = getRelativeTime(createdAtISO);
                joinElement.innerHTML = `${svgIcon} ${formattedDate} - ${formattedTime} (${relativeTimeString})`;
             } else if (!createdAtISO && joinElement) {
                 removeExistingElement();
             }

        } catch (error) {
             removeExistingElement();
        } finally {
            isProcessing = false;
        }
    }

    function handlePotentialPageChange() {
        clearTimeout(observerDebounceTimeout);
        observerDebounceTimeout = setTimeout(() => {
             addOrUpdateJoinDateElement();
        }, 600);
    }

    addOrUpdateJoinDateElement();

    const observer = new MutationObserver((mutationsList) => {
        let potentiallyRelevantChange = false;
        for (const mutation of mutationsList) {
             if (mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
                 const targetNode = mutation.target;
                 if (targetNode && (targetNode.matches?.('main, main *, .Layout-sidebar, .Layout-sidebar *, body'))) {
                      let onlySelfChange = false;
                      if ((mutation.addedNodes.length === 1 && mutation.addedNodes[0].id === ELEMENT_ID && mutation.removedNodes.length === 0) ||
                          (mutation.removedNodes.length === 1 && mutation.removedNodes[0].id === ELEMENT_ID && mutation.addedNodes.length === 0)) {
                         onlySelfChange = true;
                      }
                      if (!onlySelfChange) { potentiallyRelevantChange = true; break; }
                 }
             }
        }
        if(potentiallyRelevantChange) { handlePotentialPageChange(); }
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();