2019-05-20 21:44:53 +03:00
|
|
|
<html>
|
|
|
|
|
|
|
|
<head>
|
|
|
|
<title>Coding Stats</title>
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
|
|
<link rel="icon" data-emoji="📊" type="image/png">
|
|
|
|
<style>
|
|
|
|
body {
|
|
|
|
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
|
|
|
color: #666;
|
|
|
|
}
|
|
|
|
|
2019-09-20 13:05:22 +03:00
|
|
|
h1 {
|
|
|
|
margin-bottom: 10px;
|
|
|
|
}
|
|
|
|
|
|
|
|
h3 {
|
|
|
|
margin: 10px 0 20px 0;
|
|
|
|
}
|
|
|
|
|
2019-05-20 21:44:53 +03:00
|
|
|
.grid-container {
|
2019-05-21 15:02:04 +03:00
|
|
|
width: 75%;
|
2019-05-20 21:44:53 +03:00
|
|
|
display: grid;
|
|
|
|
grid-template-areas:
|
|
|
|
'header header header'
|
|
|
|
'sec1 sec1 sec2'
|
|
|
|
'sec1 sec1 sec3'
|
|
|
|
'sec1 sec1 sec4'
|
|
|
|
'footer footer footer';
|
|
|
|
grid-gap: 10px;
|
|
|
|
visibility: hidden;
|
|
|
|
}
|
|
|
|
|
|
|
|
.projects-container {
|
|
|
|
grid-area: sec1
|
|
|
|
}
|
|
|
|
|
|
|
|
.os-container {
|
|
|
|
grid-area: sec2
|
|
|
|
}
|
|
|
|
|
|
|
|
.editor-container {
|
|
|
|
grid-area: sec3
|
|
|
|
}
|
|
|
|
|
|
|
|
.language-container {
|
|
|
|
grid-area: sec4
|
|
|
|
}
|
|
|
|
|
|
|
|
.header-container {
|
|
|
|
grid-area: header
|
|
|
|
}
|
|
|
|
|
2019-05-21 23:40:59 +03:00
|
|
|
.input {
|
2019-05-20 21:44:53 +03:00
|
|
|
width: 300px;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
|
|
|
|
<body>
|
2019-09-20 13:05:22 +03:00
|
|
|
<h1>Wakapi</h1>
|
|
|
|
<h3>Your Coding Statistics Dashboard</h3>
|
2019-05-20 21:44:53 +03:00
|
|
|
<div class="input-container" id="input-container">
|
2019-05-21 23:40:59 +03:00
|
|
|
<label for="user">User: </label>
|
|
|
|
<input type="text" class="input" id="user-input" name="user" placeholder="Enter Username">
|
2019-09-20 13:05:22 +03:00
|
|
|
<label for="pw">Password: </label>
|
2019-05-21 23:40:59 +03:00
|
|
|
<input type="password" class="input" id="password-input" name="pw" placeholder="Enter Password">
|
2019-05-23 09:14:32 +03:00
|
|
|
<button onclick="load('today', true)">Today (live)</button>
|
|
|
|
<button onclick="load('day', false)">Day</button>
|
2019-05-20 21:57:49 +03:00
|
|
|
<button onclick="load('week', false)">Week</button>
|
|
|
|
<button onclick="load('month', false)">Month</button>
|
|
|
|
<button onclick="load('year', false)">Year</button>
|
2019-05-20 21:44:53 +03:00
|
|
|
</div>
|
|
|
|
<div class="grid-container" id="grid-container">
|
|
|
|
<div class="header-container">
|
|
|
|
<p>
|
|
|
|
<strong>Total:</strong> <span id="total-span"></span><br>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
<div class="projects-container" id="projects-container">
|
|
|
|
<canvas id="chart-projects"></canvas>
|
|
|
|
</div>
|
|
|
|
<div class="os-container" id="os-container">
|
|
|
|
<canvas id="chart-os"></canvas>
|
|
|
|
</div>
|
|
|
|
<div class="editor-container" id="editor-container">
|
|
|
|
<canvas id="chart-editor"></canvas>
|
|
|
|
</div>
|
|
|
|
<div class="language-container" id="language-container">
|
|
|
|
<canvas id="chart-language"></canvas>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js"></script>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.bundle.min.js"></script>
|
|
|
|
|
|
|
|
<script>
|
2019-07-07 12:17:45 +03:00
|
|
|
const SHOW_TOP_N = 10
|
|
|
|
|
|
|
|
const projectsCanvas = document.getElementById("chart-projects")
|
|
|
|
const osCanvas = document.getElementById("chart-os")
|
|
|
|
const editorsCanvas = document.getElementById("chart-editor")
|
|
|
|
const languagesCanvas = document.getElementById("chart-language")
|
|
|
|
|
|
|
|
let charts = []
|
2019-05-23 09:14:32 +03:00
|
|
|
|
2019-05-20 21:44:53 +03:00
|
|
|
String.prototype.toHHMMSS = function () {
|
|
|
|
var sec_num = parseInt(this, 10);
|
|
|
|
var hours = Math.floor(sec_num / 3600);
|
|
|
|
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
|
|
|
var seconds = sec_num - (hours * 3600) - (minutes * 60);
|
|
|
|
|
|
|
|
if (hours < 10) { hours = "0" + hours; }
|
|
|
|
if (minutes < 10) { minutes = "0" + minutes; }
|
|
|
|
if (seconds < 10) { seconds = "0" + seconds; }
|
|
|
|
return hours + ':' + minutes + ':' + seconds;
|
|
|
|
}
|
|
|
|
|
2019-05-20 21:57:49 +03:00
|
|
|
function load(interval, live) {
|
2019-05-21 23:40:59 +03:00
|
|
|
let user = document.getElementById('user-input').value
|
|
|
|
let password = document.getElementById('password-input').value
|
|
|
|
let hashed = btoa(`${user}:${password}`)
|
2019-07-07 12:17:45 +03:00
|
|
|
|
2019-05-20 21:57:49 +03:00
|
|
|
fetch(`${window.location.href}/api/summary?interval=${interval}&live=${live}`, {
|
2019-05-20 21:44:53 +03:00
|
|
|
method: 'GET',
|
|
|
|
headers: {
|
|
|
|
'Accept': 'application/json',
|
2019-05-21 23:40:59 +03:00
|
|
|
'Authorization': `Basic ${hashed}`
|
2019-05-20 21:44:53 +03:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.then((res) => {
|
|
|
|
if (res.status === 401) {
|
|
|
|
console.error('Unauthorized')
|
|
|
|
alert('Unauthorized')
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
})
|
|
|
|
.then((res) => res.json())
|
|
|
|
.then((json) => draw(json))
|
|
|
|
.catch(err => console.error(err));
|
|
|
|
}
|
|
|
|
|
|
|
|
function draw(data) {
|
|
|
|
let titleOptions = {
|
|
|
|
display: true,
|
|
|
|
fontSize: 16
|
|
|
|
}
|
|
|
|
|
|
|
|
function getTooltipOptions(key, type) {
|
|
|
|
return {
|
|
|
|
mode: 'single',
|
|
|
|
callbacks: {
|
|
|
|
label: (item) => {
|
|
|
|
let idx = type === 'pie' ? item.index : item.datasetIndex
|
|
|
|
let d = data[key][idx]
|
|
|
|
return `${d.key}: ${d.total.toString().toHHMMSS()}`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-07 12:17:45 +03:00
|
|
|
charts.forEach(c => c.destroy())
|
2019-05-23 09:14:32 +03:00
|
|
|
|
|
|
|
let projectChart = new Chart(projectsCanvas.getContext('2d'), {
|
2019-05-20 21:44:53 +03:00
|
|
|
type: 'horizontalBar',
|
|
|
|
data: {
|
|
|
|
datasets: data.projects
|
2019-07-07 12:17:45 +03:00
|
|
|
.slice(0, Math.min(SHOW_TOP_N, data.projects.length))
|
2019-05-20 21:44:53 +03:00
|
|
|
.map(p => {
|
|
|
|
return {
|
|
|
|
label: p.key,
|
|
|
|
data: [parseInt(p.total)],
|
|
|
|
backgroundColor: getRandomColor(p.key)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
|
|
|
options: {
|
2019-07-07 12:17:45 +03:00
|
|
|
title: Object.assign(titleOptions, { text: `Projects (top ${SHOW_TOP_N})` }),
|
2019-05-20 21:44:53 +03:00
|
|
|
tooltips: getTooltipOptions('projects', 'bar'),
|
|
|
|
legend: {
|
|
|
|
display: false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-05-23 09:14:32 +03:00
|
|
|
let osChart = new Chart(osCanvas.getContext('2d'), {
|
2019-05-20 21:44:53 +03:00
|
|
|
type: 'pie',
|
|
|
|
data: {
|
|
|
|
datasets: [{
|
2019-07-07 12:17:45 +03:00
|
|
|
data: data.operating_systems
|
|
|
|
.slice(0, Math.min(SHOW_TOP_N, data.operating_systems.length))
|
|
|
|
.map(p => parseInt(p.total)),
|
2019-05-20 21:44:53 +03:00
|
|
|
backgroundColor: data.operating_systems.map(p => getRandomColor(p.key))
|
|
|
|
}],
|
2019-07-07 12:17:45 +03:00
|
|
|
labels: data.operating_systems
|
|
|
|
.slice(0, Math.min(SHOW_TOP_N, data.operating_systems.length))
|
|
|
|
.map(p => p.key)
|
2019-05-20 21:44:53 +03:00
|
|
|
},
|
|
|
|
options: {
|
2019-07-07 12:17:45 +03:00
|
|
|
title: Object.assign(titleOptions, { text: `Operating Systems (top ${SHOW_TOP_N})` }),
|
2019-05-20 21:44:53 +03:00
|
|
|
tooltips: getTooltipOptions('operating_systems', 'pie')
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-05-23 09:14:32 +03:00
|
|
|
let editorChart = new Chart(editorsCanvas.getContext('2d'), {
|
2019-05-20 21:44:53 +03:00
|
|
|
type: 'pie',
|
|
|
|
data: {
|
|
|
|
datasets: [{
|
2019-07-07 12:17:45 +03:00
|
|
|
data: data.editors
|
|
|
|
.slice(0, Math.min(SHOW_TOP_N, data.editors.length))
|
|
|
|
.map(p => parseInt(p.total)),
|
2019-05-20 21:44:53 +03:00
|
|
|
backgroundColor: data.editors.map(p => getRandomColor(p.key))
|
|
|
|
}],
|
2019-07-07 12:17:45 +03:00
|
|
|
labels: data.editors
|
|
|
|
.slice(0, Math.min(SHOW_TOP_N, data.editors.length))
|
|
|
|
.map(p => p.key)
|
2019-05-20 21:44:53 +03:00
|
|
|
},
|
|
|
|
options: {
|
2019-07-07 12:17:45 +03:00
|
|
|
title: Object.assign(titleOptions, { text: `Editors (top ${SHOW_TOP_N})` }),
|
2019-05-20 21:44:53 +03:00
|
|
|
tooltips: getTooltipOptions('editors', 'pie')
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-05-23 09:14:32 +03:00
|
|
|
let languageChart = new Chart(languagesCanvas.getContext('2d'), {
|
2019-05-20 21:44:53 +03:00
|
|
|
type: 'pie',
|
|
|
|
data: {
|
|
|
|
datasets: [{
|
|
|
|
data: data.languages
|
2019-07-07 12:17:45 +03:00
|
|
|
.slice(0, Math.min(SHOW_TOP_N, data.languages.length))
|
2019-05-20 21:44:53 +03:00
|
|
|
.map(p => parseInt(p.total)),
|
|
|
|
backgroundColor: data.languages.map(p => getRandomColor(p.key))
|
|
|
|
}],
|
|
|
|
labels: data.languages
|
2019-07-07 12:17:45 +03:00
|
|
|
.slice(0, Math.min(SHOW_TOP_N, data.languages.length))
|
2019-05-20 21:44:53 +03:00
|
|
|
.map(p => p.key)
|
|
|
|
},
|
|
|
|
options: {
|
2019-07-07 12:17:45 +03:00
|
|
|
title: Object.assign(titleOptions, { text: `Languages (top ${SHOW_TOP_N})` }),
|
2019-05-20 21:44:53 +03:00
|
|
|
tooltips: getTooltipOptions('languages', 'pie')
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
getTotal(data.operating_systems)
|
|
|
|
document.getElementById('grid-container').style.visibility = 'visible';
|
2019-05-23 09:14:32 +03:00
|
|
|
|
2019-07-07 12:17:45 +03:00
|
|
|
charts = [projectChart, osChart, editorChart, languageChart]
|
2019-05-20 21:44:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
function getRandomColor(seed) {
|
|
|
|
seed = seed ? seed : '1234567';
|
|
|
|
Math.seedrandom(seed);
|
|
|
|
var letters = '0123456789ABCDEF'.split('');
|
|
|
|
var color = '#';
|
|
|
|
for (var i = 0; i < 6; i++) {
|
|
|
|
color += letters[Math.floor(Math.random() * 16)];
|
|
|
|
}
|
|
|
|
return color;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getTotal(data) {
|
|
|
|
let total = data.reduce((acc, d) => acc + d.total, 0)
|
|
|
|
document.getElementById("total-span").innerText = total.toString().toHHMMSS()
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://koddsson.com/posts/emoji-favicon/
|
|
|
|
const favicon = document.querySelector("link[rel=icon]");
|
|
|
|
if (favicon) {
|
|
|
|
const emoji = favicon.getAttribute("data-emoji");
|
|
|
|
if (emoji) {
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
canvas.height = 64;
|
|
|
|
canvas.width = 64;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
ctx.font = "64px serif";
|
|
|
|
ctx.fillText(emoji, 0, 64);
|
|
|
|
favicon.href = canvas.toDataURL();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
|
|
|
|
</html>
|