Atom feed for our EEG site
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Atomic EEG</title> 7 <link rel="preconnect" href="https://fonts.googleapis.com"> 8 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9 <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> 10 <style> 11 :root { 12 --bg-color: #0f1419; 13 --bg-alt-color: #171e24; 14 --text-color: #e0e7ec; 15 --text-muted: #8a9ba9; 16 --accent-color: #4dfa7b; 17 --accent-shadow: rgba(77, 250, 123, 0.3); 18 --accent-alt: #0fe0e0; 19 --border-color: #2c3840; 20 --card-bg: #1a2127; 21 --header-height: 50px; 22 } 23 24 * { 25 margin: 0; 26 padding: 0; 27 box-sizing: border-box; 28 } 29 30 body { 31 font-family: 'Roboto', sans-serif; 32 background-color: var(--bg-color); 33 color: var(--text-color); 34 line-height: 1.5; 35 overflow-x: hidden; 36 } 37 38 header { 39 position: fixed; 40 top: 0; 41 width: 100%; 42 height: var(--header-height); 43 background-color: var(--bg-alt-color); 44 border-bottom: 1px solid var(--border-color); 45 display: flex; 46 align-items: center; 47 padding: 0 20px; 48 z-index: 100; 49 } 50 51 .header-container { 52 display: flex; 53 justify-content: space-between; 54 align-items: center; 55 width: 100%; 56 max-width: 1200px; 57 margin: 0 auto; 58 } 59 60 .logo { 61 font-family: 'JetBrains Mono', monospace; 62 font-weight: 600; 63 font-size: 1.3rem; 64 color: var(--accent-color); 65 text-shadow: 0 0 10px var(--accent-shadow); 66 } 67 68 .logo span { 69 color: var(--accent-alt); 70 } 71 72 .info-panel { 73 font-family: 'JetBrains Mono', monospace; 74 font-size: 0.8rem; 75 color: var(--text-muted); 76 } 77 78 main { 79 margin-top: var(--header-height); 80 min-height: calc(100vh - var(--header-height)); 81 display: flex; 82 justify-content: center; 83 padding: 15px 20px; 84 } 85 86 .content { 87 width: 100%; 88 max-width: 1200px; 89 } 90 91 .feed-item { 92 background-color: var(--card-bg); 93 border: 1px solid var(--border-color); 94 border-radius: 4px; 95 margin-bottom: 8px; 96 overflow: hidden; 97 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 98 transition: background-color 0.2s ease; 99 } 100 101 .feed-item:hover { 102 background-color: #202930; 103 } 104 105 .feed-item-row { 106 display: flex; 107 align-items: center; 108 padding: 8px 15px; 109 width: 100%; 110 overflow: hidden; 111 } 112 113 .feed-item-date { 114 font-family: 'JetBrains Mono', monospace; 115 font-size: 0.75rem; 116 color: var(--text-muted); 117 min-width: 80px; 118 margin-right: 10px; 119 } 120 121 .feed-item-author { 122 font-family: 'JetBrains Mono', monospace; 123 color: var(--accent-alt); 124 font-size: 0.85rem; 125 min-width: 70px; 126 margin-right: 15px; 127 white-space: nowrap; 128 } 129 130 .feed-item-title { 131 flex: 1; 132 font-size: 0.95rem; 133 font-weight: 400; 134 white-space: nowrap; 135 overflow: hidden; 136 text-overflow: ellipsis; 137 margin-right: 15px; 138 } 139 140 .feed-item-title a { 141 color: var(--text-color); 142 text-decoration: none; 143 transition: color 0.2s ease; 144 } 145 146 .feed-item-title a:hover { 147 color: var(--accent-color); 148 } 149 150 .feed-item-preview { 151 flex: 2; 152 white-space: nowrap; 153 overflow: hidden; 154 text-overflow: ellipsis; 155 color: var(--text-muted); 156 font-size: 0.85rem; 157 margin-right: 15px; 158 } 159 160 .feed-item-actions { 161 display: flex; 162 align-items: center; 163 gap: 10px; 164 margin-left: auto; 165 } 166 167 .feed-item-content { 168 padding: 15px; 169 line-height: 1.6; 170 display: none; 171 border-top: 1px solid var(--border-color); 172 background-color: #1d252c; 173 } 174 175 .feed-item-content img { 176 max-width: 100%; 177 height: auto; 178 border-radius: 4px; 179 margin: 10px 0; 180 } 181 182 .feed-item-content pre, .feed-item-content code { 183 font-family: 'JetBrains Mono', monospace; 184 background-color: #1d272e; 185 border-radius: 4px; 186 padding: 0.2em 0.4em; 187 font-size: 0.9em; 188 } 189 190 .feed-item-content pre { 191 padding: 12px; 192 overflow-x: auto; 193 margin: 12px 0; 194 } 195 196 .feed-item-content blockquote { 197 border-left: 3px solid var(--accent-color); 198 padding-left: 12px; 199 margin-left: 0; 200 color: var(--text-muted); 201 } 202 203 .read-more-btn { 204 background-color: transparent; 205 border: none; 206 color: var(--accent-color); 207 cursor: pointer; 208 font-family: 'JetBrains Mono', monospace; 209 font-size: 0.8rem; 210 padding: 2px 5px; 211 border-radius: 3px; 212 transition: all 0.2s ease; 213 display: inline-flex; 214 align-items: center; 215 } 216 217 .read-more-btn:hover { 218 background-color: rgba(77, 250, 123, 0.1); 219 } 220 221 .external-link { 222 color: var(--text-muted); 223 font-size: 0.8rem; 224 display: inline-flex; 225 align-items: center; 226 text-decoration: none; 227 } 228 229 .external-link:hover { 230 color: var(--accent-alt); 231 } 232 233 .icon { 234 font-size: 0.9rem; 235 margin-left: 3px; 236 } 237 238 #loading { 239 display: flex; 240 flex-direction: column; 241 align-items: center; 242 justify-content: center; 243 min-height: 200px; 244 } 245 246 .loading-spinner { 247 border: 3px solid rgba(77, 250, 123, 0.1); 248 border-top: 3px solid var(--accent-color); 249 border-radius: 50%; 250 width: 30px; 251 height: 30px; 252 animation: spin 1s linear infinite; 253 margin-bottom: 12px; 254 } 255 256 @keyframes spin { 257 0% { transform: rotate(0deg); } 258 100% { transform: rotate(360deg); } 259 } 260 261 .loading-text { 262 font-family: 'JetBrains Mono', monospace; 263 color: var(--accent-color); 264 font-size: 0.9rem; 265 } 266 267 .error-message { 268 color: #ff4d4d; 269 text-align: center; 270 padding: 20px; 271 font-family: 'JetBrains Mono', monospace; 272 } 273 274 @media (max-width: 900px) { 275 .feed-item-preview { 276 display: none; 277 } 278 } 279 280 @media (max-width: 600px) { 281 .feed-item-author { 282 min-width: 50px; 283 margin-right: 10px; 284 } 285 286 .feed-item-date { 287 min-width: 60px; 288 margin-right: 10px; 289 } 290 } 291 </style> 292</head> 293<body> 294 <header> 295 <div class="header-container"> 296 <div class="logo">Atomic<span>EEG</span></div> 297 <div class="info-panel"> 298 <span id="entry-count">0</span> entries | <span id="source-count">0</span> sources 299 </div> 300 </div> 301 </header> 302 303 <main> 304 <section class="content"> 305 <div id="loading"> 306 <div class="loading-spinner"></div> 307 <p class="loading-text">Decoding Signal...</p> 308 </div> 309 <div id="feed-items"></div> 310 </section> 311 </main> 312 313 <script> 314 document.addEventListener('DOMContentLoaded', async () => { 315 const feedItemsContainer = document.getElementById('feed-items'); 316 const loadingContainer = document.getElementById('loading'); 317 const entryCountElement = document.getElementById('entry-count'); 318 const sourceCountElement = document.getElementById('source-count'); 319 320 // Function to format date (only date, no time) 321 function formatDate(dateString) { 322 const date = new Date(dateString); 323 return date.toLocaleDateString('en-US', { 324 year: 'numeric', 325 month: 'short', 326 day: 'numeric' 327 }); 328 } 329 330 // Function to get first line of text 331 function getFirstLine(html) { 332 // Create a temporary div to parse HTML 333 const tempDiv = document.createElement('div'); 334 tempDiv.innerHTML = html; 335 336 // Extract text content 337 const text = tempDiv.textContent || ''; 338 339 // Get first line (up to first period or newline, whichever comes first) 340 const firstPeriod = text.indexOf('.'); 341 const firstNewline = text.indexOf('\n'); 342 343 let endIndex; 344 if (firstPeriod !== -1 && firstNewline !== -1) { 345 endIndex = Math.min(firstPeriod, firstNewline); 346 } else if (firstPeriod !== -1) { 347 endIndex = firstPeriod; 348 } else if (firstNewline !== -1) { 349 endIndex = firstNewline; 350 } else { 351 // If no period or newline, take first 80 chars 352 endIndex = Math.min(text.length, 80); 353 } 354 355 return text.substring(0, endIndex + 1).trim(); 356 } 357 358 // Function to toggle content visibility 359 function toggleContent(articleId) { 360 const article = document.getElementById(articleId); 361 const content = article.querySelector('.feed-item-content'); 362 const button = article.querySelector('.read-more-btn'); 363 364 if (content.style.display === 'block') { 365 content.style.display = 'none'; 366 button.innerHTML = 'More <span class="icon">↓</span>'; 367 } else { 368 content.style.display = 'block'; 369 button.innerHTML = 'Less <span class="icon">↑</span>'; 370 } 371 } 372 373 try { 374 // Fetch the Atom feed 375 const response = await fetch('eeg.xml'); 376 if (!response.ok) { 377 throw new Error('Failed to fetch feed'); 378 } 379 380 const xmlText = await response.text(); 381 const parser = new DOMParser(); 382 const xmlDoc = parser.parseFromString(xmlText, 'text/xml'); 383 384 // Process feed entries 385 const entries = xmlDoc.getElementsByTagName('entry'); 386 const sources = new Set(); 387 388 // Update counter 389 entryCountElement.textContent = entries.length; 390 391 // Create entries HTML 392 let entriesHTML = ''; 393 394 for (let i = 0; i < entries.length; i++) { 395 const entry = entries[i]; 396 const articleId = `article-${i}`; 397 398 // Extract entry data 399 const title = entry.getElementsByTagName('title')[0]?.textContent || 'No Title'; 400 const link = entry.getElementsByTagName('link')[0]?.getAttribute('href') || '#'; 401 const contentElement = entry.getElementsByTagName('summary')[0] || entry.getElementsByTagName('content')[0]; 402 const contentText = contentElement?.textContent || ''; 403 const contentType = contentElement?.getAttribute('type') || 'text'; 404 const published = entry.getElementsByTagName('published')[0]?.textContent || 405 entry.getElementsByTagName('updated')[0]?.textContent || ''; 406 const author = entry.getElementsByTagName('author')[0]?.getElementsByTagName('name')[0]?.textContent || 'Unknown'; 407 const categories = entry.getElementsByTagName('category'); 408 409 // Extract source from category (we're using category to store source name) 410 let source = 'Unknown Source'; 411 if (categories.length > 0) { 412 source = categories[0].getAttribute('term'); 413 sources.add(source); 414 } 415 416 // Properly handle the content based on content type 417 let contentHtml; 418 if (contentType === 'html' || contentType === 'text/html') { 419 // For HTML content, create a div and set innerHTML 420 contentHtml = contentText; 421 } else { 422 // For text content, escape it and preserve newlines 423 contentHtml = contentText 424 .replace(/&/g, '&amp;') 425 .replace(/</g, '&lt;') 426 .replace(/>/g, '&gt;') 427 .replace(/\n/g, '<br>'); 428 } 429 430 // Get the first line for preview 431 const firstLine = getFirstLine(contentHtml); 432 433 // Format the entry HTML - single line layout 434 entriesHTML += ` 435 <article id="${articleId}" class="feed-item"> 436 <div class="feed-item-row"> 437 <div class="feed-item-date">${formatDate(published)}</div> 438 <div class="feed-item-author">${author}</div> 439 <div class="feed-item-title"><a href="${link}" target="_blank">${title}</a></div> 440 <div class="feed-item-preview">${firstLine}</div> 441 <div class="feed-item-actions"> 442 <button class="read-more-btn" onclick="toggleContent('${articleId}')">More <span class="icon">↓</span></button> 443 <a href="${link}" target="_blank" class="external-link">Link <span class="icon">↗</span></a> 444 </div> 445 </div> 446 <div class="feed-item-content">${contentHtml}</div> 447 </article> 448 `; 449 } 450 451 // Update sources count 452 sourceCountElement.textContent = sources.size; 453 454 // Add the toggleContent function to the global scope 455 window.toggleContent = toggleContent; 456 457 // Hide loading, show content 458 loadingContainer.style.display = 'none'; 459 feedItemsContainer.innerHTML = entriesHTML; 460 461 } catch (error) { 462 console.error('Error loading feed:', error); 463 loadingContainer.style.display = 'none'; 464 feedItemsContainer.innerHTML = ` 465 <div class="error-message"> 466 <h3>Error Loading Feed</h3> 467 <p>${error.message}</p> 468 </div> 469 `; 470 } 471 }); 472 </script> 473</body> 474</html>