GitHub Date of Creation

Display the date of creation and maintenance status for GitHub repositories.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension 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.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         GitHub Date of Creation
// @namespace    https://github.com/Sahaj33-op
// @version      3.0.0
// @description  Display the date of creation and maintenance status for GitHub repositories.
// @author       Sahaj
// @match        https://github.com/*
// @icon         https://github.githubassets.com/pinned-octocat.svg
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration & Storage ---
    const DATE_FORMAT_KEY = 'gdc.date_format';
    const URIS_KEY = 'gdc.uris';
    const PAT_KEY = 'gdc.pat';
    const SETTINGS_KEY = 'gdc.settings';

    const DEFAULT_DATE_FORMAT = 'MMMM, YYYY';
    const DEFAULT_SETTINGS = { relativeTime: true, showHealth: true };
    const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days

    // Load Cache into memory
    const cache = { data: GM_getValue(URIS_KEY, {}) };

    // --- Utilities ---
    function getRelativeTime(dateString) {
        if (!dateString) return 'unknown';
        const date = new Date(dateString);
        if (isNaN(date.getTime())) return 'unknown';

        const diffInDays = Math.floor((new Date() - date) / 86400000);
        if (diffInDays < 1) return 'today';
        if (diffInDays < 30) return `${diffInDays} day${diffInDays > 1 ? 's' : ''} ago`;

        const diffInMonths = Math.floor(diffInDays / 30);
        if (diffInMonths < 12) return `${diffInMonths} month${diffInMonths > 1 ? 's' : ''} ago`;

        const diffInYears = Math.floor(diffInMonths / 12);
        return `${diffInYears} year${diffInYears > 1 ? 's' : ''} ago`;
    }

    let cachedFormatter = null;
    let cachedFormatString = null;

    function formatAbsoluteDate(dateString, format) {
        const date = new Date(dateString);
        if (isNaN(date.getTime())) return dateString;

        if (!cachedFormatter || cachedFormatString !== format) {
            const options = { year: 'numeric' };
            if (format.includes('MMMM')) options.month = 'long';
            else if (format.includes('MMM')) options.month = 'short';
            else if (format.includes('MM')) options.month = '2-digit';
            if (format.includes('DD')) options.day = '2-digit';
            else if (format.includes('D')) options.day = 'numeric';

            cachedFormatter = new Intl.DateTimeFormat(navigator.language, options);
            cachedFormatString = format;
        }
        try {
            return cachedFormatter.format(date);
        } catch (e) {
            return date.toLocaleDateString();
        }
    }

    function getLindyBadge(dateString) {
        const years = (new Date() - new Date(dateString)) / (1000 * 60 * 60 * 24 * 365.25);
        if (years < 1) return { icon: '🌱', label: 'Sprout' };
        if (years > 10) return { icon: '🏛️', label: 'Ancient' };
        if (years > 5) return { icon: '🌳', label: 'Mature' };
        return { icon: '🌿', label: 'Established' };
    }

    function getHistoricalContext(dateString) {
        const year = new Date(dateString).getFullYear();
        const milestones = [
            { year: 2013, label: 'React 0.3' },
            { year: 2009, label: 'Node.js' },
            { year: 2015, label: 'ES6' },
            { year: 2016, label: 'Next.js' },
            { year: 2010, label: 'AngularJS' },
            { year: 2014, label: 'Vue.js' },
        ];
        const relevant = milestones.find(m => m.year === year);
        return relevant ? `Created around the time ${relevant.label} launched!` : null;
    }

    // --- Network & Cache ---
    async function fetchRepoData(owner, repo) {
        const cacheKey = `${owner}/${repo}`;
        const cachedEntry = cache.data[cacheKey];

        // Return valid cache
        if (cachedEntry && cachedEntry.created_at) {
            if (Date.now() - (cachedEntry.cached_at || 0) < CACHE_TTL_MS) {
                return cachedEntry;
            }
        }

        const pat = GM_getValue(PAT_KEY, '');
        const headers = pat ? { 'Authorization': `token ${pat}` } : {};

        const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers });
        if (!res.ok) throw new Error(res.status === 403 ? 'RATE_LIMIT_EXCEEDED' : 'API_ERROR');

        const data = await res.json();
        const result = {
            created_at: data.created_at,
            pushed_at: data.pushed_at,
            cached_at: Date.now(),
        };

        cache.data[cacheKey] = result;
        GM_setValue(URIS_KEY, cache.data); // Save to disk
        return result;
    }

    // --- Injection Logic ---
    async function injectToRepoPage() {
        const parts = window.location.pathname.split('/').filter(Boolean);
        if (parts.length < 2) return;

        const isRoot = parts.length === 2;
        const isFileView = parts.length >= 3 && ['tree', 'blob', 'edit'].includes(parts[2]);
        if (!isRoot && !isFileView) return;

        const [owner, repo] = parts;

        try {
            const data = await fetchRepoData(owner, repo);
            const settings = GM_getValue(SETTINGS_KEY, DEFAULT_SETTINGS);
            const dateFormat = GM_getValue(DATE_FORMAT_KEY, DEFAULT_DATE_FORMAT);

            const createdStr = settings.relativeTime ? `Created ${getRelativeTime(data.created_at)}` : formatAbsoluteDate(data.created_at, dateFormat);
            const healthStr = (settings.showHealth && data.pushed_at) ? ` • Last push ${getRelativeTime(data.pushed_at)}` : '';
            const lindy = getLindyBadge(data.created_at);
            const history = getHistoricalContext(data.created_at);

            let aboutCell = document.querySelector('[data-testid="about-section"], .Layout-sidebar .BorderGrid-cell');
            if (!aboutCell) {
                const headers = Array.from(document.querySelectorAll('h2'));
                const aboutHeader = headers.find(h => h.textContent.trim().toLowerCase() === 'about');
                if (aboutHeader) aboutCell = aboutHeader.parentElement;
            }
            if (!aboutCell || aboutCell.querySelector('#gdc')) return;

            const dateHTML = `
                <div id="gdc" class="mt-3" style="font-size: 12px; color: var(--color-fg-muted);">
                    <div style="display: flex; align-items: flex-start; gap: 8px;">
                        <span title="Lindy Index: ${lindy.label}" style="font-size: 16px;">${lindy.icon}</span>
                        <div>
                            <span title="Exact creation date: ${new Date(data.created_at).toLocaleString()}" style="cursor:help">
                                <svg height="16" class="octicon octicon-calendar mr-2" viewBox="0 0 16 16" width="16" aria-hidden="true" style="fill: currentColor; vertical-align: text-bottom;"><path fill-rule="evenodd" d="M13 2h-1v1.5c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5V2H6v1.5c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5V2H2c-.55 0-1 .45-1 1v11c0 .55.45 1 1 1h11c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm0 12H2V5h11v9zM5 3H4V1h1v2zm6 0h-1V1h1v2zM6 7H5V6h1v1zm2 0H7V6h1v1zm2 0H9V6h1v1zm2 0h-1V6h1v1zM4 9H3V8h1v1zm2 0H5V8h1v1zm2 0H7V8h1v1zm2 0H9V8h1v1zm2 0h-1V8h1v1zm-8 2H3v-1h1v1zm2 0H5v-1h1v1zm2 0H7v-1h1v1zm2 0H9v-1h1v1zm2 0h-1v-1h1v1zm-8 2H3v-1h1v1zm2 0H5v-1h1v1zm2 0H7v-1h1v1zm2 0H9v-1h1v1z"></path></svg>
                                ${createdStr}${healthStr}
                            </span>
                            ${history ? `<div style="font-style: italic; margin-top: 4px; opacity: 0.8;">${history}</div>` : ''}
                        </div>
                    </div>
                </div>
            `;

            const description = aboutCell.querySelector('p.f4') || aboutCell.querySelector('h2');
            if (description) description.insertAdjacentHTML('afterend', dateHTML);
            else aboutCell.insertAdjacentHTML('afterbegin', dateHTML);

        } catch (err) {
            console.warn('[GDC] Repo error:', err.message);
        }
    }

    function injectToSearchResults() {
        const repoItems = Array.from(document.querySelectorAll('.repo-list-item, .Box-row, [data-testid="results-list"] > div, .list-style-none > li'));
        if (!repoItems.length) return;

        const settings = GM_getValue(SETTINGS_KEY, DEFAULT_SETTINGS);
        const dateFormat = GM_getValue(DATE_FORMAT_KEY, DEFAULT_DATE_FORMAT);
        const pat = GM_getValue(PAT_KEY, '');
        let fetchCount = 0;

        for (const item of repoItems) {
            if (item.querySelector('.gdc-search-injected')) continue;

            const link = item.querySelector('a[href*="/"][data-hydro-click*="RESULT"], h3 a, h2 a, a.v-align-middle, a[data-testid="results-list-item-path"]');
            if (!link || !link.getAttribute('href')) continue;

            const parts = link.getAttribute('href').split('/').filter(Boolean);
            if (parts.length < 2) continue;

            const owner = parts[0];
            const repo = parts[1];
            const isCached = !!(cache.data[`${owner}/${repo}`]);

            // Rate Limit Protection for unauthenticated search
            if (!pat && !isCached && fetchCount >= 5) continue;
            if (!isCached) fetchCount++;

            const marker = document.createElement('span');
            marker.className = 'gdc-search-injected';
            item.appendChild(marker);

            // Fire asynchronously to prevent waterfall
            fetchRepoData(owner, repo).then(data => {
                const lindy = getLindyBadge(data.created_at);
                const createdStr = settings.relativeTime ? getRelativeTime(data.created_at) : formatAbsoluteDate(data.created_at, dateFormat);

                const target = item.querySelector('.f6.color-fg-muted, .text-small.color-fg-muted, .color-fg-subtle, [data-testid="search-result-item-metadata"]');
                const html = `
                    <span class="mr-3 d-inline-flex flex-items-center" style="gap:4px; margin-right: 12px; vertical-align: middle;" title="Created ${new Date(data.created_at).toLocaleDateString()}">
                        <span style="font-size: 14px;">${lindy.icon}</span>
                        <svg height="14" class="octicon octicon-calendar" viewBox="0 0 16 16" width="14" aria-hidden="true" style="fill: currentColor; opacity: 0.7;"><path fill-rule="evenodd" d="M13 2h-1v1.5c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5V2H6v1.5c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5V2H2c-.55 0-1 .45-1 1v11c0 .55.45 1 1 1h11c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm0 12H2V5h11v9zM5 3H4V1h1v2zm6 0h-1V1h1v2z"></path></svg>
                        <span style="font-size: 12px;">Created ${createdStr}</span>
                    </span>
                `;

                if (target) target.insertAdjacentHTML('afterbegin', html);
                else {
                    const fallback = document.createElement('div');
                    fallback.className = 'mt-1 text-small color-fg-subtle';
                    fallback.innerHTML = html;
                    item.appendChild(fallback);
                }
            }).catch(() => {});
        }
    }

    // --- Settings UI (Injected Modal) ---
    GM_addStyle(`
        #gdc-settings-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); display: flex; justify-content: center; align-items: center; z-index: 999999; backdrop-filter: blur(4px); }
        #gdc-settings-modal { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 24px; width: 400px; max-width: 90vw; color: #c9d1d9; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
        .gdc-title { margin: 0 0 16px 0; font-size: 18px; border-bottom: 1px solid #30363d; padding-bottom: 12px; display: flex; justify-content: space-between; align-items: center; }
        .gdc-close { cursor: pointer; color: #8b949e; font-size: 20px; line-height: 1; }
        .gdc-close:hover { color: #c9d1d9; }
        .gdc-group { margin-bottom: 16px; }
        .gdc-label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
        .gdc-desc { font-size: 12px; color: #8b949e; margin-bottom: 8px; }
        .gdc-input, .gdc-select { width: 100%; background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; padding: 8px 12px; border-radius: 6px; box-sizing: border-box; }
        .gdc-input:focus, .gdc-select:focus { outline: none; border-color: #0969da; }
        .gdc-checkbox-wrapper { display: flex; align-items: center; gap: 8px; font-size: 13px; margin-bottom: 8px; cursor: pointer; }
        .gdc-save-btn { width: 100%; background: #238636; color: white; border: none; padding: 8px; border-radius: 6px; font-weight: 600; cursor: pointer; margin-top: 8px; }
        .gdc-save-btn:hover { background: #2ea043; }
    `);

    function openSettings() {
        if (document.getElementById('gdc-settings-overlay')) return;

        const currentSettings = GM_getValue(SETTINGS_KEY, DEFAULT_SETTINGS);
        const currentPat = GM_getValue(PAT_KEY, '');
        const currentFormat = GM_getValue(DATE_FORMAT_KEY, DEFAULT_DATE_FORMAT);

        const overlay = document.createElement('div');
        overlay.id = 'gdc-settings-overlay';
        overlay.innerHTML = `
            <div id="gdc-settings-modal">
                <div class="gdc-title">
                    <span>⚙️ GitHub Date of Creation</span>
                    <span class="gdc-close" id="gdc-close-btn">&times;</span>
                </div>

                <div class="gdc-group">
                    <label class="gdc-label">Personal Access Token</label>
                    <div class="gdc-desc">Bypass API limits (60 req/hr). No scopes needed.</div>
                    <input type="text" id="gdc-pat-input" class="gdc-input" value="${currentPat}" placeholder="ghp_xxxxxxxxxxxxxxxxxxxx">
                </div>

                <div class="gdc-group">
                    <label class="gdc-label">Preferences</label>
                    <label class="gdc-checkbox-wrapper">
                        <input type="checkbox" id="gdc-relative-check" ${currentSettings.relativeTime ? 'checked' : ''}>
                        Use relative time (e.g. "4 years ago")
                    </label>
                    <label class="gdc-checkbox-wrapper">
                        <input type="checkbox" id="gdc-health-check" ${currentSettings.showHealth ? 'checked' : ''}>
                        Show last push status
                    </label>
                </div>

                <div class="gdc-group">
                    <label class="gdc-label">Absolute Date Format</label>
                    <select id="gdc-format-select" class="gdc-select">
                        <option value="MMMM, YYYY">Full Month, YYYY</option>
                        <option value="MMM D, YYYY">MMM D, YYYY</option>
                        <option value="YYYY-MM-DD">ISO Format</option>
                        <option value="DD/MM/YYYY">European</option>
                        <option value="custom">Custom...</option>
                    </select>
                    <input type="text" id="gdc-custom-format" class="gdc-input" style="display:none; margin-top:8px;" value="${currentFormat}" placeholder="e.g. YYYY-MM">
                </div>

                <button class="gdc-save-btn" id="gdc-save-btn">Save Settings</button>
            </div>
        `;
        document.body.appendChild(overlay);

        // UI Logic
        const select = document.getElementById('gdc-format-select');
        const customInput = document.getElementById('gdc-custom-format');

        if (Array.from(select.options).some(o => o.value === currentFormat)) {
            select.value = currentFormat;
        } else {
            select.value = 'custom';
            customInput.style.display = 'block';
        }

        select.addEventListener('change', () => {
            customInput.style.display = select.value === 'custom' ? 'block' : 'none';
        });

        document.getElementById('gdc-close-btn').onclick = () => overlay.remove();
        overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };

        document.getElementById('gdc-save-btn').onclick = () => {
            GM_setValue(PAT_KEY, document.getElementById('gdc-pat-input').value.trim());
            GM_setValue(SETTINGS_KEY, {
                relativeTime: document.getElementById('gdc-relative-check').checked,
                showHealth: document.getElementById('gdc-health-check').checked
            });
            GM_setValue(DATE_FORMAT_KEY, select.value === 'custom' ? customInput.value : select.value);

            overlay.remove();
            processPage(); // Re-trigger injection to update UI
        };
    }

    // Register to Tampermonkey Menu
    GM_registerMenuCommand("⚙️ Settings", openSettings);

    // --- Core Observer & Router ---
    let debounceTimer;
    function processPage() {
        const path = window.location.pathname;
        if (path.split('/').filter(Boolean).length >= 2) injectToRepoPage();
        if (path.startsWith('/search') || path.startsWith('/trending') || path.startsWith('/explore')) {
            injectToSearchResults();
        }
    }

    const domObserver = new MutationObserver(() => {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => requestAnimationFrame(processPage), 150);
    });

    // Init
    processPage();
    domObserver.observe(document.body, { childList: true, subtree: true });

    document.addEventListener('pjax:end', processPage);
    document.addEventListener('turbo:load', processPage);

    // Maintenance: Auto-purge old cache entries on script load
    (function cleanupCache() {
        const now = Date.now();
        let changed = false;
        for (const key in cache.data) {
            if (now - (cache.data[key].cached_at || 0) > CACHE_TTL_MS) {
                delete cache.data[key];
                changed = true;
            }
        }
        if (changed) GM_setValue(URIS_KEY, cache.data);
    })();

})();