devshort

private self-hosted shortlink service
git clone https://git.clttr.info/devshort.git
Log (Feed) | Files | Refs (Tags) | README | LICENSE

commit b72cb176483920927f119df508a55e2e5aa39155
parent 995f2a66d3f86733296392046e9be2762d6d43a6
Author: Florian <flokX@users.noreply.github.com>
Date:   Sat, 11 Jan 2020 12:54:50 +0100

Use vue.js and chart.js

* Replace frappe-charts with chart.js
* Use vue.js to render the chart components
* Use vue.js to control the interaction in the chart component

Diffstat:
Madmin.php | 42+++++++++++++++++++++++++++++++++++-------
Massets/main.js | 203++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
2 files changed, 159 insertions(+), 86 deletions(-)

diff --git a/admin.php b/admin.php @@ -83,12 +83,12 @@ if ($config_content["settings"]["custom_links"]) { <div class="container"> <h1 class="mt-5 text-center"><?php echo $config_content["settings"]["name"]; ?></h1> <h4 class="mb-4 text-center">admin panel</h4> - <div class="row"> + <div class="row" id="app"> <div class="col-md-4 col-lg-3"> <div class="card d-none d-md-block mb-3"> <div class="card-body"> <h5 class="card-title">Tools</h5> - <a class="card-link" id="refresh-1" href="#refresh">Refresh charts</a> + <a class="card-link" href="#reload" v-on:click="loadData">Reload charts</a> <a class="card-link" href="admin-auth.php?logout">Logout</a> </div> </div> @@ -124,18 +124,18 @@ if ($config_content["settings"]["custom_links"]) { </div> <div class="card d-md-none mb-3"> <div class="card-body text-center"> - <a class="card-link" id="refresh-2" href="#refresh">Refresh charts</a> + <a class="card-link" href="#reload" v-on:click="loadData">Reload charts</a> <a class="card-link" href="admin-auth.php?logout">Logout</a> </div> </div> </div> <div class="col-md-8 col-lg-9"> - <div class="d-flex justify-content-center"> - <div class="spinner-border text-primary my-4" id="spinner" role="status"> + <div class="d-flex justify-content-center" v-if="!loaded"> + <div class="spinner-border text-primary my-4" role="status"> <span class="sr-only">Loading...</span> </div> </div> - <div id="charts"></div> + <chart v-if="loaded" v-for="(stats, name) in shortlinks" v-bind:key="name" v-bind:name="name" v-bind:stats="stats"></chart> </div> </div> <p class="text-center d-md-none mt-1 mb-5" id="version-2">powered by <a href="https://github.com/flokX/devShort">devShort</a></p> @@ -151,7 +151,35 @@ if ($config_content["settings"]["custom_links"]) { </div> </footer> - <script src="assets/vendor/frappe-charts/frappe-charts.min.iife.js"></script> + <template id="chart-template"> + <div class="card mb-3"> + <div class="card-body"> + <div class="row"> + <div class="col-lg-6 d-flex align-items-center"> + <h3 class="card-title mb-0">{{ name }}</h3> + </div> + <div class="col-lg-6 d-flex align-items-center"> + <span>ToDo</span> + </div> + </div> + <hr> + <canvas :id="chartId" role="img" :aria-label="chartAriaLabel"></canvas> + <hr> + <div class="row"> + <div class="col-lg-9"> + <span>ToDo</span> + </div> + <div class="col-lg-3 mt-2 mt-lg-0 text-center"> + <button type="button" class="btn btn-outline-danger" v-on:click="remove">Delete shortlink</button> + </div> + </div> + </div> + </div> + </template> + + <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> + <!-- <script src="assets/vendor/vue/vue.min.js"></script> --> + <script src="assets/vendor/chart.js/Chart.bundle.min.js"></script> <script src="assets/main.js"></script> </body> diff --git a/assets/main.js b/assets/main.js @@ -1,6 +1,5 @@ /* Register variables */ const currentDate = new Date(); -const startDate = new Date(new Date().setFullYear(currentDate.getFullYear() - 1)); const spinner = document.getElementById('spinner'); const chartsDiv = document.getElementById('charts'); const statusDiv = document.getElementById('status'); @@ -11,7 +10,6 @@ function post(url, data) { 'use strict'; return fetch(url, { method: 'POST', - credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, @@ -21,29 +19,115 @@ function post(url, data) { }); } -/* Add a new shortlink */ -document.getElementById('add-form').addEventListener('submit', function (event) { - 'use strict'; - event.preventDefault(); - spinner.style.display = ''; - post('admin.php?add', { - name: document.getElementById('name').value, - url: document.getElementById('url').value - }).then(function (data) { - if (data.status === 'successful') { - document.getElementById('name').value = ''; - document.getElementById('url').value = 'https://'; - if (statusDiv.firstChild) { - statusDiv.firstChild.remove(); +// Vue chart component +Vue.component('chart', { + props: ['name', 'stats'], + data: function () { + return { + identifier: Math.floor(Math.random() * 10000), + chart: null + } + }, + template: document.getElementById('chart-template'), + mounted: function () { + let ctx = document.getElementById(this.chartId); + let dataset = []; + for (let [unixTimestamp, count] of Object.entries(this.stats)) { + let timestamp = new Date(unixTimestamp * 1000); + dataset.push({ x: timestamp, y: count }); + } + this.chart = new Chart(ctx, { + type: 'bar', + data: { + datasets: [{ + label: 'Access count', + data: dataset, + backgroundColor: 'rgba(0, 123, 255, 0.4)', + borderColor: '#007bff', + hoverBackgroundColor: 'rgba(0, 123, 255, 0.7)' + }] + }, + options: { + legend: { + display: false + }, + title: { + display: true, + text: 'Accesses to ' + this.name + }, + scales: { + xAxes: [{ + type: 'time', + distribution: 'linear', + ticks: { + min: currentDate.getTime() - (60 * 60 * 24 * 14 * 1000), + max: currentDate + }, + time: { + tooltipFormat: 'YYYY-MM-DD', + unit: 'day' + } + }], + yAxes: [{ + ticks: { + beginAtZero: true, + precision: 0 + } + }] + } } - getCharts(); - } else if (data.status === 'unvalid-url') { - statusDiv.insertAdjacentHTML('afterbegin', '<div class="alert alert-danger" role="alert">Unvalid URL. Please provide a valid URL.</div>'); - } else { - statusDiv.insertAdjacentHTML('afterbegin', '<div class="alert alert-danger" role="alert">Error. Please try again.</div>'); + }); + }, + beforeDestroy: function () { + this.chart.destroy(); + }, + methods: { + remove: function (event) { + post('admin.php?delete', { + name: this.name + }).then(function (response) { + vm.loadData(); + }); } - }); - spinner.style.display = 'none'; + }, + computed: { + chartId: function () { + return 'chart-' + this.identifier; + }, + chartAriaLabel: function () { + return 'Access statistics for ' + this.name; + } + } +}); + +// Vue app instance +var vm = new Vue({ + el: '#app', + data: { + shortlinks: [], + loaded: false + }, + methods: { + loadData: function (event) { + if (event) { + // When calling this function via a card-link + event.preventDefault(); + } + this.loaded = false; + var vm = this; + fetch('admin.php?get_stats') + .then(function (response) { + return response.json() + }) + .then(function (data) { + vm.shortlinks = data + }); + this.loaded = true; + } + }, + created: function () { + this.loadData(); + } }); /* Provide search functionality */ @@ -60,14 +144,25 @@ searchBox.addEventListener('input', function (event) { } }); -/* Refresh charts */ -function refreshCharts(event) { +/* Add a new shortlink */ +document.getElementById('add-form').addEventListener('submit', function (event) { 'use strict'; event.preventDefault(); - getCharts(); -} -document.getElementById('refresh-1').addEventListener('click', refreshCharts); -document.getElementById('refresh-2').addEventListener('click', refreshCharts); + post('admin.php?add', { + name: document.getElementById('name').value, + url: document.getElementById('url').value + }).then(function (data) { + if (data.status === 'successful') { + document.getElementById('name').value = ''; + document.getElementById('url').value = 'https://'; + vm.loadData(); + } else if (data.status === 'unvalid-url') { + statusDiv.insertAdjacentHTML('afterbegin', '<div class="alert alert-danger" role="alert">Unvalid URL. Please provide a valid URL.</div>'); + } else { + statusDiv.insertAdjacentHTML('afterbegin', '<div class="alert alert-danger" role="alert">Error. Please try again.</div>'); + } + }); +}); /* Check for updates */ fetch('https://devshort.flokX.dev/api.php?mode=version&current=' + version).then(function (response) { @@ -80,53 +175,3 @@ fetch('https://devshort.flokX.dev/api.php?mode=version&current=' + version).then document.getElementById('version-1').insertAdjacentHTML('beforeend', inner); document.getElementById('version-2').insertAdjacentHTML('beforeend', inner); }); - -/* Get charts and date (remove old when necessary) */ -function getCharts() { - 'use strict'; - spinner.style.display = 'block'; - while (chartsDiv.firstChild) { - chartsDiv.firstChild.remove(); - } - fetch('admin.php?get_stats').then(function (response) { - return response.json(); - }).then(function (json) { - for (let [name, data] of Object.entries(json)) { - chartsDiv.insertAdjacentHTML('beforeend', `<div id="card-${name}" class="card text-center mb-3"> - <div class="card-header">${name}</div> - <div class="card-body p-2"> - <div id="heatmap-${name}" class="overflow-auto"></div> - </div> - <div class="card-footer text-muted"> - <a id="export-${name}" href="#export" class="card-link">Export chart</a><a id="delete-${name}" href="#delete" class="card-link">Delete shortlink and dataset</a> - </div> -</div>`); - let heatmap = new frappe.Chart('div#heatmap-' + name, { - type: 'heatmap', - title: 'Access statistics for ' + name, - data: { - dataPoints: data, - start: startDate, - end: currentDate - }, - countLabel: 'Access(es)', - discreteDomains: 0 - }); - document.getElementById('export-' + name).addEventListener('click', function (event) { - event.preventDefault(); - heatmap.export(); - }); - document.getElementById('delete-' + name).addEventListener('click', function (event) { - event.preventDefault(); - post('admin.php?delete', { - name: name - }).then(function () { - document.getElementById('card-' + name).remove(); - }); - }); - } - spinner.style.display = 'none'; - }); -} - -getCharts();