Skip to content

Commit 569ae30

Browse files
committed
news banner
1 parent edf58b0 commit 569ae30

File tree

3 files changed

+303
-0
lines changed

3 files changed

+303
-0
lines changed

public/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
1313
<script defer src="/scripts/gh-pages-spa.js"></script>
1414
<script src="/scripts/stars-tag.js"></script>
15+
<script
16+
src="/scripts/banner.js"
17+
data-contentful-space="9b7cw6d22w7j"
18+
data-contentful-env="master"
19+
data-contentful-access-token="VLOgnMG0y_TvCERWx3PnfBiWEl-NeYAwaVdMk9LPf-g">
20+
</script>
1521
</head>
1622
<body>
1723
<noscript>

public/scripts/banner.js

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
(function () {
2+
'use strict';
3+
4+
// ---- Configuration via script tag data-attributes ----
5+
var currentScript = document.currentScript || (function () {
6+
var scripts = document.getElementsByTagName('script');
7+
return scripts[scripts.length - 1];
8+
})();
9+
10+
if (!currentScript) {
11+
console.warn('[NewsBanner] Could not find current <script> element.');
12+
return;
13+
}
14+
15+
var dataset = currentScript.dataset || {};
16+
var SPACE_ID = dataset.contentfulSpace;
17+
var ENV_ID = dataset.contentfulEnv || 'master';
18+
var ACCESS_TOKEN = dataset.contentfulAccessToken;
19+
20+
if (!SPACE_ID || !ACCESS_TOKEN) {
21+
console.warn('[NewsBanner] Missing Contentful config.');
22+
return;
23+
}
24+
25+
var STORAGE_KEY = 'newsBannerState';
26+
var ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
27+
28+
// ---------------------------------------------------------
29+
// LocalStorage helpers
30+
// ---------------------------------------------------------
31+
function getStoredState() {
32+
try {
33+
var raw = localStorage.getItem(STORAGE_KEY);
34+
return raw ? JSON.parse(raw) : null;
35+
} catch (_) {
36+
return null;
37+
}
38+
}
39+
40+
function saveState(state) {
41+
try {
42+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
43+
} catch (_) {}
44+
}
45+
46+
function shouldShowBanner(contentSignature) {
47+
var state = getStoredState();
48+
if (!state) return true;
49+
50+
var lastShown = new Date(state.lastShownAt).getTime();
51+
if (isNaN(lastShown)) return true;
52+
53+
// Content changed → show again
54+
if (state.contentSignature !== contentSignature) return true;
55+
56+
// Over one week → show again
57+
if (Date.now() - lastShown > ONE_WEEK_MS) return true;
58+
59+
return false;
60+
}
61+
62+
// ---------------------------------------------------------
63+
// Markdown parser — safe subset (bold, italics, links)
64+
// ---------------------------------------------------------
65+
function escapeHtml(str) {
66+
return str
67+
.replace(/&/g, '&amp;')
68+
.replace(/</g, '&lt;')
69+
.replace(/>/g, '&gt;');
70+
}
71+
72+
function markdownToHtml(md) {
73+
if (!md) return '';
74+
75+
var html = escapeHtml(md);
76+
77+
// Links: [text](url)
78+
html = html.replace(
79+
/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
80+
'<a href="$2" target="_blank" rel="noopener noreferrer" style="color:#00c0b5;text-decoration:underline;">$1</a>'
81+
);
82+
83+
// Bold
84+
html = html.replace(/(\*\*|__)(.+?)\1/g, '<strong>$2</strong>');
85+
86+
// Italic
87+
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
88+
89+
// Newlines → <br>
90+
html = html.replace(/\n{2,}/g, '<br><br>');
91+
html = html.replace(/\n/g, '<br>');
92+
93+
return html;
94+
}
95+
96+
// ---------------------------------------------------------
97+
// Create Banner Element (styled for RF / RoboCon)
98+
// ---------------------------------------------------------
99+
function createBanner(messageHtml, onClose) {
100+
var el = document.createElement('div');
101+
el.setAttribute('role', 'status');
102+
el.setAttribute('aria-live', 'polite');
103+
el.setAttribute('aria-label', 'News announcement');
104+
105+
// Outer positioning
106+
el.style.position = 'fixed';
107+
el.style.left = '16px';
108+
el.style.right = '16px';
109+
el.style.bottom = '16px';
110+
el.style.zIndex = '9999';
111+
el.style.display = 'flex';
112+
el.style.justifyContent = 'center';
113+
el.style.pointerEvents = 'none'; // outer wrapper, inner content handles clicks
114+
115+
var inner = document.createElement('div');
116+
inner.style.pointerEvents = 'auto';
117+
118+
// Inner box
119+
inner.style.display = 'flex';
120+
inner.style.alignItems = 'flex-start';
121+
inner.style.gap = '12px';
122+
inner.style.width = '100%';
123+
inner.style.maxWidth = '960px';
124+
125+
inner.style.padding = '14px 18px';
126+
inner.style.boxSizing = 'border-box';
127+
128+
// Dark, slightly transparent background so it sits on top of both light & dark sections
129+
inner.style.background = 'rgba(15,23,42,0.92)'; // slate-900-ish
130+
inner.style.border = '1px solid #00c0b5'; // slate-400-ish
131+
inner.style.color = '#E5E7EB'; // slate-200
132+
inner.style.boxShadow = '0 18px 45px rgba(0,0,0,0.55)';
133+
134+
inner.style.fontFamily = 'system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif';
135+
inner.style.fontSize = '1rem';
136+
inner.style.lineHeight = '1.5';
137+
138+
// Slide-in transition
139+
inner.style.transform = 'translateY(120%)';
140+
inner.style.opacity = '0';
141+
inner.style.transition = 'transform .3s ease-out, opacity .3s ease-out';
142+
143+
// Text
144+
var text = document.createElement('div');
145+
text.innerHTML = messageHtml;
146+
text.style.flex = '1 1 auto';
147+
text.style.marginRight = '4px';
148+
text.style.maxHeight = '6.5rem';
149+
text.style.overflowY = 'auto';
150+
151+
// Close button
152+
var btn = document.createElement('button');
153+
btn.type = 'button';
154+
btn.setAttribute('aria-label', 'Close announcement');
155+
btn.innerHTML = '&times;';
156+
btn.style.flex = '0 0 auto';
157+
btn.style.fontSize = '24px';
158+
btn.style.border = 'none';
159+
btn.style.background = 'transparent';
160+
btn.style.color = '#9CA3AF';
161+
btn.style.cursor = 'pointer';
162+
btn.style.lineHeight = '1';
163+
btn.style.alignSelf = 'center';
164+
165+
btn.onmouseenter = function () {
166+
btn.style.color = '#F9FAFB';
167+
};
168+
btn.onmouseleave = function () {
169+
btn.style.color = '#9CA3AF';
170+
};
171+
172+
btn.onclick = function () {
173+
if (onClose) onClose();
174+
175+
inner.style.transform = 'translateY(120%)';
176+
inner.style.opacity = '0';
177+
178+
inner.addEventListener('transitionend', function () {
179+
el.remove();
180+
}, { once: true });
181+
182+
setTimeout(function () {
183+
if (el.parentNode) el.parentNode.removeChild(el);
184+
}, 500);
185+
};
186+
187+
inner.appendChild(text);
188+
inner.appendChild(btn);
189+
el.appendChild(inner);
190+
191+
// Mobile tweaks (edge-to-edge bar at bottom)
192+
function applyResponsive() {
193+
var isMobile = window.matchMedia && window.matchMedia('(max-width: 640px)').matches;
194+
if (isMobile) {
195+
el.style.left = '0';
196+
el.style.right = '0';
197+
el.style.bottom = '0';
198+
inner.style.borderRadius = '0';
199+
inner.style.maxWidth = '100%';
200+
inner.style.boxShadow = '0 -8px 25px rgba(0,0,0,0.65)';
201+
} else {
202+
el.style.left = '16px';
203+
el.style.right = '16px';
204+
el.style.bottom = '16px';
205+
inner.style.maxWidth = '960px';
206+
inner.style.boxShadow = '0 18px 45px rgba(0,0,0,0.55)';
207+
}
208+
}
209+
210+
applyResponsive();
211+
if (window.matchMedia) {
212+
window.matchMedia('(max-width: 640px)').addEventListener('change', applyResponsive);
213+
} else {
214+
window.addEventListener('resize', applyResponsive);
215+
}
216+
217+
// Trigger slide-in
218+
inner.__show = function () {
219+
requestAnimationFrame(function () {
220+
requestAnimationFrame(function () {
221+
inner.style.transform = 'translateY(0)';
222+
inner.style.opacity = '1';
223+
});
224+
});
225+
};
226+
227+
// store reference for later
228+
el.__inner = inner;
229+
return el;
230+
}
231+
232+
// ---------------------------------------------------------
233+
// Rendering
234+
// ---------------------------------------------------------
235+
function renderBanner(text, contentSignature) {
236+
if (!shouldShowBanner(contentSignature)) return;
237+
238+
var html = markdownToHtml(text);
239+
var el = createBanner(html, function () {
240+
saveState({
241+
lastShownAt: new Date().toISOString(),
242+
contentSignature: contentSignature
243+
});
244+
});
245+
246+
var append = function () {
247+
document.body.appendChild(el);
248+
if (el.__inner && typeof el.__inner.__show === 'function') {
249+
el.__inner.__show();
250+
}
251+
};
252+
253+
if (document.readyState === 'loading') {
254+
document.addEventListener('DOMContentLoaded', append);
255+
} else {
256+
append();
257+
}
258+
}
259+
260+
// ---------------------------------------------------------
261+
// Fetch from Contentful
262+
// ---------------------------------------------------------
263+
var url =
264+
'https://cdn.contentful.com/spaces/' + SPACE_ID +
265+
'/environments/' + ENV_ID +
266+
'/entries?content_type=news-banner&limit=1&order=-sys.updatedAt';
267+
268+
fetch(url, { headers: { Authorization: 'Bearer ' + ACCESS_TOKEN } })
269+
.then(function (res) { return res.json(); })
270+
.then(function (data) {
271+
if (!data.items || !data.items.length) return;
272+
273+
var entry = data.items[0];
274+
var fields = entry.fields || {};
275+
var textField = fields.text;
276+
277+
var locale =
278+
typeof textField === 'string'
279+
? null
280+
: (textField ? Object.keys(textField)[0] : null);
281+
282+
var text =
283+
typeof textField === 'string'
284+
? textField
285+
: (textField && textField[locale]) || '';
286+
287+
if (!text) return;
288+
289+
var signature = text;
290+
renderBanner(text, signature);
291+
})
292+
.catch(function (err) {
293+
console.warn('[NewsBanner] Failed to load:', err);
294+
});
295+
296+
})();

src/components/NewsBanner.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
22
<div style="background-color: black;">
33
<div
4+
v-if="$t('newsBanner')"
45
class="banner font-code type-center p-2xsmall border-bottom-white border-thin"
56
v-html="$t('newsBanner')" />
67
</div>

0 commit comments

Comments
 (0)