Add site analytics (close #8)
This commit is contained in:
parent
88d7b539b9
commit
a3391f5ab6
BIN
database.mwb
BIN
database.mwb
Binary file not shown.
@ -66,10 +66,23 @@ define("STRINGS", [
|
||||
"page id" => "Page ID (slug)",
|
||||
"add page" => "Add page",
|
||||
"page settings" => "Page Settings",
|
||||
"analytics" => "Analytics",
|
||||
"today" => "Today",
|
||||
"this week" => "This Week",
|
||||
"visit" => "visit",
|
||||
"visits" => "visits",
|
||||
"page view" => "page view",
|
||||
"page views" => "page views",
|
||||
"site" => "Site",
|
||||
"filter by site" => "Filter by site",
|
||||
"all sites" => "All Sites",
|
||||
"filter" => "Filter",
|
||||
"start date" => "Start date",
|
||||
"end date" => "End date",
|
||||
"recent actions" => "Recent Actions",
|
||||
"overview" => "Overview",
|
||||
"views per visit" => "views per visit",
|
||||
"visits over time" => "Visits Over Time",
|
||||
"no data" => "No data.",
|
||||
"visitor map" => "Visitor Map"
|
||||
]);
|
264
lib/countries_2_3.php
Normal file
264
lib/countries_2_3.php
Normal file
@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An array of two-letter to three-letter country codes, from
|
||||
* http://country.io/iso3.json
|
||||
*/
|
||||
$COUNTRY_CODES = array (
|
||||
'BD' => 'BGD',
|
||||
'BE' => 'BEL',
|
||||
'BF' => 'BFA',
|
||||
'BG' => 'BGR',
|
||||
'BA' => 'BIH',
|
||||
'BB' => 'BRB',
|
||||
'WF' => 'WLF',
|
||||
'BL' => 'BLM',
|
||||
'BM' => 'BMU',
|
||||
'BN' => 'BRN',
|
||||
'BO' => 'BOL',
|
||||
'BH' => 'BHR',
|
||||
'BI' => 'BDI',
|
||||
'BJ' => 'BEN',
|
||||
'BT' => 'BTN',
|
||||
'JM' => 'JAM',
|
||||
'BV' => 'BVT',
|
||||
'BW' => 'BWA',
|
||||
'WS' => 'WSM',
|
||||
'BQ' => 'BES',
|
||||
'BR' => 'BRA',
|
||||
'BS' => 'BHS',
|
||||
'JE' => 'JEY',
|
||||
'BY' => 'BLR',
|
||||
'BZ' => 'BLZ',
|
||||
'RU' => 'RUS',
|
||||
'RW' => 'RWA',
|
||||
'RS' => 'SRB',
|
||||
'TL' => 'TLS',
|
||||
'RE' => 'REU',
|
||||
'TM' => 'TKM',
|
||||
'TJ' => 'TJK',
|
||||
'RO' => 'ROU',
|
||||
'TK' => 'TKL',
|
||||
'GW' => 'GNB',
|
||||
'GU' => 'GUM',
|
||||
'GT' => 'GTM',
|
||||
'GS' => 'SGS',
|
||||
'GR' => 'GRC',
|
||||
'GQ' => 'GNQ',
|
||||
'GP' => 'GLP',
|
||||
'JP' => 'JPN',
|
||||
'GY' => 'GUY',
|
||||
'GG' => 'GGY',
|
||||
'GF' => 'GUF',
|
||||
'GE' => 'GEO',
|
||||
'GD' => 'GRD',
|
||||
'GB' => 'GBR',
|
||||
'GA' => 'GAB',
|
||||
'SV' => 'SLV',
|
||||
'GN' => 'GIN',
|
||||
'GM' => 'GMB',
|
||||
'GL' => 'GRL',
|
||||
'GI' => 'GIB',
|
||||
'GH' => 'GHA',
|
||||
'OM' => 'OMN',
|
||||
'TN' => 'TUN',
|
||||
'JO' => 'JOR',
|
||||
'HR' => 'HRV',
|
||||
'HT' => 'HTI',
|
||||
'HU' => 'HUN',
|
||||
'HK' => 'HKG',
|
||||
'HN' => 'HND',
|
||||
'HM' => 'HMD',
|
||||
'VE' => 'VEN',
|
||||
'PR' => 'PRI',
|
||||
'PS' => 'PSE',
|
||||
'PW' => 'PLW',
|
||||
'PT' => 'PRT',
|
||||
'SJ' => 'SJM',
|
||||
'PY' => 'PRY',
|
||||
'IQ' => 'IRQ',
|
||||
'PA' => 'PAN',
|
||||
'PF' => 'PYF',
|
||||
'PG' => 'PNG',
|
||||
'PE' => 'PER',
|
||||
'PK' => 'PAK',
|
||||
'PH' => 'PHL',
|
||||
'PN' => 'PCN',
|
||||
'PL' => 'POL',
|
||||
'PM' => 'SPM',
|
||||
'ZM' => 'ZMB',
|
||||
'EH' => 'ESH',
|
||||
'EE' => 'EST',
|
||||
'EG' => 'EGY',
|
||||
'ZA' => 'ZAF',
|
||||
'EC' => 'ECU',
|
||||
'IT' => 'ITA',
|
||||
'VN' => 'VNM',
|
||||
'SB' => 'SLB',
|
||||
'ET' => 'ETH',
|
||||
'SO' => 'SOM',
|
||||
'ZW' => 'ZWE',
|
||||
'SA' => 'SAU',
|
||||
'ES' => 'ESP',
|
||||
'ER' => 'ERI',
|
||||
'ME' => 'MNE',
|
||||
'MD' => 'MDA',
|
||||
'MG' => 'MDG',
|
||||
'MF' => 'MAF',
|
||||
'MA' => 'MAR',
|
||||
'MC' => 'MCO',
|
||||
'UZ' => 'UZB',
|
||||
'MM' => 'MMR',
|
||||
'ML' => 'MLI',
|
||||
'MO' => 'MAC',
|
||||
'MN' => 'MNG',
|
||||
'MH' => 'MHL',
|
||||
'MK' => 'MKD',
|
||||
'MU' => 'MUS',
|
||||
'MT' => 'MLT',
|
||||
'MW' => 'MWI',
|
||||
'MV' => 'MDV',
|
||||
'MQ' => 'MTQ',
|
||||
'MP' => 'MNP',
|
||||
'MS' => 'MSR',
|
||||
'MR' => 'MRT',
|
||||
'IM' => 'IMN',
|
||||
'UG' => 'UGA',
|
||||
'TZ' => 'TZA',
|
||||
'MY' => 'MYS',
|
||||
'MX' => 'MEX',
|
||||
'IL' => 'ISR',
|
||||
'FR' => 'FRA',
|
||||
'IO' => 'IOT',
|
||||
'SH' => 'SHN',
|
||||
'FI' => 'FIN',
|
||||
'FJ' => 'FJI',
|
||||
'FK' => 'FLK',
|
||||
'FM' => 'FSM',
|
||||
'FO' => 'FRO',
|
||||
'NI' => 'NIC',
|
||||
'NL' => 'NLD',
|
||||
'NO' => 'NOR',
|
||||
'NA' => 'NAM',
|
||||
'VU' => 'VUT',
|
||||
'NC' => 'NCL',
|
||||
'NE' => 'NER',
|
||||
'NF' => 'NFK',
|
||||
'NG' => 'NGA',
|
||||
'NZ' => 'NZL',
|
||||
'NP' => 'NPL',
|
||||
'NR' => 'NRU',
|
||||
'NU' => 'NIU',
|
||||
'CK' => 'COK',
|
||||
'XK' => 'XKX',
|
||||
'CI' => 'CIV',
|
||||
'CH' => 'CHE',
|
||||
'CO' => 'COL',
|
||||
'CN' => 'CHN',
|
||||
'CM' => 'CMR',
|
||||
'CL' => 'CHL',
|
||||
'CC' => 'CCK',
|
||||
'CA' => 'CAN',
|
||||
'CG' => 'COG',
|
||||
'CF' => 'CAF',
|
||||
'CD' => 'COD',
|
||||
'CZ' => 'CZE',
|
||||
'CY' => 'CYP',
|
||||
'CX' => 'CXR',
|
||||
'CR' => 'CRI',
|
||||
'CW' => 'CUW',
|
||||
'CV' => 'CPV',
|
||||
'CU' => 'CUB',
|
||||
'SZ' => 'SWZ',
|
||||
'SY' => 'SYR',
|
||||
'SX' => 'SXM',
|
||||
'KG' => 'KGZ',
|
||||
'KE' => 'KEN',
|
||||
'SS' => 'SSD',
|
||||
'SR' => 'SUR',
|
||||
'KI' => 'KIR',
|
||||
'KH' => 'KHM',
|
||||
'KN' => 'KNA',
|
||||
'KM' => 'COM',
|
||||
'ST' => 'STP',
|
||||
'SK' => 'SVK',
|
||||
'KR' => 'KOR',
|
||||
'SI' => 'SVN',
|
||||
'KP' => 'PRK',
|
||||
'KW' => 'KWT',
|
||||
'SN' => 'SEN',
|
||||
'SM' => 'SMR',
|
||||
'SL' => 'SLE',
|
||||
'SC' => 'SYC',
|
||||
'KZ' => 'KAZ',
|
||||
'KY' => 'CYM',
|
||||
'SG' => 'SGP',
|
||||
'SE' => 'SWE',
|
||||
'SD' => 'SDN',
|
||||
'DO' => 'DOM',
|
||||
'DM' => 'DMA',
|
||||
'DJ' => 'DJI',
|
||||
'DK' => 'DNK',
|
||||
'VG' => 'VGB',
|
||||
'DE' => 'DEU',
|
||||
'YE' => 'YEM',
|
||||
'DZ' => 'DZA',
|
||||
'US' => 'USA',
|
||||
'UY' => 'URY',
|
||||
'YT' => 'MYT',
|
||||
'UM' => 'UMI',
|
||||
'LB' => 'LBN',
|
||||
'LC' => 'LCA',
|
||||
'LA' => 'LAO',
|
||||
'TV' => 'TUV',
|
||||
'TW' => 'TWN',
|
||||
'TT' => 'TTO',
|
||||
'TR' => 'TUR',
|
||||
'LK' => 'LKA',
|
||||
'LI' => 'LIE',
|
||||
'LV' => 'LVA',
|
||||
'TO' => 'TON',
|
||||
'LT' => 'LTU',
|
||||
'LU' => 'LUX',
|
||||
'LR' => 'LBR',
|
||||
'LS' => 'LSO',
|
||||
'TH' => 'THA',
|
||||
'TF' => 'ATF',
|
||||
'TG' => 'TGO',
|
||||
'TD' => 'TCD',
|
||||
'TC' => 'TCA',
|
||||
'LY' => 'LBY',
|
||||
'VA' => 'VAT',
|
||||
'VC' => 'VCT',
|
||||
'AE' => 'ARE',
|
||||
'AD' => 'AND',
|
||||
'AG' => 'ATG',
|
||||
'AF' => 'AFG',
|
||||
'AI' => 'AIA',
|
||||
'VI' => 'VIR',
|
||||
'IS' => 'ISL',
|
||||
'IR' => 'IRN',
|
||||
'AM' => 'ARM',
|
||||
'AL' => 'ALB',
|
||||
'AO' => 'AGO',
|
||||
'AQ' => 'ATA',
|
||||
'AS' => 'ASM',
|
||||
'AR' => 'ARG',
|
||||
'AU' => 'AUS',
|
||||
'AT' => 'AUT',
|
||||
'AW' => 'ABW',
|
||||
'IN' => 'IND',
|
||||
'AX' => 'ALA',
|
||||
'AZ' => 'AZE',
|
||||
'IE' => 'IRL',
|
||||
'ID' => 'IDN',
|
||||
'UA' => 'UKR',
|
||||
'QA' => 'QAT',
|
||||
'MZ' => 'MOZ',
|
||||
);
|
@ -8,6 +8,7 @@
|
||||
|
||||
use GeoIp2\Database\Reader;
|
||||
|
||||
// Override with a valid public IP when testing on localhost
|
||||
//$_SERVER['REMOTE_ADDR'] = "206.127.96.82";
|
||||
|
||||
try {
|
||||
@ -71,6 +72,8 @@ try {
|
||||
$country = $record->country->name;
|
||||
$region = $record->mostSpecificSubdivision->name;
|
||||
$city = $record->city->name;
|
||||
$countrycode = $record->country->isoCode;
|
||||
$regioncode = $record->mostSpecificSubdivision->isoCode;
|
||||
$lat = $record->location->latitude;
|
||||
$lon = $record->location->longitude;
|
||||
|
||||
@ -85,6 +88,8 @@ try {
|
||||
"country" => $country,
|
||||
"region" => $region,
|
||||
"city" => $city,
|
||||
"countrycode" => $countrycode,
|
||||
"regioncode" => $regioncode,
|
||||
"lat" => $lat,
|
||||
"lon" => $lon,
|
||||
"time" => $time
|
||||
|
18
pages.php
18
pages.php
@ -34,6 +34,24 @@ define("PAGES", [
|
||||
"static/js/editorparent.js"
|
||||
]
|
||||
],
|
||||
"analytics" => [
|
||||
"title" => "analytics",
|
||||
"navbar" => true,
|
||||
"icon" => "fas fa-chart-bar",
|
||||
"styles" => [
|
||||
"static/css/tempusdominus-bootstrap-4.min.css",
|
||||
"static/css/vertline.css"
|
||||
],
|
||||
"scripts" => [
|
||||
"static/js/moment.min.js",
|
||||
"static/js/Chart.min.js",
|
||||
"static/js/topojson.min.js",
|
||||
"static/js/d3.min.js",
|
||||
"static/js/datamaps.all.min.js",
|
||||
"static/js/tempusdominus-bootstrap-4.min.js",
|
||||
"static/js/analy_reports.js"
|
||||
]
|
||||
],
|
||||
"404" => [
|
||||
"title" => "404 error"
|
||||
]
|
||||
|
269
pages/analytics.php
Normal file
269
pages/analytics.php
Normal file
@ -0,0 +1,269 @@
|
||||
<?php
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
require_once __DIR__ . '/../required.php';
|
||||
|
||||
redirectifnotloggedin();
|
||||
|
||||
$select_filter = [];
|
||||
|
||||
if (!is_empty($VARS['siteid'])) {
|
||||
if ($database->has('sites', ['siteid' => $VARS['siteid']])) {
|
||||
$select_filter["siteid"] = $VARS['siteid'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_empty($VARS['after'])) {
|
||||
if (strtotime($VARS['after']) !== FALSE) {
|
||||
$select_filter["time[>]"] = date("Y-m-d H:i:s", strtotime($VARS['after']));
|
||||
}
|
||||
}
|
||||
if (!is_empty($VARS['before'])) {
|
||||
if (strtotime($VARS['before']) !== FALSE) {
|
||||
$select_filter["time[<]"] = date("Y-m-d H:i:s", strtotime($VARS['before']));
|
||||
}
|
||||
}
|
||||
|
||||
$where = [];
|
||||
if (count($select_filter) == 1) {
|
||||
$where = $select_filter;
|
||||
} else if (count($select_filter) > 1) {
|
||||
$where = ["AND" => $select_filter];
|
||||
}
|
||||
|
||||
$where["LIMIT"] = 1000;
|
||||
$where["ORDER"] = ["time" => "DESC"];
|
||||
|
||||
$records = $database->select("analytics", [
|
||||
"[>]sites" => ["siteid" => "siteid"],
|
||||
"[>]pages" => ["pageid" => "pageid"],
|
||||
], [
|
||||
"analytics.siteid", "analytics.pageid", "uuid", "country", "region", "city",
|
||||
"countrycode", "regioncode",
|
||||
"lat", "lon", "time", "pages.title (pagetitle)", "pages.slug (pageslug)",
|
||||
"sites.sitename"
|
||||
], $where);
|
||||
?>
|
||||
|
||||
<!-- Filter bar -->
|
||||
<div class="card p-2">
|
||||
<form class="form-inline" action="app.php" method="GET">
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-sync"></i></button>
|
||||
<label for="siteid_select" class="sr-only"><?php lang("filter by site") ?></label>
|
||||
<div class="input-group mx-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fas fa-sitemap"></i></span>
|
||||
</div>
|
||||
<select name="siteid" class="form-control pr-4" id="siteid_select">
|
||||
<option value=""><?php lang("all sites"); ?></option>
|
||||
<?php
|
||||
$sites = $database->select("sites", ["siteid", "sitename"]);
|
||||
foreach ($sites as $s) {
|
||||
$selected = "";
|
||||
if (!empty($select_filter["siteid"]) && $select_filter["siteid"] == $s['siteid']) {
|
||||
$selected = "selected";
|
||||
}
|
||||
?>
|
||||
<option value="<?php echo $s['siteid']; ?>" <?php echo $selected; ?>><?php echo $s['sitename']; ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<span class="vertline d-none d-lg-inline"></span>
|
||||
|
||||
<label for="date_after" class="sr-only"><?php lang("filter after date") ?></label>
|
||||
<label for="date_before" class="sr-only"><?php lang("filter before date") ?></label>
|
||||
<div class="input-group mx-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fas fa-calendar"></i></span>
|
||||
</div>
|
||||
<input type="text" id="date_after" name="after" value="<?php echo htmlspecialchars($VARS['after']); ?>" class="form-control" placeholder="<?php lang("start date"); ?>" data-toggle="datetimepicker" data-target="#date_after" />
|
||||
<div class="input-group-prepend input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-caret-right"></i></span>
|
||||
</div>
|
||||
<input type="text" id="date_before" name="before" value="<?php echo htmlspecialchars($VARS['before']); ?>" class="form-control" placeholder="<?php lang("end date"); ?>" data-toggle="datetimepicker" data-target="#date_before" />
|
||||
</div>
|
||||
|
||||
|
||||
<input type="hidden" name="page" value="analytics" />
|
||||
<button type="submit" class="btn btn-secondary"><i class="fas fa-filter"></i> <?php lang("filter"); ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Data views -->
|
||||
<?php
|
||||
if (count($records) > 0) {
|
||||
?>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 col-sm-6 col-md-6 col-lg-4 mb-4">
|
||||
<!-- Overview -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title"><?php lang("overview"); ?></h4>
|
||||
<?php
|
||||
$uuids = [];
|
||||
foreach ($records as $r) {
|
||||
if (!in_array($r["uuid"], $uuids)) {
|
||||
$uuids[] = $r["uuid"];
|
||||
}
|
||||
}
|
||||
$visits = count($uuids);
|
||||
$views = count($records);
|
||||
$ratio = round($views / $visits, 1);
|
||||
?>
|
||||
<h5>
|
||||
<i class="fas fa-users fa-fw"></i> <?php echo $visits; ?> <?php lang("visits") ?> <br />
|
||||
<i class="fas fa-eye fa-fw"></i> <?php echo $views; ?> <?php lang("page views") ?> <br />
|
||||
<i class="fas fa-percent fa-fw"></i> <?php echo $ratio; ?> <?php lang("views per visit") ?>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visits Over Time -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title"><?php lang("visits over time"); ?></h4>
|
||||
<?php
|
||||
$format = "Y-m-00 00:00:00";
|
||||
$max = $records[0];
|
||||
$min = $records[count($records) - 1];
|
||||
$diff = strtotime($max['time']) - strtotime($min['time']);
|
||||
if ($diff < 60 * 60) { // 1 hour
|
||||
$format = "Y-m-d H:i:00";
|
||||
} else if ($diff < 60 * 60 * 24 * 3) { // 3 days
|
||||
$format = "Y-m-d H:00:00";
|
||||
} else if ($diff < 60 * 60 * 24 * 60) { // 30 days
|
||||
$format = "Y-m-d 00:00:00";
|
||||
}
|
||||
|
||||
$counted = [];
|
||||
foreach ($records as $r) {
|
||||
$rf = date($format, strtotime($r['time']));
|
||||
if (array_key_exists($rf, $counted)) {
|
||||
$counted[$rf] ++;
|
||||
} else {
|
||||
$counted[$rf] = 1;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<script nonce="<?php echo $SECURE_NONCE; ?>">
|
||||
var visitsOverTimeData = [
|
||||
<?php foreach ($counted as $d => $c) { ?>
|
||||
{
|
||||
x: "<?php echo $d; ?>",
|
||||
y: <?php echo $c; ?>
|
||||
},
|
||||
<?php } ?>
|
||||
];
|
||||
</script>
|
||||
<div class="w-100 position-relative">
|
||||
<canvas id="visitsOverTime"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Actions -->
|
||||
<div class="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title"><?php lang("recent actions"); ?></h4>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<?php
|
||||
$max = 10;
|
||||
$i = 0;
|
||||
foreach ($records as $r) {
|
||||
$i++;
|
||||
if ($i > $max) {
|
||||
break;
|
||||
}
|
||||
?>
|
||||
<div class="list-group-item">
|
||||
<div>
|
||||
<div><i class="fas fa-user fa-fw"></i> <?php echo substr($r["uuid"], 0, 8); ?></div>
|
||||
</div>
|
||||
<div class="row justify-content-between">
|
||||
<div class="col-12 col-sm-6 d-flex flex-column">
|
||||
<span><i class="fas fa-clock fa-fw"></i> <?php echo date("g:i A", strtotime($r["time"])); ?></span>
|
||||
<span><i class="fas fa-file fa-fw"></i> <?php echo $r["pagetitle"]; ?></span>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 d-flex flex-column">
|
||||
<span><i class="fas fa-calendar fa-fw"></i> <?php echo date("M j Y", strtotime($r["time"])); ?></span>
|
||||
<span><i class="fas fa-sitemap fa-fw"></i> <?php echo $r["sitename"]; ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div><i class="fas fa-globe fa-fw"></i> <?php echo $r["country"]; ?></div>
|
||||
<div><i class="fas fa-map-marker fa-fw"></i> <?php echo $r["city"] . ", " . $r["region"]; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visitor Map -->
|
||||
<div class="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title"><?php lang("visitor map"); ?></h4>
|
||||
<?php
|
||||
require_once __DIR__ . "/../lib/countries_2_3.php";
|
||||
$countries = [];
|
||||
$states = [];
|
||||
foreach ($records as $r) {
|
||||
if (array_key_exists($COUNTRY_CODES[$r['countrycode']], $countries)) {
|
||||
$countries[$COUNTRY_CODES[$r['countrycode']]] ++;
|
||||
} else {
|
||||
$countries[$COUNTRY_CODES[$r['countrycode']]] = 1;
|
||||
}
|
||||
if ($r['countrycode'] === "US") {
|
||||
if (array_key_exists($r['regioncode'], $states)) {
|
||||
$states[$r['regioncode']] ++;
|
||||
} else {
|
||||
$states[$r['regioncode']] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
$countrymapdata = [];
|
||||
foreach ($countries as $id => $count) {
|
||||
$countrymapdata[] = [$id, $count];
|
||||
}
|
||||
$statemapdata = [];
|
||||
foreach ($states as $id => $count) {
|
||||
$statemapdata[] = [$id, $count];
|
||||
}
|
||||
?>
|
||||
<script nonce="<?php echo $SECURE_NONCE; ?>">
|
||||
visitorMap_Countries = <?php echo json_encode($countrymapdata); ?>;
|
||||
visitorMap_States = <?php echo json_encode($statemapdata); ?>;
|
||||
</script>
|
||||
<div class="w-100" id="visitorMapWorld"></div>
|
||||
<div class="w-100" id="visitorMapUSA"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col-12 col-sm-10 col-md-8 col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-info-circle"></i> <?php lang("no data"); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
?>
|
@ -52,7 +52,7 @@ if ($_SESSION['mobile'] === TRUE) {
|
||||
. "frame-src 'self'; "
|
||||
. "font-src 'self'; "
|
||||
. "connect-src *; "
|
||||
. "style-src 'self' 'nonce-$SECURE_NONCE' $captcha_server; "
|
||||
. "style-src 'self' 'unsafe-inline' $captcha_server; "
|
||||
. "script-src 'self' 'nonce-$SECURE_NONCE' $captcha_server");
|
||||
}
|
||||
|
||||
|
204
static/css/tempusdominus-bootstrap-4.min.css
vendored
Normal file
204
static/css/tempusdominus-bootstrap-4.min.css
vendored
Normal file
@ -0,0 +1,204 @@
|
||||
/*@preserve
|
||||
* Tempus Dominus Bootstrap4 v5.0.0-alpha16 (https://tempusdominus.github.io/bootstrap-4/)
|
||||
* Copyright 2016-2018 Jonathan Peterson
|
||||
* Licensed under MIT (https://github.com/tempusdominus/bootstrap-3/blob/master/LICENSE)
|
||||
*/
|
||||
|
||||
.sr-only, .bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after, .bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after, .bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after, .bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after, .bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after, .bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after, .bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after, .bootstrap-datetimepicker-widget .btn[data-action="clear"]::after, .bootstrap-datetimepicker-widget .btn[data-action="today"]::after, .bootstrap-datetimepicker-widget .picker-switch::after, .bootstrap-datetimepicker-widget table th.prev::after, .bootstrap-datetimepicker-widget table th.next::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0; }
|
||||
|
||||
.bootstrap-datetimepicker-widget {
|
||||
list-style: none; }
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu {
|
||||
display: block;
|
||||
margin: 2px 0;
|
||||
padding: 4px;
|
||||
width: 14rem; }
|
||||
@media (min-width: 576px) {
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||
width: 38em; } }
|
||||
@media (min-width: 768px) {
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||
width: 38em; } }
|
||||
@media (min-width: 992px) {
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||
width: 38em; } }
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu:before, .bootstrap-datetimepicker-widget.dropdown-menu:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute; }
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before {
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 7px solid #ccc;
|
||||
border-bottom-color: rgba(0, 0, 0, 0.2);
|
||||
top: -7px;
|
||||
left: 7px; }
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid white;
|
||||
top: -6px;
|
||||
left: 8px; }
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.top:before {
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-top: 7px solid #ccc;
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
bottom: -7px;
|
||||
left: 6px; }
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.top:after {
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid white;
|
||||
bottom: -6px;
|
||||
left: 7px; }
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.float-right:before {
|
||||
left: auto;
|
||||
right: 6px; }
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.float-right:after {
|
||||
left: auto;
|
||||
right: 7px; }
|
||||
.bootstrap-datetimepicker-widget .list-unstyled {
|
||||
margin: 0; }
|
||||
.bootstrap-datetimepicker-widget a[data-action] {
|
||||
padding: 6px 0; }
|
||||
.bootstrap-datetimepicker-widget a[data-action]:active {
|
||||
box-shadow: none; }
|
||||
.bootstrap-datetimepicker-widget .timepicker-hour, .bootstrap-datetimepicker-widget .timepicker-minute, .bootstrap-datetimepicker-widget .timepicker-second {
|
||||
width: 54px;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
margin: 0; }
|
||||
.bootstrap-datetimepicker-widget button[data-action] {
|
||||
padding: 6px; }
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after {
|
||||
content: "Increment Hours"; }
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after {
|
||||
content: "Increment Minutes"; }
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after {
|
||||
content: "Decrement Hours"; }
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after {
|
||||
content: "Decrement Minutes"; }
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after {
|
||||
content: "Show Hours"; }
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after {
|
||||
content: "Show Minutes"; }
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after {
|
||||
content: "Toggle AM/PM"; }
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after {
|
||||
content: "Clear the picker"; }
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="today"]::after {
|
||||
content: "Set the date to today"; }
|
||||
.bootstrap-datetimepicker-widget .picker-switch {
|
||||
text-align: center; }
|
||||
.bootstrap-datetimepicker-widget .picker-switch::after {
|
||||
content: "Toggle Date and Time Screens"; }
|
||||
.bootstrap-datetimepicker-widget .picker-switch td {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: auto;
|
||||
width: auto;
|
||||
line-height: inherit; }
|
||||
.bootstrap-datetimepicker-widget .picker-switch td span {
|
||||
line-height: 2.5;
|
||||
height: 2.5em;
|
||||
width: 100%; }
|
||||
.bootstrap-datetimepicker-widget table {
|
||||
width: 100%;
|
||||
margin: 0; }
|
||||
.bootstrap-datetimepicker-widget table td,
|
||||
.bootstrap-datetimepicker-widget table th {
|
||||
text-align: center;
|
||||
border-radius: 0.25rem; }
|
||||
.bootstrap-datetimepicker-widget table th {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
width: 20px; }
|
||||
.bootstrap-datetimepicker-widget table th.picker-switch {
|
||||
width: 145px; }
|
||||
.bootstrap-datetimepicker-widget table th.disabled, .bootstrap-datetimepicker-widget table th.disabled:hover {
|
||||
background: none;
|
||||
color: #868e96;
|
||||
cursor: not-allowed; }
|
||||
.bootstrap-datetimepicker-widget table th.prev::after {
|
||||
content: "Previous Month"; }
|
||||
.bootstrap-datetimepicker-widget table th.next::after {
|
||||
content: "Next Month"; }
|
||||
.bootstrap-datetimepicker-widget table thead tr:first-child th {
|
||||
cursor: pointer; }
|
||||
.bootstrap-datetimepicker-widget table thead tr:first-child th:hover {
|
||||
background: #e9ecef; }
|
||||
.bootstrap-datetimepicker-widget table td {
|
||||
height: 54px;
|
||||
line-height: 54px;
|
||||
width: 54px; }
|
||||
.bootstrap-datetimepicker-widget table td.cw {
|
||||
font-size: .8em;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
color: #868e96; }
|
||||
.bootstrap-datetimepicker-widget table td.day {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
width: 20px; }
|
||||
.bootstrap-datetimepicker-widget table td.day:hover, .bootstrap-datetimepicker-widget table td.hour:hover, .bootstrap-datetimepicker-widget table td.minute:hover, .bootstrap-datetimepicker-widget table td.second:hover {
|
||||
background: #e9ecef;
|
||||
cursor: pointer; }
|
||||
.bootstrap-datetimepicker-widget table td.old, .bootstrap-datetimepicker-widget table td.new {
|
||||
color: #868e96; }
|
||||
.bootstrap-datetimepicker-widget table td.today {
|
||||
position: relative; }
|
||||
.bootstrap-datetimepicker-widget table td.today:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
border: solid transparent;
|
||||
border-width: 0 0 7px 7px;
|
||||
border-bottom-color: #007bff;
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px; }
|
||||
.bootstrap-datetimepicker-widget table td.active, .bootstrap-datetimepicker-widget table td.active:hover {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); }
|
||||
.bootstrap-datetimepicker-widget table td.active.today:before {
|
||||
border-bottom-color: #fff; }
|
||||
.bootstrap-datetimepicker-widget table td.disabled, .bootstrap-datetimepicker-widget table td.disabled:hover {
|
||||
background: none;
|
||||
color: #868e96;
|
||||
cursor: not-allowed; }
|
||||
.bootstrap-datetimepicker-widget table td span {
|
||||
display: inline-block;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
line-height: 54px;
|
||||
margin: 2px 1.5px;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem; }
|
||||
.bootstrap-datetimepicker-widget table td span:hover {
|
||||
background: #e9ecef; }
|
||||
.bootstrap-datetimepicker-widget table td span.active {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); }
|
||||
.bootstrap-datetimepicker-widget table td span.old {
|
||||
color: #868e96; }
|
||||
.bootstrap-datetimepicker-widget table td span.disabled, .bootstrap-datetimepicker-widget table td span.disabled:hover {
|
||||
background: none;
|
||||
color: #868e96;
|
||||
cursor: not-allowed; }
|
||||
.bootstrap-datetimepicker-widget.usetwentyfour td.hour {
|
||||
height: 27px;
|
||||
line-height: 27px; }
|
||||
|
||||
.input-group [data-toggle="datetimepicker"] {
|
||||
cursor: pointer; }
|
13
static/css/vertline.css
Normal file
13
static/css/vertline.css
Normal file
@ -0,0 +1,13 @@
|
||||
/*
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
.vertline {
|
||||
margin: 1px 3px;
|
||||
height: 100%;
|
||||
max-height: 50px;
|
||||
width: 1px;
|
||||
background-color: rgba(0,0,0,.20);
|
||||
}
|
10
static/js/Chart.min.js
vendored
Normal file
10
static/js/Chart.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
159
static/js/analy_reports.js
Normal file
159
static/js/analy_reports.js
Normal file
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Note: This script is *not* called "analytics.js" because adblockers.
|
||||
*/
|
||||
|
||||
|
||||
$(function () {
|
||||
$('#date_after').datetimepicker({
|
||||
format: "MMM D YYYY h:mm A",
|
||||
useCurrent: false,
|
||||
icons: {
|
||||
time: "fas fa-clock",
|
||||
date: "fas fa-calendar",
|
||||
up: "fas fa-arrow-up",
|
||||
down: "fas fa-arrow-down"
|
||||
}
|
||||
});
|
||||
$('#date_before').datetimepicker({
|
||||
format: "MMM D YYYY h:mm A",
|
||||
useCurrent: true,
|
||||
icons: {
|
||||
time: "fas fa-clock",
|
||||
date: "fas fa-calendar",
|
||||
up: "fas fa-arrow-up",
|
||||
down: "fas fa-arrow-down"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var visitsOverTime = new Chart($("#visitsOverTime"), {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: visitsOverTimeData
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'time',
|
||||
}],
|
||||
yAxes: [{
|
||||
type: 'linear',
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: function (value) {
|
||||
if (Number.isInteger(value)) {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
title: function (item) {
|
||||
var lbl = item[0].xLabel;
|
||||
if (lbl.endsWith("-00 00:00:00")) {
|
||||
return moment(lbl).format("MMM YYYY");
|
||||
} else if (lbl.endsWith(" 00:00:00")) {
|
||||
return moment(lbl).format("MMM D YYYY");
|
||||
} else if (lbl.endsWith(":00:00")) {
|
||||
return moment(lbl).format("MMM D YYYY ha");
|
||||
} else if (lbl.endsWith(":00")) {
|
||||
return moment(lbl).format("MMM D YYYY h:mma");
|
||||
}
|
||||
return item[0].xLabel;
|
||||
},
|
||||
label: function (item) {
|
||||
return item.yLabel + " visits";
|
||||
}
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
borderWidth: 2,
|
||||
borderColor: "#ff0000",
|
||||
backgroundColor: "#ffffff00",
|
||||
tension: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
function getVisitorMapData(source) {
|
||||
var visitorMapDataset = {};
|
||||
|
||||
var onlyValues = source.map(function (obj) {
|
||||
return obj[1];
|
||||
});
|
||||
var minValue = Math.min.apply(null, onlyValues);
|
||||
var maxValue = Math.max.apply(null, onlyValues);
|
||||
|
||||
var paletteScale = d3.scale.linear()
|
||||
.domain([minValue, maxValue])
|
||||
.range(["#A5D6A7", "#124016"]); // blue color
|
||||
|
||||
source.forEach(function (item) { //
|
||||
// item example value ["USA", 70]
|
||||
var iso = item[0],
|
||||
value = item[1];
|
||||
visitorMapDataset[iso] = {numberOfThings: value, fillColor: paletteScale(value)};
|
||||
});
|
||||
|
||||
return visitorMapDataset;
|
||||
}
|
||||
|
||||
var visitorMap;
|
||||
|
||||
function showVisitorMap(data, scope, containerid) {
|
||||
$("visitorMap").html("");
|
||||
visitorMap = new Datamap({
|
||||
element: document.getElementById(containerid),
|
||||
scope: scope,
|
||||
responsive: true,
|
||||
fills: {defaultFill: '#F5F5F5'},
|
||||
data: data,
|
||||
geographyConfig: {
|
||||
borderColor: '#DEDEDE',
|
||||
highlightBorderWidth: 2,
|
||||
// don't change color on mouse hover
|
||||
highlightFillColor: function (geo) {
|
||||
return geo['fillColor'] || '#E8F5E9';
|
||||
},
|
||||
// only change border
|
||||
highlightBorderColor: '#00C853',
|
||||
// show desired information in tooltip
|
||||
popupTemplate: function (geo, data) {
|
||||
// don't show tooltip if country don't present in dataset
|
||||
if (!data) {
|
||||
//return;
|
||||
}
|
||||
// tooltip content
|
||||
return ['<div class="hoverinfo">',
|
||||
'<strong>', geo.properties.name, '</strong>',
|
||||
'<br>Visits: <strong>', data.numberOfThings, '</strong>',
|
||||
'</div>'].join('');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showVisitorMap(getVisitorMapData(visitorMap_Countries), 'world', 'visitorMapWorld');
|
||||
showVisitorMap(getVisitorMapData(visitorMap_States), 'usa', 'visitorMapUSA');
|
||||
|
||||
$(window).on('resize', function () {
|
||||
visitorMap.resize();
|
||||
});
|
5
static/js/d3.min.js
vendored
Normal file
5
static/js/d3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
static/js/datamaps.all.min.js
vendored
Normal file
3
static/js/datamaps.all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
static/js/moment.min.js
vendored
Normal file
7
static/js/moment.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
static/js/tempusdominus-bootstrap-4.min.js
vendored
Normal file
7
static/js/tempusdominus-bootstrap-4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/js/topojson.min.js
vendored
Normal file
1
static/js/topojson.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user