Display the date of creation and maintenance status for GitHub repositories.
// ==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">×</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);
})();
})();