From 6927db79843c84c81112d45a3243d3a372f6bcfa Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Tue, 29 Sep 2026 15:10:58 +0200 Subject: [PATCH] Add random MISP tip of the week widget --- layouts/index.html | 2 + layouts/partials/tip-of-the-week.html | 22 +++++ static/css/custom.css | 47 ++++++++++ static/js/tip-of-the-week.js | 130 ++++++++++++++++++++++++++ 4 files changed, 201 insertions(+) create mode 100644 layouts/partials/tip-of-the-week.html create mode 100644 static/js/tip-of-the-week.js diff --git a/layouts/index.html b/layouts/index.html index 80a9fdc1..dd454241 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -18,6 +18,8 @@ {{ partial "features.html" . }} + {{ partial "tip-of-the-week.html" . }} + {{ partial "download.html" . }} {{ partial "testimonials.html" . }} diff --git a/layouts/partials/tip-of-the-week.html b/layouts/partials/tip-of-the-week.html new file mode 100644 index 00000000..b17d0e26 --- /dev/null +++ b/layouts/partials/tip-of-the-week.html @@ -0,0 +1,22 @@ +
+
+
+ +
+ MISP tip of the week +

A useful tip from the MISP community

+

Loading a tip…

+ +
+ +
+
+
+ diff --git a/static/css/custom.css b/static/css/custom.css index 65e268b3..52b1d9ca 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1354,6 +1354,53 @@ ul ul { } } +/* Tip of the week */ +.tip-of-the-week { + background: #f2f8fa; + padding: 58px 0; +} + +.tip-of-the-week__card { + align-items: flex-start; + background: #fff; + border: 1px solid #d5e7eb; + border-left: 5px solid var(--primary-accent, #00a3b8); + border-radius: var(--radius-md, 8px); + box-shadow: var(--shadow-sm, 0 4px 15px rgba(0, 0, 0, .08)); + display: flex; + gap: 24px; + padding: 32px 36px; +} + +.tip-of-the-week__icon { + align-items: center; + background: #fff4ce; + border-radius: 50%; + color: #a96e00; + display: flex; + flex: 0 0 54px; + font-size: 23px; + height: 54px; + justify-content: center; +} + +.tip-of-the-week__content { flex: 1 1 auto; min-width: 0; } +.tip-of-the-week__eyebrow { color: #087f98; display: block; font-size: 12px; font-weight: 800; letter-spacing: .12em; margin-bottom: 7px; text-transform: uppercase; } +.tip-of-the-week h2 { font-size: 24px; margin: 0 0 10px; } +.tip-of-the-week__text { color: #536972; font-size: 16px; line-height: 1.7; margin: 0; white-space: pre-line; } +.tip-of-the-week__link { display: inline-block; font-size: 13px; font-weight: 700; margin-top: 14px; } +.tip-of-the-week__link i { margin-left: 6px; } +.tip-of-the-week__refresh { background: transparent; border: 1px solid #c6dce1; border-radius: 20px; color: #087f98; flex: 0 0 auto; font-size: 13px; font-weight: 700; padding: 8px 14px; } +.tip-of-the-week__refresh:hover, .tip-of-the-week__refresh:focus { background: #e5f5f7; border-color: #8fcbd4; } +.tip-of-the-week__refresh i { margin-right: 6px; } + +@media (max-width: 767px) { + .tip-of-the-week { padding: 38px 0; } + .tip-of-the-week__card { flex-wrap: wrap; gap: 18px; padding: 26px 24px; } + .tip-of-the-week__content { flex-basis: calc(100% - 78px); } + .tip-of-the-week__refresh { margin-left: 78px; } +} + /* Feature catalogue */ .features-page { padding: 0 !important; diff --git a/static/js/tip-of-the-week.js b/static/js/tip-of-the-week.js new file mode 100644 index 00000000..fb90b555 --- /dev/null +++ b/static/js/tip-of-the-week.js @@ -0,0 +1,130 @@ +(function () { + "use strict"; + + var sourceUrl = "https://raw.githubusercontent.com/cudeso/misp-tip-of-the-week/main/misp-tip-of-the-week.json"; + var repositoryUrl = "https://github.com/cudeso/misp-tip-of-the-week"; + var titleElement = document.getElementById("tip-of-the-week-title"); + var textElement = document.getElementById("tip-of-the-week-text"); + var linkElement = document.getElementById("tip-of-the-week-link"); + var refreshButton = document.getElementById("tip-of-the-week-refresh"); + var tips = []; + var currentIndex = -1; + + if (!titleElement || !textElement || !linkElement || !refreshButton) { + return; + } + + function firstString(object, keys) { + for (var i = 0; i < keys.length; i += 1) { + if (typeof object[keys[i]] === "string" && object[keys[i]].trim()) { + return object[keys[i]].trim(); + } + } + return ""; + } + + function normalizeTip(tip) { + if (typeof tip === "string") { + return { title: "MISP tip of the week", text: tip, url: "" }; + } + + if (!tip || typeof tip !== "object") { + return null; + } + + var text = firstString(tip, ["tip", "description", "content", "text", "body"]); + if (!text) { + return null; + } + + return { + title: firstString(tip, ["title", "name", "subject"]) || "MISP tip of the week", + text: text, + url: firstString(tip, ["url", "link", "reference", "source"]) + }; + } + + function findEntries(data) { + if (Array.isArray(data)) { + return data; + } + + if (!data || typeof data !== "object") { + return []; + } + + var preferredEntries = data.tips || data.items || data.entries; + if (Array.isArray(preferredEntries)) { + return preferredEntries; + } + + var keys = Object.keys(data); + for (var i = 0; i < keys.length; i += 1) { + if (Array.isArray(data[keys[i]])) { + return data[keys[i]]; + } + } + + return []; + } + + function safeHttpUrl(value) { + if (!value) { + return ""; + } + + try { + var url = new URL(value, repositoryUrl); + return url.protocol === "https:" || url.protocol === "http:" ? url.href : ""; + } catch (error) { + return ""; + } + } + + function showRandomTip() { + var nextIndex; + + if (!tips.length) { + return; + } + + do { + nextIndex = Math.floor(Math.random() * tips.length); + } while (tips.length > 1 && nextIndex === currentIndex); + + currentIndex = nextIndex; + titleElement.textContent = tips[currentIndex].title; + textElement.textContent = tips[currentIndex].text; + + var tipUrl = safeHttpUrl(tips[currentIndex].url); + linkElement.href = tipUrl || repositoryUrl; + linkElement.hidden = !tipUrl; + } + + refreshButton.addEventListener("click", showRandomTip); + + fetch(sourceUrl, { headers: { "Accept": "application/json" } }) + .then(function (response) { + if (!response.ok) { + throw new Error("Tip request failed with status " + response.status); + } + return response.json(); + }) + .then(function (data) { + tips = findEntries(data).map(normalizeTip).filter(Boolean); + + if (!tips.length) { + throw new Error("The tip feed did not contain any supported tips"); + } + + showRandomTip(); + refreshButton.hidden = tips.length < 2; + }) + .catch(function () { + titleElement.textContent = "MISP tip of the week"; + textElement.textContent = "The community tip could not be loaded right now."; + linkElement.href = repositoryUrl; + linkElement.textContent = "Browse all tips"; + linkElement.hidden = false; + }); +}());