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, '&')
732 .replace(/</g, '<')
733 .replace(/>/g, '>')
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>