| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656 |
- // log htmx on dev
- // htmx.logAll();
- // add text/html accept header to receive html instead of json for the requests
- document.body.addEventListener('htmx:configRequest', function(evt) {
- evt.detail.headers["Accept"] = "text/html,*/*";
- });
- // redirect to homepage
- document.body.addEventListener("redirectToHomepage", function() {
- setTimeout(() => {
- window.location.replace("/");
- }, 1500);
- });
- // reset form if event is sent from the backend
- function resetForm(id) {
- return function() {
- const form = document.getElementById(id);
- if (!form) return;
- form.reset();
- }
- }
- document.body.addEventListener('resetChangePasswordForm', resetForm("change-password"));
- document.body.addEventListener('resetChangeEmailForm', resetForm("change-email"));
- // an htmx extension to use the specifed params in the path instead of the query or body
- htmx.defineExtension("path-params", {
- onEvent: function(name, evt) {
- if (name === "htmx:configRequest") {
- evt.detail.path = evt.detail.path.replace(/{([^}]+)}/g, function(_, param) {
- var val = evt.detail.parameters[param]
- delete evt.detail.parameters[param]
- return val === undefined ? '{' + param + '}' : encodeURIComponent(val)
- })
- }
- }
- })
- // find closest element
- function closest(selector, elm) {
- let element = elm || this;
- while (element && element.nodeType === 1) {
- if (element.matches(selector)) {
- return element;
- }
- element = element.parentNode;
- }
- return null;
- };
- // get url query param
- function getQueryParams() {
- const search = window.location.search.replace("?", "");
- const query = {};
- search.split("&").map(q => {
- const keyvalue = q.split("=");
- query[keyvalue[0]] = keyvalue[1];
- });
- return query;
- }
- // trim text
- function trimText(selector, length) {
- const element = document.querySelector(selector);
- if (!element) return;
- let text = element.textContent;
- if (typeof text !== "string") return;
- text = text.trim();
- if (text.length > length) {
- element.textContent = text.split("").slice(0, length).join("") + "...";
- }
- }
- function formatDateHour(selector) {
- const element = document.querySelector(selector);
- if (!element) return;
- const dateString = element.dataset.date;
- if (!dateString) return;
- const date = new Date(dateString);
- element.textContent = date.getHours() + ":" + date.getMinutes();
- }
- // show QR code
- function handleQRCode(element) {
- const dialog = document.querySelector("#link-dialog");
- const dialogContent = dialog.querySelector(".content-wrapper");
- if (!dialogContent) return;
- openDialog("link-dialog", "qrcode");
- dialogContent.textContent = "";
- const qrcode = new QRCode(dialogContent, {
- text: element.dataset.url,
- width: 200,
- height: 200,
- colorDark : "#000000",
- colorLight : "#ffffff",
- correctLevel : QRCode.CorrectLevel.H
- });
- }
- // copy the link to clipboard
- function handleCopyLink(element) {
- navigator.clipboard.writeText(element.dataset.url);
- }
- // copy the link and toggle copy button style
- function handleShortURLCopyLink(element) {
- handleCopyLink(element);
- const clipboard = element.parentNode.querySelector(".clipboard") || closest(".clipboard", element);
- if (!clipboard || clipboard.classList.contains("copied")) return;
- clipboard.classList.add("copied");
- setTimeout(function() {
- clipboard.classList.remove("copied");
- }, 1000);
- }
- // TODO: make it an extension
- // open and close dialog
- function openDialog(id, name) {
- const dialog = document.getElementById(id);
- if (!dialog) return;
- dialog.classList.add("open");
- if (name) {
- dialog.classList.add(name);
- }
- }
- function closeDialog() {
- const dialog = document.querySelector(".dialog");
- if (!dialog) return;
- while (dialog.classList.length > 0) {
- dialog.classList.remove(dialog.classList[0]);
- }
- dialog.classList.add("dialog");
- }
- window.addEventListener("click", function(event) {
- const dialog = document.querySelector(".dialog");
- if (dialog && event.target === dialog) {
- closeDialog();
- }
- });
- // handle navigation in the table of links
- function setLinksLimit(event) {
- const buttons = Array.from(document.querySelectorAll('table .nav .limit button'));
- const limitInput = document.querySelector('#limit');
- if (!limitInput || !buttons || !buttons.length) return;
- limitInput.value = event.target.textContent;
- buttons.forEach(b => {
- b.disabled = b.textContent === event.target.textContent;
- });
- }
- function setLinksSkip(event, action) {
- const buttons = Array.from(document.querySelectorAll('table .nav .pagination button'));
- const limitElm = document.querySelector('#limit');
- const totalElm = document.querySelector('#total');
- const skipElm = document.querySelector('#skip');
- if (!buttons || !limitElm || !totalElm || !skipElm) return;
- const skip = parseInt(skipElm.value);
- const limit = parseInt(limitElm.value);
- const total = parseInt(totalElm.value);
- skipElm.value = action === "next" ? skip + limit : Math.max(skip - limit, 0);
- document.querySelectorAll('.pagination .next').forEach(elm => {
- elm.disabled = total <= parseInt(skipElm.value) + limit;
- });
- document.querySelectorAll('.pagination .prev').forEach(elm => {
- elm.disabled = parseInt(skipElm.value) <= 0;
- });
- }
- function updateLinksNav() {
- const totalElm = document.querySelector('#total');
- const skipElm = document.querySelector('#skip');
- const limitElm = document.querySelector('#limit');
- if (!totalElm || !skipElm || !limitElm) return;
- const total = parseInt(totalElm.value);
- const skip = parseInt(skipElm.value);
- const limit = parseInt(limitElm.value);
- document.querySelectorAll('.pagination .next').forEach(elm => {
- elm.disabled = total <= skip + limit;
- });
- document.querySelectorAll('.pagination .prev').forEach(elm => {
- elm.disabled = skip <= 0;
- });
- }
- function resetLinkNav() {
- const totalElm = document.querySelector('#total');
- const skipElm = document.querySelector('#skip');
- const limitElm = document.querySelector('#limit');
- if (!totalElm || !skipElm || !limitElm) return;
- skipElm.value = 0;
- limitElm.value = 10;
- const skip = parseInt(skipElm.value);
- const limit = parseInt(limitElm.value);
- document.querySelectorAll('.pagination .next').forEach(elm => {
- elm.disabled = total <= skip + limit;
- });
- document.querySelectorAll('.pagination .prev').forEach(elm => {
- elm.disabled = skip <= 0;
- });
- document.querySelectorAll('table .nav .limit button').forEach(b => {
- b.disabled = b.textContent === limit.toString();
- });
- }
- // 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);
- }
|