I created the Stackapp IGB Stack Flair a while ago and I think it's a good time for a revision now. Is there anything that can be done better, shorter, more elegant, more performant, etc.?
I thought of using something like a map/associative array for the SE sites in [This has been implemented. --G.B., April 21, 2017]addSiteLinkFor()
, for instance.
Please note:
- My indent style is Whitesmiths adapted by the opening brace on the same line to save vertical space here.
- The spaces after/before opening/closing parenthesis are intentional.
- 120 chars line length are intentional (I can share a GreaseMonkey script that overcomes this width limit of SE's sites if you want.)
/*
* IGB Stack Flair – A dynamic flair for Stack Exchange sites
* Copyright (C) 2017 Gerold Broser ([email protected])
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* FIXME:
* - use jQuery
* - cookie caching doesn't seem to work, perhaps a Firefox add-on issue
* TODO:
* - test in browsers other than Firefox, Opera, Chrome
* - add further SE sites to 'sites' prior to 'addSiteLinkFor()'
* Future:
* - add i18n
*/
let usersAJAX = null
let userAJAX = null
let users = null
let seUser = null // had to be renamed from 'user' since otherwise it conflicts with Wikipedia
let cookieExpirationTime = null
let usingCachedInfo = true
/** A dynamic flair for Stack Exchange sites inspired by Stack Exchange's native User Flairs
* ( http://stackoverflow.com/users/flair/ ) with advanced features:
*
* ● Including Stack Exchange sites with more than 101 reputation (compared to 200).
* ● Detailed reputation summary (compared to K and M abbreviations).
* ● Detailed statistics about Questions, Answers, Reputation, Gold, Silver and Bronze badges for each included site
* (compared to <null>)
* ● Updated in real-time (compared to 24–36 hrs).
*
* @version 17.4.20
* @param elementId The HTML element's id IGB Stack Flair is to be inserted in. See 'igbstackflair.html' for an example.
* @param networkProfileId As it can be seen under 'Network Profile' at top-right on your SE site's profile page.
* @param transformScale Scale factor for resizing IGB Stack Flair.
* @param retentionPeriod The period in hours that IGB Stack Flair's stores site information in its internal cache.
* Note that SE has a daily limit of 300 retrievals per (unauthenticated) user for its databases.
* ( see https://api.stackexchange.com/docs/throttle )
* Note that during the retention period no current data are displayed, of course.
* @param maxSiteLinksCount Number of site links that are displayed in IGB Stack Flair at a max.
* @param maxSiteLinksPerLine Number of site links that are displayed in one line of IGB Stack Flair at a max.
*
* @see https://stackapps.com/questions/5004/igb-stack-flair-a-dynamic-flair-for-stack-exchange-sites
* @see https://sourceforge.net/projects/igbstackflair/
*/
// From http://stackoverflow.com/questions/11796093/named-parameters-in-javascript/11796776#11796776
// by http://stackoverflow.com/users/218196/felix-kling
function igbStackFlair( {
elementId='igbstackflair',
networkProfileId=1937747,
transformScale=1.0,
retentionPeriod=1,
maxSiteLinksCount=99,
maxSiteLinksPerLine=8 } = {} ) {
users = JSON.parse( cookie( 'stackUsers' + networkProfileId ) )
user = JSON.parse( cookie( 'stackUser' + networkProfileId ) )
cookieExpirationTime = cookie( 'stackUser' + networkProfileId + 'Expiration' )
if ( users === null || seUser === null ) {
cookieExpirationTime = nowPlusOffsetToUtcString( retentionPeriod )
usingCachedInfo = false
if ( window.XMLHttpRequest ) { // Google Chrome, Mozilla Firefox, Opera, Safari, IE 10
usersAJAX = new XMLHttpRequest()
userAJAX = new XMLHttpRequest()
}
else if ( window.ActiveXObject ) { // Internet Explorer 9 and lower
try {
usersAJAX = new ActiveXObject( 'Msxml2.XMLHTTP.6.0' )
userAJAX = new ActiveXObject( 'Msxml2.XMLHTTP.6.0' )
}
catch(e) {
try {
usersAJAX = new ActiveXObject( 'Msxml2.XMLHTTP.3.0' )
userAJAX = new ActiveXObject( 'Msxml2.XMLHTTP.3.0' )
}
catch(e) {
console.error( "igbstackflair.js:\n" + e )
document.getElementById( elementId ).innerHTML = "igbstackflair.js:<br />" + e
}
}
}
if ( usersAJAX == null || userAJAX == null ) {
alert( "igbstackflair.js: Your browser doesn't support AJAX." )
return false
}
} // if ( no cookies )
document.getElementById( elementId ).innerHTML =
'<table cellspacing="0" cellpadding="0" style="transform:scale(' + transformScale +
'); background-color:#e0eaf1; font-family:Arial,Liberation Sans,DejaVu Sans,sans-serif;">' +
'<tr><td rowspan="3" style="padding:2px"><a id="stackProfileImageLink" target="_blank">' +
'<img id="stackProfileImage" width="72" height="72" style="text-align:center; vertical-align:middle;" /></a></td>' +
'<td> <strong><a id="networkProfileLink" style="text-decoration:none;" target="_blank"></a><strong></td>' +
'<td style="text-align:right;">' +
'<img id="stackInfo" src="https://upload.wikimedia.org/wikipedia/commons/3/33/Info_icon_002.svg" ' +
'alt="Info" title="Info" width="14" height="14" style="vertical-align:top;"/></td></tr>' +
'<tr><td colspan="2"> <strong id="stackReputation">0</strong> ' +
' <span style="color:gold">●</span> <span id="stackGold">0</span>' +
' <span style="color:silver">●</span> <span id="stackSilver">0</span>' +
' <span style="color:darkgoldenrod">●</span> <span id="stackBronze">0</span> </td></tr>' +
'<tr><td id="stackSiteLinks" colspan="2"> </td></tr></table>'
document.getElementById( 'stackInfo' ).title =
( usingCachedInfo ? "Cached data used. Data will be updated at " : "Retrieved data used. Data will be cached until " ) +
new Date( cookieExpirationTime ).toLocaleString()
if ( users !== null ) {
addFlair( networkProfileId, retentionPeriod, maxSiteLinksCount, maxSiteLinksPerLine )
}
else {
let usersURL = 'https://api.stackexchange.com/2.2/users/' +
networkProfileId + '/associated?filter=!4XDqt9bmJLeeGDHz_Ky)Dc9Buv'
usersAJAX.open( 'GET', usersURL, true )
usersAJAX.onreadystatechange = function() {
if ( this.readyState == 4 ) {
if ( this.status == 200 ) {
users = JSON.parse(this.responseText)
document.cookie = "stackUsers" + networkProfileId + "=" + encodeURIComponent(JSON.stringify(users)) +
";expires=" + cookieExpirationTime
document.cookie = "stackUser" + networkProfileId + "Expiration=" + cookieExpirationTime +
";expires=" + cookieExpirationTime
addSites( networkProfileId, retentionPeriod, maxSiteLinksCount, maxSiteLinksPerLine )
} // if (status OK)
else {
console.error( "igbstackflair.js: " + usersURL + "\n" + this.statusText + ": " + this.responseText )
document.getElementById( elementId )
.innerHTML = "igbstackflair.js: " + usersURL + "<br />" + this.statusText + ": "+this.responseText
}
} // if ( status ready )
} // onreadystatechange function()
usersAJAX.send(null)
}
} // igbStackFlair()
/** Adds SE site items.
*
* @param networkProfileId°
* @param retentionPeriod°
* @param maxSiteLinksCount°
* @param maxSiteLinksPerLine°
*
* °See function igbStackFlair() above for descriptions of these parameters.
*/
function addSites( networkProfileId, retentionPeriod, maxSiteLinksCount, maxSiteLinksPerLine ) {
let networkProfileLink = 'http://stackexchange.com/users/' + users.items[0].account_id + '?tab=accounts'
users.items.sort(function(u1, u2) { // sort sites by descending reputation
return u2.reputation - u1.reputation
}) // sort function(...)
if (seUser === null) {
let userURL = 'https://api.stackexchange.com/2.2/users/' + users.items[0].user_id + '?site=' +
users.items[0].site_url.substring(7) + '&filter=!*MxNUg6fdlYu_0rC&'
userAJAX.open( 'GET', userURL, true)
userAJAX.onreadystatechange = function() {
if ( this.readyState == 4 ) {
if ( this.status == 200 ) {
seUser = JSON.parse(this.responseText).items[0]
document.cookie = "stackUser" + networkProfileId + "=" + encodeURIComponent(JSON.stringify(user)) +
";expires=" + cookieExpirationTime
addLinks( networkProfileLink, maxSiteLinksCount, maxSiteLinksPerLine )
} // if ( status OK )
else {
console.error( "igbstackflair.js: " + userURL + "\n" + this.statusText + ": " + this.responseText )
document.getElementById( elementId )
.innerHTML = "igbstackflair.js: " + userURL + "<br />" + this.statusText + ": " + this.responseText
}
} // if (status ready)
} // onreadystatechange function()
userAJAX.send( null )
}
else { // seUser !== null
addLinks( networkProfileLink, maxSiteLinksCount, maxSiteLinksPerLine )
}
document.getElementById( 'stackInfo' )
.title += " - " + users.quota_remaining + " of " + users.quota_max + " requests remaining"
} // addSites()
/** Adds links to the profile image and to site items.
*
* @param networkProfileLink Link to the StackExchange Network Profile.
* @param maxSiteLinksCount°
* @param maxSiteLinksPerLine°
*
* °See function igbStackFlair() above for descriptions of these parameters.
*/
function addLinks( networkProfileLink, maxSiteLinksCount, maxSiteLinksPerLine ) {
document.getElementById( 'stackProfileImageLink' ).href = networkProfileLink
with ( document.getElementById( 'stackProfileImage' ) ) {
src = seUser.profile_image
alt = "profile image"
title = seUser.display_name
}
with ( document.getElementById( 'networkProfileLink' ) ) {
href = networkProfileLink
alt = seUser.display_name
innerHTML = seUser.display_name
}
for ( let u = 0; u < ( maxSiteLinksCount < users.items.length ? maxSiteLinksCount : users.items.length ); u++ ) {
if ( users.items[u].reputation > 101 )
addSiteLinkFor( users.items[u], maxSiteLinksCount, maxSiteLinksPerLine )
}
document.getElementById( 'stackReputation' ).innerHTML =
parseInt( document.getElementById( 'stackReputation' ).innerHTML ).toLocaleString('en')
} // addLinks()
let reputation = 0
let gold = 0
let silver = 0
let bronze = 0
let siteLinksPerLineSoFar = 0
let sites = {
academia:{short:'AC'},
askubuntu:{short:'ask'},
codegolf:{short:'PCG'},
codereview:{short:'CR'},
crypto:{short:'crpt'},
cs:{short:'CS'},
cstheory:{short:'CST'},
dba:{short:'DBA'},
electronics:{short:'elec'},
english:{short:'EN'},
german:{short:'DE'},
mechanics:{short:'Mech'},
meta:{short:'meta'},
physics:{short:'Ph'},
serverfault:{short:'SF'},
softwareengineering:{short:'SE'},
softwarerecs:{short:'SW'},
stackapps:{short:'StA'},
stackoverflow:{short:'SO'},
superuser:{short:'SU'},
unix:{short:'UNIX'},
ux:{short:'UX'},
webapps:{short:'apps'},
workplace:{short:'work'}
}
/** Adds links to site items.
*
* @param siteUser The SE site's user.
* @param maxSiteLinksCount°
* @param maxSiteLinksPerLine°
*
* °See function igbStackFlair() above for descriptions of these parameters.
*/
function addSiteLinkFor( siteUser, maxSiteLinksCount, maxSiteLinksPerLine ) {
document.getElementById( 'stackReputation' ).innerHTML = reputation += siteUser.reputation
document.getElementById( 'stackGold' ).innerHTML = gold += siteUser.badge_counts.gold
document.getElementById( 'stackSilver' ).innerHTML = silver += siteUser.badge_counts.silver
document.getElementById( 'stackBronze' ).innerHTML = bronze += siteUser.badge_counts.bronze
let siteLinks = document.getElementById( 'stackSiteLinks' )
let siteLink = document.createElement( 'a' )
with ( siteLink ) {
target = '_blank'
href = siteUser.site_url + '/users/' + siteUser.user_id
style.textDecoration = 'none'
let siteImage = document.createElement( 'img' )
if ( siteUser.site_url.indexOf('meta') == 8 ) {
siteImage.src = 'https://meta.stackexchange.com/content/Sites/stackexchangemeta/img/icon-16.png'
siteImage.alt = sites['meta'].short
}
else if (siteUser.site_url.indexOf('stackoverflow') == 7 ) {
siteImage.src = 'https://cdn.sstatic.net/Sites/stackoverflow/img/icon-16.png'
siteImage.alt = sites['stackoverflow'].short
}
else {
siteServer = siteUser.site_url.substr( 8, siteUser.site_url.indexOf('.') - 8 )
siteImage.src = 'https://cdn.sstatic.net/Sites/' + siteServer + '/img/icon-16.png'
siteImage.alt = sites[siteServer].short
}
// Note: No space between ':' and counts since neither ' ' nor character U+00A0 (NO-BREAK SPACE)
// are rendered correctly in title (in FF)
siteImage.title = decodeURIComponent(siteUser.site_name) + " - Questions:" + parseInt( siteUser.question_count ).toLocaleString('en') +
" Answers:" + parseInt( siteUser.answer_count ).toLocaleString('en') + " Reputation:" + parseInt( siteUser.reputation ).toLocaleString('en') +
" Gold:" + parseInt( siteUser.badge_counts.gold ).toLocaleString('en') + " Silver:" + parseInt( siteUser.badge_counts.silver ).toLocaleString('en') +
" Bronze:" + parseInt( siteUser.badge_counts.bronze ).toLocaleString('en')
siteImage.style.verticalAlign = 'middle'
appendChild( siteImage )
} // with ( siteLink )
siteLinks.appendChild( siteLink )
siteLinks.innerHTML += " "
if ( ++siteLinksPerLineSoFar >= maxSiteLinksPerLine && maxSiteLinksCount > maxSiteLinksPerLine ) {
siteLinks.innerHTML += "<br /> "
siteLinksPerLineSoFar = 0
}
} // addSiteLinkFor( siteUser )
/** TODO comment
*
* @param key TODO comment
*/
// From http://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript/25473576#25473576
// by http://stackoverflow.com/users/996638/simon-steinberger
function cookie( key ) {
let value = document.cookie.match( '(^|;) ?' + key + '=([^;]*)(;|$)' )
return value ? decodeURIComponent( value[2] ) : null
}
/** Returns an UTC string containing the current time plus the given offset.
*
* @param offsetHours TODO comment
* @returns An UTC string containing the current time plus the given offset.
*/
function nowPlusOffsetToUtcString( offsetHours ) {
let date = new Date()
date.setTime( date.getTime() + ( offsetHours * 3600000 ) )
return date.toUTCString()
}