···
+
color: var(--text-muted);
+
font-family: 'JetBrains Mono', monospace;
+
color: var(--accent-alt);
+
font-family: 'JetBrains Mono', monospace;
+
transition: all 0.2s ease;
+
color: var(--accent-color);
+
text-decoration: underline;
···
scrollbar-width: none; /* For Firefox */
+
cursor: pointer; /* Show pointer cursor for the entire sidebar */
.timeline-sidebar::-webkit-scrollbar {
···
font-family: 'JetBrains Mono', monospace;
+
transition: all 0.2s ease;
···
+
transition: all 0.2s ease;
+
.timeline-year:hover, .timeline-month:hover {
+
color: var(--accent-color);
+
transform: scale(1.05);
···
color: var(--accent-color);
+
background-color: rgba(77, 250, 123, 0.1);
color: var(--accent-alt);
+
background-color: rgba(77, 250, 123, 0.05);
.timeline-year.active::after {
···
+
flex-direction: column;
+
align-items: flex-start;
+
flex-direction: column;
+
align-items: flex-start;
+
height: calc(100vh - 140px);
+
/* People tab styling */
+
font-family: 'JetBrains Mono', monospace;
+
color: var(--accent-color);
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+
background-color: var(--card-bg);
+
border: 1px solid var(--border-color);
+
transition: all 0.2s ease;
+
border-left-color: var(--accent-color);
+
background-color: rgba(77, 250, 123, 0.03);
+
font-family: 'JetBrains Mono', monospace;
+
color: var(--accent-alt);
+
color: var(--text-muted);
+
color: var(--text-muted);
+
transition: color 0.2s ease;
+
color: var(--accent-color);
+
font-family: 'JetBrains Mono', monospace;
+
flex-direction: column;
+
color: var(--accent-color);
+
color: var(--text-muted);
+
font-family: 'JetBrains Mono', monospace;
+
color: var(--text-muted);
+
flex-direction: column;
+
background-color: rgba(77, 250, 123, 0.03);
+
color: var(--text-color);
+
transition: color 0.2s ease;
+
color: var(--accent-color);
+
font-family: 'JetBrains Mono', monospace;
+
color: var(--text-muted);
@media (max-width: 600px) {
···
+
justify-content: space-between;
+
grid-template-columns: 1fr;
+
justify-content: space-around;
+
height: calc(100vh - 150px);
···
<div class="header-container">
+
<div class="header-left">
+
<div class="logo">Atomic<span>EEG</span></div>
+
<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>
+
<button class="tab-button" data-tab="people">People</button>
+
<div class="external-links">
+
<a href="https://www.cst.cam.ac.uk/research/eeg" target="_blank" class="header-link">Home</a>
+
<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>
+
<div id="people-items" class="tab-content" data-tab="people">
+
<h2 class="people-header">EEG Contributors</h2>
+
<div class="people-container"></div>
<aside class="timeline-sidebar" id="timeline-sidebar">
<!-- Timeline will be populated via JavaScript -->
···
// Tab switching functionality
+
// Create global variables to store state
+
let globalFeedObserver = null;
+
let lastActiveYear = null;
+
let lastActiveMonth = null;
+
function setupObserver(options) {
+
// Create a new intersection observer for handling timeline scrolling
+
return new IntersectionObserver((entries) => {
+
entries.forEach(entry => {
+
if (entry.isIntersecting) {
+
const year = entry.target.getAttribute('data-year');
+
const month = entry.target.getAttribute('data-month');
+
const activeTab = document.querySelector('.tab-content.active');
+
const activeTabId = activeTab.getAttribute('data-tab');
+
// Only process if we're on posts or links tab
+
if ((activeTabId === 'posts' || activeTabId === 'links') && year && month) {
+
// Clear all active classes
+
document.querySelectorAll('.timeline-year.active, .timeline-month.active').forEach(el => {
+
el.classList.remove('active');
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
+
const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`);
+
yearEl.classList.add('active');
+
// Store the last active year globally
+
monthEl.classList.add('active');
+
monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
// Store the last active month globally
+
lastActiveMonth = month;
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
+
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');
+
// Show or hide timeline sidebar based on active tab
+
if (tabName === 'people') {
+
timeline.style.display = 'none';
+
document.querySelector('.content').style.paddingRight = '0';
+
timeline.style.display = 'flex';
+
document.querySelector('.content').style.paddingRight = 'var(--sidebar-width)';
+
// Reset timeline highlighting when switching between posts/links
+
document.querySelectorAll('.timeline-year.active, .timeline-month.active').forEach(el => {
+
el.classList.remove('active');
+
// Check if we have stored the active year/month
+
// If we don't have stored dates yet, try to find them from the active timeline elements
+
if (!lastActiveYear || !lastActiveMonth) {
+
const activeYearEl = document.querySelector('.timeline-year.active');
+
const activeMonthEl = document.querySelector('.timeline-month.active');
+
lastActiveYear = activeYearEl.getAttribute('data-year');
+
lastActiveMonth = activeMonthEl.getAttribute('data-month');
+
// If still no active month, try to find the first visible item in current view
+
const previousTabName = document.querySelector('.tab-button.active').getAttribute('data-tab');
+
const selector = previousTabName === 'posts' ? '.feed-item' : '.link-item';
+
const visibleItems = Array.from(document.querySelectorAll(selector))
+
const rect = item.getBoundingClientRect();
+
return rect.top >= 0 && rect.bottom <= window.innerHeight;
+
if (visibleItems.length > 0) {
+
lastActiveYear = visibleItems[0].getAttribute('data-year');
+
lastActiveMonth = visibleItems[0].getAttribute('data-month');
+
// If switching to links view, ensure link items are properly observed
+
if (tabName === 'links') {
+
// Disconnect and recreate the observer to ensure proper tracking
+
if (globalFeedObserver) {
+
globalFeedObserver.disconnect();
+
// Setup a new observer
+
globalFeedObserver = setupObserver({
+
// Observe all items in the active tab
+
// If we have active year/month from previous tab, find closest match
+
if (lastActiveYear && lastActiveMonth) {
+
// Find link items from this time period
+
let selector = `.link-item[data-year="${lastActiveYear}"][data-month="${lastActiveMonth}"]`;
+
let matchingItems = document.querySelectorAll(selector);
+
// If no exact match, try just matching the year
+
if (matchingItems.length === 0) {
+
selector = `.link-item[data-year="${lastActiveYear}"]`;
+
matchingItems = document.querySelectorAll(selector);
+
// If still no match, find the closest date
+
if (matchingItems.length === 0) {
+
const targetDate = new Date(lastActiveYear, lastActiveMonth);
+
const allLinkItems = Array.from(document.querySelectorAll('.link-item'));
+
// Sort by closest date
+
if (allLinkItems.length > 0) {
+
allLinkItems.sort((a, b) => {
+
const dateA = new Date(a.getAttribute('data-year'), a.getAttribute('data-month'));
+
const dateB = new Date(b.getAttribute('data-year'), b.getAttribute('data-month'));
+
return Math.abs(dateA - targetDate) - Math.abs(dateB - targetDate);
+
if (allLinkItems.length > 0) {
+
matchingItems = [allLinkItems[0]];
+
// Scroll to the matching item
+
if (matchingItems.length > 0) {
+
matchingItems[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
+
const year = matchingItems[0].getAttribute('data-year');
+
const month = matchingItems[0].getAttribute('data-month');
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
+
const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`);
+
if (yearEl) yearEl.classList.add('active');
+
monthEl.classList.add('active');
+
monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
// If no active period, default to highlighting first visible
+
const visibleLinks = Array.from(document.querySelectorAll('.link-item'))
+
const rect = item.getBoundingClientRect();
+
return rect.top >= 0 && rect.bottom <= window.innerHeight;
+
if (visibleLinks.length > 0) {
+
const firstVisible = visibleLinks[0];
+
const year = firstVisible.getAttribute('data-year');
+
const month = firstVisible.getAttribute('data-month');
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
+
const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`);
+
if (yearEl) yearEl.classList.add('active');
+
monthEl.classList.add('active');
+
monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
} else if (tabName === 'posts') {
+
if (globalFeedObserver) {
+
globalFeedObserver.disconnect();
+
globalFeedObserver = setupObserver({
+
// If we have active year/month from previous tab, find closest match
+
if (lastActiveYear && lastActiveMonth) {
+
// Find feed items from this time period
+
let selector = `.feed-item[data-year="${lastActiveYear}"][data-month="${lastActiveMonth}"]`;
+
let matchingItems = document.querySelectorAll(selector);
+
// If no exact match, try just matching the year
+
if (matchingItems.length === 0) {
+
selector = `.feed-item[data-year="${lastActiveYear}"]`;
+
matchingItems = document.querySelectorAll(selector);
+
// If still no match, find the closest date
+
if (matchingItems.length === 0) {
+
const targetDate = new Date(lastActiveYear, lastActiveMonth);
+
const allFeedItems = Array.from(document.querySelectorAll('.feed-item'));
+
// Sort by closest date
+
if (allFeedItems.length > 0) {
+
allFeedItems.sort((a, b) => {
+
const dateA = new Date(a.getAttribute('data-year'), a.getAttribute('data-month'));
+
const dateB = new Date(b.getAttribute('data-year'), b.getAttribute('data-month'));
+
return Math.abs(dateA - targetDate) - Math.abs(dateB - targetDate);
+
if (allFeedItems.length > 0) {
+
matchingItems = [allFeedItems[0]];
+
// Scroll to the matching item
+
if (matchingItems.length > 0) {
+
matchingItems[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
+
const year = matchingItems[0].getAttribute('data-year');
+
const month = matchingItems[0].getAttribute('data-month');
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
+
const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`);
+
if (yearEl) yearEl.classList.add('active');
+
monthEl.classList.add('active');
+
monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
// If no active period, default to highlighting first visible
+
const visiblePosts = Array.from(document.querySelectorAll('.feed-item'))
+
const rect = item.getBoundingClientRect();
+
return rect.top >= 0 && rect.bottom <= window.innerHeight;
+
if (visiblePosts.length > 0) {
+
const firstVisible = visiblePosts[0];
+
const year = firstVisible.getAttribute('data-year');
+
const month = firstVisible.getAttribute('data-month');
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
+
const monthEl = document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`);
+
if (yearEl) yearEl.classList.add('active');
+
monthEl.classList.add('active');
+
monthEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
const feedItemsContainer = document.getElementById('feed-items');
const loadingContainer = document.getElementById('loading');
// Function to format date (only date, no time)
function formatDate(dateString) {
···
const entries = xmlDoc.getElementsByTagName('entry');
const sources = new Set();
+
// 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
+
// 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
+
globalFeedObserver = setupObserver(observerOptions);
// Hide loading, show content
loadingContainer.style.display = 'none';
feedItemsContainer.innerHTML = entriesHTML;
+
// Helper function to observe all items with date attributes
+
function observeAllDateItems() {
+
// Observe all feed items for scroll tracking
+
document.querySelectorAll('.feed-item').forEach(item => {
+
globalFeedObserver.observe(item);
+
// Also observe link items for timeline highlighting
+
document.querySelectorAll('.link-item').forEach(item => {
+
globalFeedObserver.observe(item);
+
// Initial observation of all items
+
// Set initial display state for timeline based on initial active tab
+
const initialActiveTab = document.querySelector('.tab-button.active').getAttribute('data-tab');
+
if (initialActiveTab === 'people') {
+
document.getElementById('timeline-sidebar').style.display = 'none';
+
document.querySelector('.content').style.paddingRight = '0';
+
// Initialize the last active date from the first visible item
+
const selector = initialActiveTab === 'posts' ? '.feed-item' : '.link-item';
+
const visibleItems = Array.from(document.querySelectorAll(selector))
+
const rect = item.getBoundingClientRect();
+
return rect.top >= 0 && rect.bottom <= window.innerHeight;
+
if (visibleItems.length > 0) {
+
lastActiveYear = visibleItems[0].getAttribute('data-year');
+
lastActiveMonth = visibleItems[0].getAttribute('data-month');
···
// Update the links container
linksContainer.innerHTML = linksHTML;
+
const peopleContainer = document.querySelector('.people-container');
+
const peopleMap = new Map(); // Map to store people data
+
// Fetch the mapping.json file to get author information
+
const mappingResponse = await fetch('mapping.json');
+
if (!mappingResponse.ok) {
+
throw new Error('Failed to fetch mapping data');
+
const mappingData = await mappingResponse.json();
+
// Process author information from mapping data
+
Object.entries(mappingData).forEach(([feedUrl, info]) => {
+
const { name, site } = info;
+
if (!peopleMap.has(name)) {
+
// Associate entries with authors
+
entriesArray.forEach(entry => {
+
// Find the person who matches this entry's author
+
// (taking into account potential differences in formatting)
+
const person = Array.from(peopleMap.values()).find(p =>
+
p.name === entry.author ||
+
entry.author.includes(p.name) ||
+
p.name.includes(entry.author)
+
person.posts.push(entry);
+
// Track most recent post date
+
const entryDate = new Date(entry.published);
+
if (!person.mostRecent || entryDate > new Date(person.mostRecent.published)) {
+
person.mostRecent = entry;
+
// Generate HTML for people cards
+
Array.from(peopleMap.values())
+
.sort((a, b) => b.postCount - a.postCount) // Sort by post count
+
const recentPosts = person.posts
+
.sort((a, b) => new Date(b.published) - new Date(a.published))
+
.slice(0, 3); // Get top 3 most recent posts
+
<div class="person-card">
+
<div class="person-name">${person.name}</div>
+
<div class="person-site"><a href="${person.feedUrl}" target="_blank" rel="noopener">${person.site}</a></div>
+
<div class="person-stats">
+
<div class="person-stat">
+
<div class="stat-value">${person.postCount}</div>
+
<div class="stat-label">Posts</div>
+
<div class="person-stat">
+
<div class="stat-value">${person.mostRecent ? formatDate(person.mostRecent.published) : 'N/A'}</div>
+
<div class="stat-label">Latest</div>
+
${recentPosts.length > 0 ? `
+
<div class="person-recent">
+
<div class="recent-title">RECENT POSTS</div>
+
<div class="recent-posts">
+
${recentPosts.map(post => `
+
<div class="recent-post">
+
<a href="${post.link}" target="_blank">${post.title}</a>
+
<div class="recent-post-date">${formatDate(post.published)}</div>
+
peopleContainer.innerHTML = peopleHTML;
+
// 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');
+
// Store the selected date globally
+
if (month !== null && month !== undefined) {
+
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');
+
const activeTabId = activeTab.getAttribute('data-tab');
// Look for the target within the active tab
const targetItem = activeTab.querySelector(selector);
+
// If no matching items in this tab or people tab is active, do nothing
+
if (targetItem && activeTabId !== 'people') {
targetItem.scrollIntoView({ behavior: 'smooth', block: 'start' });
+
// Highlight the selected timeline period
+
document.querySelectorAll('.timeline-year.active, .timeline-month.active').forEach(el => {
+
el.classList.remove('active');
+
const yearEl = document.querySelector(`.timeline-year[data-year="${year}"]`);
+
const monthEl = month !== null && month !== undefined ?
+
document.querySelector(`.timeline-month[data-year="${year}"][data-month="${month}"]`) : null;
+
if (yearEl) yearEl.classList.add('active');
+
if (monthEl) monthEl.classList.add('active');