|
@@ -269,452 +269,6 @@ function canSendVerificationEmail() {
|
|
|
checkbox.classList.add('hidden');
|
|
checkbox.classList.add('hidden');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// create views chart label
|
|
|
|
|
-function createViewsChartLabel(ctx) {
|
|
|
|
|
- const period = ctx.dataset.period;
|
|
|
|
|
- let labels = [];
|
|
|
|
|
-
|
|
|
|
|
- if (period === "day") {
|
|
|
|
|
- const nowHour = new Date().getHours();
|
|
|
|
|
- for (let i = 23; i >= 0; --i) {
|
|
|
|
|
- let h = nowHour - i;
|
|
|
|
|
- if (h < 0) h = 24 + h;
|
|
|
|
|
- labels.push(`${Math.floor(h)}:00`);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (period === "week") {
|
|
|
|
|
- const nowDay = new Date().getDate();
|
|
|
|
|
- for (let i = 6; i >= 0; --i) {
|
|
|
|
|
- const date = new Date(new Date().setDate(nowDay - i));
|
|
|
|
|
- labels.push(`${date.getDate()} ${date.toLocaleString("default",{month:"short"})}`);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (period === "month") {
|
|
|
|
|
- const nowDay = new Date().getDate();
|
|
|
|
|
- for (let i = 29; i >= 0; --i) {
|
|
|
|
|
- const date = new Date(new Date().setDate(nowDay - i));
|
|
|
|
|
- labels.push(`${date.getDate()} ${date.toLocaleString("default",{month:"short"})}`);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (period === "year") {
|
|
|
|
|
- const nowMonth = new Date().getMonth();
|
|
|
|
|
- for (let i = 11; i >= 0; --i) {
|
|
|
|
|
- const date = new Date(new Date().setMonth(nowMonth - i));
|
|
|
|
|
- labels.push(`${date.toLocaleString("default",{month:"short"})} ${date.toLocaleString("default",{year:"numeric"})}`);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return labels;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// create views chart
|
|
|
|
|
-function createViewsChart() {
|
|
|
|
|
- const canvases = document.querySelectorAll("canvas.visits");
|
|
|
|
|
- if (!canvases || !canvases.length) return;
|
|
|
|
|
-
|
|
|
|
|
- canvases.forEach(ctx => {
|
|
|
|
|
- const data = JSON.parse(ctx.dataset.data);
|
|
|
|
|
- const period = ctx.dataset.period;
|
|
|
|
|
-
|
|
|
|
|
- const labels = createViewsChartLabel(ctx);
|
|
|
|
|
- const maxTicksLimitX = period === "year" ? 6 : period === "month" ? 15 : 12;
|
|
|
|
|
-
|
|
|
|
|
- const gradient = ctx.getContext("2d").createLinearGradient(0, 0, 0, 300);
|
|
|
|
|
- gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");
|
|
|
|
|
- gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
|
|
|
|
|
-
|
|
|
|
|
- new Chart(ctx, {
|
|
|
|
|
- type: "line",
|
|
|
|
|
- data: {
|
|
|
|
|
- labels: labels,
|
|
|
|
|
- datasets: [{
|
|
|
|
|
- label: "Views",
|
|
|
|
|
- data,
|
|
|
|
|
- tension: 0.3,
|
|
|
|
|
-
|
|
|
|
|
- elements: {
|
|
|
|
|
- point: {
|
|
|
|
|
- pointRadius: 0,
|
|
|
|
|
- pointHoverRadius: 4
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- fill: {
|
|
|
|
|
- target: "start",
|
|
|
|
|
- },
|
|
|
|
|
- backgroundColor: gradient,
|
|
|
|
|
- borderColor: "rgb(179, 157, 219)",
|
|
|
|
|
- borderWidth: 1,
|
|
|
|
|
- }]
|
|
|
|
|
- },
|
|
|
|
|
- options: {
|
|
|
|
|
- plugins: {
|
|
|
|
|
- legend: {
|
|
|
|
|
- display: false,
|
|
|
|
|
- },
|
|
|
|
|
- tooltip: {
|
|
|
|
|
- backgroundColor: "rgba(255, 255, 255, 0.95)",
|
|
|
|
|
- titleColor: "#333",
|
|
|
|
|
- titleFont: { weight: "normal", size: 15 },
|
|
|
|
|
- bodyFont: { weight: "normal", size: 16 },
|
|
|
|
|
- bodyColor: "rgb(179, 157, 219)",
|
|
|
|
|
- padding: 12,
|
|
|
|
|
- cornerRadius: 2,
|
|
|
|
|
- borderColor: "rgba(0, 0, 0, 0.1)",
|
|
|
|
|
- borderWidth: 1,
|
|
|
|
|
- displayColors: false,
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- responsive: true,
|
|
|
|
|
- interaction: {
|
|
|
|
|
- intersect: false,
|
|
|
|
|
- usePointStyle: true,
|
|
|
|
|
- mode: "index",
|
|
|
|
|
- },
|
|
|
|
|
- scales: {
|
|
|
|
|
- y: {
|
|
|
|
|
- grace: "10%",
|
|
|
|
|
- beginAtZero: true,
|
|
|
|
|
- ticks: {
|
|
|
|
|
- maxTicksLimit: 5
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- x: {
|
|
|
|
|
- ticks: {
|
|
|
|
|
- maxTicksLimit: maxTicksLimitX,
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // reset the display: block style that chart.js applies automatically
|
|
|
|
|
- ctx.style.display = "";
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// beautify browser lables
|
|
|
|
|
-function beautifyBrowserName(name) {
|
|
|
|
|
- if (name === "firefox") return "Firefox";
|
|
|
|
|
- if (name === "chrome") return "Chrome";
|
|
|
|
|
- if (name === "edge") return "Edge";
|
|
|
|
|
- if (name === "opera") return "Opera";
|
|
|
|
|
- if (name === "safari") return "Safari";
|
|
|
|
|
- if (name === "other") return "Other";
|
|
|
|
|
- if (name === "ie") return "IE";
|
|
|
|
|
- return name;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// create browsers chart
|
|
|
|
|
-function createBrowsersChart() {
|
|
|
|
|
- const canvases = document.querySelectorAll("canvas.browsers");
|
|
|
|
|
- if (!canvases || !canvases.length) return;
|
|
|
|
|
-
|
|
|
|
|
- canvases.forEach(ctx => {
|
|
|
|
|
- const data = JSON.parse(ctx.dataset.data);
|
|
|
|
|
- const period = ctx.dataset.period;
|
|
|
|
|
-
|
|
|
|
|
- const gradient = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
|
|
|
|
|
- const gradientHover = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
|
|
|
|
|
- gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");
|
|
|
|
|
- gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
|
|
|
|
|
- gradientHover.addColorStop(0, "rgba(179, 157, 219, 0.9)");
|
|
|
|
|
- gradientHover.addColorStop(1, "rgba(179, 157, 219, 0.4)");
|
|
|
|
|
-
|
|
|
|
|
- new Chart(ctx, {
|
|
|
|
|
- type: "bar",
|
|
|
|
|
- data: {
|
|
|
|
|
- labels: data.map(d => beautifyBrowserName(d.name)),
|
|
|
|
|
- datasets: [{
|
|
|
|
|
- label: "Views",
|
|
|
|
|
- data: data.map(d => d.value),
|
|
|
|
|
- backgroundColor: gradient,
|
|
|
|
|
- borderColor: "rgba(179, 157, 219, 1)",
|
|
|
|
|
- borderWidth: 1,
|
|
|
|
|
- hoverBackgroundColor: gradientHover,
|
|
|
|
|
- hoverBorderWidth: 2
|
|
|
|
|
- }]
|
|
|
|
|
- },
|
|
|
|
|
- options: {
|
|
|
|
|
- indexAxis: "y",
|
|
|
|
|
- plugins: {
|
|
|
|
|
- legend: {
|
|
|
|
|
- display: false,
|
|
|
|
|
- },
|
|
|
|
|
- tooltip: {
|
|
|
|
|
- backgroundColor: "rgba(255, 255, 255, 0.95)",
|
|
|
|
|
- titleColor: "#333",
|
|
|
|
|
- titleFont: { weight: "normal", size: 15 },
|
|
|
|
|
- bodyFont: { weight: "normal", size: 16 },
|
|
|
|
|
- bodyColor: "rgb(179, 157, 219)",
|
|
|
|
|
- padding: 12,
|
|
|
|
|
- cornerRadius: 2,
|
|
|
|
|
- borderColor: "rgba(0, 0, 0, 0.1)",
|
|
|
|
|
- borderWidth: 1,
|
|
|
|
|
- displayColors: false,
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- responsive: true,
|
|
|
|
|
- interaction: {
|
|
|
|
|
- intersect: false,
|
|
|
|
|
- mode: "index",
|
|
|
|
|
- axis: "y"
|
|
|
|
|
- },
|
|
|
|
|
- scales: {
|
|
|
|
|
- x: {
|
|
|
|
|
- grace: "5%",
|
|
|
|
|
- beginAtZero: true,
|
|
|
|
|
- ticks: {
|
|
|
|
|
- maxTicksLimit: 6,
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // reset the display: block style that chart.js applies automatically
|
|
|
|
|
- ctx.style.display = "";
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// create referrers chart
|
|
|
|
|
-function createReferrersChart() {
|
|
|
|
|
- const canvases = document.querySelectorAll("canvas.referrers");
|
|
|
|
|
- if (!canvases || !canvases.length) return;
|
|
|
|
|
-
|
|
|
|
|
- canvases.forEach(ctx => {
|
|
|
|
|
- const data = JSON.parse(ctx.dataset.data);
|
|
|
|
|
- const period = ctx.dataset.period;
|
|
|
|
|
- let max = Array.from(data).sort((a, b) => a.value > b.value ? -1 : 1)[0];
|
|
|
|
|
-
|
|
|
|
|
- let tooltipEnabled = true;
|
|
|
|
|
- let hoverBackgroundColor = "rgba(179, 157, 219, 1)";
|
|
|
|
|
- let hoverBorderWidth = 2;
|
|
|
|
|
- let borderColor = "rgba(179, 157, 219, 1)";
|
|
|
|
|
- if (data.length === 0) {
|
|
|
|
|
- data.push({ name: "No views.", value: 1 });
|
|
|
|
|
- max = { value: 1000 };
|
|
|
|
|
- tooltipEnabled = false;
|
|
|
|
|
- hoverBackgroundColor = "rgba(179, 157, 219, 0.1)";
|
|
|
|
|
- hoverBorderWidth = 1;
|
|
|
|
|
- borderColor = "rgba(179, 157, 219, 0.2)";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- new Chart(ctx, {
|
|
|
|
|
- type: "doughnut",
|
|
|
|
|
- data: {
|
|
|
|
|
- labels: data.map(d => d.name.replace(/\[dot\]/g, ".")),
|
|
|
|
|
- datasets: [{
|
|
|
|
|
- label: "Views",
|
|
|
|
|
- data: data.map(d => d.value),
|
|
|
|
|
- backgroundColor: data.map(d => `rgba(179, 157, 219, ${Math.max((d.value / max.value) - 0.2, 0.1).toFixed(2)})`),
|
|
|
|
|
- borderWidth: 1,
|
|
|
|
|
- borderColor,
|
|
|
|
|
- hoverBackgroundColor,
|
|
|
|
|
- hoverBorderWidth,
|
|
|
|
|
- }]
|
|
|
|
|
- },
|
|
|
|
|
- options: {
|
|
|
|
|
- plugins: {
|
|
|
|
|
- legend: {
|
|
|
|
|
- position: "left",
|
|
|
|
|
- labels: {
|
|
|
|
|
- boxWidth: 25,
|
|
|
|
|
- font: { size: 11 }
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- tooltip: {
|
|
|
|
|
- enabled: tooltipEnabled,
|
|
|
|
|
- backgroundColor: "rgba(255, 255, 255, 0.95)",
|
|
|
|
|
- titleColor: "#333",
|
|
|
|
|
- titleFont: { weight: "normal", size: 15 },
|
|
|
|
|
- bodyFont: { weight: "normal", size: 16 },
|
|
|
|
|
- bodyColor: "rgb(179, 157, 219)",
|
|
|
|
|
- padding: 12,
|
|
|
|
|
- cornerRadius: 2,
|
|
|
|
|
- borderColor: "rgba(0, 0, 0, 0.1)",
|
|
|
|
|
- borderWidth: 1,
|
|
|
|
|
- displayColors: false,
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- responsive: false,
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // reset the display: block style that chart.js applies automatically
|
|
|
|
|
- ctx.style.display = "";
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-// beautify browser lables
|
|
|
|
|
-function beautifyOsName(name) {
|
|
|
|
|
- if (name === "android") return "Android";
|
|
|
|
|
- if (name === "ios") return "iOS";
|
|
|
|
|
- if (name === "linux") return "Linux";
|
|
|
|
|
- if (name === "macos") return "macOS";
|
|
|
|
|
- if (name === "windows") return "Windows";
|
|
|
|
|
- if (name === "other") return "Other";
|
|
|
|
|
- return name;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-// create operation systems chart
|
|
|
|
|
-function createOsChart() {
|
|
|
|
|
- const canvases = document.querySelectorAll("canvas.os");
|
|
|
|
|
- if (!canvases || !canvases.length) return;
|
|
|
|
|
-
|
|
|
|
|
- canvases.forEach(ctx => {
|
|
|
|
|
- const data = JSON.parse(ctx.dataset.data);
|
|
|
|
|
- const period = ctx.dataset.period;
|
|
|
|
|
-
|
|
|
|
|
- const gradient = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
|
|
|
|
|
- const gradientHover = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
|
|
|
|
|
- gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");
|
|
|
|
|
- gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
|
|
|
|
|
- gradientHover.addColorStop(0, "rgba(179, 157, 219, 0.9)");
|
|
|
|
|
- gradientHover.addColorStop(1, "rgba(179, 157, 219, 0.4)");
|
|
|
|
|
-
|
|
|
|
|
- new Chart(ctx, {
|
|
|
|
|
- type: "bar",
|
|
|
|
|
- data: {
|
|
|
|
|
- labels: data.map(d => beautifyOsName(d.name)),
|
|
|
|
|
- datasets: [{
|
|
|
|
|
- label: "Views",
|
|
|
|
|
- data: data.map(d => d.value),
|
|
|
|
|
- backgroundColor: gradient,
|
|
|
|
|
- borderColor: "rgba(179, 157, 219, 1)",
|
|
|
|
|
- borderWidth: 1,
|
|
|
|
|
- hoverBackgroundColor: gradientHover,
|
|
|
|
|
- hoverBorderWidth: 2
|
|
|
|
|
- }]
|
|
|
|
|
- },
|
|
|
|
|
- options: {
|
|
|
|
|
- indexAxis: "y",
|
|
|
|
|
- plugins: {
|
|
|
|
|
- legend: {
|
|
|
|
|
- display: false,
|
|
|
|
|
- },
|
|
|
|
|
- tooltip: {
|
|
|
|
|
- backgroundColor: "rgba(255, 255, 255, 0.95)",
|
|
|
|
|
- titleColor: "#333",
|
|
|
|
|
- titleFont: { weight: "normal", size: 15 },
|
|
|
|
|
- bodyFont: { weight: "normal", size: 16 },
|
|
|
|
|
- bodyColor: "rgb(179, 157, 219)",
|
|
|
|
|
- padding: 12,
|
|
|
|
|
- cornerRadius: 2,
|
|
|
|
|
- borderColor: "rgba(0, 0, 0, 0.1)",
|
|
|
|
|
- borderWidth: 1,
|
|
|
|
|
- displayColors: false,
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- responsive: true,
|
|
|
|
|
- interaction: {
|
|
|
|
|
- intersect: false,
|
|
|
|
|
- mode: "index",
|
|
|
|
|
- axis: "y"
|
|
|
|
|
- },
|
|
|
|
|
- scales: {
|
|
|
|
|
- x: {
|
|
|
|
|
- grace:"5%",
|
|
|
|
|
- beginAtZero: true,
|
|
|
|
|
- ticks: {
|
|
|
|
|
- maxTicksLimit: 6,
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // reset the display: block style that chart.js applies automatically
|
|
|
|
|
- ctx.style.display = "";
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// add data to the map
|
|
|
|
|
-function feedMapData(period) {
|
|
|
|
|
- const map = document.querySelector("svg.map");
|
|
|
|
|
- const paths = map.querySelectorAll("path");
|
|
|
|
|
- if (!map || !paths || !paths.length) return;
|
|
|
|
|
-
|
|
|
|
|
- let data = JSON.parse(map.dataset[period || "day"]);
|
|
|
|
|
- if (!data) return;
|
|
|
|
|
-
|
|
|
|
|
- let max = data.sort((a, b) => a.value > b.value ? -1 : 1)[0];
|
|
|
|
|
-
|
|
|
|
|
- if (!max) max = { value: 1 }
|
|
|
|
|
-
|
|
|
|
|
- data = data.reduce((a, c) => ({ ...a, [c.name]: c.value }), {});
|
|
|
|
|
-
|
|
|
|
|
- for (let i = 0; i < paths.length; ++i) {
|
|
|
|
|
- const id = paths[i].dataset.id;
|
|
|
|
|
- const views = data[id] || 0;
|
|
|
|
|
- paths[i].dataset.views = views;
|
|
|
|
|
- const colorLevel = Math.ceil((views / max.value) * 6);
|
|
|
|
|
- const classList = paths[i].classList;
|
|
|
|
|
- for (let j = 1; j < 7; j++) {
|
|
|
|
|
- paths[i].classList.remove(`color-${j}`);
|
|
|
|
|
- }
|
|
|
|
|
- paths[i].classList.add(`color-${colorLevel}`)
|
|
|
|
|
- paths[i].dataset.views = views;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// handle map tooltip hover
|
|
|
|
|
-function mapTooltipHoverOver() {
|
|
|
|
|
- const tooltip = document.querySelector("#map-tooltip");
|
|
|
|
|
- if (!tooltip) return;
|
|
|
|
|
- if (!event.target.dataset.id) return mapTooltipHoverOut();
|
|
|
|
|
- if (!tooltip.classList.contains("active")) {
|
|
|
|
|
- tooltip.classList.add("visible");
|
|
|
|
|
- }
|
|
|
|
|
- tooltip.dataset.tooltip = `${event.target.ariaLabel}: ${event.target.dataset.views || 0}`;
|
|
|
|
|
- const rect = event.target.getBoundingClientRect();
|
|
|
|
|
- tooltip.style.top = rect.top + (rect.height / 2) + "px";
|
|
|
|
|
- tooltip.style.left = rect.left + (rect.width / 2) + "px";
|
|
|
|
|
- event.target.classList.add("active");
|
|
|
|
|
-}
|
|
|
|
|
-function mapTooltipHoverOut() {
|
|
|
|
|
- const tooltip = document.querySelector("#map-tooltip");
|
|
|
|
|
- const map = document.querySelector("svg.map");
|
|
|
|
|
- const paths = map.querySelectorAll("path");
|
|
|
|
|
- if (!tooltip || !map) return;
|
|
|
|
|
- tooltip.classList.remove("visible");
|
|
|
|
|
- for (let i = 0; i < paths.length; ++i) {
|
|
|
|
|
- paths[i].classList.remove("active");
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// create stats charts
|
|
|
|
|
-function createCharts() {
|
|
|
|
|
- createViewsChart();
|
|
|
|
|
- createBrowsersChart();
|
|
|
|
|
- createReferrersChart();
|
|
|
|
|
- createOsChart();
|
|
|
|
|
- feedMapData();
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// change stats period for showing charts and data
|
|
|
|
|
-function changeStatsPeriod(event) {
|
|
|
|
|
- const period = event.target.dataset.period;
|
|
|
|
|
- if (!period) return;
|
|
|
|
|
- const canvases = document.querySelector("#stats").querySelectorAll("[data-period]");
|
|
|
|
|
- const buttons = document.querySelector("#stats").querySelectorAll(".nav");
|
|
|
|
|
- if (!buttons || !canvases) return;
|
|
|
|
|
- buttons.forEach(b => b.disabled = false);
|
|
|
|
|
- event.target.disabled = true;
|
|
|
|
|
- canvases.forEach(canvas => {
|
|
|
|
|
- if (canvas.dataset.period === period) {
|
|
|
|
|
- canvas.classList.remove("hidden");
|
|
|
|
|
- } else {
|
|
|
|
|
- canvas.classList.add("hidden");
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- feedMapData(period);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
// htmx prefetch extension
|
|
// htmx prefetch extension
|
|
|
// https://github.com/bigskysoftware/htmx-extensions/blob/main/src/preload/README.md
|
|
// https://github.com/bigskysoftware/htmx-extensions/blob/main/src/preload/README.md
|
|
|
htmx.defineExtension('preload', {
|
|
htmx.defineExtension('preload', {
|