GitHub Join Date

Displays user's join date/time/age.

Από την 27/03/2025. Δείτε την τελευταία έκδοση.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==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 });

})();