<h1 contenteditable="true">Leaderboard</h1>
	<ul class="leaderboard">
		<!-- place for persons / isotope plugin -->
	</ul>
	<div class="add-person-popup">
		<div class="box">
			<input type="text" id="rand-icon" class="random-icon" value="šŸ¦Š">
			<br>
			<input type="text" id="nickname" size="15" placeholder="Nickname">
			<br>
			<input type="number" id="score" placeholder="score">
			<br>
			<button class="cancel">CANCEL</button>
			<button class="ok">OK</button>
		</div>
	</div>
	<div class="edit-popup">
		<div class="box">
			<textarea id="tarea"></textarea>
			<button class="cancel">CANCEL</button>
			<button class="ok">OK</button>
		</div>
	</div>
	<div class="link-popup">
		<div class="box">
			<textarea id="tarea-link"></textarea>
			<button class="close">CLOSE</button>
		</div>
	</div>
	

	<div class="add-person">+</div>
	<div class="edit">šŸ“‹</div>
	<div class="link">šŸ”—</div>





<a id="gh-icon" target="_blank" href="https://github.com/tgogos/leaderboard"><img loading="lazy" width="149" height="149" src="https://github.blog/wp-content/uploads/2008/12/forkme_right_darkblue_121621.png?resize=149%2C149" class="attachment-full size-full" alt="Fork me on GitHub" data-recalc-dims="1"></a>
html, body, div, figure, p, h1, h2, ul, li, input, button, textarea {margin:0; padding:0; box-sizing: border-box; font-family: sans-serif;}
body {background-color: #9b8975; padding: 0 20%;}
h1 {padding:30px 0; text-align: center; font-size: 50px; color: #fff8dc;}
.leaderboard {list-style: none;}
.leaderboard .person {background-color: #2d3e50; padding: 0 0 0 70px; width: 100%; height: 70px; margin-bottom: 12px; overflow: hidden; position: relative;}
.leaderboard .person .icon {position: absolute; top: 0; left: 0; font-size: 50px; line-height: 70px; background: #fff8dc;}
.leaderboard .person .nickname {padding: 0 0 0 10px; font-size: 30px; line-height: 70px; color: #fff;}
.leaderboard .person .score {position: absolute; top: 0; right: 0; font-size: 50px; line-height: 70px; background: #dc3546; color: #fff; width: 120px; text-align: center; z-index: 10; cursor: pointer;}
.leaderboard .person.open .point-btns { right: 120px; }
.leaderboard .person .point-btns {position: absolute; list-style: none; top: 0; right: -100%; height: 100%; font-size: 0; transition: right 0.5s ease-out; z-index: 1;}
.leaderboard .person .point-btns .point-btn {display: inline-block; height: 100%; width: 70px; background-color: #ccc; font-size: 30px; text-align: center; line-height: 70px; cursor: pointer;}
.leaderboard .person .point-btns .point-btn--1 {background: #efd68d; color: #a79255;}
.leaderboard .person .point-btns .point-btn--2 {background: #f7ce52; color: #9a802f;}
.leaderboard .person .point-btns .point-btn--5 {background: #fec107; color: #735d1a;}
.leaderboard .person .point-btns .point-btn-1 {background: #5ace72; color: #bef5ca;}
.leaderboard .person .point-btns .point-btn-2 {background: #3dc159; color: #a0efb1;}
.leaderboard .person .point-btns .point-btn-5 {background: #29a744; color: #a0efb1; border-right: 2px solid #1e9237;}

.add-person {position: fixed; bottom: 30px; right: 30px; width: 50px; height: 50px; border-radius: 50%; background: #17a2b9; line-height: 50px; text-align: center; font-size: 40px; color: #fff; cursor: pointer;}
.add-person-popup {display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 20; background-color: rgb(0 0 0 / 70%);}
body.show-add-person .add-person-popup {display: block;}
body.show-add-person {overflow: hidden;}
.add-person-popup .box {display: inline-block; height: 500px; width: 500px; background: #353a40; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); border: 6px dashed #fff8dc; text-align:center;}
.add-person-popup .box .random-icon {width: 200px; height: 200px; font-size: 100px; line-height: 200px; background: #fff; display: inline-block; border-radius: 50%; margin: 25px 0; border: none; padding: 0; text-align: center; color: #000;}
.add-person-popup .box input {height: 60px; line-height: 50px; font-size: 50px; border: none; background: #9b8975; color: #fff; text-align: center; width: 95%; margin-bottom: 10px;}
.add-person-popup .box input::-webkit-input-placeholder {color: #7d6c59;}
.add-person-popup .box #score {width: 250px;}
.add-person-popup .box button {cursor: pointer; text-align: center; position: absolute; bottom: 0; width: 50%; height: 60px; border: none; font-size: 25px; font-weight: 700; color: #fff;} 
.add-person-popup .box button.ok {right: 0; background: #29a744;}
.add-person-popup .box button.cancel {left: 0; background: #1b252f;}

.edit {position: fixed; bottom: 30px; right: 90px; width: 50px; height: 50px; line-height: 50px; text-align: center; font-size: 40px; color: #fff; cursor: pointer;}
.edit-popup {display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 20; background-color: rgb(0 0 0 / 70%);}
body.show-edit .edit-popup {display: block;}
body.show-edit {overflow: hidden;}
.edit-popup .box {display: inline-block; height: 500px; width: 500px; background: #353a40; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); border: 6px dashed #fff8dc; text-align:center;}
.edit-popup .box textarea {width: 100%; background: #353a40; box-sizing: border-box; font-size: 20px; color: #fff; border: none; padding: 15px; height: calc(100% - 60px);}
.edit-popup .box button {cursor: pointer; text-align: center; position: absolute; bottom: 0; width: 50%; height: 60px; border: none; font-size: 25px; font-weight: 700; color: #fff;} 
.edit-popup .box button.ok {right: 0; background: #29a744;}
.edit-popup .box button.cancel {left: 0; background: #1b252f;}

.link {position: fixed; bottom: 30px; left: 30px; width: 50px; height: 50px; line-height: 50px; text-align: center; font-size: 40px; color: #fff; cursor: pointer;}
.link-popup {display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 20; background-color: rgb(0 0 0 / 70%);}
body.show-link .link-popup {display: block;}
body.show-link {overflow: hidden;}
.link-popup .box {display: inline-block; height: 500px; width: 500px; background: #353a40; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); border: 6px dashed #fff8dc; text-align:center;}
.link-popup .box textarea {width: 100%; background: #353a40; box-sizing: border-box; font-size: 20px; color: #fff; border: none; padding: 15px; height: calc(100% - 60px);}
.link-popup .box textarea::selection {background: #ccc;}
.link-popup .box button {cursor: pointer; text-align: center; position: absolute; bottom: 0; width: 50%; height: 60px; border: none; font-size: 25px; font-weight: 700; color: #fff;} 
.link-popup .box button.close {left: 0; background: #1b252f; width: 100%;}


#gh-icon {position: absolute; top: 0; right: 0;}
var leaderboard_iso = null;
var local_storage_available = false;
var data            = [];
var emojis          = "šŸ¶ šŸ± šŸ­ šŸ¹ šŸ° šŸ¦Š šŸ» šŸ¼ šŸØ šŸÆ šŸ¦ šŸ® šŸ· šŸ½ šŸø šŸµ šŸ™ˆ šŸ™‰ šŸ™Š šŸ’ šŸ” šŸ§ šŸ¦ šŸ¤ šŸ£ šŸ„ šŸ¦† šŸ¦… šŸ¦‰ šŸ¦‡ šŸŗ šŸ— šŸ“ šŸ¦„ šŸ šŸ› šŸ¦‹ šŸŒ šŸž šŸœ šŸ¦Ÿ šŸ¦— šŸ•· šŸ•ø šŸ¦‚ šŸ¢ šŸ šŸ¦Ž šŸ¦– šŸ¦• šŸ™ šŸ¦‘ šŸ¦ šŸ¦ž šŸ¦€ šŸ” šŸ  šŸŸ šŸ¬ šŸ³ šŸ‹ šŸ¦ˆ šŸŠ šŸ… šŸ† šŸ¦“ šŸ¦ šŸ¦§ šŸ˜ šŸ¦› šŸ¦ šŸŖ šŸ« šŸ¦’ šŸ¦˜ šŸƒ šŸ‚ šŸ„ šŸŽ šŸ– šŸ šŸ‘ šŸ¦™ šŸ šŸ¦Œ šŸ• šŸ© šŸ¦® šŸ•ā€šŸ¦ŗ šŸˆ šŸ“ šŸ¦ƒ šŸ¦š šŸ¦œ šŸ¦¢ šŸ¦© šŸ‡ šŸ¦ šŸ¦Ø šŸ¦” šŸ¦¦ šŸ¦„ šŸ šŸ€ šŸ¦”".split(" ");

var test_data = `šŸ’,Green Monkey,14
šŸ¦Ž,Orange Iguana,13
šŸ¦œ,Purple Parrot,12
šŸŸ,Blue Barracuda,10
šŸ†,Red Jaguar,9
šŸ,Silver Snake,8`;

var template        = `
<li class="person">
	<p class="icon">{{icon}}</p>
	<p class="nickname">{{nickname}}</p>
	<p class="score">{{score}}</p>
	<ul class="point-btns">
		<li class="point-btn point-btn--5" data-points-value="-5">-5</li>
		<li class="point-btn point-btn--2" data-points-value="-2">-2</li>
		<li class="point-btn point-btn--1" data-points-value="-1">-1</li>
		<li class="point-btn point-btn-1" data-points-value="1">+1</li>
		<li class="point-btn point-btn-2" data-points-value="2">+2</li>
		<li class="point-btn point-btn-5" data-points-value="5">+5</li>
	</ul>
</li>
`;

$( document ).ready(function() {

	// check if local storage is available
	// updates `local_storage_available` variable
	// test_local_storage(); // can't use local storage at codepen, chrome fails

	// some button handlers...
	initialize_basic_btn_listeners();


	// intialize isotope plugin
	leaderboard_iso = $('.leaderboard').isotope({
		// options
		itemSelector: '.person',
		layoutMode: 'vertical',
		getSortData: {
			score: '.score parseInt'
		}
	});
	

	// 1st step:
	// check for data in "URLSearchParams"
	// If the url has data in the query string
	// give priority to this data
	url = new URL((window.location.href));
	query_string = url.searchParams.get("q");
	if (query_string != null) {
		decoded_str = decodeURIComponent(window.atob(query_string));
		data = JSON.parse(decoded_str);

		// save data to local storage
		// refresh without data in the url
		if (local_storage_available) {
			localStorage.setItem('leaderboard_data', JSON.stringify(data));
			window.location.href = [location.protocol, '//', location.host, location.pathname].join('');
		}
		return;
	}

	// 2nd step:
	// check for data in the local storage
	if (local_storage_available) {
		data = JSON.parse(localStorage.getItem('leaderboard_data'));
		
		// add data to the UI
		if (data != null) {
			build_leaderboard();
		} else {
			data = [];
		}
	}
  
  //  codepen example
  $("#tarea").val(test_data);
  $('.edit-popup .ok').trigger('click');
  $("body").toggleClass("show-edit");
  
  $('.person .score').last().parent().toggleClass("open");
  
});

function sort_leaderboard(){
	leaderboard_iso.isotope( 'updateSortData');
	leaderboard_iso.isotope({ sortBy: "score", sortAscending : false });
}



function initialize_basic_btn_listeners() {
	
	// adding a new person listeners
	//
	$('.add-person').on('click',function(){
		scroll_to_top_smooth();
		$("body").toggleClass("show-add-person");
		// pick random animal emoji and clear form
		$('#rand-icon').val(emojis[getRandomInt(0,emojis.length)]);
		$('#nickname').val("");
		$('#score').val("");
	});

	$('.add-person-popup .cancel').on('click',function(){
		$("body").toggleClass("show-add-person");
	});

	$('.add-person-popup .ok').on('click',function(){
		var new_person = {
			icon:    $('#rand-icon').val(),
			nickname: $('#nickname').val(),
			score:    $('#score').val()
		};
		data.push(new_person);
		build_leaderboard();
		$("body").toggleClass("show-add-person");
	});


	// editing
	// 
	$('.edit').on('click',function(){
		scroll_to_top_smooth();
		$("body").toggleClass("show-edit");
		// load data to textarea
		data_str = "";
		data.forEach(function(person) {
			data_str = data_str + (person.icon + "," + person.nickname + "," + person.score + "\n");
		});
		$("#tarea").val(data_str);
	});

	$('.edit-popup .cancel').on('click',function(){
		$("body").toggleClass("show-edit");
	});

	$('.edit-popup .ok').on('click',function(){
		read_data_from_text( $('#tarea').val().trim() );
		build_leaderboard();
		$("body").toggleClass("show-edit");
	});


	// link sharing
	// 
	$('.link').on('click',function(){
		scroll_to_top_smooth();
		$("body").toggleClass("show-link");
		// load data to textarea
		$("#tarea-link").val(generate_share_link());
		$('#tarea-link').select();
	});

	$('.link-popup .close').on('click',function(){
		$("body").toggleClass("show-link");
	});
}



function initialize_person_btn_listeners() {
	$('.person .score').on('click',function(){
		$(this).parent().toggleClass("open");
	});

	// +/- points buttons
	// also updates data[]
	$('.point-btn').on('click', async function(event){
		event.stopPropagation();
		var $li            = $(this).parent().parent();
		var $score         = $li.find('.score');
		var current_score  = parseInt( $score.text() );
		var points_clicked = parseInt($(this).attr('data-points-value'));
		var target_score   = current_score + points_clicked;

		// update UI
		if (target_score > current_score) {
			for (i=current_score ; i<=target_score ; i++) {
				$score.text(i);
				await sleep(150);
			}
		} else {
			for (i=current_score ; i>=target_score ; i--) {
				$score.text(i);
				await sleep(150);
			}
		}
		sort_leaderboard();

		// update data[]
		data.forEach(function(person) {
			if ((person.nickname == $li.find('.nickname').text()) && (person.icon == $li.find('.icon').text())) {
				person.score = target_score;
			}
		})
		// save data to local storage
		// localStorage.setItem('leaderboard_data', JSON.stringify(data));
	});
}

function build_leaderboard() {
	// remove all current list items
	$('.leaderboard').empty();

	if (data.length == 0) return;

	// append all person-list-items
	data.forEach(function(person) {
		$('.leaderboard').append(
			template.replace(/{{icon}}/gm,person.icon)
					.replace(/{{nickname}}/gm,person.nickname)
					.replace(/{{score}}/gm,person.score));
	})

	// reload isotope
	leaderboard_iso.isotope('reloadItems');

	// save data to local storage
	// localStorage.setItem('leaderboard_data', JSON.stringify(data));

	// add button listeners for score +/-
	initialize_person_btn_listeners();
	sort_leaderboard();
}

function read_data_from_text(data_str) {
	data = [];
	if (data_str == "") {return;}
	var lines = data_str.split('\n');
	lines.forEach(function(line) {
		tokens = line.split(',');
		data.push({
			icon:     tokens[0],
			nickname: tokens[1],
			score:    tokens[2]
		});
	});
}





function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Returns a random number between min (inclusive) and max (exclusive)
 */
function getRandomArbitrary(min, max) {
    return Math.random() * (max - min) + min;
}

/**
 * Returns a random integer between min (inclusive) and max (inclusive).
 * The value is no lower than min (or the next integer greater than min
 * if min isn't an integer) and no greater than max (or the next integer
 * lower than max if max isn't an integer).
 * Using Math.round() will give you a non-uniform distribution!
 */
function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function generate_share_link() {
	// encode to BASE64 the data object
	base64_str = btoa(encodeURIComponent(JSON.stringify(data)));

	share_url = new URL( [location.protocol, '//', location.host, location.pathname].join('') );
	share_url.searchParams.append("q",base64_str)
	return share_url.href;
}

function scroll_to_top_smooth() {
	window.scrollTo({
		top: 0,
		behavior: 'smooth'
	});
}

function test_local_storage(){
    var test = 'test';
    try {
        localStorage.setItem(test, test);
        localStorage.removeItem(test);
        local_storage_available = true;
    } catch(e) {
        local_storage_available = false;
    }
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js
  2. https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.min.js