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: #0a170f; 13 --bg-alt-color: #132018; 14 --text-color: #e0ece5; 15 --text-muted: #8aa99a; 16 --accent-color: #4dfa7b; 17 --accent-shadow: rgba(77, 250, 123, 0.3); 18 --accent-alt: #4db380; 19 --border-color: #2c4035; 20 --card-bg: #152720; 21 --header-height: 50px; 22 --sidebar-width: 80px; 23 } 24 25 * { 26 margin: 0; 27 padding: 0; 28 box-sizing: border-box; 29 } 30 31 body { 32 font-family: 'Roboto', sans-serif; 33 background-color: var(--bg-color); 34 color: var(--text-color); 35 line-height: 1.5; 36 overflow-x: hidden; 37 } 38 39 header { 40 position: fixed; 41 top: 0; 42 width: 100%; 43 height: var(--header-height); 44 background-color: var(--bg-alt-color); 45 border-bottom: 1px solid var(--border-color); 46 display: flex; 47 align-items: center; 48 padding: 0 20px; 49 z-index: 100; 50 } 51 52 .header-container { 53 display: flex; 54 justify-content: space-between; 55 align-items: center; 56 width: 100%; 57 max-width: 1200px; 58 margin: 0 auto; 59 } 60 61 .logo { 62 font-family: 'JetBrains Mono', monospace; 63 font-weight: 600; 64 font-size: 1.3rem; 65 color: var(--accent-color); 66 text-shadow: 0 0 10px var(--accent-shadow); 67 } 68 69 .logo span { 70 color: var(--accent-alt); 71 } 72 73 .info-panel { 74 font-family: 'JetBrains Mono', monospace; 75 font-size: 0.8rem; 76 color: var(--text-muted); 77 } 78 79 main { 80 margin-top: var(--header-height); 81 min-height: calc(100vh - var(--header-height)); 82 display: flex; 83 position: relative; 84 padding: 15px 20px; 85 } 86 87 .content { 88 width: 100%; 89 max-width: 1200px; 90 margin: 0 auto; 91 padding-right: var(--sidebar-width); 92 } 93 94 .timeline-sidebar { 95 position: fixed; 96 top: var(--header-height); 97 right: 0; 98 width: var(--sidebar-width); 99 height: calc(100vh - var(--header-height)); 100 background-color: var(--bg-alt-color); 101 border-left: 1px solid var(--border-color); 102 display: flex; 103 flex-direction: column; 104 overflow-y: auto; 105 padding: 15px 0; 106 z-index: 50; 107 scrollbar-width: none; /* For Firefox */ 108 } 109 110 .timeline-sidebar::-webkit-scrollbar { 111 display: none; /* For Chrome/Safari/Edge */ 112 } 113 114 .timeline-year { 115 padding: 5px 0; 116 text-align: center; 117 color: var(--text-muted); 118 font-size: 0.8rem; 119 font-family: 'JetBrains Mono', monospace; 120 position: relative; 121 } 122 123 .timeline-month { 124 padding: 3px 0; 125 text-align: center; 126 color: var(--text-muted); 127 font-size: 0.7rem; 128 opacity: 0.8; 129 position: relative; 130 } 131 132 .timeline-year::before, 133 .timeline-month::before { 134 content: ''; 135 position: absolute; 136 left: 20px; 137 top: 50%; 138 width: 7px; 139 height: 1px; 140 background-color: var(--border-color); 141 } 142 143 .timeline-year::after { 144 content: ''; 145 position: absolute; 146 left: 15px; 147 top: 50%; 148 transform: translateY(-50%); 149 width: 4px; 150 height: 4px; 151 border-radius: 50%; 152 background-color: var(--accent-color); 153 } 154 155 .timeline-month::after { 156 content: ''; 157 position: absolute; 158 left: 16px; 159 top: 50%; 160 transform: translateY(-50%); 161 width: 2px; 162 height: 2px; 163 border-radius: 50%; 164 background-color: var(--accent-alt); 165 } 166 167 .timeline-year.active { 168 color: var(--accent-color); 169 font-weight: 600; 170 } 171 172 .timeline-month.active { 173 color: var(--accent-alt); 174 font-weight: 600; 175 } 176 177 .timeline-year.active::after { 178 width: 8px; 179 height: 8px; 180 left: 13px; 181 box-shadow: 0 0 8px var(--accent-shadow); 182 } 183 184 .timeline-month.active::after { 185 width: 4px; 186 height: 4px; 187 left: 15px; 188 } 189 190 .feed-item { 191 background-color: var(--card-bg); 192 border: 1px solid var(--border-color); 193 border-radius: 4px; 194 margin-bottom: 8px; 195 overflow: hidden; 196 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 197 transition: background-color 0.2s ease; 198 } 199 200 .feed-item:hover { 201 background-color: #1a3028; 202 } 203 204 .feed-item-row { 205 display: flex; 206 align-items: flex-start; 207 padding: 8px 15px; 208 width: 100%; 209 overflow: hidden; 210 position: relative; 211 } 212 213 .feed-item-left { 214 display: flex; 215 align-items: center; 216 margin-right: 10px; 217 position: sticky; 218 top: 8px; 219 } 220 221 .feed-item-date { 222 font-family: 'JetBrains Mono', monospace; 223 font-size: 0.75rem; 224 color: var(--text-muted); 225 min-width: 80px; 226 margin-right: 10px; 227 position: sticky; 228 top: 8px; 229 } 230 231 .feed-item-author { 232 font-family: 'JetBrains Mono', monospace; 233 color: var(--accent-alt); 234 font-size: 0.85rem; 235 min-width: 70px; 236 margin-right: 15px; 237 white-space: nowrap; 238 position: sticky; 239 top: 8px; 240 } 241 242 .feed-item-title { 243 font-size: 0.95rem; 244 font-weight: 400; 245 display: inline; 246 } 247 248 .feed-item-title a { 249 color: var(--text-color); 250 text-decoration: none; 251 transition: color 0.2s ease; 252 } 253 254 .feed-item-title a:hover { 255 color: var(--accent-color); 256 } 257 258 .feed-item-content-wrapper { 259 flex: 1; 260 overflow: hidden; 261 white-space: nowrap; 262 } 263 264 .feed-item-preview { 265 color: var(--text-muted); 266 font-size: 0.85rem; 267 overflow: hidden; 268 text-overflow: ellipsis; 269 white-space: nowrap; 270 transition: all 0.3s ease; 271 display: inline; 272 margin-left: 8px; 273 } 274 275 .feed-item-actions { 276 display: flex; 277 align-items: center; 278 gap: 10px; 279 margin-left: auto; 280 } 281 282 .feed-item { 283 border-left: 3px solid transparent; 284 transition: all 0.3s ease; 285 } 286 287 .feed-item:hover { 288 border-left-color: var(--accent-color); 289 background-color: rgba(77, 250, 123, 0.03); 290 } 291 292 .references-container { 293 padding: 5px 15px; 294 border-top: 1px dashed var(--border-color); 295 background-color: rgba(77, 250, 123, 0.02); 296 } 297 298 .reference-item { 299 display: flex; 300 align-items: center; 301 padding: 4px 0; 302 line-height: 1.3; 303 } 304 305 .reference-indicator { 306 color: var(--accent-color); 307 margin-right: 5px; 308 font-size: 0.85rem; 309 } 310 311 312 .feed-item:hover .feed-item-content-wrapper { 313 white-space: normal; 314 } 315 316 .feed-item:hover .feed-item-preview { 317 white-space: normal; 318 line-height: 1.4; 319 max-height: none; 320 display: inline; 321 margin-left: 8px; 322 } 323 324 .preview-links, 325 .preview-references { 326 font-size: 0.8rem; 327 display: none; 328 flex-wrap: wrap; 329 align-items: center; 330 gap: 8px; 331 margin-top: 5px; 332 padding-top: 5px; 333 border-top: 1px dotted var(--border-color); 334 } 335 336 .external-link-item[title*="github.com"] { 337 background-color: rgba(77, 180, 128, 0.08); 338 color: var(--accent-alt); 339 } 340 341 .feed-item:hover .preview-links, 342 .feed-item:hover .preview-references { 343 display: flex; 344 } 345 346 .reference-header { 347 font-family: 'JetBrains Mono', monospace; 348 color: var(--text-muted); 349 font-size: 0.9rem; 350 margin-bottom: 5px; 351 } 352 353 .reference-link { 354 color: var(--text-color); 355 text-decoration: none; 356 transition: color 0.2s ease; 357 } 358 359 .reference-link:hover { 360 color: var(--accent-color); 361 } 362 363 .reference-author { 364 color: var(--text-muted); 365 font-size: 0.85rem; 366 margin-left: 5px; 367 } 368 369 .external-links-label { 370 color: var(--text-muted); 371 font-family: 'JetBrains Mono', monospace; 372 margin-right: 10px; 373 } 374 375 .external-link-item { 376 display: inline-block; 377 color: var(--accent-alt); 378 text-decoration: none; 379 background-color: rgba(77, 180, 128, 0.08); 380 padding: 2px 6px; 381 border-radius: 3px; 382 transition: all 0.2s ease; 383 } 384 385 .external-link-item:hover { 386 background-color: rgba(77, 180, 128, 0.15); 387 text-decoration: underline; 388 } 389 390 .external-links-toggle { 391 background: transparent; 392 border: none; 393 color: var(--text-muted); 394 font-family: 'JetBrains Mono', monospace; 395 font-size: 0.75rem; 396 padding: 2px 5px; 397 cursor: pointer; 398 display: inline-flex; 399 align-items: center; 400 border-radius: 3px; 401 margin-left: 10px; 402 } 403 404 .external-links-toggle:hover { 405 background-color: rgba(77, 180, 128, 0.05); 406 color: var(--accent-alt); 407 } 408 409 .feed-item-content { 410 padding: 15px; 411 line-height: 1.6; 412 display: none; 413 border-top: 1px solid var(--border-color); 414 background-color: #1a2e24; 415 } 416 417 .feed-item-content img { 418 max-width: 100%; 419 height: auto; 420 border-radius: 4px; 421 margin: 10px 0; 422 } 423 424 .feed-item-content pre, .feed-item-content code { 425 font-family: 'JetBrains Mono', monospace; 426 background-color: #183025; 427 border-radius: 4px; 428 padding: 0.2em 0.4em; 429 font-size: 0.9em; 430 } 431 432 .feed-item-content pre { 433 padding: 12px; 434 overflow-x: auto; 435 margin: 12px 0; 436 } 437 438 .feed-item-content blockquote { 439 border-left: 3px solid var(--accent-color); 440 padding-left: 12px; 441 margin-left: 0; 442 color: var(--text-muted); 443 } 444 445 .read-more-btn, 446 .external-links-toggle, 447 .references-toggle { 448 background-color: transparent; 449 border: none; 450 color: var(--accent-color); 451 cursor: pointer; 452 font-size: 1rem; 453 padding: 2px 8px; 454 border-radius: 3px; 455 transition: all 0.2s ease; 456 display: inline-block; 457 } 458 459 .read-more-btn:hover, 460 .external-links-toggle:hover, 461 .references-toggle:hover { 462 background-color: rgba(77, 250, 123, 0.1); 463 transform: scale(1.1); 464 } 465 466 .external-link { 467 color: var(--text-muted); 468 font-size: 1rem; 469 display: inline-block; 470 text-decoration: none; 471 padding: 2px 8px; 472 border-radius: 3px; 473 transition: all 0.2s ease; 474 } 475 476 .external-link:hover { 477 color: var(--accent-alt); 478 transform: scale(1.1); 479 } 480 481 #loading { 482 display: flex; 483 flex-direction: column; 484 align-items: center; 485 justify-content: center; 486 min-height: 200px; 487 } 488 489 .loading-spinner { 490 border: 3px solid rgba(77, 250, 123, 0.1); 491 border-top: 3px solid var(--accent-color); 492 border-radius: 50%; 493 width: 30px; 494 height: 30px; 495 animation: spin 1s linear infinite; 496 margin-bottom: 12px; 497 } 498 499 @keyframes spin { 500 0% { transform: rotate(0deg); } 501 100% { transform: rotate(360deg); } 502 } 503 504 .loading-text { 505 font-family: 'JetBrains Mono', monospace; 506 color: var(--accent-color); 507 font-size: 0.9rem; 508 } 509 510 .error-message { 511 color: #ff4d4d; 512 text-align: center; 513 padding: 20px; 514 font-family: 'JetBrains Mono', monospace; 515 } 516 517 @media (max-width: 900px) { 518 .feed-item-preview { 519 display: none; 520 } 521 } 522 523 @media (max-width: 600px) { 524 .feed-item-author { 525 min-width: 50px; 526 margin-right: 10px; 527 } 528 529 .feed-item-date { 530 min-width: 60px; 531 margin-right: 10px; 532 } 533 } 534 </style> 535</head> 536<body> 537 <header> 538 <div class="header-container"> 539 <div class="logo">Atomic<span>EEG</span></div> 540 <div class="info-panel"> 541 <span id="entry-count">0</span> entries | <span id="source-count">0</span> sources 542 </div> 543 </div> 544 </header> 545 546 <main> 547 <section class="content"> 548 <div id="loading"> 549 <div class="loading-spinner"></div> 550 <p class="loading-text">Growing Content...</p> 551 </div> 552 <div id="feed-items"></div> 553 </section> 554 <aside class="timeline-sidebar" id="timeline-sidebar"> 555 <!-- Timeline will be populated via JavaScript --> 556 </aside> 557 </main> 558 559 <script> 560 document.addEventListener('DOMContentLoaded', async () => { 561 // Add hover event listeners after DOM content is loaded 562 function setupHoverEffects() { 563 // Keep track of the currently active item 564 let currentHoveredItem = null; 565 566 document.querySelectorAll('.feed-item').forEach(item => { 567 item.addEventListener('mouseenter', () => { 568 // Close all sections in previously hovered item 569 if (currentHoveredItem && currentHoveredItem !== item) { 570 // Remove this section - we no longer show the full content 571 572 // No need to close preview content now since it's controlled by CSS hover 573 574 // References are now controlled by CSS hover 575 } 576 577 // Set this as current hovered item 578 currentHoveredItem = item; 579 580 // Remove this section - we no longer show the full content 581 582 // Preview content is shown automatically by CSS on hover 583 }); 584 }); 585 } 586 const feedItemsContainer = document.getElementById('feed-items'); 587 const loadingContainer = document.getElementById('loading'); 588 const entryCountElement = document.getElementById('entry-count'); 589 const sourceCountElement = document.getElementById('source-count'); 590 591 // Function to format date (only date, no time) 592 function formatDate(dateString) { 593 const date = new Date(dateString); 594 return date.toLocaleDateString('en-US', { 595 year: 'numeric', 596 month: 'short', 597 day: 'numeric' 598 }); 599 } 600 601 // Function to get a paragraph preview of text 602 function getTextPreview(html, maxLength = 300) { 603 // Create a temporary div to parse HTML 604 const tempDiv = document.createElement('div'); 605 tempDiv.innerHTML = html; 606 607 // Extract text content and remove extra whitespace 608 const text = tempDiv.textContent || ''; 609 const cleanText = text.replace(/\s+/g, ' ').trim(); 610 611 // Get a reasonable preview length (about a paragraph) 612 if (cleanText.length <= maxLength) { 613 return cleanText; 614 } 615 616 // Try to find a good break point 617 let endIndex = maxLength; 618 619 // Look for the last sentence break within our limit 620 const lastPeriod = cleanText.lastIndexOf('.', maxLength); 621 if (lastPeriod > maxLength / 2) { 622 endIndex = lastPeriod + 1; 623 } else { 624 // Look for the last space to avoid cutting words 625 const lastSpace = cleanText.lastIndexOf(' ', maxLength); 626 if (lastSpace > 0) { 627 endIndex = lastSpace; 628 } 629 } 630 631 return cleanText.substring(0, endIndex) + '...'; 632 } 633 634 // Function to get first line for preview in post listing 635 function getFirstLine(html) { 636 // Create a temporary div to parse HTML 637 const tempDiv = document.createElement('div'); 638 tempDiv.innerHTML = html; 639 640 // Extract text content 641 const text = tempDiv.textContent || ''; 642 const cleanText = text.replace(/\s+/g, ' ').trim(); 643 644 // Get first sentence, or about 80 chars 645 const firstPeriod = cleanText.indexOf('.'); 646 647 let endIndex; 648 if (firstPeriod !== -1 && firstPeriod < 100) { 649 endIndex = firstPeriod; 650 } else { 651 // If no suitable period, take first 80 chars 652 endIndex = Math.min(cleanText.length, 80); 653 // Look for the last space to avoid cutting words 654 const lastSpace = cleanText.lastIndexOf(' ', endIndex); 655 if (lastSpace > endIndex / 2) { 656 endIndex = lastSpace; 657 } 658 } 659 660 return cleanText.substring(0, endIndex + 1).trim(); 661 } 662 663 // Function removed - we no longer toggle full content 664 665 // Removed the external links toggle function as it's no longer needed 666 667 // Reference toggle function removed - references are now shown with CSS on hover 668 669 try { 670 // Fetch the Atom feed and threads data in parallel 671 const [feedResponse, threadsResponse] = await Promise.all([ 672 fetch('eeg.xml'), 673 fetch('threads.json') 674 ]); 675 676 if (!feedResponse.ok) { 677 throw new Error('Failed to fetch feed'); 678 } 679 680 if (!threadsResponse.ok) { 681 throw new Error('Failed to fetch threads data'); 682 } 683 684 const xmlText = await feedResponse.text(); 685 const threadsData = await threadsResponse.json(); 686 687 const parser = new DOMParser(); 688 const xmlDoc = parser.parseFromString(xmlText, 'text/xml'); 689 690 // Process feed entries 691 const entries = xmlDoc.getElementsByTagName('entry'); 692 const sources = new Set(); 693 694 // Update counter 695 entryCountElement.textContent = entries.length; 696 697 // Map to store entries by ID for easy lookup 698 const entriesById = {}; 699 700 // First pass: extract all entries and build the ID map 701 for (let i = 0; i < entries.length; i++) { 702 const entry = entries[i]; 703 704 // Extract entry data 705 const id = entry.getElementsByTagName('id')[0]?.textContent || ''; 706 const title = entry.getElementsByTagName('title')[0]?.textContent || 'No Title'; 707 const link = entry.getElementsByTagName('link')[0]?.getAttribute('href') || '#'; 708 const contentElement = entry.getElementsByTagName('summary')[0] || entry.getElementsByTagName('content')[0]; 709 const contentText = contentElement?.textContent || ''; 710 const contentType = contentElement?.getAttribute('type') || 'text'; 711 const published = entry.getElementsByTagName('published')[0]?.textContent || 712 entry.getElementsByTagName('updated')[0]?.textContent || ''; 713 const author = entry.getElementsByTagName('author')[0]?.getElementsByTagName('name')[0]?.textContent || 'Unknown'; 714 const categories = entry.getElementsByTagName('category'); 715 716 // Extract source from category (we're using category to store source name) 717 let source = 'Unknown Source'; 718 if (categories.length > 0) { 719 source = categories[0].getAttribute('term'); 720 sources.add(source); 721 } 722 723 // Properly handle the content based on content type 724 let contentHtml; 725 if (contentType === 'html' || contentType === 'text/html') { 726 // For HTML content, create a div and set innerHTML 727 contentHtml = contentText; 728 } else { 729 // For text content, escape it and preserve newlines 730 contentHtml = contentText 731 .replace(/&/g, '&amp;') 732 .replace(/</g, '&lt;') 733 .replace(/>/g, '&gt;') 734 .replace(/\n/g, '<br>'); 735 } 736 737 // Get the first line and paragraph preview 738 const firstLine = getFirstLine(contentHtml); 739 const textPreview = getTextPreview(contentHtml); 740 741 // Store the entry data 742 entriesById[id] = { 743 id, 744 articleId: `article-${i}`, 745 title, 746 link, 747 contentHtml, 748 firstLine, 749 textPreview, 750 published, 751 author, 752 source, 753 threadGroup: null, 754 isThreadParent: false, 755 threadParentId: null, 756 inThread: false, 757 threadPosition: 0, 758 externalLinks: [], 759 }; 760 } 761 762 // Process reference relationships and external links 763 for (const entryId in entriesById) { 764 if (threadsData[entryId]) { 765 const threadInfo = threadsData[entryId]; 766 const entry = entriesById[entryId]; 767 768 // Track external links for this entry 769 entry.externalLinks = []; 770 if (threadInfo.external_links && threadInfo.external_links.length > 0) { 771 entry.externalLinks = threadInfo.external_links.map(link => ({ 772 url: link.url, 773 normalized_url: link.normalized_url 774 })); 775 } 776 777 // Track references to other posts (outgoing links) 778 entry.referencesTo = []; 779 if (threadInfo.references && threadInfo.references.length > 0) { 780 // Filter for only in-feed references 781 threadInfo.references.forEach(ref => { 782 if (ref.in_feed === true && entriesById[ref.id]) { 783 entry.referencesTo.push({ 784 id: ref.id, 785 title: ref.title, 786 link: ref.link, 787 author: entriesById[ref.id].author 788 }); 789 } 790 }); 791 } 792 793 // Track posts that reference this one (incoming links) 794 entry.referencedBy = []; 795 if (threadInfo.referenced_by && threadInfo.referenced_by.length > 0) { 796 // Filter for only in-feed references 797 threadInfo.referenced_by.forEach(ref => { 798 if (ref.in_feed === true && entriesById[ref.id]) { 799 entry.referencedBy.push({ 800 id: ref.id, 801 title: ref.title, 802 link: ref.link, 803 author: entriesById[ref.id].author 804 }); 805 } 806 }); 807 } 808 } 809 } 810 811 // Sort by date and create HTML 812 const entriesArray = Object.values(entriesById); 813 entriesArray.sort((a, b) => new Date(b.published) - new Date(a.published)); 814 815 // Create a timeline structure by year/month 816 const timeline = new Map(); 817 const monthNames = [ 818 'January', 'February', 'March', 'April', 'May', 'June', 819 'July', 'August', 'September', 'October', 'November', 'December' 820 ]; 821 const shortMonthNames = [ 822 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 823 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' 824 ]; 825 826 // Group entries by year and month for the timeline 827 entriesArray.forEach(entry => { 828 const date = new Date(entry.published); 829 const year = date.getFullYear(); 830 const month = date.getMonth(); 831 832 if (!timeline.has(year)) { 833 timeline.set(year, new Map()); 834 } 835 836 const yearMap = timeline.get(year); 837 if (!yearMap.has(month)) { 838 yearMap.set(month, []); 839 } 840 841 yearMap.get(month).push(entry); 842 }); 843 844 // Process all entries in strict date order 845 let entriesHTML = ''; 846 const processedArticleIds = new Set(); 847 848 // Create a copy of entriesArray to process strictly by date 849 const entriesByDate = [...entriesArray]; 850 851 // Process each entry in date order 852 for (const entry of entriesByDate) { 853 // Skip entries already processed 854 if (processedArticleIds.has(entry.articleId)) continue; 855 856 const date = new Date(entry.published); 857 const dateAttr = `data-year="${date.getFullYear()}" data-month="${date.getMonth()}"`; 858 859 // Add entry 860 entriesHTML += ` 861 <article id="${entry.articleId}" class="feed-item" ${dateAttr}> 862 <div class="feed-item-row"> 863 <div class="feed-item-left"> 864 <a href="${entry.link}" target="_blank" class="external-link" title="Open original post">🔗</a> 865 </div> 866 <div class="feed-item-date">${formatDate(entry.published)}</div> 867 <div class="feed-item-author">${entry.author}</div> 868 <div class="feed-item-content-wrapper"> 869 <div class="feed-item-title"><a href="${entry.link}" target="_blank">${entry.title}</a></div><div class="feed-item-preview">${entry.textPreview}</div> 870 871 ${entry.externalLinks && entry.externalLinks.length > 0 ? ` 872 <div class="preview-links"> 873 <span class="external-links-label">External links:</span> 874 ${entry.externalLinks.map(link => { 875 const url = new URL(link.url); 876 let displayText = url.hostname.replace('www.', ''); 877 878 // Special handling for GitHub links 879 if (url.hostname === 'github.com' || url.hostname === 'gist.github.com') { 880 // Extract the parts from pathname (remove leading slash) 881 const parts = url.pathname.substring(1).split('/').filter(part => part); 882 if (parts.length >= 2) { 883 displayText = `github:${parts[0]}/${parts[1]}`; 884 } 885 } 886 887 // Special handling for Wikipedia links 888 if (url.hostname === 'en.wikipedia.org' || url.hostname === 'wikipedia.org' || url.hostname.endsWith('.wikipedia.org')) { 889 const titlePart = url.pathname.split('/').pop(); 890 if (titlePart) { 891 const title = decodeURIComponent(titlePart).replace(/_/g, ' '); 892 displayText = `wikipedia:${title}`; 893 } 894 } 895 896 return `<a href="${link.url}" target="_blank" class="external-link-item" title="${link.url}">${displayText}</a>`; 897 }).join(', ')} 898 </div> 899 ` : ''} 900 901 ${entry.referencesTo && entry.referencesTo.length > 0 ? ` 902 <div class="preview-references"> 903 <span class="external-links-label">References:</span> 904 ${entry.referencesTo.map(ref => ` 905 <a href="${ref.link}" target="_blank" class="external-link-item" title="${ref.title} by ${ref.author}">→ ${ref.title}</a> 906 `).join(', ')} 907 </div> 908 ` : ''} 909 910 ${entry.referencedBy && entry.referencedBy.length > 0 ? ` 911 <div class="preview-references"> 912 <span class="external-links-label">Referenced by:</span> 913 ${entry.referencedBy.map(ref => ` 914 <a href="${ref.link}" target="_blank" class="external-link-item" title="${ref.title} by ${ref.author}">← ${ref.title}</a> 915 `).join(', ')} 916 </div> 917 ` : ''} 918 </div> 919 </div> 920 </article> 921 `; 922 923 processedArticleIds.add(entry.articleId); 924 } 925 926 // All articles have been processed in the main loop above 927 928 // Update sources count 929 sourceCountElement.textContent = sources.size; 930 931 // No toggle functions needed anymore 932 933 // Build timeline sidebar 934 const timelineSidebar = document.getElementById('timeline-sidebar'); 935 let timelineHTML = ''; 936 937 // Sort years in descending order 938 const sortedYears = Array.from(timeline.keys()).sort((a, b) => b - a); 939 940 sortedYears.forEach(year => { 941 const yearMap = timeline.get(year); 942 timelineHTML += `<div class="timeline-year" data-year="${year}">${year}</div>`; 943 944 // Sort months in descending order (Dec to Jan) 945 const sortedMonths = Array.from(yearMap.keys()).sort((a, b) => b - a); 946 947 sortedMonths.forEach(month => { 948 const entries = yearMap.get(month); 949 timelineHTML += `<div class="timeline-month" data-year="${year}" data-month="${month}">${shortMonthNames[month]}</div>`; 950 }); 951 }); 952 953 timelineSidebar.innerHTML = timelineHTML; 954 955 // Set up scroll observer to highlight timeline items 956 const observerOptions = { 957 root: null, 958 rootMargin: '0px', 959 threshold: 0.3 960 }; 961 962 // Skip adding data attributes - we've already done this during HTML generation 963 964 // Create observer to track which period is in view 965 const feedObserver = new IntersectionObserver((entries) => { 966 entries.forEach(entry => { 967 if (entry.isIntersecting) { 968 const year = entry.target.getAttribute('data-year'); 969 const month = entry.target.getAttribute('data-month'); 970 971 if (year && month) { 972 // Clear all active classes 973 document.querySelectorAll('.timeline-year.active, .timeline-month.active').forEach(el => { 974 el.classList.remove('active'); 975 }); 976 977 // Set active classes 978 const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`); 979 const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`); 980 981 if (yearEl) yearEl.classList.add('active'); 982 if (monthEl) { 983 monthEl.classList.add('active'); 984 monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); 985 } 986 } 987 } 988 }); 989 }, observerOptions); 990 991 // Hide loading, show content 992 loadingContainer.style.display = 'none'; 993 feedItemsContainer.innerHTML = entriesHTML; 994 995 // Observe all feed items for scroll tracking 996 document.querySelectorAll('.feed-item').forEach(item => { 997 feedObserver.observe(item); 998 }); 999 1000 // Set up hover effects 1001 setupHoverEffects(); 1002 1003 // Make timeline items clickable to scroll to relevant posts 1004 document.querySelectorAll('.timeline-year, .timeline-month').forEach(item => { 1005 item.addEventListener('click', () => { 1006 const year = item.getAttribute('data-year'); 1007 const month = item.getAttribute('data-month'); 1008 1009 // Find the first element with this date 1010 let selector = `[data-year="${year}"]`; 1011 if (month !== null && month !== undefined) { 1012 selector += `[data-month="${month}"]`; 1013 } 1014 1015 console.log("Looking for selector:", selector); 1016 const targetItem = document.querySelector(selector); 1017 1018 if (targetItem) { 1019 console.log("Found target item:", targetItem); 1020 targetItem.scrollIntoView({ behavior: 'smooth', block: 'start' }); 1021 } else { 1022 console.log("No target item found for selector:", selector); 1023 } 1024 }); 1025 }); 1026 1027 } catch (error) { 1028 console.error('Error loading feed:', error); 1029 loadingContainer.style.display = 'none'; 1030 feedItemsContainer.innerHTML = ` 1031 <div class="error-message"> 1032 <h3>Error Loading Feed</h3> 1033 <p>${error.message}</p> 1034 </div> 1035 `; 1036 } 1037 }); 1038 </script> 1039</body> 1040</html>