···
64
+
align-items: baseline;
70
+
color: var(--text-muted);
71
+
font-family: 'JetBrains Mono', monospace;
72
+
white-space: nowrap;
81
+
color: var(--accent-alt);
82
+
text-decoration: none;
84
+
font-family: 'JetBrains Mono', monospace;
85
+
transition: all 0.2s ease;
86
+
white-space: nowrap;
89
+
.header-link:hover {
90
+
color: var(--accent-color);
91
+
text-decoration: underline;
···
scrollbar-width: none; /* For Firefox */
178
+
cursor: pointer; /* Show pointer cursor for the entire sidebar */
.timeline-sidebar::-webkit-scrollbar {
···
font-family: 'JetBrains Mono', monospace;
192
+
transition: all 0.2s ease;
···
202
+
transition: all 0.2s ease;
205
+
.timeline-year:hover, .timeline-month:hover {
206
+
color: var(--accent-color);
207
+
transform: scale(1.05);
···
color: var(--accent-color);
248
+
background-color: rgba(77, 250, 123, 0.1);
249
+
border-radius: 4px;
color: var(--accent-alt);
255
+
background-color: rgba(77, 250, 123, 0.05);
256
+
border-radius: 4px;
.timeline-year.active::after {
···
731
+
.header-container {
732
+
flex-direction: column;
733
+
align-items: flex-start;
738
+
flex-direction: column;
739
+
align-items: flex-start;
744
+
white-space: normal;
756
+
.timeline-sidebar {
758
+
height: calc(100vh - 140px);
770
+
/* People tab styling */
772
+
font-family: 'JetBrains Mono', monospace;
773
+
color: var(--accent-color);
774
+
margin-bottom: 20px;
779
+
.people-container {
781
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
786
+
background-color: var(--card-bg);
787
+
border: 1px solid var(--border-color);
788
+
border-radius: 4px;
790
+
transition: all 0.2s ease;
794
+
.person-card:hover {
795
+
border-left-color: var(--accent-color);
796
+
background-color: rgba(77, 250, 123, 0.03);
800
+
font-family: 'JetBrains Mono', monospace;
801
+
color: var(--accent-alt);
803
+
margin-bottom: 8px;
809
+
color: var(--text-muted);
810
+
margin-bottom: 12px;
814
+
color: var(--text-muted);
815
+
text-decoration: none;
816
+
transition: color 0.2s ease;
819
+
.person-site a:hover {
820
+
color: var(--accent-color);
826
+
margin-bottom: 15px;
827
+
font-family: 'JetBrains Mono', monospace;
828
+
font-size: 0.85rem;
833
+
flex-direction: column;
834
+
align-items: center;
838
+
color: var(--accent-color);
844
+
color: var(--text-muted);
845
+
font-size: 0.75rem;
853
+
font-family: 'JetBrains Mono', monospace;
854
+
color: var(--text-muted);
855
+
font-size: 0.85rem;
856
+
margin-bottom: 8px;
861
+
flex-direction: column;
867
+
background-color: rgba(77, 250, 123, 0.03);
868
+
border-radius: 3px;
873
+
color: var(--text-color);
874
+
text-decoration: none;
875
+
transition: color 0.2s ease;
878
+
.recent-post a:hover {
879
+
color: var(--accent-color);
882
+
.recent-post-date {
883
+
font-family: 'JetBrains Mono', monospace;
884
+
color: var(--text-muted);
885
+
font-size: 0.75rem;
@media (max-width: 600px) {
···
903
+
justify-content: space-between;
908
+
font-size: 0.75rem;
910
+
text-align: center;
913
+
.people-container {
914
+
grid-template-columns: 1fr;
919
+
justify-content: space-around;
926
+
.timeline-sidebar {
928
+
height: calc(100vh - 150px);
···
<div class="header-container">
713
-
<div class="logo">Atomic<span>EEG</span></div>
936
+
<div class="header-left">
937
+
<div class="logo">Atomic<span>EEG</span></div>
938
+
<div class="tagline">musings from the Energy & Environment Group at the University of Cambridge</div>
<button class="tab-button active" data-tab="posts">Posts</button>
<button class="tab-button" data-tab="links">Links</button>
943
+
<button class="tab-button" data-tab="people">People</button>
718
-
<div class="info-panel">
719
-
<span id="entry-count">0</span> entries | <span id="source-count">0</span> sources
945
+
<div class="external-links">
946
+
<a href="https://www.cst.cam.ac.uk/research/eeg" target="_blank" class="header-link">Home</a>
947
+
<a href="https://watch.eeg.cl.cam.ac.uk" target="_blank" class="header-link">Videos</a>
···
<div id="feed-items" class="tab-content active" data-tab="posts"></div>
<div id="link-items" class="tab-content" data-tab="links"></div>
960
+
<div id="people-items" class="tab-content" data-tab="people">
961
+
<h2 class="people-header">EEG Contributors</h2>
962
+
<div class="people-container"></div>
<aside class="timeline-sidebar" id="timeline-sidebar">
<!-- Timeline will be populated via JavaScript -->
···
// Tab switching functionality
999
+
// Create global variables to store state
1000
+
let globalFeedObserver = null;
1001
+
let lastActiveYear = null;
1002
+
let lastActiveMonth = null;
1004
+
function setupObserver(options) {
1005
+
// Create a new intersection observer for handling timeline scrolling
1006
+
return new IntersectionObserver((entries) => {
1007
+
entries.forEach(entry => {
1008
+
if (entry.isIntersecting) {
1009
+
const year = entry.target.getAttribute('data-year');
1010
+
const month = entry.target.getAttribute('data-month');
1012
+
// Get the active tab
1013
+
const activeTab = document.querySelector('.tab-content.active');
1014
+
const activeTabId = activeTab.getAttribute('data-tab');
1016
+
// Only process if we're on posts or links tab
1017
+
if ((activeTabId === 'posts' || activeTabId === 'links') && year && month) {
1018
+
// Clear all active classes
1019
+
document.querySelectorAll('.timeline-year.active, .timeline-month.active').forEach(el => {
1020
+
el.classList.remove('active');
1023
+
// Set active classes
1024
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
1025
+
const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`);
1028
+
yearEl.classList.add('active');
1029
+
// Store the last active year globally
1030
+
lastActiveYear = year;
1033
+
monthEl.classList.add('active');
1034
+
monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
1035
+
// Store the last active month globally
1036
+
lastActiveMonth = month;
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
1048
+
const timeline = document.getElementById('timeline-sidebar');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
···
button.classList.add('active');
document.querySelector(`.tab-content[data-tab="${tabName}"]`).classList.add('active');
1062
+
// Show or hide timeline sidebar based on active tab
1063
+
if (tabName === 'people') {
1064
+
timeline.style.display = 'none';
1065
+
document.querySelector('.content').style.paddingRight = '0';
1067
+
timeline.style.display = 'flex';
1068
+
document.querySelector('.content').style.paddingRight = 'var(--sidebar-width)';
1070
+
// Reset timeline highlighting when switching between posts/links
1071
+
document.querySelectorAll('.timeline-year.active, .timeline-month.active').forEach(el => {
1072
+
el.classList.remove('active');
1075
+
// Check if we have stored the active year/month
1076
+
// If we don't have stored dates yet, try to find them from the active timeline elements
1077
+
if (!lastActiveYear || !lastActiveMonth) {
1078
+
const activeYearEl = document.querySelector('.timeline-year.active');
1079
+
const activeMonthEl = document.querySelector('.timeline-month.active');
1081
+
if (activeYearEl) {
1082
+
lastActiveYear = activeYearEl.getAttribute('data-year');
1085
+
if (activeMonthEl) {
1086
+
lastActiveMonth = activeMonthEl.getAttribute('data-month');
1088
+
// If still no active month, try to find the first visible item in current view
1089
+
const previousTabName = document.querySelector('.tab-button.active').getAttribute('data-tab');
1090
+
const selector = previousTabName === 'posts' ? '.feed-item' : '.link-item';
1091
+
const visibleItems = Array.from(document.querySelectorAll(selector))
1093
+
const rect = item.getBoundingClientRect();
1094
+
return rect.top >= 0 && rect.bottom <= window.innerHeight;
1097
+
if (visibleItems.length > 0) {
1098
+
lastActiveYear = visibleItems[0].getAttribute('data-year');
1099
+
lastActiveMonth = visibleItems[0].getAttribute('data-month');
1106
+
// If switching to links view, ensure link items are properly observed
1107
+
if (tabName === 'links') {
1108
+
// Disconnect and recreate the observer to ensure proper tracking
1109
+
if (globalFeedObserver) {
1110
+
globalFeedObserver.disconnect();
1113
+
// Setup a new observer
1114
+
globalFeedObserver = setupObserver({
1116
+
rootMargin: '0px',
1120
+
// Observe all items in the active tab
1121
+
observeAllDateItems();
1123
+
// If we have active year/month from previous tab, find closest match
1124
+
if (lastActiveYear && lastActiveMonth) {
1125
+
// Find link items from this time period
1126
+
let selector = `.link-item[data-year="${lastActiveYear}"][data-month="${lastActiveMonth}"]`;
1127
+
let matchingItems = document.querySelectorAll(selector);
1130
+
// If no exact match, try just matching the year
1131
+
if (matchingItems.length === 0) {
1132
+
selector = `.link-item[data-year="${lastActiveYear}"]`;
1133
+
matchingItems = document.querySelectorAll(selector);
1136
+
// If still no match, find the closest date
1137
+
if (matchingItems.length === 0) {
1138
+
const targetDate = new Date(lastActiveYear, lastActiveMonth);
1139
+
const allLinkItems = Array.from(document.querySelectorAll('.link-item'));
1141
+
// Sort by closest date
1142
+
if (allLinkItems.length > 0) {
1143
+
allLinkItems.sort((a, b) => {
1144
+
const dateA = new Date(a.getAttribute('data-year'), a.getAttribute('data-month'));
1145
+
const dateB = new Date(b.getAttribute('data-year'), b.getAttribute('data-month'));
1147
+
return Math.abs(dateA - targetDate) - Math.abs(dateB - targetDate);
1150
+
// Use closest match
1151
+
if (allLinkItems.length > 0) {
1152
+
matchingItems = [allLinkItems[0]];
1157
+
// Scroll to the matching item
1158
+
if (matchingItems.length > 0) {
1159
+
matchingItems[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
1161
+
// Update timeline
1162
+
const year = matchingItems[0].getAttribute('data-year');
1163
+
const month = matchingItems[0].getAttribute('data-month');
1165
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
1166
+
const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`);
1168
+
if (yearEl) yearEl.classList.add('active');
1170
+
monthEl.classList.add('active');
1171
+
monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
1175
+
// If no active period, default to highlighting first visible
1176
+
const visibleLinks = Array.from(document.querySelectorAll('.link-item'))
1178
+
const rect = item.getBoundingClientRect();
1179
+
return rect.top >= 0 && rect.bottom <= window.innerHeight;
1182
+
if (visibleLinks.length > 0) {
1183
+
const firstVisible = visibleLinks[0];
1184
+
const year = firstVisible.getAttribute('data-year');
1185
+
const month = firstVisible.getAttribute('data-month');
1187
+
if (year && month) {
1188
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
1189
+
const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`);
1191
+
if (yearEl) yearEl.classList.add('active');
1193
+
monthEl.classList.add('active');
1194
+
monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
1199
+
} else if (tabName === 'posts') {
1200
+
// Same for posts view
1201
+
if (globalFeedObserver) {
1202
+
globalFeedObserver.disconnect();
1205
+
globalFeedObserver = setupObserver({
1207
+
rootMargin: '0px',
1211
+
observeAllDateItems();
1213
+
// If we have active year/month from previous tab, find closest match
1214
+
if (lastActiveYear && lastActiveMonth) {
1215
+
// Find feed items from this time period
1216
+
let selector = `.feed-item[data-year="${lastActiveYear}"][data-month="${lastActiveMonth}"]`;
1217
+
let matchingItems = document.querySelectorAll(selector);
1220
+
// If no exact match, try just matching the year
1221
+
if (matchingItems.length === 0) {
1222
+
selector = `.feed-item[data-year="${lastActiveYear}"]`;
1223
+
matchingItems = document.querySelectorAll(selector);
1226
+
// If still no match, find the closest date
1227
+
if (matchingItems.length === 0) {
1228
+
const targetDate = new Date(lastActiveYear, lastActiveMonth);
1229
+
const allFeedItems = Array.from(document.querySelectorAll('.feed-item'));
1231
+
// Sort by closest date
1232
+
if (allFeedItems.length > 0) {
1233
+
allFeedItems.sort((a, b) => {
1234
+
const dateA = new Date(a.getAttribute('data-year'), a.getAttribute('data-month'));
1235
+
const dateB = new Date(b.getAttribute('data-year'), b.getAttribute('data-month'));
1237
+
return Math.abs(dateA - targetDate) - Math.abs(dateB - targetDate);
1240
+
// Use closest match
1241
+
if (allFeedItems.length > 0) {
1242
+
matchingItems = [allFeedItems[0]];
1247
+
// Scroll to the matching item
1248
+
if (matchingItems.length > 0) {
1249
+
matchingItems[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
1251
+
// Update timeline
1252
+
const year = matchingItems[0].getAttribute('data-year');
1253
+
const month = matchingItems[0].getAttribute('data-month');
1255
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
1256
+
const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`);
1258
+
if (yearEl) yearEl.classList.add('active');
1260
+
monthEl.classList.add('active');
1261
+
monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
1265
+
// If no active period, default to highlighting first visible
1266
+
const visiblePosts = Array.from(document.querySelectorAll('.feed-item'))
1268
+
const rect = item.getBoundingClientRect();
1269
+
return rect.top >= 0 && rect.bottom <= window.innerHeight;
1272
+
if (visiblePosts.length > 0) {
1273
+
const firstVisible = visiblePosts[0];
1274
+
const year = firstVisible.getAttribute('data-year');
1275
+
const month = firstVisible.getAttribute('data-month');
1277
+
if (year && month) {
1278
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
1279
+
const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`);
1281
+
if (yearEl) yearEl.classList.add('active');
1283
+
monthEl.classList.add('active');
1284
+
monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
const feedItemsContainer = document.getElementById('feed-items');
const loadingContainer = document.getElementById('loading');
787
-
const entryCountElement = document.getElementById('entry-count');
788
-
const sourceCountElement = document.getElementById('source-count');
// Function to format date (only date, no time)
function formatDate(dateString) {
···
const entries = xmlDoc.getElementsByTagName('entry');
const sources = new Set();
894
-
entryCountElement.textContent = entries.length;
1400
+
// No longer updating the entry count element since it's been removed
// Map to store entries by ID for easy lookup
···
// All articles have been processed in the main loop above
1236
-
// Update sources count
1237
-
sourceCountElement.textContent = sources.size;
1742
+
// No longer updating the source count element since it's been removed
// No toggle functions needed anymore
···
// Skip adding data attributes - we've already done this during HTML generation
// Create observer to track which period is in view
1273
-
const feedObserver = new IntersectionObserver((entries) => {
1274
-
entries.forEach(entry => {
1275
-
if (entry.isIntersecting) {
1276
-
const year = entry.target.getAttribute('data-year');
1277
-
const month = entry.target.getAttribute('data-month');
1279
-
if (year && month) {
1280
-
// Clear all active classes
1281
-
document.querySelectorAll('.timeline-year.active, .timeline-month.active').forEach(el => {
1282
-
el.classList.remove('active');
1285
-
// Set active classes
1286
-
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
1287
-
const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`);
1289
-
if (yearEl) yearEl.classList.add('active');
1291
-
monthEl.classList.add('active');
1292
-
monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
1297
-
}, observerOptions);
1778
+
globalFeedObserver = setupObserver(observerOptions);
// Hide loading, show content
loadingContainer.style.display = 'none';
feedItemsContainer.innerHTML = entriesHTML;
1303
-
// Observe all feed items for scroll tracking
1304
-
document.querySelectorAll('.feed-item').forEach(item => {
1305
-
feedObserver.observe(item);
1784
+
// Helper function to observe all items with date attributes
1785
+
function observeAllDateItems() {
1786
+
// Observe all feed items for scroll tracking
1787
+
document.querySelectorAll('.feed-item').forEach(item => {
1788
+
globalFeedObserver.observe(item);
1791
+
// Also observe link items for timeline highlighting
1792
+
document.querySelectorAll('.link-item').forEach(item => {
1793
+
globalFeedObserver.observe(item);
1308
-
// Also observe link items for timeline highlighting
1309
-
document.querySelectorAll('.link-item').forEach(item => {
1310
-
feedObserver.observe(item);
1797
+
// Initial observation of all items
1798
+
observeAllDateItems();
1800
+
// Set initial display state for timeline based on initial active tab
1801
+
const initialActiveTab = document.querySelector('.tab-button.active').getAttribute('data-tab');
1802
+
if (initialActiveTab === 'people') {
1803
+
document.getElementById('timeline-sidebar').style.display = 'none';
1804
+
document.querySelector('.content').style.paddingRight = '0';
1806
+
// Initialize the last active date from the first visible item
1807
+
const selector = initialActiveTab === 'posts' ? '.feed-item' : '.link-item';
1808
+
const visibleItems = Array.from(document.querySelectorAll(selector))
1810
+
const rect = item.getBoundingClientRect();
1811
+
return rect.top >= 0 && rect.bottom <= window.innerHeight;
1814
+
if (visibleItems.length > 0) {
1815
+
lastActiveYear = visibleItems[0].getAttribute('data-year');
1816
+
lastActiveMonth = visibleItems[0].getAttribute('data-month');
···
// Update the links container
linksContainer.innerHTML = linksHTML;
1994
+
// Process people data
1995
+
const peopleContainer = document.querySelector('.people-container');
1996
+
const peopleMap = new Map(); // Map to store people data
1998
+
// Fetch the mapping.json file to get author information
1999
+
const mappingResponse = await fetch('mapping.json');
2000
+
if (!mappingResponse.ok) {
2001
+
throw new Error('Failed to fetch mapping data');
2003
+
const mappingData = await mappingResponse.json();
2005
+
// Process author information from mapping data
2006
+
Object.entries(mappingData).forEach(([feedUrl, info]) => {
2007
+
const { name, site } = info;
2008
+
if (!peopleMap.has(name)) {
2009
+
peopleMap.set(name, {
2020
+
// Associate entries with authors
2021
+
entriesArray.forEach(entry => {
2022
+
// Find the person who matches this entry's author
2023
+
// (taking into account potential differences in formatting)
2024
+
const person = Array.from(peopleMap.values()).find(p =>
2025
+
p.name === entry.author ||
2026
+
entry.author.includes(p.name) ||
2027
+
p.name.includes(entry.author)
2031
+
person.posts.push(entry);
2032
+
person.postCount++;
2034
+
// Track most recent post date
2035
+
const entryDate = new Date(entry.published);
2036
+
if (!person.mostRecent || entryDate > new Date(person.mostRecent.published)) {
2037
+
person.mostRecent = entry;
2042
+
// Generate HTML for people cards
2043
+
let peopleHTML = '';
2044
+
Array.from(peopleMap.values())
2045
+
.sort((a, b) => b.postCount - a.postCount) // Sort by post count
2046
+
.forEach(person => {
2047
+
const recentPosts = person.posts
2048
+
.sort((a, b) => new Date(b.published) - new Date(a.published))
2049
+
.slice(0, 3); // Get top 3 most recent posts
2052
+
<div class="person-card">
2053
+
<div class="person-name">${person.name}</div>
2054
+
<div class="person-site"><a href="${person.feedUrl}" target="_blank" rel="noopener">${person.site}</a></div>
2056
+
<div class="person-stats">
2057
+
<div class="person-stat">
2058
+
<div class="stat-value">${person.postCount}</div>
2059
+
<div class="stat-label">Posts</div>
2061
+
<div class="person-stat">
2062
+
<div class="stat-value">${person.mostRecent ? formatDate(person.mostRecent.published) : 'N/A'}</div>
2063
+
<div class="stat-label">Latest</div>
2067
+
${recentPosts.length > 0 ? `
2068
+
<div class="person-recent">
2069
+
<div class="recent-title">RECENT POSTS</div>
2070
+
<div class="recent-posts">
2071
+
${recentPosts.map(post => `
2072
+
<div class="recent-post">
2073
+
<a href="${post.link}" target="_blank">${post.title}</a>
2074
+
<div class="recent-post-date">${formatDate(post.published)}</div>
2084
+
peopleContainer.innerHTML = peopleHTML;
1490
-
// Make timeline items clickable to scroll to relevant posts
2089
+
// Make timeline items clickable to scroll to relevant posts or links
document.querySelectorAll('.timeline-year, .timeline-month').forEach(item => {
item.addEventListener('click', () => {
const year = item.getAttribute('data-year');
const month = item.getAttribute('data-month');
2095
+
// Store the selected date globally
2096
+
lastActiveYear = year;
2097
+
if (month !== null && month !== undefined) {
2098
+
lastActiveMonth = month;
// Find the first element with this date
let selector = `[data-year="${year}"]`;
if (month !== null && month !== undefined) {
···
const activeTab = document.querySelector('.tab-content.active');
2110
+
const activeTabId = activeTab.getAttribute('data-tab');
// Look for the target within the active tab
const targetItem = activeTab.querySelector(selector);
2115
+
// If no matching items in this tab or people tab is active, do nothing
2116
+
if (targetItem && activeTabId !== 'people') {
targetItem.scrollIntoView({ behavior: 'smooth', block: 'start' });
2119
+
// Highlight the selected timeline period
2120
+
document.querySelectorAll('.timeline-year.active, .timeline-month.active').forEach(el => {
2121
+
el.classList.remove('active');
2124
+
// Set active classes
2125
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
2126
+
const monthEl = month !== null && month !== undefined ?
2127
+
document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`) : null;
2129
+
if (yearEl) yearEl.classList.add('active');
2130
+
if (monthEl) monthEl.classList.add('active');