Initial commit: Digital signage system for transit departures, weather, and news ticker

This commit is contained in:
2025-12-31 13:53:19 +01:00
commit a0c997f7d4
21 changed files with 5320 additions and 0 deletions

297
ticker.js Normal file
View File

@@ -0,0 +1,297 @@
/**
* ticker.js - A modular ticker component for displaying RSS feed content
* Fetches and displays content from an RSS feed in a scrolling ticker at the bottom of the page
*/
class TickerManager {
constructor(options = {}) {
// Default options
this.options = {
elementId: 'ticker-container',
scrollSpeed: 60, // Animation duration in seconds
maxItems: 10, // Maximum number of items to display
...options
};
// Create ticker container immediately
this.createTickerContainer();
// State
this.items = [];
this.isScrolling = false;
this.animationFrameId = null;
// Initialize RSS manager
this.rssManager = new RssManager();
// Subscribe to RSS updates
this.rssManager.onUpdate(items => {
this.items = items.slice(0, this.options.maxItems);
this.updateTicker();
});
// Initialize
this.init();
}
/**
* Create the ticker container
*/
createTickerContainer() {
// Create container if it doesn't exist
if (!document.getElementById(this.options.elementId)) {
console.log('Creating ticker container');
const container = document.createElement('div');
container.id = this.options.elementId;
container.className = 'ticker-container';
// Create ticker content
const tickerContent = document.createElement('div');
tickerContent.className = 'ticker-content';
container.appendChild(tickerContent);
// Add to document
document.body.appendChild(container);
// Add styles
this.addTickerStyles();
}
}
/**
* Initialize the ticker
*/
init() {
console.log('Initializing TickerManager...');
try {
// Set initial scroll speed
this.setScrollSpeed(this.options.scrollSpeed);
// Add initial loading message
const tickerContent = document.querySelector(`#${this.options.elementId} .ticker-content`);
if (tickerContent) {
tickerContent.innerHTML = '<div class="ticker-item">Loading news...</div>';
}
// Ensure ticker is visible
const container = document.getElementById(this.options.elementId);
if (container) {
container.style.display = 'block';
}
console.log('TickerManager initialized successfully');
} catch (error) {
console.error('Error initializing TickerManager:', error);
}
}
/**
* Add ticker styles
*/
addTickerStyles() {
// Check if styles already exist
if (!document.getElementById('ticker-styles')) {
const styleElement = document.createElement('style');
styleElement.id = 'ticker-styles';
// Define styles
styleElement.textContent = `
.ticker-container {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: linear-gradient(to right, #3c3b6e, #b22234, #ffffff);
color: white;
overflow: hidden;
height: 40px;
z-index: 100;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2);
}
body.vertical .ticker-container {
transform: rotate(90deg);
transform-origin: left bottom;
width: 100vh;
position: fixed;
bottom: 0;
left: 0;
height: 40px;
}
body.vertical-reverse .ticker-container {
transform: rotate(-90deg);
transform-origin: right bottom;
width: 100vh;
position: fixed;
bottom: 0;
right: 0;
height: 40px;
}
body.upsidedown .ticker-container {
transform: rotate(180deg);
}
.ticker-content {
display: flex;
align-items: center;
height: 100%;
white-space: nowrap;
position: absolute;
left: 0;
transform: translateX(100%);
}
.ticker-item {
display: inline-block;
padding: 0 30px;
color: white;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
.ticker-item:nth-child(3n+1) {
background-color: rgba(178, 34, 52, 0.7); /* Red */
}
.ticker-item:nth-child(3n+2) {
background-color: rgba(255, 255, 255, 0.7); /* White */
color: #3c3b6e; /* Dark blue text for readability */
text-shadow: none;
}
.ticker-item:nth-child(3n+3) {
background-color: rgba(60, 59, 110, 0.7); /* Blue */
}
@keyframes ticker-scroll {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}
/* Animation direction for different orientations */
body.vertical .ticker-content {
animation-direction: reverse; /* Reverse for vertical to maintain readability */
}
body.upsidedown .ticker-content {
animation-direction: reverse; /* Reverse for upside down to maintain readability */
}
/* Dark mode styles */
body.dark-mode .ticker-container {
background: linear-gradient(to right, #1a1a4f, #8b1a29, #e6e6e6);
}
body.dark-mode .ticker-item:nth-child(3n+1) {
background-color: rgba(139, 26, 41, 0.7); /* Darker Red */
}
body.dark-mode .ticker-item:nth-child(3n+2) {
background-color: rgba(230, 230, 230, 0.7); /* Off-White */
color: #1a1a4f; /* Darker blue text */
}
body.dark-mode .ticker-item:nth-child(3n+3) {
background-color: rgba(26, 26, 79, 0.7); /* Darker Blue */
}
`;
// Add to document
document.head.appendChild(styleElement);
}
}
/**
* Set the ticker scroll speed
* @param {number} speed - Speed in seconds for one complete scroll cycle
*/
setScrollSpeed(speed) {
this.options.scrollSpeed = speed;
const container = document.getElementById(this.options.elementId);
if (container) {
// Reset animation by removing and re-adding content
const content = container.querySelector('.ticker-content');
if (content) {
const clone = content.cloneNode(true);
container.style.setProperty('--ticker-speed', `${speed}s`);
content.remove();
container.appendChild(clone);
}
console.log(`Ticker speed set to ${speed} seconds`);
}
}
/**
* Update ticker content
*/
updateTicker() {
console.log('Updating ticker content...');
const tickerContent = document.querySelector(`#${this.options.elementId} .ticker-content`);
if (tickerContent) {
// Clear existing content
tickerContent.innerHTML = '';
if (this.items.length === 0) {
console.log('No items to display in ticker');
const tickerItem = document.createElement('div');
tickerItem.className = 'ticker-item';
tickerItem.textContent = 'Loading news...';
tickerContent.appendChild(tickerItem);
return;
}
console.log(`Adding ${this.items.length} items to ticker`);
// Add items
this.items.forEach((item, index) => {
const tickerItem = document.createElement('div');
tickerItem.className = 'ticker-item';
// Create link if available, using displayText or falling back to title
if (item.link) {
const link = document.createElement('a');
link.href = item.link;
link.target = '_blank';
link.textContent = item.displayText || item.title;
link.style.color = 'inherit';
link.style.textDecoration = 'none';
tickerItem.appendChild(link);
} else {
tickerItem.textContent = item.displayText || item.title;
}
tickerContent.appendChild(tickerItem);
});
console.log('Ticker content updated successfully');
// Calculate total width of content
const totalWidth = Array.from(tickerContent.children)
.reduce((width, item) => width + item.offsetWidth, 0);
// Calculate animation duration based on content width
const duration = Math.max(totalWidth / 100, this.options.scrollSpeed);
// Reset and start animation
tickerContent.style.animation = 'none';
tickerContent.offsetHeight; // Force reflow
tickerContent.style.animation = `ticker-scroll ${duration}s linear infinite`;
console.log(`Animation duration set to ${duration}s based on content width ${totalWidth}px`);
} else {
console.error('Ticker content element not found');
}
}
}
// Export the TickerManager class for use in other modules
window.TickerManager = TickerManager;