mirror of
https://github.com/SophiaAtkinson/steamwidgets-web.git
synced 2025-06-27 01:57:40 -07:00
Stats
This commit is contained in:
@ -22,5 +22,7 @@ return [
|
||||
array('/api/query/server', 'GET', 'api@queryServerInfo'),
|
||||
array('/api/query/user', 'GET', 'api@queryUserInfo'),
|
||||
array('/api/resource/query', 'GET', 'api@queryResource'),
|
||||
array('/stats/{pw}', 'GET', 'stats@index'),
|
||||
array('/stats/query/{pw}', 'ANY', 'stats@query'),
|
||||
array('$404', 'ANY', 'error404@index')
|
||||
];
|
||||
|
@ -28,6 +28,9 @@ class ApiController extends BaseController {
|
||||
|
||||
$data = SteamApp::querySteamData($appid, $language);
|
||||
|
||||
//Save hit
|
||||
HitsModel::addHit(HitsModel::HITTYPE_MODULE_APP);
|
||||
|
||||
return json(array('code' => 200, 'appid' => $appid, 'lang' => $language, 'data' => $data));
|
||||
} catch (\Exception $e) {
|
||||
return json(array('code' => 500, 'msg' => $e->getMessage()));
|
||||
@ -47,6 +50,9 @@ class ApiController extends BaseController {
|
||||
|
||||
$data = SteamServer::querySteamData(env('STEAM_API_KEY'), $addr);
|
||||
|
||||
//Save hit
|
||||
HitsModel::addHit(HitsModel::HITTYPE_MODULE_SERVER);
|
||||
|
||||
return json(array('code' => 200, 'addr' => $addr, 'data' => $data));
|
||||
} catch (\Exception $e) {
|
||||
return json(array('code' => 500, 'msg' => $e->getMessage()));
|
||||
@ -66,6 +72,9 @@ class ApiController extends BaseController {
|
||||
|
||||
$data = SteamUser::querySteamData(env('STEAM_API_KEY'), $steamid);
|
||||
|
||||
//Save hit
|
||||
HitsModel::addHit(HitsModel::HITTYPE_MODULE_USER);
|
||||
|
||||
return json(array('code' => 200, 'steamid' => $steamid, 'data' => $data));
|
||||
} catch (\Exception $e) {
|
||||
return json(array('code' => 500, 'msg' => $e->getMessage()));
|
||||
|
114
app/controller/stats.php
Normal file
114
app/controller/stats.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Stats controller
|
||||
*/
|
||||
class StatsController extends BaseController
|
||||
{
|
||||
const INDEX_LAYOUT = 'layout';
|
||||
|
||||
/**
|
||||
* Perform base initialization
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(self::INDEX_LAYOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles URL: /stats/{pw}
|
||||
*
|
||||
* @param Asatru\Controller\ControllerArg $request
|
||||
* @return Asatru\View\ViewHandler
|
||||
*/
|
||||
public function index($request)
|
||||
{
|
||||
if ($request->arg('pw') !== env('APP_STATSPASSWORD')) {
|
||||
throw new Exception('Invalid password');
|
||||
}
|
||||
|
||||
$start = date('Y-m-d', strtotime('-30 days'));
|
||||
$end = date('Y-m-d', strtotime('-1 day'));
|
||||
|
||||
$predefined_dates = [
|
||||
'Last week' => date('Y-m-d', strtotime('-7 days')),
|
||||
'Last two weeks' => date('Y-m-d', strtotime('-14 days')),
|
||||
'Last month' => date('Y-m-d', strtotime('-1 month')),
|
||||
'Last three months' => date('Y-m-d', strtotime('-3 months')),
|
||||
'Last year' => date('Y-m-d', strtotime('-1 year')),
|
||||
'Lifetime' => date('Y-m-d', strtotime(HitsModel::getInitialStartDate()))
|
||||
];
|
||||
|
||||
return parent::view([
|
||||
['content', 'stats'],
|
||||
], [
|
||||
'render_stats_to' => 'hits-stats',
|
||||
'render_stats_start' => $start,
|
||||
'render_stats_end' => $end,
|
||||
'render_stats_pw' => $request->arg('pw'),
|
||||
'predefined_dates' => $predefined_dates
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles URL: /stats/query/{pw}
|
||||
*
|
||||
* @param Asatru\Controller\ControllerArg $request
|
||||
* @return Asatru\View\JsonHandler
|
||||
*/
|
||||
public function query($request)
|
||||
{
|
||||
try {
|
||||
if ($request->arg('pw') !== env('APP_STATSPASSWORD')) {
|
||||
throw new Exception('Invalid password');
|
||||
}
|
||||
|
||||
$start = $request->params()->query('start', '');
|
||||
if ($start === '') {
|
||||
$start = date('Y-m-d', strtotime('-30 days'));
|
||||
}
|
||||
|
||||
$end = $request->params()->query('end', '');
|
||||
if ($end === '') {
|
||||
$end = date('Y-m-d', strtotime('-1 day'));
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$data[HitsModel::HITTYPE_MODULE_APP] = [];
|
||||
$data[HitsModel::HITTYPE_MODULE_SERVER] = [];
|
||||
$data[HitsModel::HITTYPE_MODULE_USER] = [];
|
||||
|
||||
$hits = HitsModel::getHitsPerDay($start, $end);
|
||||
|
||||
$dayDiff = (new DateTime($end))->diff((new DateTime($start)))->format('%a');
|
||||
|
||||
$count_total = [];
|
||||
$count_total[HitsModel::HITTYPE_MODULE_APP] = 0;
|
||||
$count_total[HitsModel::HITTYPE_MODULE_SERVER] = 0;
|
||||
$count_total[HitsModel::HITTYPE_MODULE_USER] = 0;
|
||||
|
||||
for ($i = 0; $i < $hits->count(); $i++) {
|
||||
$count_total[$hits->get($i)->get('hittype')] += $hits->get($i)->get('count');
|
||||
|
||||
$data[$hits->get($i)->get('hittype')][$hits->get($i)->get('created_at')][] = $hits->get($i)->get('count');
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'data' => $data,
|
||||
'counts' => $count_total,
|
||||
'count_total' => $count_total[HitsModel::HITTYPE_MODULE_APP] + $count_total[HitsModel::HITTYPE_MODULE_SERVER] + $count_total[HitsModel::HITTYPE_MODULE_USER],
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
'day_diff' => $dayDiff
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
51
app/migrations/HitsModel.php
Normal file
51
app/migrations/HitsModel.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
Asatru PHP - Migration for hits
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class specifies a migration
|
||||
*/
|
||||
class HitsModel_Migration {
|
||||
private $database = null;
|
||||
private $connection = null;
|
||||
|
||||
/**
|
||||
* Store the PDO connection handle
|
||||
*
|
||||
* @param \PDO $pdo The PDO connection handle
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($pdo)
|
||||
{
|
||||
$this->connection = $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the table shall be created or modified
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$this->database = new Asatru\Database\Migration('hits', $this->connection);
|
||||
$this->database->drop();
|
||||
$this->database->add('id INT NOT NULL AUTO_INCREMENT PRIMARY KEY');
|
||||
$this->database->add('hash_token VARCHAR(512) NOT NULL');
|
||||
$this->database->add('hittype VARCHAR(100) NOT NULL');
|
||||
$this->database->add('created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
|
||||
$this->database->create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the table shall be dropped
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
if ($this->database)
|
||||
$this->database->drop();
|
||||
}
|
||||
}
|
1
app/migrations/migrations.list
Normal file
1
app/migrations/migrations.list
Normal file
@ -0,0 +1 @@
|
||||
47f3d3306fbd25715600db55b33b1542293ab5eb593205d6f5884bcaafa5d9a4627c76af04b684962c99a9518ef6b3d10e983203f6b5a8576912445d8f285fb7
|
100
app/models/HitsModel.php
Normal file
100
app/models/HitsModel.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class HitsModel
|
||||
*
|
||||
* Responsible for hit calculation
|
||||
*/
|
||||
class HitsModel extends \Asatru\Database\Model
|
||||
{
|
||||
const HITTYPE_MODULE_APP = 'mod_app';
|
||||
const HITTYPE_MODULE_SERVER = 'mod_server';
|
||||
const HITTYPE_MODULE_USER = 'mod_user';
|
||||
|
||||
/**
|
||||
* Validate hit type
|
||||
*
|
||||
* @param $type
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function validateHitType($type)
|
||||
{
|
||||
try {
|
||||
$types = [self::HITTYPE_MODULE_APP, self::HITTYPE_MODULE_SERVER, self::HITTYPE_MODULE_USER];
|
||||
|
||||
if (!in_array($type, $types)) {
|
||||
throw new Exception('Invalid hit type: ' . $type);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add hit to database
|
||||
*
|
||||
* @param $address
|
||||
* @param $type
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function addHit($type)
|
||||
{
|
||||
try {
|
||||
static::validateHitType($type);
|
||||
|
||||
$token = md5($_SERVER['REMOTE_ADDR']);
|
||||
|
||||
HitsModel::raw('INSERT INTO `' . self::tableName() . '` (hash_token, hittype, created_at) VALUES(?, ?, CURRENT_TIMESTAMP)', [
|
||||
$token,
|
||||
$type
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all hits of the given range
|
||||
*
|
||||
* @param $start
|
||||
* @param $end
|
||||
* @return Asatru\Database\Collection
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function getHitsPerDay($start, $end)
|
||||
{
|
||||
try {
|
||||
$result = HitsModel::raw('SELECT DATE(created_at) AS created_at, COUNT(hash_token) AS count, hittype FROM `' . self::tableName() . '` WHERE DATE(created_at) >= ? AND DATE(created_at) <= ? GROUP BY DATE(created_at), hittype ORDER BY created_at ASC', [
|
||||
$start,
|
||||
$end
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get initial start date
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getInitialStartDate()
|
||||
{
|
||||
$data = HitsModel::raw('SELECT created_at FROM `' . self::tableName() . '` WHERE id = 1')->first();
|
||||
return $data->get('created_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the associated table name of the migration
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function tableName()
|
||||
{
|
||||
return 'hits';
|
||||
}
|
||||
}
|
@ -4,7 +4,14 @@
|
||||
* Put here your application specific JavaScript implementations
|
||||
*/
|
||||
|
||||
window.vue = new Vue({
|
||||
import './../sass/app.scss';
|
||||
|
||||
window.axios = require('axios');
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
import Chart from 'chart.js/auto';
|
||||
|
||||
window.vue = new Vue({
|
||||
el: '#app',
|
||||
|
||||
data: {
|
||||
@ -26,6 +33,145 @@
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ajaxRequest: function (method, url, data = {}, successfunc = function(data){}, finalfunc = function(){}, config = {})
|
||||
{
|
||||
let func = window.axios.get;
|
||||
if (method == 'post') {
|
||||
func = window.axios.post;
|
||||
} else if (method == 'patch') {
|
||||
func = window.axios.patch;
|
||||
} else if (method == 'delete') {
|
||||
func = window.axios.delete;
|
||||
}
|
||||
|
||||
func(url, data, config)
|
||||
.then(function(response){
|
||||
successfunc(response.data);
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
})
|
||||
.finally(function(){
|
||||
finalfunc();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
renderStats: function(pw, elem, start, end = '') {
|
||||
window.vue.ajaxRequest('post', window.location.origin + '/stats/query/' + pw, { start: start, end: end }, function(response){
|
||||
if (response.code == 200) {
|
||||
document.getElementById('inp-date-from').value = response.start;
|
||||
document.getElementById('inp-date-till').value = response.end;
|
||||
document.getElementById('count-total').innerHTML = response.count_total;
|
||||
document.getElementById('count-app').innerHTML = response.counts.mod_app;
|
||||
document.getElementById('count-server').innerHTML = response.counts.mod_server;
|
||||
document.getElementById('count-user').innerHTML = response.counts.mod_user;
|
||||
|
||||
let content = document.getElementById(elem);
|
||||
if (content) {
|
||||
let labels = [];
|
||||
let data_app = [];
|
||||
let data_server = [];
|
||||
let data_user = [];
|
||||
|
||||
let day = 60 * 60 * 24 * 1000;
|
||||
let dt = new Date(Date.parse(start));
|
||||
|
||||
for (let i = 0; i <= response.day_diff; i++) {
|
||||
let curDate = new Date(dt.getTime() + day * i);
|
||||
let curDay = curDate.getDate();
|
||||
let curMonth = curDate.getMonth() + 1;
|
||||
|
||||
if (curDay < 10) {
|
||||
curDay = '0' + curDay;
|
||||
}
|
||||
|
||||
if (curMonth < 10) {
|
||||
curMonth = '0' + curMonth;
|
||||
}
|
||||
|
||||
labels.push(curDate.getFullYear() + '-' + curMonth + '-' + curDay);
|
||||
data_app.push(0);
|
||||
data_server.push(0);
|
||||
data_user.push(0);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(response.data.mod_app)) {
|
||||
labels.forEach(function(lblElem, lblIndex){
|
||||
if (lblElem == key) {
|
||||
data_app[lblIndex] = parseInt(value[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(response.data.mod_server)) {
|
||||
labels.forEach(function(lblElem, lblIndex){
|
||||
if (lblElem == key) {
|
||||
data_server[lblIndex] = parseInt(value[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(response.data.mod_user)) {
|
||||
labels.forEach(function(lblElem, lblIndex){
|
||||
if (lblElem == key) {
|
||||
data_user[lblIndex] = parseInt(value[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const config = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'App',
|
||||
backgroundColor: 'rgb(0, 162, 232)',
|
||||
borderColor: 'rgb(0, 162, 232)',
|
||||
data: data_app,
|
||||
},
|
||||
{
|
||||
label: 'Server',
|
||||
backgroundColor: 'rgb(163, 73, 164)',
|
||||
borderColor: 'rgb(163, 73, 164)',
|
||||
data: data_server,
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
backgroundColor: 'rgb(240, 155, 90)',
|
||||
borderColor: 'rgb(240, 155, 90)',
|
||||
data: data_user,
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: function(value) {if (value % 1 === 0) {return value;}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (window.statsChart !== null) {
|
||||
window.statsChart.destroy();
|
||||
}
|
||||
|
||||
window.statsChart = new Chart(
|
||||
content,
|
||||
config
|
||||
);
|
||||
}
|
||||
} else {
|
||||
alert(response.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@ -36,4 +182,4 @@ window.hljs = hljs;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
window.hljs.highlightAll();
|
||||
});
|
||||
});
|
@ -109,6 +109,14 @@ body {
|
||||
background-color: rgb(200, 200, 200);
|
||||
}
|
||||
|
||||
.stats-input {
|
||||
padding: 8px;
|
||||
color: rgb(50, 50, 50);
|
||||
background-color: rgb(200, 200, 200);
|
||||
border: 1px solid rgb(100, 100, 100);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.scroll-to-top {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
|
@ -10,7 +10,6 @@
|
||||
<link rel="icon" type="image/png" href="{{ asset('img/logo.png') }}"/>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{{ asset('css/bulma.css') }}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{{ asset('css/app.css') }}"/>
|
||||
|
||||
<title>{{ env('APP_TITLE') }}</title>
|
||||
|
||||
@ -54,6 +53,11 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.vue.initNavBar();
|
||||
|
||||
@if ((isset($render_stats_to)) && (isset($render_stats_pw)))
|
||||
window.statsChart = null;
|
||||
window.vue.renderStats('{{ $render_stats_pw }}', '{{ $render_stats_to }}', '{{ $render_stats_start }}');
|
||||
@endif
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
25
app/views/stats.php
Normal file
25
app/views/stats.php
Normal file
@ -0,0 +1,25 @@
|
||||
<div class="content-section content-top-margin">
|
||||
<h1>Stats</h1>
|
||||
|
||||
<div class="hits-summary">
|
||||
<div>
|
||||
Range: <input class="stats-input" type="date" id="inp-date-from"/> <input class="stats-input" type="date" id="inp-date-till"/>
|
||||
<select class="stats-input" onchange="window.vue.renderStats('{{ $render_stats_pw }}', '{{ $render_stats_to }}', this.value, '{{ $render_stats_end }}');">
|
||||
<option value="{{ $render_stats_start }}">- Select a range -</option>
|
||||
@foreach ($predefined_dates as $key => $value)
|
||||
<option value="{{ $value }}">{{ $key }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<a class="button is-dark" href="javascript:void(0);" onclick="window.vue.renderStats('{{ $render_stats_pw }}', '{{ $render_stats_to }}', document.getElementById('inp-date-from').value, document.getElementById('inp-date-till').value);">Go</a>
|
||||
</div>
|
||||
|
||||
<div>Sum: <div class="is-inline-block" id="count-total"></div></div>
|
||||
<div>App count: <div class="is-inline-block" id="count-app"></div></div>
|
||||
<div>Server count: <div class="is-inline-block" id="count-server"></div></div>
|
||||
<div>User count: <div class="is-inline-block" id="count-user"></div></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<canvas id="hits-stats"></canvas>
|
||||
</div>
|
||||
</div>
|
51
package-lock.json
generated
51
package-lock.json
generated
@ -313,6 +313,20 @@
|
||||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
@ -357,6 +371,11 @@
|
||||
"integrity": "sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw==",
|
||||
"dev": true
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz",
|
||||
"integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w=="
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
@ -396,6 +415,14 @@
|
||||
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
|
||||
"dev": true
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
@ -497,6 +524,11 @@
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.211",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.211.tgz",
|
||||
@ -610,6 +642,21 @@
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
@ -836,14 +883,12 @@
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
|
@ -28,6 +28,8 @@
|
||||
"webpack-cli": "^4.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"chart.js": "^3.9.1",
|
||||
"highlight.js": "^11.6.0",
|
||||
"style-loader": "^1.2.1"
|
||||
}
|
||||
|
@ -1,159 +0,0 @@
|
||||
/*
|
||||
app.scss
|
||||
*/
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1088px) {
|
||||
.navbar-start {
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
transform: translate(11%, -0%)
|
||||
}
|
||||
}
|
||||
|
||||
.is-font-title {
|
||||
font-family: 'Helvetica', Verdana, Arial, sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content-top-margin {
|
||||
margin-top: 90px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1088px) {
|
||||
.content-top-margin {
|
||||
margin-top: 58px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-section h1 {
|
||||
font-size: 2.0em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.5em;
|
||||
color: rgb(100, 100, 100);
|
||||
}
|
||||
|
||||
.content-section h3 {
|
||||
font-size: 1.2em;
|
||||
color: rgb(100, 100, 100);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.content-section p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content-section a {
|
||||
color: #3273dc;
|
||||
}
|
||||
|
||||
.content-section a:hover {
|
||||
color: #3273dc;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.content-section pre {
|
||||
padding: unset;
|
||||
margin-top: -15px;
|
||||
margin-bottom: -61px;
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.content-section code {
|
||||
background-color: rgb(230, 230, 230);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.content-section ul {
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
.content-section li {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.content-section table {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.content-section tbody {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.content-section td {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.tr-colored {
|
||||
background-color: rgb(200, 200, 200);
|
||||
}
|
||||
|
||||
.content-section hr {
|
||||
background-color: rgb(200, 200, 200);
|
||||
}
|
||||
|
||||
.scroll-to-top {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.scroll-to-top-inner {
|
||||
background-color: rgb(100, 100, 100);
|
||||
border-radius: 50%;
|
||||
padding: 12px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
}
|
||||
|
||||
.scroll-to-top-inner a {
|
||||
color: rgb(200, 200, 200);
|
||||
}
|
||||
|
||||
.scroll-to-top-inner a:hover {
|
||||
color: rgb(200, 200, 200);
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
color: rgb(100, 100, 100);
|
||||
background-color: rgb(235, 235, 235);
|
||||
padding: 1rem 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.footer-frame {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-content a {
|
||||
color: rgb(130, 130, 130);
|
||||
}
|
||||
|
||||
.footer-content a:hover {
|
||||
color: rgb(150, 150, 150);
|
||||
text-decoration: none;
|
||||
}
|
File diff suppressed because one or more lines are too long
13
public/js/app.js.LICENSE.txt
Normal file
13
public/js/app.js.LICENSE.txt
Normal file
@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* @kurkle/color v0.2.1
|
||||
* https://github.com/kurkle/color#readme
|
||||
* (c) 2022 Jukka Kurkela
|
||||
* Released under the MIT License
|
||||
*/
|
||||
|
||||
/*!
|
||||
* Chart.js v3.9.1
|
||||
* https://www.chartjs.org
|
||||
* (c) 2022 Chart.js Contributors
|
||||
* Released under the MIT License
|
||||
*/
|
Reference in New Issue
Block a user