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:
M | admin.php | | | 42 | +++++++++++++++++++++++++++++++++++------- |
M | assets/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¤t=' + version).then(function (response) {
@@ -80,53 +175,3 @@ fetch('https://devshort.flokX.dev/api.php?mode=version¤t=' + 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();