Wanikani Leaderboard

Get levels from usernames and order them in a competitive list

// ==UserScript==
// @name         Wanikani Leaderboard
// @namespace    http://tampermonkey.net/
// @version      1.11
// @description  Get levels from usernames and order them in a competitive list
// @author       Dani2
// @include      https://www.wanikani.com/dashboard
// @include      https://www.wanikani.com/
// @require      https://unpkg.com/sweetalert/dist/sweetalert.min.js
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    //------------------------------
    // Wanikani Framework
    //------------------------------
    if (!window.wkof) {
        let response = confirm('WaniKani Leaderboard script requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.');

        if (response) {
            window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
        }

        return;
    }

    const config = {

    };

    wkof.include('Menu, Settings');
    wkof.ready('Menu, Settings').then(install_menu).then(install_settings);

    //------------------------------
	// Menu
	//------------------------------
    var settings_dialog;
    var defaults = {
        userOrderOption: 'key1',
        numberOfLeaderboardTabless: '1'
    };

    function install_menu() {
        wkof.Menu.insert_script_link({
            script_id: 'Leaderboard',
            name: 'Leaderboard',
            submenu:   'Settings',
            title:     'Leaderboard',
            on_click:  open_settings
        });
    }

    function open_settings() {
        settings_dialog.open();
    }
    function install_settings() {
        settings_dialog = new wkof.Settings({
            script_id: 'Leaderboard',
            name: 'Leaderboard',
            title: 'Leaderboard',
            on_save: process_settings,
            settings: {
                tabset_id: {
                    type: 'tabset',
                    content: {
                        page_id1: {type: 'page', label:  'Add user', hover_tip:'', content: {
                            addUser: {type:'input', label:'Add a user', hover_tip: 'Add a user to leaderboard', placeholder: 'username', validate: ''}
                        }},
                        page_id2: {type: 'page', label:  'Sort Order', hover_tip:'', content: {
                            'userOrderOption': {type: 'dropdown', label: 'Sorting order', hover_tip: 'Select how you want users to be ordered.', default:'keyDefault', full_width: true, on_change: changeSortOrder,
                                          content: {
                                              keyDefault: '--Sort Order--',
                                              key1: 'Level -> Burn% -> Name',
                                              key2: 'Level -> Name',
                                              key3: 'Burn% -> Level -> Name',
                                              key4: 'Burn% -> Name',
                                              key5: 'Name Ascending',
                                              key6: 'Name Descending',
                                          }
                                         }
                        }},
                        page_id3: {type: 'page', label:  'Number of tables', hover_tip:'', content: {
                            'numberOfLeaderboardTabless': {type:'dropdown', label:'Number of leaderboard tables', hover_tip: 'The amount of tables the added users will be split between', default:'0', full_width: true,
                                         on_change: changeNumberofTables,
                                         content:{0:'--table nr.--', 1:'1 (Min)', 2:'2',3:'3'}},
                        }}
                    }
                }
            }
        });
        settings_dialog.load().then(function(){
            settings_dialog.save();
        });
    }
    function process_settings(){
        settings_dialog.save();
        addUser(wkof.settings.Leaderboard.addUser);
    }

    //manually delete cache
    function emptyList(){
        deleteLeaderboardRelatedCache().then(function() {
            usersInfoList = [];
            processArray();
        });
    }

    //------------------------------
	// Time
	//------------------------------

    var timeSinceLastRefresh = 1539614371504;
    var timeSinceLastRefreshText = '';

    function updateTimeSinceRefreshText (){
        let times = millisecondsToDayHourMinute(Date.now()-timeSinceLastRefresh);
        let daysPassed = times[0] === 1 ? ' day ' : ' days ';
        let hoursPassed = times[1] === 1 ? ' hour ' : ' hours ';
        let minutesPassed = times[2] === 1 ? ' minute ' : ' minutes ';
        timeSinceLastRefreshText = times[0] + daysPassed + times[1] + hoursPassed + times[2] + minutesPassed;
    }

    function millisecondsToDayHourMinute(time){
        let daysPassed = 24 * 60 * 60 * 1000,
            hoursPassed = 60 * 60 * 1000,
            day = Math.floor(time / daysPassed),
            hour = Math.floor( (time - day * daysPassed) / hoursPassed),
            minute = Math.round( (time - day * daysPassed - hour * hoursPassed) / 60000),
            pad = function(n){ return n < 10 && n != 0 ? '0' + n : n; };
        if( minute === 60 ){
            hour++;
            minute = 0;
        }
        if( hour === 24 ){
            day++;
            hour = 0;
        }
        return [day, pad(hour), pad(minute)];
    }

    //------------------------------
    // Caching
    //------------------------------

    //userlist
    var usersInfoList = [];
    var userSortingMethod = 'key1';
    var numberOfLeaderboardTables = '1';

    function saveUserListToCache(userList){
        wkof.file_cache.save('leaderboard_userList', userList).then(function(){
            //console.log('Save complete!');
        });
    }

    function getUserListFromCache(){
        let deferred = $.Deferred();
        wkof.file_cache.load('leaderboard_userList')
            .then(function(settings) {
            deferred.resolve(settings);
        }).catch(e => {
            console.log('Leaderboard - No cache found');
            deferred.resolve();
        });
        return deferred.promise();
    }

    //not called anywhere is for debugging purposes
    function deleteLeaderboardRelatedCache(cacheName = null){
        let deferred = $.Deferred();
        if(cacheName){
            console.log('deleting: ' + cacheName);
            wkof.file_cache.delete(cacheName).then(function() {//delete specific cached file
                deferred.resolve();
            });
        } else {
            console.log('deleting leaderboard related caches');
            wkof.file_cache.delete(/^leaderboard_/).then(function() {//delete all leaderboard related caching
                deferred.resolve();
            });
        }
        return deferred.promise();
    }

    //refresh time
    function getTimeSinceLastRefreshFromCache(){
        let deferred = $.Deferred();
        wkof.file_cache.load('leaderboard_timeSinceLastRefresh').then(function(settings) {
            deferred.resolve(settings);
        }).catch(e => {
            wkof.file_cache.save('leaderboard_timeSinceLastRefresh', Date.now()).then(function(){
            }).catch(e => {
                console.log(e);
            });
            timeSinceLastRefreshText = '0 days 0 hours 0 minutes';
            deferred.resolve(Date.now());
        });
        return deferred.promise();
    }

    function refreshDashboard(){
        wkof.file_cache.save('leaderboard_timeSinceLastRefresh', Date.now()).then(function(){
        }).catch(e => {
            console.log(e);
        });

        timeSinceLastRefreshText = '0 days 0 hours 0 minutes';
        processArray();
    }

    function saveSortingMethod(){
        wkof.file_cache.save('leaderboard_sortingMethod', userSortingMethod).then(function(){
            //console.log('Save complete! method:' + userSortingMethod);
        });
    }

    function getUserSortingMethodFromCache(){
        let deferred = $.Deferred();
        wkof.file_cache.load('leaderboard_sortingMethod').then(function(settings) {
            deferred.resolve(settings);
        }).catch(e => {
            userSortingMethod = 'key1';
            deferred.resolve();
        });
        return deferred.promise();
    }

    function saveNumberOfLeaderboardTables(){
        wkof.file_cache.save('leaderboard_numberOfTables', numberOfLeaderboardTables).then(function(){
            //console.log('Save complete! nr of tables:' + numberOfLeaderboardTables);
        });
    }

    function getNumberOfLeaderboardTablesFromCache(){
        let deferred = $.Deferred();
        wkof.file_cache.load('leaderboard_numberOfTables').then(function(settings) {
            deferred.resolve(settings);
        }).catch(e => {
            numberOfLeaderboardTables = '1';
            deferred.resolve();
        });
        return deferred.promise();
    }

    //------------------------------
	// Global variables
	//------------------------------
    const wkRealms = ['快', '苦', '死', '地獄', '天堂', '現実', '!!'];
    const wkRealmNames = ['Pleasant', 'Painful', 'Death', 'Hell', 'Paradise', 'Reality', 'Error'];
    const leaderboardColors = ['none', 'apprColor', 'guruColor', 'masterColor', 'enlightenedColor', 'burnedColor', 'errorColor'];

    //admin accounts, admin is any account with the Leader badge or a unique flair on the forums (list may be incomplete; users with no wk account like 'WaniMeKani' or 'system' are omitted)
    const adminNamesInfinity = ['viet', 'viet', 'Kristen', 'kristen', 'koichi', 'sam', 'oldbonsai', 'arpit.jalan', 'arpit', 'jenk', 'WaniKaniJavi', 'wanikanijavi', 'gomakuma'];//∞
    const adminNamesStar = ['TofuguKanae', 'tofugukanae', 'CyrusS', 'cyruss', 'mamimumason', 'a-regular-durtle', 'koichi-descended', 'RachelG', 'rachelg', 'arlo', 'camfugu', 'TofuguJenny', 'tofugujenny'];//★
    const adminNamesNone = ['dax', 'HAWK', 'hawk', 'blake.erickson', 'blake', 'CidPollendina', 'cidpollendina', 'Aya', 'aya', 'mamimumason'];//none

    const adminIdentifier = 'adminUserLeaderboard';
    const accountNotFoundMessage = ' (not found!)';

    //total number of WaniKani items
    const totalNumberOfWKItems = 8910;

    var toggleLeaderboardWidth0 = 0;
    var toggleLeaderboardWidth1 = 0;
    var toggleLeaderboardWidth2 = 0;
    var usersThatLeveledUp = '';

    //------------------------------
	// Get user information (name, level, avatar, realm)
	//------------------------------

    //determine what realm (pleasant, painful, etc.) a user is in
    function setRealm(userLevel){
        switch(Math.ceil(userLevel / 10) * 10) {
            case 70:
                return 5;//reality+
            case 60:
                return 5;//reality
            case 50:
                return 4;//heaven
            case 40:
                return 3;//hell
            case 30:
                return 2;//death
            case 20:
                return 1;//painful
            case 10:
                return 0;//pleasant
            default:
                return 6;//error
        }
    }

    async function checkIfUserAlreadyOnLeaderboard(name = ''){
        await delay();
        for (var i = 0; i < usersInfoList.length; i++){
            if(usersInfoList[i].name.toLowerCase() === name.toLowerCase() || usersInfoList[i].name.toLowerCase() === name+accountNotFoundMessage.toLowerCase()){
                return false;
                break;
            }
        }
        return true;
    }

    //add a single user
    async function addUser(name=''){
        let addUser = true;
        name = name.toLowerCase().split(' ').join('');
        checkIfUserAlreadyOnLeaderboard(name).then(
            async function(result){
                if(result){
                    //create default user info
                    const obj = {};
                    obj['name'] = name;
                    obj['level'] = 0;
                    obj['avatar_link'] = 'https://www.gravatar.com/avatar/65977e18f599e0319495b468c92b5179?s=300&d=https://cdn.wanikani.com/default-avatar-300x300-20121121.png';
                    obj['realm_number'] = 6;
                    obj['srs_distribution'] = [{}];
                    obj['wasUserFound'] = false;
                    obj['totalBurnPercentage'] = 0;

                    let object = [obj];
                    const promise = object.map(assignLevelAndAvatarFromWkProfile);//process a single user
                    await Promise.all(promise);

                    usersInfoList.push(object[0]);//add single user to the rest of the users

                    inference();
                } else{
                    if(typeof swal === "function"){
                        swal('A user with that name already exists in the list.');
                    } else{
                        alert('A user with that name already exists in the list.');
                    }
                }
        });
    }

    //delete a single user
    function deleteUser(name = ''){
        name = name.currentTarget.className.split(" ")[0];//get username from classnames
        for(var i = usersInfoList.length - 1; i >= 0; i--) {
            if(usersInfoList[i].name.split(accountNotFoundMessage).join('') === name) {
                usersInfoList.splice(i, 1);
            }
        }
        saveUserListToCache(usersInfoList);

        //if list empty reset refresh time to zero
        if(usersInfoList.length === 0){
            deleteLeaderboardRelatedCache();
            refreshDashboard();
        };

        createLeaderboard();
    }

    function changeSortOrder(){
        if(wkof.settings.Leaderboard.userOrderOption !== 'keyDefault'){
            userSortingMethod = wkof.settings.Leaderboard.userOrderOption;//save user chosen sorting method
        }
        saveSortingMethod();
        inference();
        settings_dialog.close();
    }

    function changeNumberofTables(){
        if(wkof.settings.Leaderboard.numberOfLeaderboardTabless !== '0'){
            numberOfLeaderboardTables = wkof.settings.Leaderboard.numberOfLeaderboardTabless;//save user chosen number of tables
        }
        saveNumberOfLeaderboardTables();
        createLeaderboard();
        settings_dialog.close();
    }

    //for sorting and saving userlist to cache
    function inference(){
        //determine sorting order for users
        switch(userSortingMethod) {
            case 'key1'://lv->burn->name
                usersInfoList.sort(function(a, b){
                    if(a.level !== b.level) {
                        return b.level - a.level;
                    }else if(a.totalBurnPercentage !== b.totalBurnPercentage) {
                        return b.totalBurnPercentage - a.totalBurnPercentage;
                    }else {
                        return a.name.localeCompare(b.name, 'en');
                    }
                });
                break;
            case 'key2'://lv->name
                usersInfoList.sort(function(a, b){
                    if(a.level !== b.level) {
                        return b.level - a.level;
                    }else {
                        return a.name.localeCompare(b.name, 'en');
                    }
                });
                break;
            case 'key3'://burn->lv->name
                usersInfoList.sort(function(a, b){
                    if(a.totalBurnPercentage !== b.totalBurnPercentage) {
                        return b.totalBurnPercentage - a.totalBurnPercentage;
                    }else if(a.level !== b.level) {
                        return b.level - a.level;
                    }else {
                        return a.name.localeCompare(b.name, 'en');
                    }
                });
                break;
            case 'key4'://burn->name
                usersInfoList.sort(function(a, b){
                    if(a.totalBurnPercentage !== b.totalBurnPercentage) {
                        return b.totalBurnPercentage - a.totalBurnPercentage;
                    }else {
                        return a.name.localeCompare(b.name, 'en');
                    }
                });
                break;
            case 'key5'://name ascending
                usersInfoList.sort(function(a, b){ return a.name.localeCompare(b.name, 'en');});
                break;
            case 'key6'://name descending
                usersInfoList.sort(function(a, b){ return b.name.localeCompare(a.name, 'en');});
                break;
        }

        saveUserListToCache(usersInfoList);//save sorting result and any added users

        createLeaderboard();//renew html
    }

    //throttle the requests a little
    function delay(){
        return new Promise(resolve => setTimeout(resolve, 250));
    }

    //get level, avatar, name and SRS stats from wk profile
    async function assignLevelAndAvatarFromWkProfile(item)
    {
        await delay();

        let xmlhttp;
        let userName = '';
        let userLevel = 0;
        let userGravatarLink = '';
        let srsCountsLabeled = [];
        let hasUserLeveledUp = false;
        let userFound = false;

        if (window.XMLHttpRequest)
        {// code for IE7+, Firefox, Chrome, Opera, Safari
            xmlhttp=new XMLHttpRequest();

            xmlhttp.onreadystatechange= function()
            {
                if (xmlhttp.readyState==4 && xmlhttp.status==200)
                {
                    //we get user information from the profile page e.g. wanikani.com/users/koichi
                    let userNameStart = xmlhttp.responseText.indexOf("<title>WaniKani / Profile / ");
                    let userLevelStart = xmlhttp.responseText.indexOf(",\"level\":");
                    let userGravatarStart = xmlhttp.responseText.indexOf("\",\"gravatar\":\"");
                    let userSRSDistributionStart = xmlhttp.responseText.indexOf(",\"requested_information\":{\"");

                    //get username
                    for(let i = 28; i < (28+item.name.length); i++){
                        userName+=xmlhttp.responseText[userNameStart+i];
                    }

                    //check to see if user given name and web retrieved user name are equal
                    if(userName.toLowerCase() === item.name.toLowerCase()){
                        userLevel = xmlhttp.responseText[userLevelStart+9]+xmlhttp.responseText[userLevelStart+10];
                        if (userLevel[1] == ','){ userLevel = userLevel[0];}//remove comma from single digit levels.

                        //check to see if user is already on the leaderboards
                        let found = usersInfoList.find(function(element) {
                            return element.name === userName.toLowerCase();
                        });
                        if(found !== undefined){
                            //check to see if user has leveled up, so we can display that later
                            if(found.level < userLevel && found.level != 0){
                                usersThatLeveledUp += found.name + ' ' + found.level + ' -> ' + userLevel + ', \n';
                                hasUserLeveledUp = true;
                            }
                        }

                        //get gravatar link
                        for(let i = 14; i < 46; i++){
                            userGravatarLink+=xmlhttp.responseText[userGravatarStart+i];
                        }

                        userFound = true;
                    } else { //a wanikani profile page didn't exist for this username and we got redirected to dashboard

                        //check to see if username is already on the leaderboards
                        /*let found = usersInfoList.find(function(element) {
                            return element.name === item.name;
                        });
                        //Account may have had a username change or been deleted
                        if(found !== undefined && item.wasUserFound){
                            alert(item.name + ' this username cannot be found. the account may have had a name change, been deleted or an error may have occured.');
                        }*/

                        userLevel = -1;
                        userGravatarLink = 'https://www.gravatar.com/avatar/65977e18f599e0319495b468c92b5179?s=300&d=https://cdn.wanikani.com/default-avatar-300x300-20121121.png';//default avatar
                        userFound = false;
                    }

                    //get SRS scores
                    let srsCounts = '';
                    for(let i = 23; i < (599); i++){
                        srsCounts+=xmlhttp.responseText[userSRSDistributionStart+i];
                    }

                    const filters = [
                        ['apprentice', ''],['guru', ''],['master', ''],['enlighten', ''],['burned', ''],['radicals', ''],['kanji', ''],['vocabulary', ''],['total', ''],
                        ['"', ''],['{', ''],['}', ''],[',', ''],[':::', ':'],['::', ':']];
                    const srsDistributionNames = ['apprRad','apprKan','apprVoc','apprTotal','guruRad','guruKan','guruVoc','guruTotal','masterRad','masterKan','masterVoc','masterTotal',
                                                 'enlightRad','enlightKan','enlightVoc','enlightTotal','burnRad','burnKan','burnVoc','burnTotal'];

                    filters.forEach(function(element) {
                        srsCounts = srsCounts.split(element[0]).join(element[1]);
                    });
                    srsCounts = srsCounts.substring(0, srsCounts.indexOf(';'));
                    srsCounts = srsCounts.substring(1, srsCounts.length);
                    srsCounts += ':';

                    const obj = {};
                    let tempNumber = '';
                    let j = 0;
                    for (let i = 0; i < srsCounts.length; i++){
                        if (srsCounts[i] != ':') {
                            tempNumber += srsCounts[i];
                        } else {
                            obj[srsDistributionNames[j]] = tempNumber;
                            j++;tempNumber = '';
                        }

                    }
                    srsCountsLabeled.push(obj);
                }
            }
            xmlhttp.open("GET", '/users/' + item.name, false);
            xmlhttp.send();
        }

        item.level = userLevel;//assign level
        item.avatar_link = userGravatarLink;//assign gravatarlink
        item.realm_number = setRealm(item.level);//assign realm
        item.srs_distribution = srsCountsLabeled;//assign SRS stats
        item.hasLeveledUp = hasUserLeveledUp;//whether or not user leveled up since last refresh
        item.wasUserFound = userFound;//whether the user name yielded result in the past (is used to detect name changes or deletion of account)
        item.totalBurnPercentage = Math.round((srsCountsLabeled[0].burnTotal/totalNumberOfWKItems*100) * 100) / 100;
    }

    function showLevelUps(){
        if(usersThatLeveledUp != ''){
            usersThatLeveledUp = usersThatLeveledUp.substring(0,usersThatLeveledUp.length-3);//remove the ', '

            if(typeof swal === "function"){
                swal('The following user(s) leveled up: \n', usersThatLeveledUp);
            } else{
                alert('The following user(s) leveled up: \n' + usersThatLeveledUp);
            }
            usersThatLeveledUp = '';
        }
    }

    async function processArray() {
        $('.leaderboard_loader').css('display', 'inline');
        $('.leaderboardSpan').addClass('blurry-text');

        //process array in parallel
        const promises = usersInfoList.map(assignLevelAndAvatarFromWkProfile);//refresh all
        await Promise.all(promises);

        showLevelUps();
        inference();
    }

    //not called anywhere is for debugging purposes
    /*async function testData(){
        const obj = {};
        obj['name'] = 'testing';
        obj['level'] = 1;
        obj['avatar_link'] = 'https://www.gravatar.com/avatar/65977e18f599e0319495b468c92b5179?s=300&d=https://cdn.wanikani.com/default-avatar-300x300-20121121.png';
        obj['realm_number'] = 1;
        obj['srs_distribution'] = [{}];
        obj['hasLeveledUp'] = false;

        let object = [obj];

        usersInfoList.push(object[0]);//add single user to the rest of the users

        inference();
    }*/

    function startup() {
        //for testing purposes
        //deleteLeaderboardRelatedCache();
        //testData();

        //get cache
        getTimeSinceLastRefreshFromCache().then(function(result) {
            timeSinceLastRefresh = result;
            updateTimeSinceRefreshText();
        });

        //get cache
        getUserSortingMethodFromCache().then(function(result) {
            userSortingMethod = result;
            if (userSortingMethod == null){
                userSortingMethod = 'key1';
            }
        });

        //get cache
        getNumberOfLeaderboardTablesFromCache().then(function(result) {
            numberOfLeaderboardTables = result;
            if (numberOfLeaderboardTables == null){
                numberOfLeaderboardTables = '1';
            }
        });

        //get cache
        getUserListFromCache().then(function(result) {
            if(result !== [] && result != undefined){
                usersInfoList = result;
                createLeaderboard();
            } else { //rebuilt anew
                //usersInfoList = usersInfoListTestData; //testing
                processArray();
            }
        });
    }

    //------------------------------
	// Styling
	//------------------------------

    const leaderboardTableCss = `
        /*.none*/

        #leaderboard > .span4 .kotoba-table-list table .none a, #leaderboard > .span4 .kotoba-table-list table .none {
            color: black;
        }
        #leaderboard > .span4 .kotoba-table-list table tr td span {
            padding: 0em 0.3em;
        }

        /*COLORS*/

        .apprColor {
            background-color: #f100a1;
            background-image: -moz-linear-gradient(top, #f0a, #dd0093);
            background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f0a), to(#dd0093));
            background-image: -webkit-linear-gradient(top, #f0a, #dd0093);
            background-image: -o-linear-gradient(top, #f0a, #dd0093);
            background-image: linear-gradient(to bottom, #f0a, #dd0093);
            background-repeat: repeat-x;
        }
        .guruColor {
            background-color: #a100f1;
            background-image: -moz-linear-gradient(top, #a0f, #9300dd);
            background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#a0f), to(#9300dd));
            background-image: -webkit-linear-gradient(top, #a0f, #9300dd);
            background-image: -o-linear-gradient(top, #a0f, #9300dd);
            background-image: linear-gradient(to bottom, #a0f, #9300dd);
            background-repeat: repeat-x;
        }
        .masterColor {
            background-color: #294ddb;/*183FD8*/
            background-image: -moz-linear-gradient(top, #5571e2, #2545C3);
            background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5571e2), to(#2545C3));
            background-image: -webkit-linear-gradient(top, #5571e2, #2545C3);
            background-image: -o-linear-gradient(top, #5571e2, #2545C3);
            background-image: linear-gradient(to bottom, #5571e2, #2545C3);
            background-repeat: repeat-x;
        }
        .enlightenedColor {
            background-color: #00a1f1;
            background-image: -moz-linear-gradient(top, #0af, #0093dd);
            background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0af), to(#0093dd));
            background-image: -webkit-linear-gradient(top, #0af, #0093dd);
            background-image: -o-linear-gradient(top, #0af, #0093dd);
            background-image: linear-gradient(to bottom, #0af, #0093dd);
            background-repeat: repeat-x;
        }
        .burnedColor {
            background-color: #faac05;
            background-image: -moz-linear-gradient(top, #fbc550, #faac05);
            background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbc550), to(#faac05));
            background-image: -webkit-linear-gradient(top, #fbc550, #faac05);
            background-image: -o-linear-gradient(top, #fbc550, #faac05);
            background-image: linear-gradient(to bottom, #fbc550, #faac05);
            background-repeat: repeat-x;
        }
        .customColor1 {
            background-color: #dd0093;
        }
        .errorColor{
            background-color: maroon;
        }

        /*END COLORS*/

        /*used to move level*/
        .floatRight {
            float: right;
        }

        /*TOOLTIP*/
        [tooltip]:before {
            /* needed - do not touch */
            content: attr(tooltip);
            position: absolute;
            opacity: 0;
            z-index: 1;

            /* customizable */
            transition: all 0.15s ease;
            padding: 10px;
            color: #333;
            border-radius: 10px;
            box-shadow: 2px 2px 1px silver;
        }

        /*TOOLTIP*/

        [tooltip]:hover:before {
            /* needed - do not touch */
            opacity: 1;

            /* customizable */
            background: yellow;
            margin-top: -50px;
            margin-left: 20px;
        }

        [tooltip]:not([tooltip-persistent]):before {
            pointer-events: none;
        }

        a.tooltipImg strong {line-height:30px;}
        a.tooltipImg span {
            z-index:10;display:none; padding:7px 10px;
            margin-top:30px; margin-left:-160px;
            width:300px; line-height:16px;
        }
        a.tooltipImg:hover span{
            display:inline; position:absolute;
            border:2px solid #FFF;  color:#EEE;
            background:#333 url(https://cdn.wanikani.com/default-avatar-300x300-20121121.png) repeat-x 0 0;
        }

        .callout {z-index:20;position:absolute;border:0;top:-14px;left:120px;}

        a.tooltipImg span
        {
            border-radius:2px;
            box-shadow: 0px 0px 8px 4px #666;
            /*opacity: 0.8;*/
        }
        a.tooltipImg:before {
            pointer-events: none;
        }

        /*END TOOLTIP*/
        /*LEADERBOARD*/

        td.leaderboard-userImg > img {
            border-radius: 50%;
            width: 25px;
            height: 25px;
            max-height: 25px;
        }
        #leaderboard_loader {
            border: 16px solid #f3f3f3;
            border-radius: 50%;
            border-top: 16px solid #3498db;
            width: 30px;
            height: 30px;
            -webkit-animation: spin 2s linear infinite; /* Safari */
            animation: spin 2s linear infinite;
            position: absolute;
            top: 40%;
            left: 40%;
            display:none;
        }
        /* Safari */
        @-webkit-keyframes spin {
            0% { -webkit-transform: rotate(0deg); }
            100% { -webkit-transform: rotate(360deg); }
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        .textshadow .blurry-text {
            color: transparent;
            text-shadow: 0 0 5px rgba(0,0,0,0.5);
        }
        .blurry-text, .blurry-text section .small-caps, .blurry-text section table tbody tr, .blurry-text section table tbody tr td a span {
            color: transparent;
            text-shadow: 0 0 5px rgba(0,0,0,0.5);
        }
        #leaderboard-files-import {
            width: 0.1px;
	        height: 0.1px;
	        opacity: 0;
	        overflow: hidden;
	        position: absolute;
	        z-index: -1;
        }
        .leaderboard-img-center {
            display: block;
            margin-left: auto;
            margin-right: auto;
        }
        /*END LEADERBOARD*/
        `;

    var leaderboardStyling = document.createElement('style');
    leaderboardStyling.type='text/css';
    if(leaderboardStyling.styleSheet){
        leaderboardStyling.styleSheet.cssText = leaderboardTableCss;
    }else{
        leaderboardStyling.appendChild(document.createTextNode(leaderboardTableCss));
    }
    document.getElementsByTagName('head')[0].appendChild(leaderboardStyling);

    //------------------------------
	// Leaderboard
	//------------------------------

    function processImportedUsers(file) {
        let reader = new FileReader();
        reader.readAsText(file);
        reader.onload = function(event){
            let csv = event.target.result;
            csv = csv.replace(/[^ -~]+/g,' ');//remove non printable characters
            csv = csv.split(',').join(' ');//in case multiple spreadsheet columns are used
            csv += ' ';

            let temp = '';
            let preExistingUsers = '';
            for (let i = 0; i < csv.length; i++){
                if (csv[i] !== ' ' || csv[i].length === 0) {
                    temp += csv[i];
                } else if (temp !== '') {
                    temp = temp.toLowerCase();

                    //check to see if user is already on the leaderboards
                    let isInList = false;
                    for (let j = 0; j < usersInfoList.length; j++){
                        if(usersInfoList[j].name.toLowerCase() === temp || usersInfoList[j].name.toLowerCase() === temp+accountNotFoundMessage.toLowerCase()){
                            isInList = true;
                            break;
                        }
                    }
                    if(!isInList){
                        //create default user info
                        const obj = {};
                        obj['name'] = temp;
                        obj['level'] = 0;
                        obj['avatar_link'] = 'https://www.gravatar.com/avatar/65977e18f599e0319495b468c92b5179?s=300&d=https://cdn.wanikani.com/default-avatar-300x300-20121121.png';
                        obj['realm_number'] = 6;
                        obj['srs_distribution'] = [{}];
                        obj['hasLeveledUp'] = false;
                        obj['wasUserFound'] = false;
                        obj['totalBurnPercentage'] = 0;

                        let object = [obj];
                        usersInfoList.push(object[0]);//add single user to the rest of the users
                    } else{
                        //the following user is already on the board
                        preExistingUsers += temp+', ';
                    }

                    temp = '';
                }
            }
            if(preExistingUsers != ''){
                preExistingUsers = preExistingUsers.substring(0,preExistingUsers.length-2);//remove the ', '

                 //check if custom box was included succesfully
                if(typeof swal === "function"){
                    swal("The following username(s) already exist on the leaderboard:", preExistingUsers);
                } else{
                    alert('The following username(s) already exist on the leaderboard: \n'+preExistingUsers);
                }
            }
            processArray();
        };
        reader.onerror = function(){
            if(typeof swal === "function"){
                swal('Unable to read ' + file.fileName);
            } else{
                alert('Unable to read ' + file.fileName);
            }
        };
    }

    function importUsers(evt){
        // Check for the various File API support.
        if (window.File && window.FileReader && window.FileList && window.Blob) {
            // All the File APIs are supported.
        } else {
            if(typeof swal === "function"){
                swal('The File APIs are not fully supported in this browser.');
            } else{
                alert('The File APIs are not fully supported in this browser.');
            }
        }

        var files = evt.target.files;
        var file = files[0];

        // read the file metadata
        if(file.type === 'application/vnd.ms-excel'){
            // read the file contents
            processImportedUsers(file);
        } else {
            if(typeof swal === "function"){
                swal('File type \'' + file.type + '\' is incorrect.', '\r\nUse a .cvs file.');
            } else{
                alert('File type \'' + file.type + '\' is incorrect. \r\nUse a .cvs file.');
            }
        }
    }

    function exportUsers(){
        let csvContent = "data:text/csv;charset=utf-8,";
        usersInfoList.forEach(function(infoArray, index){
            let dataString = infoArray.name;//+','+infoArray.level+',https://www.gravatar.com/avatar/'+infoArray.avatar_link+','+wkRealmNames[infoArray.realm_number]+','+infoArray.srs_distribution;
            csvContent += dataString + "\n";
        });
        if(usersInfoList.length != 0){
            let encodedUri = encodeURI(csvContent);
            let link = document.createElement("a");
            link.setAttribute("href", encodedUri);
            link.setAttribute("download", "my_leaderboard.csv");
            document.body.appendChild(link);
            link.click();
        } else {
            if(typeof swal === "function"){
                swal('There were no users to export.');
            } else{
                alert('There were no users to export.');
            }
        }
    }

    //see if account is an admin account
    function isAdmin(name){
        if($.inArray(name, adminNamesInfinity) != -1){
            return 'sym-∞ ' + adminIdentifier;
        } else if ($.inArray(name, adminNamesStar) != -1) {
            return 'sym-★ ' + adminIdentifier;
        } else if ($.inArray(name, adminNamesNone) != -1){
            return 'sym- ' + adminIdentifier;
        }
        return '';//not an admin account
    }

    //change admin level to ∞ or ★ or ' ' on hover
    function adminHovering(){
        $(".adminUserLeaderboard").hover(function(){
            let hoverSymbol = '∞';
            let symType = $(this)[0].className.substring(0,5);
            switch(symType) {
                case 'sym-∞':
                    hoverSymbol = '∞';
                    break;
                case 'sym-★':
                    hoverSymbol = '★';
                    break;
                default:
                    hoverSymbol = '';
            }
            $(this).children(':nth-child(2)').text(hoverSymbol);
        }, function(){
            $(this).children(':nth-child(2)').text($(this).children(':nth-child(2)')[0].className.substring(0,2));//turn symbol back to level
        });
    };

    //change leaderboard width
    function updateWidth(evt){//change icon when widened
        let classname = document.getElementsByClassName('leaderboardSpan');

        switch(evt.target.tableParam) {
            case 0:
                if(toggleLeaderboardWidth0){
                    shortenTable(classname, evt.target.tableParam);
                    toggleLeaderboardWidth0 = 0;
                } else {
                    widenTable(classname, evt.target.tableParam);
                    toggleLeaderboardWidth0 = 1;
                }
                break;
            case 1:
                if(toggleLeaderboardWidth1){
                    shortenTable(classname, evt.target.tableParam);
                    toggleLeaderboardWidth1 = 0;
                } else {
                    widenTable(classname, evt.target.tableParam);
                    toggleLeaderboardWidth1 = 1;
                }
                break;
            case 2:
                if(toggleLeaderboardWidth2){
                    shortenTable(classname, evt.target.tableParam);
                    toggleLeaderboardWidth2 = 0;
                } else {
                    widenTable(classname, evt.target.tableParam);
                    toggleLeaderboardWidth2 = 1;
                }
                break;
        }
    }

    function widenTable(classname, number){
        classname[number].setAttribute("style", "width: 100%;");
        classname[number].setAttribute("title", "Shorten screen");
    }

    function shortenTable(classname, number){
        classname[number].setAttribute("style", "width: ;");
        classname[number].setAttribute("title", "Widen screen");
    }

    function createLeaderboard() {
        let sectionContents = "";
        let timeSinceLastRefreshHtml = '';

        //console.log(usersInfoList);

        //loop to create multiple tables
        for (var i = 0; i < numberOfLeaderboardTables; i++){
            let numberOfUsersPerTable = usersInfoList.length/numberOfLeaderboardTables;
            let startNumberTable = Math.ceil(numberOfUsersPerTable*i);
            let endNumberTable = Math.floor((usersInfoList.length/numberOfLeaderboardTables)*(i+1));

            /*console.log('LOOP NUMBER:');
            console.log(i);
            console.log('number of users:');
            console.log(usersInfoList.length);
            console.log('number of tables:');
            console.log(numberOfLeaderboardTables);
            console.log('number of users per table:');
            console.log(numberOfUsersPerTable);
            console.log('start number:');
            console.log(startNumberTable);
            console.log('end number:');
            console.log(endNumberTable);
            console.log('--------------------------------------------------------------');*/

            //if userlist has three tables, an odd number of users and this is not the final table, add a user to the end
            if (numberOfLeaderboardTables == 2 && usersInfoList.length%2 != 0 && endNumberTable != usersInfoList.length){
                endNumberTable++;
            }

            //if userlist has three tables and tablelenght not a root of three
            if (numberOfLeaderboardTables == 3 && usersInfoList.length%3 != 0 && endNumberTable != usersInfoList.length){
                endNumberTable++;
            }


            //if userlist has an even number of user, three tables and this is not the final table, add a user to the end
            //if (usersInfoList.length%2 != 1 && endNumberTable != usersInfoList.length && ){
           //     endNumberTable++;
           // }
//

            //if this is not the final table add one more user to the end
            if(endNumberTable >= usersInfoList.length){
               //endNumberTable--;
            }

            //if this is not the final table add one more user to the end
            if(endNumberTable != usersInfoList.length){
               //endNumberTable++;
            }

            sectionContents += `
                <div class="leaderboardSpan span4">
                    <section class="kotoba-table-list dashboard-sub-section" style="position: relative;">
                        <h3 class="small-caps">Leaderboard</h3>
                        <i class="leaderboard-settings icon-plus" title="Add user" style="position:absolute; top:7.5px; right:5px;"></i>
                        <i class="leaderboard-refresh icon-refresh" title="Refresh leaderboard" style="position:absolute; top:7.5px; right:25px;"></i>
                        <i class="leaderboard-resize icon-resize-horizontal" title="Widen screen" style="position:absolute; top:7.5px; right:45px;"></i>
                        <i class="leaderboard-export icon-circle-arrow-down" title="Download leaderboard" style="position:absolute; top:7.5px; left:5px;"></i>
                        <input type="file" id="leaderboard-files-import" name="files[]" accept=".csv" multiple /><label class="icon-circle-arrow-up" for="leaderboard-files-import" title="Upload leaderboard" style="position:absolute; top:7.5px; left:25px;"></label>
                        <div id="leaderboard_loader" class="leaderboard_loader"></div>
                        <table>
                           <tbody>`;
            //if no users have been added yet
            if(usersInfoList.length == 0){
                timeSinceLastRefreshText = '0 days 0 hours 0 minutes';
                sectionContents += `<tr class="none-available">
                                        <td>
                                        <div>
                                            <i class="icon-user"></i>
                                        </div>
                                        You haven't added any users yet. <br /><br />
                                        Use (<i class='icon-plus' ></i>) to add users.
                                        </td>
                                    </tr>`
                timeSinceLastRefreshHtml = `<div class="see-more">
                                                <a class="small-caps">
                                                &nbsp;
                                                </a>
                                            </div>`;
            } else {
                timeSinceLastRefreshHtml = `<div class="see-more">
                                                <a class="small-caps" style="padding: 3.5px 15px 0px 15px;">Time since last refresh...</a>
                                                ${timeSinceLastRefreshText}
                                                (
                                                <a class="tooltipImg icon-question">
                                                    <span>
                                                        <strong style="background-color: black">**Updating Leaderboard**</strong><br />
                                                        <i style="background-color: black">Leaderboard updates only when refreshed (<i class='icon-refresh' ></i>) manually.</i>
                                                    </span>
                                                </a>
                                                )
                                            </div>`;
            }
            for (var j = startNumberTable; j < endNumberTable; j++){
                //check if user is admin
                let adminClass = isAdmin(usersInfoList[j].name);

                //check if username was valid
                let userErrorNotFoundMessage = usersInfoList[j].wasUserFound ? '' : accountNotFoundMessage;

                //calculate burn percentage
                let burnTotal = usersInfoList[j].level != 3 ? `Total Burned: ${usersInfoList[j].srs_distribution[0].burnTotal}, ${usersInfoList[j].totalBurnPercentage}%` : '&#9888; Users without subscription will show as level 3.';

                //for user achievements
                let userCelebrationIcon = '';
                //has user leveled up?
                if(usersInfoList[j].hasLeveledUp){userCelebrationIcon = 'icon-level-up';}
                //does user have 100% burned?
                if(usersInfoList[j].totalBurnPercentage >= 100){userCelebrationIcon = 'icon-trophy';}

                let burnPercentageGradient = usersInfoList[j].realm_number === 5 ? `background: linear-gradient(to right,
                    #faac05,
                    #faac05 ${usersInfoList[j].totalBurnPercentage}%,
                    rgba(255,255,255) ${usersInfoList[j].totalBurnPercentage+1.25}%,
                    #fbc550 ${usersInfoList[j].totalBurnPercentage+2.5}%);` : '';

                sectionContents += `<tr class="${leaderboardColors[usersInfoList[j].realm_number]}" style="${burnPercentageGradient}">
                                        <td style="text-align:center;" tooltip="${wkRealmNames[usersInfoList[j].realm_number]}">
                                            <span>${wkRealms[usersInfoList[j].realm_number]}</span>
                                        </td>
                                        <td tooltip="${burnTotal}">
                                            <a href="users/${usersInfoList[j].name}" class="${adminClass}">
                                                <span>${usersInfoList[j].name + userErrorNotFoundMessage}</span>
                                                <span class="${usersInfoList[j].level} floatRight">${usersInfoList[j].level}</span>
                                                <i class="${userCelebrationIcon} floatRight"></i>
                                            </a>
                                        </td>
                                        <td class="${usersInfoList[j].name} leaderboard-userImg" tooltip="Remove user?">
                                            <img class="leaderboard-img-center" src="https://www.gravatar.com/avatar/${usersInfoList[j].avatar_link}?s=300&d=https://cdn.wanikani.com/default-avatar-300x300-20121121.png"/>
                                        </td>
                                    </tr>`;
            }
            sectionContents += `
                               </tbody>
                           </table>
                           ${timeSinceLastRefreshHtml}
                       </section>
                   </div>`;
        }
        let leaderboardTableStyle = '<div id="leaderboard" class="row">';
        sectionContents += `</div>`;

        //check if leaderboards is already there
        if(document.getElementById("leaderboard")) {
            $('#leaderboard').replaceWith(leaderboardTableStyle+sectionContents);//replace existing board
        } else {
            if ($('section.progression').length) {
                $('section.progression').after(leaderboardTableStyle);
            }
            else {
                $('section.srs-progress').after(leaderboardTableStyle);
            }
            $('#leaderboard').append(sectionContents);
        }

        //eventlisteners
        let classname = document.getElementsByClassName('leaderboard-settings');
        for (let i = 0; i < classname.length; i++) {
            classname[i].addEventListener('click', open_settings);
        }
        classname = document.getElementsByClassName('leaderboard-refresh');
        for (let i = 0; i < classname.length; i++) {
            classname[i].addEventListener('click', refreshDashboard);
        }
        classname = document.getElementsByClassName('leaderboard-resize');
        for (let i = 0; i < classname.length; i++) {
            classname[i].addEventListener('click', updateWidth);
            classname[i].tableParam = i;
        }
        classname = document.getElementsByClassName('leaderboard-export');
        for (let i = 0; i < classname.length; i++) {
            classname[i].addEventListener('click', exportUsers);
        }
        document.getElementById('leaderboard-files-import').addEventListener('change', importUsers);

        $('#leaderboard').find('.leaderboard-userImg').on('click', deleteUser);

        adminHovering();
    }

    startup();
})();