Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions layouts/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

{{ partial "features.html" . }}

{{ partial "tip-of-the-week.html" . }}

{{ partial "download.html" . }}

{{ partial "testimonials.html" . }}
Expand Down
22 changes: 22 additions & 0 deletions layouts/partials/tip-of-the-week.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<section class="tip-of-the-week" aria-labelledby="tip-of-the-week-title">
<div class="container">
<div class="tip-of-the-week__card">
<div class="tip-of-the-week__icon" aria-hidden="true">
<i class="fas fa-lightbulb"></i>
</div>
<div class="tip-of-the-week__content">
<span class="tip-of-the-week__eyebrow">MISP tip of the week</span>
<h2 id="tip-of-the-week-title">A useful tip from the MISP community</h2>
<p id="tip-of-the-week-text" class="tip-of-the-week__text" aria-live="polite">Loading a tip&hellip;</p>
<a id="tip-of-the-week-link" class="tip-of-the-week__link" href="https://github.com/cudeso/misp-tip-of-the-week" target="_blank" rel="noopener noreferrer" hidden>
Learn more <i class="fas fa-arrow-right" aria-hidden="true"></i>
</a>
</div>
<button id="tip-of-the-week-refresh" class="tip-of-the-week__refresh" type="button" aria-label="Show another MISP tip" title="Show another tip" hidden>
<i class="fas fa-random" aria-hidden="true"></i>
<span>Another tip</span>
</button>
</div>
</div>
</section>
<script src="{{ "js/tip-of-the-week.js" | relURL }}" defer></script>
47 changes: 47 additions & 0 deletions static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
130 changes: 130 additions & 0 deletions static/js/tip-of-the-week.js
Original file line number Diff line number Diff line change
@@ -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" } })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard the Fetch API before invoking it

In browsers or embedded webviews without the Fetch API, this call throws a synchronous ReferenceError, so the promise .catch() is never reached and the widget remains stuck on “Loading a tip…” rather than displaying its graceful fallback. Feature-detect window.fetch and invoke the same fallback path when it is unavailable.

Useful? React with 👍 / 👎.

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