|
| 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, '&') |
| 68 | + .replace(/</g, '<') |
| 69 | + .replace(/>/g, '>'); |
| 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 = '×'; |
| 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 | +})(); |
0 commit comments