| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
// Language color map for GitHub |
| 5 |
var LANG_COLORS = { |
| 6 |
'JavaScript': '#f1e05a', 'TypeScript': '#3178c6', 'Python': '#3572A5', |
| 7 |
'PHP': '#4F5D95', 'Ruby': '#701516', 'Go': '#00ADD8', 'Rust': '#dea584', |
| 8 |
'CSS': '#563d7c', 'HTML': '#e34c26', 'C': '#555555', 'C++': '#f34b7d', |
| 9 |
'Java': '#b07219', 'Swift': '#F05138', 'Kotlin': '#A97BFF', 'Dart': '#00B4AB', |
| 10 |
'Shell': '#89e051', 'Vue': '#41b883', 'Svelte': '#ff3e00' |
| 11 |
}; |
| 12 |
|
| 13 |
function formatNumber(n) { |
| 14 |
if (!n && n !== 0) return '—'; |
| 15 |
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; |
| 16 |
if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; |
| 17 |
return n.toString(); |
| 18 |
} |
| 19 |
|
| 20 |
function formatDate(iso) { |
| 21 |
if (!iso) return ''; |
| 22 |
var d = new Date(iso); |
| 23 |
var now = new Date(); |
| 24 |
var diff = Math.floor((now - d) / 1000); |
| 25 |
if (diff < 60) return 'Updated just now'; |
| 26 |
if (diff < 3600) return 'Updated ' + Math.floor(diff / 60) + 'm ago'; |
| 27 |
if (diff < 86400) return 'Updated ' + Math.floor(diff / 3600) + 'h ago'; |
| 28 |
if (diff < 2592000) return 'Updated ' + Math.floor(diff / 86400) + 'd ago'; |
| 29 |
return 'Updated ' + d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); |
| 30 |
} |
| 31 |
|
| 32 |
function getCached(key, maxAge) { |
| 33 |
try { |
| 34 |
var raw = localStorage.getItem(key); |
| 35 |
if (!raw) return null; |
| 36 |
var obj = JSON.parse(raw); |
| 37 |
if (Date.now() - obj.ts > maxAge * 60 * 1000) return null; |
| 38 |
return obj.data; |
| 39 |
} catch (e) { return null; } |
| 40 |
} |
| 41 |
|
| 42 |
function setCache(key, data) { |
| 43 |
try { localStorage.setItem(key, JSON.stringify({ ts: Date.now(), data: data })); } catch (e) { } |
| 44 |
} |
| 45 |
|
| 46 |
function typoCssVarsForEl(typo, prefix, el) { |
| 47 |
if (!typo || typeof typo !== 'object') return; |
| 48 |
var map = { |
| 49 |
family: 'font-family', weight: 'font-weight', style: 'font-style', |
| 50 |
decoration: 'text-decoration', transform: 'text-transform', |
| 51 |
sizeDesktop: 'font-size-d', sizeTablet: 'font-size-t', sizeMobile: 'font-size-m', |
| 52 |
lineHeightDesktop: 'line-height-d', lineHeightTablet: 'line-height-t', lineHeightMobile: 'line-height-m', |
| 53 |
letterSpacingDesktop: 'letter-spacing-d', letterSpacingTablet: 'letter-spacing-t', letterSpacingMobile: 'letter-spacing-m', |
| 54 |
wordSpacingDesktop: 'word-spacing-d', wordSpacingTablet: 'word-spacing-t', wordSpacingMobile: 'word-spacing-m' |
| 55 |
}; |
| 56 |
Object.keys(map).forEach(function (k) { |
| 57 |
if (typo[k] !== undefined && typo[k] !== '') { |
| 58 |
var v = typo[k]; |
| 59 |
if (['sizeDesktop','sizeTablet','sizeMobile'].indexOf(k) !== -1) { |
| 60 |
v = v + (typo.sizeUnit || 'px'); |
| 61 |
} else if (['lineHeightDesktop','lineHeightTablet','lineHeightMobile'].indexOf(k) !== -1) { |
| 62 |
v = v + (typo.lineHeightUnit || ''); |
| 63 |
} else if (['letterSpacingDesktop','letterSpacingTablet','letterSpacingMobile'].indexOf(k) !== -1) { |
| 64 |
v = v + (typo.letterSpacingUnit || 'px'); |
| 65 |
} else if (['wordSpacingDesktop','wordSpacingTablet','wordSpacingMobile'].indexOf(k) !== -1) { |
| 66 |
v = v + (typo.wordSpacingUnit || 'px'); |
| 67 |
} |
| 68 |
el.style.setProperty(prefix + map[k], String(v)); |
| 69 |
} |
| 70 |
}); |
| 71 |
} |
| 72 |
|
| 73 |
document.querySelectorAll('.bkbg-ghc-app').forEach(function (app) { |
| 74 |
var opts = {}; |
| 75 |
try { opts = JSON.parse(app.dataset.opts || '{}'); } catch (e) { } |
| 76 |
|
| 77 |
var mode = opts.mode || 'repo'; |
| 78 |
var username = opts.username || ''; |
| 79 |
var repoName = opts.repoName || ''; |
| 80 |
var showAvatar = opts.showAvatar !== false; |
| 81 |
var showBio = opts.showBio !== false; |
| 82 |
var showStats = opts.showStats !== false; |
| 83 |
var showLanguage = opts.showLanguage !== false; |
| 84 |
var showTopics = opts.showTopics !== false; |
| 85 |
var showLastUpdate = opts.showLastUpdate !== false; |
| 86 |
var openInNewTab = opts.openInNewTab !== false; |
| 87 |
var cacheMinutes = opts.cacheMinutes || 60; |
| 88 |
var cardRadius = opts.cardRadius || 16; |
| 89 |
var padding = opts.padding || 24; |
| 90 |
var avatarSize = opts.avatarSize || 64; |
| 91 |
var align2 = opts.align2 || 'center'; |
| 92 |
var maxWidth = opts.maxWidth || 480; |
| 93 |
var cardBg = opts.cardBg || '#0d1117'; |
| 94 |
var headingColor = opts.headingColor || '#e6edf3'; |
| 95 |
var descColor = opts.descColor || '#8b949e'; |
| 96 |
var metaColor = opts.metaColor || '#8b949e'; |
| 97 |
var statBg = opts.statBg || '#161b22'; |
| 98 |
var linkColor = opts.linkColor || '#58a6ff'; |
| 99 |
var borderColor = opts.borderColor || '#30363d'; |
| 100 |
var sectionBg = opts.sectionBg || ''; |
| 101 |
|
| 102 |
if (!username) { app.style.display = 'none'; return; } |
| 103 |
if (sectionBg) app.style.background = sectionBg; |
| 104 |
|
| 105 |
typoCssVarsForEl(opts.typoHeading, '--bkbg-ghc-hd-', app); |
| 106 |
typoCssVarsForEl(opts.typoBody, '--bkbg-ghc-bd-', app); |
| 107 |
typoCssVarsForEl(opts.typoMeta, '--bkbg-ghc-mt-', app); |
| 108 |
|
| 109 |
// Alignment wrapper |
| 110 |
app.style.textAlign = align2; |
| 111 |
|
| 112 |
// Loading state |
| 113 |
var loading = document.createElement('div'); |
| 114 |
loading.className = 'bkbg-ghc-loading'; |
| 115 |
loading.style.cssText = 'background:' + cardBg + ';border:1px solid ' + borderColor + ';border-radius:' + cardRadius + 'px;max-width:' + maxWidth + 'px;margin:' + (align2 === 'center' ? '0 auto' : align2 === 'right' ? '0 0 0 auto' : '0') + ';color:' + metaColor + ';'; |
| 116 |
var spinner = document.createElement('div'); |
| 117 |
spinner.className = 'bkbg-ghc-spinner'; |
| 118 |
loading.appendChild(spinner); |
| 119 |
loading.appendChild(document.createTextNode('Loading GitHub data…')); |
| 120 |
app.appendChild(loading); |
| 121 |
|
| 122 |
// API URL |
| 123 |
var apiUrl = mode === 'repo' && repoName |
| 124 |
? 'https://api.github.com/repos/' + username + '/' + repoName |
| 125 |
: 'https://api.github.com/users/' + username; |
| 126 |
|
| 127 |
var cacheKey = 'bkbg_ghc_' + apiUrl; |
| 128 |
var cached = getCached(cacheKey, cacheMinutes); |
| 129 |
|
| 130 |
function renderCard(data) { |
| 131 |
app.removeChild(loading); |
| 132 |
|
| 133 |
var marginStyle = align2 === 'center' ? '0 auto' : align2 === 'right' ? '0 0 0 auto' : '0'; |
| 134 |
var ghUrl = data.html_url || ('https://github.com/' + username); |
| 135 |
|
| 136 |
var card = document.createElement('a'); |
| 137 |
card.className = 'bkbg-ghc-card'; |
| 138 |
card.href = ghUrl; |
| 139 |
card.target = openInNewTab ? '_blank' : '_self'; |
| 140 |
card.rel = 'noopener noreferrer'; |
| 141 |
card.style.cssText = 'background:' + cardBg + ';border:1px solid ' + borderColor + ';border-radius:' + cardRadius + 'px;padding:' + padding + 'px;max-width:' + maxWidth + 'px;margin:' + marginStyle + ';display:block;color:inherit;text-decoration:none;'; |
| 142 |
|
| 143 |
// Header |
| 144 |
var header = document.createElement('div'); |
| 145 |
header.className = 'bkbg-ghc-header'; |
| 146 |
|
| 147 |
if (showAvatar && data.owner && data.owner.avatar_url) { |
| 148 |
var img = document.createElement('img'); |
| 149 |
img.className = 'bkbg-ghc-avatar'; |
| 150 |
img.src = data.owner.avatar_url; |
| 151 |
img.alt = data.owner.login; |
| 152 |
img.width = avatarSize; |
| 153 |
img.height = avatarSize; |
| 154 |
img.style.cssText = 'width:' + avatarSize + 'px;height:' + avatarSize + 'px;border-radius:' + (mode === 'repo' ? '8px' : '50%') + ';object-fit:cover;'; |
| 155 |
header.appendChild(img); |
| 156 |
} else if (showAvatar && data.avatar_url) { |
| 157 |
var img2 = document.createElement('img'); |
| 158 |
img2.className = 'bkbg-ghc-avatar'; |
| 159 |
img2.src = data.avatar_url; |
| 160 |
img2.alt = data.login; |
| 161 |
img2.width = avatarSize; |
| 162 |
img2.height = avatarSize; |
| 163 |
img2.style.cssText = 'width:' + avatarSize + 'px;height:' + avatarSize + 'px;border-radius:50%;object-fit:cover;'; |
| 164 |
header.appendChild(img2); |
| 165 |
} |
| 166 |
|
| 167 |
var nameCol = document.createElement('div'); |
| 168 |
var nameRow = document.createElement('div'); |
| 169 |
nameRow.className = 'bkbg-ghc-name-row'; |
| 170 |
var nameLink = document.createElement('a'); |
| 171 |
nameLink.href = ghUrl; |
| 172 |
nameLink.target = openInNewTab ? '_blank' : '_self'; |
| 173 |
nameLink.rel = 'noopener noreferrer'; |
| 174 |
nameLink.textContent = mode === 'repo' ? (data.full_name || username + '/' + repoName) : (data.name || data.login || username); |
| 175 |
nameLink.style.color = linkColor; |
| 176 |
nameRow.appendChild(nameLink); |
| 177 |
|
| 178 |
var handleDiv = document.createElement('div'); |
| 179 |
handleDiv.className = 'bkbg-ghc-handle'; |
| 180 |
handleDiv.style.color = metaColor; |
| 181 |
handleDiv.textContent = mode === 'repo' ? (data.language ? data.language : '') : ('@' + (data.login || username)); |
| 182 |
nameCol.appendChild(nameRow); |
| 183 |
if (handleDiv.textContent) nameCol.appendChild(handleDiv); |
| 184 |
header.appendChild(nameCol); |
| 185 |
card.appendChild(header); |
| 186 |
|
| 187 |
// Bio / Description |
| 188 |
if (showBio) { |
| 189 |
var descText = mode === 'repo' ? data.description : data.bio; |
| 190 |
if (descText) { |
| 191 |
var desc = document.createElement('div'); |
| 192 |
desc.className = 'bkbg-ghc-desc'; |
| 193 |
desc.style.color = descColor; |
| 194 |
desc.textContent = descText; |
| 195 |
card.appendChild(desc); |
| 196 |
} |
| 197 |
} |
| 198 |
|
| 199 |
// Repo meta row (language + topics) |
| 200 |
if (mode === 'repo') { |
| 201 |
var metaRow = document.createElement('div'); |
| 202 |
metaRow.className = 'bkbg-ghc-meta-row'; |
| 203 |
metaRow.style.color = metaColor; |
| 204 |
|
| 205 |
if (showLanguage && data.language) { |
| 206 |
var langSpan = document.createElement('span'); |
| 207 |
langSpan.className = 'bkbg-ghc-lang'; |
| 208 |
var dot = document.createElement('span'); |
| 209 |
dot.className = 'bkbg-ghc-lang-dot'; |
| 210 |
dot.setAttribute('data-lang', data.language); |
| 211 |
dot.style.background = LANG_COLORS[data.language] || '#8b949e'; |
| 212 |
langSpan.appendChild(dot); |
| 213 |
langSpan.appendChild(document.createTextNode(data.language)); |
| 214 |
metaRow.appendChild(langSpan); |
| 215 |
} |
| 216 |
|
| 217 |
if (showTopics && data.topics && data.topics.length) { |
| 218 |
data.topics.slice(0, 5).forEach(function (topic) { |
| 219 |
var t = document.createElement('a'); |
| 220 |
t.className = 'bkbg-ghc-topic'; |
| 221 |
t.href = 'https://github.com/topics/' + topic; |
| 222 |
t.target = '_blank'; |
| 223 |
t.rel = 'noopener noreferrer'; |
| 224 |
t.textContent = topic; |
| 225 |
t.style.cssText = 'background:rgba(56,139,253,0.15);color:' + linkColor + ';'; |
| 226 |
metaRow.appendChild(t); |
| 227 |
}); |
| 228 |
} |
| 229 |
|
| 230 |
if (metaRow.children.length) card.appendChild(metaRow); |
| 231 |
} |
| 232 |
|
| 233 |
// Stats |
| 234 |
if (showStats) { |
| 235 |
var statsRow = document.createElement('div'); |
| 236 |
statsRow.className = 'bkbg-ghc-stats'; |
| 237 |
|
| 238 |
var statDefs = mode === 'repo' |
| 239 |
? [ |
| 240 |
{ icon: '⭐', val: formatNumber(data.stargazers_count), lbl: 'stars' }, |
| 241 |
{ icon: '🍴', val: formatNumber(data.forks_count), lbl: 'forks' }, |
| 242 |
{ icon: '👁', val: formatNumber(data.watchers_count), lbl: 'watchers' } |
| 243 |
] |
| 244 |
: [ |
| 245 |
{ icon: '📦', val: formatNumber(data.public_repos), lbl: 'repos' }, |
| 246 |
{ icon: '👥', val: formatNumber(data.followers), lbl: 'followers' }, |
| 247 |
{ icon: '👤', val: formatNumber(data.following), lbl: 'following' } |
| 248 |
]; |
| 249 |
|
| 250 |
statDefs.forEach(function (s) { |
| 251 |
var tile = document.createElement('div'); |
| 252 |
tile.className = 'bkbg-ghc-stat'; |
| 253 |
tile.style.cssText = 'background:' + statBg + ';border:1px solid ' + borderColor + ';border-radius:8px;'; |
| 254 |
|
| 255 |
var val = document.createElement('span'); |
| 256 |
val.className = 'bkbg-ghc-stat-val'; |
| 257 |
val.style.color = headingColor; |
| 258 |
val.textContent = s.val; |
| 259 |
|
| 260 |
var lbl = document.createElement('span'); |
| 261 |
lbl.className = 'bkbg-ghc-stat-lbl'; |
| 262 |
lbl.style.color = metaColor; |
| 263 |
lbl.textContent = s.lbl; |
| 264 |
|
| 265 |
tile.appendChild(val); |
| 266 |
tile.appendChild(lbl); |
| 267 |
statsRow.appendChild(tile); |
| 268 |
}); |
| 269 |
|
| 270 |
card.appendChild(statsRow); |
| 271 |
} |
| 272 |
|
| 273 |
// Footer - last update |
| 274 |
if (showLastUpdate) { |
| 275 |
var updatedAt = mode === 'repo' ? data.pushed_at : data.updated_at; |
| 276 |
if (updatedAt) { |
| 277 |
var footer = document.createElement('div'); |
| 278 |
footer.className = 'bkbg-ghc-footer'; |
| 279 |
footer.style.color = metaColor; |
| 280 |
footer.textContent = formatDate(updatedAt); |
| 281 |
card.appendChild(footer); |
| 282 |
} |
| 283 |
} |
| 284 |
|
| 285 |
app.appendChild(card); |
| 286 |
} |
| 287 |
|
| 288 |
function renderError(msg) { |
| 289 |
app.removeChild(loading); |
| 290 |
var err = document.createElement('div'); |
| 291 |
err.className = 'bkbg-ghc-error'; |
| 292 |
err.style.cssText = 'background:' + cardBg + ';border:1px solid #f43f5e;border-radius:' + cardRadius + 'px;max-width:' + maxWidth + 'px;margin:' + (align2 === 'center' ? '0 auto' : align2 === 'right' ? '0 0 0 auto' : '0') + ';color:#f43f5e;'; |
| 293 |
err.textContent = '⚠ ' + msg; |
| 294 |
app.appendChild(err); |
| 295 |
} |
| 296 |
|
| 297 |
function fetchData() { |
| 298 |
fetch(apiUrl, { headers: { Accept: 'application/vnd.github.mercy-preview+json' } }) |
| 299 |
.then(function (r) { |
| 300 |
if (!r.ok) throw new Error('GitHub API returned ' + r.status); |
| 301 |
return r.json(); |
| 302 |
}) |
| 303 |
.then(function (data) { |
| 304 |
setCache(cacheKey, data); |
| 305 |
renderCard(data); |
| 306 |
}) |
| 307 |
.catch(function (e) { |
| 308 |
renderError('Could not load GitHub data. ' + e.message); |
| 309 |
}); |
| 310 |
} |
| 311 |
|
| 312 |
if (cached) { |
| 313 |
// Small delay to avoid flicker |
| 314 |
setTimeout(function () { renderCard(cached); }, 0); |
| 315 |
} else { |
| 316 |
fetchData(); |
| 317 |
} |
| 318 |
}); |
| 319 |
})(); |
| 320 |
|