<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(40);
}
} else {
for (i=current_score ; i>=target_score ; i--) {
$score.text(i);
await sleep(40);
}
}
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;
}
}
This Pen doesn't use any external CSS resources.