Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| import { MONTHS } from '../config.js'; | |
| export class Timeline { | |
| constructor(container) { | |
| this.container = container; | |
| this.events = []; | |
| this.sortedEvents = []; | |
| this.selectedIndex = -1; | |
| this.isPlaying = false; | |
| this.onEventSelect = () => {}; | |
| this.onPlayStateChange = () => {}; | |
| } | |
| setEvents(events) { | |
| this.events = events; | |
| // Sort events by date for playback | |
| this.sortedEvents = [...events].sort((a, b) => new Date(a.date) - new Date(b.date)); | |
| this.render(); | |
| } | |
| render() { | |
| const grouped = this.groupByMonth(this.events); | |
| const sortedKeys = Object.keys(grouped).sort(); | |
| let currentYear = null; | |
| let monthsHtml = ''; | |
| for (const monthKey of sortedKeys) { | |
| const [year] = monthKey.split('-'); | |
| // Add year separator when year changes | |
| if (year !== currentYear) { | |
| monthsHtml += `<div class="timeline-year">${year}</div>`; | |
| currentYear = year; | |
| } | |
| monthsHtml += this.renderMonth(monthKey, grouped[monthKey]); | |
| } | |
| this.container.innerHTML = ` | |
| <button class="timeline-play-btn" aria-label="Play"> | |
| <svg class="play-icon" width="20" height="20" viewBox="0 0 20 20" fill="currentColor"> | |
| <path d="M6 4l10 6-10 6V4z"/> | |
| </svg> | |
| <svg class="pause-icon" width="20" height="20" viewBox="0 0 20 20" fill="currentColor"> | |
| <rect x="5" y="4" width="3" height="12"/> | |
| <rect x="12" y="4" width="3" height="12"/> | |
| </svg> | |
| </button> | |
| <div class="timeline-months"> | |
| ${monthsHtml} | |
| </div> | |
| `; | |
| this.attachEventListeners(); | |
| } | |
| groupByMonth(events) { | |
| return events.reduce((acc, event) => { | |
| const date = new Date(event.date); | |
| const key = `${date.getFullYear()}-${String(date.getMonth()).padStart(2, '0')}`; | |
| if (!acc[key]) { | |
| acc[key] = []; | |
| } | |
| acc[key].push(event); | |
| return acc; | |
| }, {}); | |
| } | |
| renderMonth(monthKey, events) { | |
| const [year, month] = monthKey.split('-'); | |
| const monthName = MONTHS[parseInt(month)]; | |
| const eventsHtml = events | |
| .sort((a, b) => new Date(a.date) - new Date(b.date)) | |
| .map(event => this.renderEvent(event)) | |
| .join(''); | |
| return ` | |
| <div class="timeline-month" data-month="${monthKey}"> | |
| <span class="timeline-month-label">${monthName}</span> | |
| <div class="timeline-events"> | |
| ${eventsHtml} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| renderEvent(event) { | |
| const date = new Date(event.date); | |
| const day = date.getDate(); | |
| const eventIndex = this.sortedEvents.findIndex(e => e.id === event.id); | |
| const isSelected = eventIndex === this.selectedIndex; | |
| return ` | |
| <button | |
| class="timeline-event ${event.type} ${isSelected ? 'selected' : ''}" | |
| data-event-id="${event.id}" | |
| data-event-index="${eventIndex}" | |
| aria-label="${event.name} - ${day} ${MONTHS[date.getMonth()]}" | |
| > | |
| <span class="timeline-event-tooltip"> | |
| <strong>${event.name}</strong><br> | |
| ${day} ${MONTHS[date.getMonth()]} | |
| </span> | |
| </button> | |
| `; | |
| } | |
| attachEventListeners() { | |
| // Play button | |
| const playBtn = this.container.querySelector('.timeline-play-btn'); | |
| if (playBtn) { | |
| playBtn.addEventListener('click', () => this.togglePlay()); | |
| } | |
| // Event clicks | |
| this.container.querySelectorAll('.timeline-event').forEach(el => { | |
| el.addEventListener('click', () => { | |
| const eventIndex = parseInt(el.dataset.eventIndex, 10); | |
| this.selectEventByIndex(eventIndex); | |
| }); | |
| }); | |
| } | |
| togglePlay() { | |
| this.isPlaying = !this.isPlaying; | |
| this.updatePlayButton(); | |
| this.onPlayStateChange(this.isPlaying); | |
| } | |
| play() { | |
| this.isPlaying = true; | |
| this.updatePlayButton(); | |
| } | |
| pause() { | |
| this.isPlaying = false; | |
| this.updatePlayButton(); | |
| } | |
| updatePlayButton() { | |
| const playBtn = this.container.querySelector('.timeline-play-btn'); | |
| if (playBtn) { | |
| playBtn.classList.toggle('playing', this.isPlaying); | |
| playBtn.setAttribute('aria-label', this.isPlaying ? 'Pause' : 'Play'); | |
| } | |
| } | |
| selectEventByIndex(index) { | |
| if (index < 0 || index >= this.sortedEvents.length) return; | |
| this.selectedIndex = index; | |
| const event = this.sortedEvents[index]; | |
| // Update visual selection | |
| this.container.querySelectorAll('.timeline-event').forEach(el => { | |
| const elIndex = parseInt(el.dataset.eventIndex, 10); | |
| el.classList.toggle('selected', elIndex === index); | |
| }); | |
| // Scroll event into view | |
| const eventEl = this.container.querySelector(`[data-event-index="${index}"]`); | |
| if (eventEl) { | |
| eventEl.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); | |
| } | |
| this.onEventSelect(event, index); | |
| } | |
| selectEvent(eventId) { | |
| const index = this.sortedEvents.findIndex(e => e.id === eventId); | |
| if (index !== -1) { | |
| this.selectEventByIndex(index); | |
| } | |
| } | |
| // Advance to next event, returns false if at end | |
| nextEvent() { | |
| if (this.selectedIndex < this.sortedEvents.length - 1) { | |
| this.selectEventByIndex(this.selectedIndex + 1); | |
| return true; | |
| } | |
| return false; | |
| } | |
| getCurrentEvent() { | |
| if (this.selectedIndex >= 0 && this.selectedIndex < this.sortedEvents.length) { | |
| return this.sortedEvents[this.selectedIndex]; | |
| } | |
| return null; | |
| } | |
| getNextEvent() { | |
| if (this.selectedIndex >= 0 && this.selectedIndex < this.sortedEvents.length - 1) { | |
| return this.sortedEvents[this.selectedIndex + 1]; | |
| } | |
| return null; | |
| } | |
| clearSelection() { | |
| this.selectedIndex = -1; | |
| this.container.querySelectorAll('.timeline-event').forEach(el => { | |
| el.classList.remove('selected'); | |
| }); | |
| } | |
| setOnEventSelect(callback) { | |
| this.onEventSelect = callback; | |
| } | |
| setOnPlayStateChange(callback) { | |
| this.onPlayStateChange = callback; | |
| } | |
| } | |