3 of 9 Not everything deserves it's own tag. If you disagree, feel free to take it up on meta.

IGB Stack Flair – A dynamic flair for stackexchange.com sites

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 addSiteLinksFor(), 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 @"§$%&! 24"-widescreen-monitor-space-wasting width limit of SE's sites if you want.)

Code

/*
 * IGB Stack Flair - A dynamic flair for stackexchange.com sites 
 * Copyright (C) 2017 Gerold Broser (geribro@users.sourceforge.net)
 * 
 * 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:
 *      - cookie caching doesn't seem to work, perhaps a Firefox add-on issue
 * TODO:
 *      - test in browsers other than FF
 *      - add further SE sites to addSiteLinksFor()
 *      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 StackExchange sites similar to those that are provided at http://stackoverflow.com/users/flair/
 * but with content of sites with more than 101 reputation updated in real-time.
 * 
 * @version 17.4.15
 * @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 at top-right on your user page.
 * @param transformScale Scale factor for resizing IGB Stack Flair.
 * @param cacheHours The number of hours IGB Stack Flair's internal cache is not refreshed.
 *                   Note that SE has a daily limit of 300 retrievals per user for its databases.
 *                   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.
 * 
 */
// From http://stackoverflow.com/questions/11796093/named-parameters-in-javascript
function igbStackFlair( 
        { elementId='igbstackflair', 
            networkProfileId=1937747, 
            transformScale=1.0, 
            cacheHours=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( cacheHours )
        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>&nbsp;<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">&nbsp;<strong id="stackReputation">0</strong>&nbsp;' +
                        ' <span style="color:gold">●</span>&thinsp;<span id="stackGold">0</span>' +
                        ' <span style="color:silver">●</span>&thinsp;<span id="stackSilver">0</span>' +
                        ' <span style="color:darkgoldenrod">●</span>&thinsp;<span id="stackBronze">0</span>&nbsp;</td></tr>' +
            '<tr><td id="stackSiteLinks" colspan="2">&nbsp;</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, cacheHours, 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

                    addItems( networkProfileId, cacheHours, 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 site items for SE sites.
 * 
 * @param networkProfileId°
 * @param cacheHours°
 * @param maxSiteLinksCount°
 * @param maxSiteLinksPerLine°
 * 
 * °See igbStackFlair() above for descriptions of these parameters.
 */
function addItems( networkProfileId, cacheHours, 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"  
    } // addItems()

/** Adds links to the profile image and to site items.
 * 
 * @param networkProfileLink Link to the StackExchange Network Profile.
 * @param maxSiteLinksCount°
 * @param maxSiteLinksPerLine°
 * 
 * °See 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 )
            addSiteLinksFor( users.items[u], maxSiteLinksCount, maxSiteLinksPerLine )
        }
    } // addLinks()


let reputation = 0
let gold = 0
let silver = 0
let bronze = 0
let siteLinksPerLineSoFar   = 0 

/** Adds links to site items.
 * 
 * @param siteUser The SE site's user.
 * @param maxSiteLinksCount°
 * @param maxSiteLinksPerLine°
 * 
 * °See igbStackFlair() above for descriptions of these parameters.
 */
function addSiteLinksFor( 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' )
        switch( true ) {
            case href.indexOf( "academia" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/academia/img/icon-16.png'
                siteImage.alt = 'AC'
                break
            case href.indexOf( "askubuntu" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/askubuntu/img/icon-16.png'
                siteImage.alt = 'ask'
                break
            case href.indexOf( "codegolf" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/codegolf/img/icon-16.png'
                siteImage.alt = 'PCG'
                break
            case href.indexOf( "codereview" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/codereview/img/icon-16.png'
                siteImage.alt = 'CR'
                break
            case href.indexOf( "crypto" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/crypto/img/icon-16.png'
                siteImage.alt = 'Crpt'
                break
            case href.indexOf( "cs." ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/cs/img/icon-16.png'
                siteImage.alt = 'CS'
                break
            case href.indexOf( "cstheory" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/cstheory/img/icon-16.png'
                siteImage.alt = 'CST'
                break
            case href.indexOf( "dba" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/dba/img/icon-16.png'
                siteImage.alt = 'DBA'
                break
            case href.indexOf( "electronics" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/electronics/img/icon-16.png'
                siteImage.alt = 'ET'
                break
            case href.indexOf( "english" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/english/img/icon-16.png'
                siteImage.alt = 'EN'
                break
            case href.indexOf( "german" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/german/img/icon-16.png'
                siteImage.alt = 'DE'
                break
            case href.indexOf( "mechanics" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/mechanics/img/icon-16.png'
                siteImage.alt = 'Mv'
                break
            case href.indexOf( "meta" ) == 8:
                siteImage.src = 'https://meta.stackexchange.com/content/Sites/stackexchangemeta/img/icon-16.png'
                siteImage.alt = 'SE'
                break
            case href.indexOf( "physics" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/physics/img/icon-16.png'
                siteImage.alt = 'Ph'
                break
            case href.indexOf( "programmers" ) == 8:
                siteImage.src = ''
                siteImage.alt = 'Prg'
                break
            case href.indexOf( "puzzling" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/puzzling/img/icon-16.png'
                siteImage.alt = 'Pzz'
                break
            case href.indexOf( "serverfault" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/serverfault/img/icon-16.png'
                siteImage.alt = 'SF'
                break
            case href.indexOf( "softwareengineering" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/softwarerecs/img/icon-16.png'
                siteImage.alt = 'SWE'
                break
            case href.indexOf( "softwarerecs" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/softwareengineering/img/icon-16.png'
                siteImage.alt = 'SWR'
                break
            case href.indexOf( "stackapps" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/stackapps/img/icon-16.png'
                siteImage.alt = 'SA'
                break
            case href.indexOf( "stackoverflow" ) == 7: // !!!
                siteImage.src = 'https://cdn.sstatic.net/Sites/stackoverflow/img/icon-16.png'
                siteImage.alt = 'SO'
                break
            case href.indexOf( "superuser" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/superuser/img/icon-16.png'
                siteImage.alt = 'SU'
                break
            case href.indexOf( "unix" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/unix/img/icon-16.png'
                siteImage.alt = 'UL'
                break
            case href.indexOf( "ux" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/ux/img/icon-16.png'
                siteImage.alt = 'UX'
                break
            case href.indexOf( "webapps" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/webapps/img/icon-16.png'
                siteImage.alt = 'Web'
                break
            case href.indexOf( "workplace" ) == 8:
                siteImage.src = 'https://cdn.sstatic.net/Sites/workplace/img/icon-16.png'
                siteImage.alt = 'Wrk'
                break
            default:
                siteImage.src = ''
                siteImage.alt = siteUser.site_url.substr(8,2)
            } // switch ( stack site )
        // Note: No space between ':' and counts since neither '&nbsp;' nor character U+00A0 (NO-BREAK SPACE)
        //       are rendered correctly in title (in FF)
        siteImage.title = decodeURIComponent(siteUser.site_name) + " - Questions:" + siteUser.question_count +
                          "  Answers:" + siteUser.answer_count + "  Reputation:" + siteUser.reputation + 
                              "  Gold:" + siteUser.badge_counts.gold + "  Silver:" + siteUser.badge_counts.silver +
                              "  Bronze:" + siteUser.badge_counts.bronze
        siteImage.style.verticalAlign = 'middle'
        appendChild( siteImage )
        } // with ( siteLink )
    siteLinks.appendChild( siteLink )

    siteLinks.innerHTML += "&nbsp;"
    siteLinksPerLineSoFar++
    if ( siteLinksPerLineSoFar >= maxSiteLinksPerLine && maxSiteLinksCount > maxSiteLinksPerLine ) {
         siteLinks.innerHTML += "<br />&nbsp;"
         siteLinksPerLineSoFar = 0
        }
    } // addSiteLinksFor()

/** TODO comment
 * 
 * @param key TODO comment
 */
// From http://stackoverflow.com/a/25473576/1744774 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()
    }