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