This commit is contained in:
Daniel Brendel
2022-09-08 12:02:23 +02:00
parent 3edc2c4f0e
commit b02e52feef
15 changed files with 528 additions and 166 deletions

View File

@ -22,5 +22,7 @@ return [
array('/api/query/server', 'GET', 'api@queryServerInfo'), array('/api/query/server', 'GET', 'api@queryServerInfo'),
array('/api/query/user', 'GET', 'api@queryUserInfo'), array('/api/query/user', 'GET', 'api@queryUserInfo'),
array('/api/resource/query', 'GET', 'api@queryResource'), 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') array('$404', 'ANY', 'error404@index')
]; ];

View File

@ -28,6 +28,9 @@ class ApiController extends BaseController {
$data = SteamApp::querySteamData($appid, $language); $data = SteamApp::querySteamData($appid, $language);
//Save hit
HitsModel::addHit(HitsModel::HITTYPE_MODULE_APP);
return json(array('code' => 200, 'appid' => $appid, 'lang' => $language, 'data' => $data)); return json(array('code' => 200, 'appid' => $appid, 'lang' => $language, 'data' => $data));
} catch (\Exception $e) { } catch (\Exception $e) {
return json(array('code' => 500, 'msg' => $e->getMessage())); return json(array('code' => 500, 'msg' => $e->getMessage()));
@ -47,6 +50,9 @@ class ApiController extends BaseController {
$data = SteamServer::querySteamData(env('STEAM_API_KEY'), $addr); $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)); return json(array('code' => 200, 'addr' => $addr, 'data' => $data));
} catch (\Exception $e) { } catch (\Exception $e) {
return json(array('code' => 500, 'msg' => $e->getMessage())); return json(array('code' => 500, 'msg' => $e->getMessage()));
@ -66,6 +72,9 @@ class ApiController extends BaseController {
$data = SteamUser::querySteamData(env('STEAM_API_KEY'), $steamid); $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)); return json(array('code' => 200, 'steamid' => $steamid, 'data' => $data));
} catch (\Exception $e) { } catch (\Exception $e) {
return json(array('code' => 500, 'msg' => $e->getMessage())); return json(array('code' => 500, 'msg' => $e->getMessage()));

114
app/controller/stats.php Normal file
View 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()
]);
}
}
}

View 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();
}
}

View File

@ -0,0 +1 @@
47f3d3306fbd25715600db55b33b1542293ab5eb593205d6f5884bcaafa5d9a4627c76af04b684962c99a9518ef6b3d10e983203f6b5a8576912445d8f285fb7

100
app/models/HitsModel.php Normal file
View 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';
}
}

View File

@ -4,6 +4,13 @@
* Put here your application specific JavaScript implementations * Put here your application specific JavaScript implementations
*/ */
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({ window.vue = new Vue({
el: '#app', el: '#app',
@ -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);
}
});
},
} }
}); });

View File

@ -109,6 +109,14 @@ body {
background-color: rgb(200, 200, 200); 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 { .scroll-to-top {
position: fixed; position: fixed;
z-index: 3; z-index: 3;

View File

@ -10,7 +10,6 @@
<link rel="icon" type="image/png" href="{{ asset('img/logo.png') }}"/> <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/bulma.css') }}"/>
<link rel="stylesheet" type="text/css" href="{{ asset('css/app.css') }}"/>
<title>{{ env('APP_TITLE') }}</title> <title>{{ env('APP_TITLE') }}</title>
@ -54,6 +53,11 @@
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
window.vue.initNavBar(); 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> </script>
</body> </body>

25
app/views/stats.php Normal file
View 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"/>&nbsp;<input class="stats-input" type="date" id="inp-date-till"/>&nbsp;
<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>
&nbsp;<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
View File

@ -313,6 +313,20 @@
"picomatch": "^2.0.4" "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": { "big.js": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@ -357,6 +371,11 @@
"integrity": "sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw==", "integrity": "sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw==",
"dev": true "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": { "chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@ -396,6 +415,14 @@
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
"dev": true "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": { "commander": {
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@ -497,6 +524,11 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true "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": { "electron-to-chromium": {
"version": "1.4.211", "version": "1.4.211",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.211.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.211.tgz",
@ -610,6 +642,21 @@
"path-exists": "^4.0.0" "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": { "fsevents": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@ -836,14 +883,12 @@
"mime-db": { "mime-db": {
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
"dev": true
}, },
"mime-types": { "mime-types": {
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"requires": { "requires": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
} }

View File

@ -28,6 +28,8 @@
"webpack-cli": "^4.10.0" "webpack-cli": "^4.10.0"
}, },
"dependencies": { "dependencies": {
"axios": "^0.27.2",
"chart.js": "^3.9.1",
"highlight.js": "^11.6.0", "highlight.js": "^11.6.0",
"style-loader": "^1.2.1" "style-loader": "^1.2.1"
} }

View File

@ -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

View 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
*/