Files
test/news2.html
2025-11-27 22:25:36 +00:00

917 lines
39 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.13.1/jquery-ui.js" integrity="sha256-6XMVI0zB8cRzfZjqKcD01PBsAy3FlDASrlC8SxCpInY=" crossorigin="anonymous"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RLX News</title>
<style>
/* --- Flexbox Sticky Footer --- */
html {
height: 100%;
}
body {
min-height: 100%;
display: flex;
flex-direction: column;
margin: 0;
}
html, body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
padding: 10px;
box-sizing: border-box;
padding-bottom: 0;
}
#full-display-container, #sad-display-area {
flex-grow: 1;
}
.form-container {
max-width: 800px;
position: relative;
padding: 15px;
background: white;
margin-bottom: 10px;
flex-shrink: 0;
}
.toggle-btn, button { font-size: 20px; padding: 8px 15px; cursor: pointer; }
button { background-color: #4CAF50; color: white; border: none; border-radius: 4px; }
button:hover { background-color: #45a049; }
.form-group label { font-size: 20px; margin-bottom: 5px; display: block; }
input, textarea { width: 100%; padding: 10px; font-size: 18px; box-sizing: border-box; }
#full-display-container {
max-width: 100%;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
}
#sad-display-area {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 10px;
min-height: 0;
}
#top-stories-container, #bottom-stories-container {
flex-grow: 1;
overflow: hidden;
min-height: 0;
}
#top-stories-container { flex-basis: 50%; flex-shrink: 0; }
#bottom-stories-container { background: #e9e9e9; padding: 5px; border-radius: 8px; }
.scroller-inner {
animation-name: continuous-scroll;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.content-block {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
}
@keyframes continuous-scroll {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
}
.news-item { background: white; padding: 5px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); display: flex; align-items: center; }
.news-item img { width: 150px; height: 100px; object-fit: cover; border-radius: 6px; margin-right: 10px; }
.news-content { flex: 2; }
.headline { color: #333; font-size: 36px; margin: 0 0 5px 0; line-height: 1.0; font-weight: bold; }
.summary { color: #666; font-size: 28px; margin: 0 0 5px 0; line-height: 1.0; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; line-clamp: 4; }
.storylink { color: #007BFF; text-decoration: none; }
.storylink:hover { text-decoration: underline; }
.relevance-high { background-color: lightblue; }
.relevance-really-high { background-color: cyan; }
.relevance-super-high { background-color: yellow; }
.relevance-mazza-high { background-color: orange; }
.relevance-cheech-high { background-color: #FF8790; }
@keyframes flashRedOutline {
0% { outline: 7px solid red; }
50% { outline: 7px solid transparent; }
100% { outline: 10px solid red; }
}
.new-story-flash {
animation: flashRedOutline 2s linear infinite;
border-radius: 8px; /* Match the news-item border-radius */
}
#ticker-container {
width: 100%;
background-color: black;
overflow: hidden;
padding: 5px 0;
box-sizing: border-box;
z-index: 1000;
flex-shrink: 0;
}
#ticker-content {
display: inline-block;
white-space: nowrap;
/* animation removed, now handled by JS */
}
#ticker-content > span {
margin: 0 20px;
font-size: 30px;
}
.ticker-year { color: lightgray; }
.ticker-event { color: white; }
.ticker-report { color: lightblue; }
.ticker-wikimedia { color: lightpink; } /* Style for Wikimedia events */
.ticker-holiday { color: lightgreen; } /* Style for holiday events */
.ticker-upcoming { color: cyan; } /* Style for upcoming events */
@media (max-width: 768px) {
#full-display-container {
grid-template-columns: 1fr;
}
.content-block {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="form-container" id="form-container">
<button class="toggle-btn" onclick="toggleForm();">Expand News Search Dialog</button>
<div id="searchForm" style="display: none;">
<div class="form-group"><label for="startTime">Start Time (Zulu/UTC):</label><input type="datetime-local" id="startTime" name="startTime" required></div>
<div class="form-group"><label for="endTime">End Time (Zulu/UTC):</label><input type="datetime-local" id="endTime" name="endTime" required></div>
<div class="form-group"><label for="keyTerms">Key Terms (comma-separated):</label><textarea id="keyTerms" name="keyTerms" rows="3" required>weather,flood,fire,fog,snow,emergency,wind,ice,rain,power,explosion,warmer,colder,drown,stream,river,air,wind,destroyed,rime,glaze,river,ice,creek,crash,thunder,spinup,black ice,fog,spill,pileup,pile-up,gust,frozen,funnel,rainfall,fatal,injury,sleet,injured,frost,dead,death,landslide,culvert,slippery,wildfire,tornado,blizzard,creek,hail,thunderstorm,downburst,microburst,crash,heatstroke,derecho,lightning,hypothermia,slide,flow,ski,water,innundation,victim,victims,flooding,flooded,snowing,freezing rain,clouds,cloud,storm,aircraft</textarea></div>
<button onclick="updatenews();">Submit</button>
</div>
</div>
<div id="full-display-container"></div>
<div class="display-area" id="sad-display-area" style="display: none;">
<div id="top-stories-container"></div>
<div id="bottom-stories-container"></div>
</div>
<div id="ticker-container" style="display: none;">
<div id="ticker-content"></div>
</div>
<script>
let lastTickerData = null;
let isSadMode = false;
var refreshTimer;
var tickerWatchdogTimer;
const NUM_TOP_STORIES = 8;
const BOTTOM_SCROLLER_SPEED_MULTIPLIER = 2;
window.currentWikimediaEvents = []; // Initialize the global variable
let tickerCycleCount = 0;
let upcomingHolidays = [];
let triviaQuestions = [];
let lastTriviaFetchTime = 0;
// Always fetch fresh Wikimedia events
// No need to cache them between cycles
const TARGET_BROADCAST_SECONDS = 150;
const PIXELS_PER_SECOND_SPEED = 150; // Adjust this value to control scroll speed. Higher is faster.
let lastTickerUpdateTime = Date.now();
let lastNewsData = null;
let currentNewsUrl = 'https://wx.stoat.org/lsr.php?news3=potato';
let nextTickerHtml = null;
let animationId = null;
let tickerPosition = 0;
let lastTime = 0;
let tickerContent = document.getElementById('ticker-content');
let injectionHtml = null;
function animateTicker(currentTime) {
if (!lastTime) lastTime = currentTime;
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
tickerPosition -= PIXELS_PER_SECOND_SPEED * deltaTime;
const scrollWidth = tickerContent.scrollWidth;
if (tickerPosition <= -scrollWidth) {
updateTickerContent();
tickerPosition = 0;
}
tickerContent.style.transform = `translateX(${tickerPosition}px)`;
animationId = requestAnimationFrame(animateTicker);
}
function updateTickerContent() {
if (nextTickerHtml) {
tickerContent.innerHTML = nextTickerHtml;
const containerWidth = document.getElementById('ticker-container').clientWidth;
tickerPosition = -containerWidth;
nextTickerHtml = null;
// Fetch new
fetchAndDisplayTickerData(false);
}
}
function fetchAndDisplayNews(url = 'https://wx.stoat.org/lsr.php?news3=potato') {
// Update the current URL if provided
if (url) {
currentNewsUrl = url;
}
$.getJSON(currentNewsUrl, function(newsData) {
// Sort the data
newsData.sort((a, b) => {
if (b.impact_score !== a.impact_score) return b.impact_score - a.impact_score;
return new Date(b.timeutc) - new Date(a.timeutc);
});
// Check if data has changed
if (lastNewsData && JSON.stringify(newsData) === JSON.stringify(lastNewsData)) {
console.log('News data unchanged, skipping update');
return;
}
// Update cache
lastNewsData = newsData;
if (isSadMode) {
const topContainer = document.getElementById('top-stories-container');
const bottomContainer = document.getElementById('bottom-stories-container');
topContainer.innerHTML = '';
bottomContainer.innerHTML = '';
const topStories = newsData.slice(0, NUM_TOP_STORIES);
const scrollingStories = newsData.slice(NUM_TOP_STORIES);
function createScroller(stories, durationMultiplier, isBottomScroller = false) {
if (stories.length === 0) return null;
const scrollerInner = document.createElement('div');
scrollerInner.className = 'scroller-inner';
const contentBlock1 = document.createElement('div');
contentBlock1.className = 'content-block';
stories.forEach(news => contentBlock1.appendChild(createNewsItem(news)));
const contentBlock2 = contentBlock1.cloneNode(true);
contentBlock2.setAttribute('aria-hidden', 'true');
scrollerInner.appendChild(contentBlock1);
scrollerInner.appendChild(contentBlock2);
const duration = stories.length * durationMultiplier;
scrollerInner.style.animationName = 'continuous-scroll';
scrollerInner.style.animationDuration = `${duration}s`;
// Ensure no delay is applied to any scroller
scrollerInner.style.animationDelay = '0s';
return scrollerInner;
}
const topScroller = createScroller(topStories, 7, false);
if (topScroller) topContainer.appendChild(topScroller);
const bottomScroller = createScroller(scrollingStories, BOTTOM_SCROLLER_SPEED_MULTIPLIER, true);
if (bottomScroller) bottomContainer.appendChild(bottomScroller);
} else {
const fullContainer = document.getElementById('full-display-container');
fullContainer.innerHTML = '';
newsData.forEach(news => fullContainer.appendChild(createNewsItem(news)));
}
});
}
function createNewsItem(news) {
const newsItem = document.createElement('div');
newsItem.className = 'news-item';
let score = news.impact_score;
const storyTime = new Date(news.timeutc);
const currentTime = new Date();
const oneHourInMs = 3600000;
// Add flashRedOutline class if the story is less than 1 hour old
if (currentTime - storyTime < oneHourInMs) {
newsItem.classList.add('new-story-flash');
}
const relevanceClasses = {
'relevance-high': score > 15,
'relevance-really-high': score > 25,
'relevance-super-high': score > 50,
'relevance-mazza-high': score > 90,
'relevance-cheech-high': score > 150
};
Object.entries(relevanceClasses).filter(([, c]) => c).forEach(([cN]) => newsItem.classList.add(cN));
newsItem.innerHTML = `<a href="${news.storylink}" target="_blank"><img src="${news.imageurl}"></a><div class="news-content"><h2 class="headline"><a href="${news.storylink}" target="_blank" class="storylink">(${extractTextBetweenHttpAndCom(news.storylink)}) ${news.headline}</a></h2><p class="summary">${news.summary} ${convertPostgresTimestamp(news.timeutc)}L</p></div>`;
return newsItem;
}
function extractTextBetweenHttpAndCom(url) {
url = url.replace(/www\./, '');
const match = url.match(/https?:\/\/(.*?)\.com/);
return match && match[1] ? match[1].toUpperCase() : 'FAUXNEWS';
}
function convertPostgresTimestamp(timestamp) {
const d = new Date(timestamp.replace('Z', ''));
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
}
function toggleForm() {
const formContent = document.getElementById("searchForm");
const toggleBtn = document.querySelector(".toggle-btn");
if (formContent.style.display === "none") {
formContent.style.display = "block";
toggleBtn.textContent = "Collapse News Search Dialog";
if (refreshTimer) clearInterval(refreshTimer);
// Stop ticker when form is expanded (not in SAD mode)
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
if (tickerWatchdogTimer) {
clearInterval(tickerWatchdogTimer);
tickerWatchdogTimer = null;
}
// Hide ticker when form is expanded
updateTickerVisibility();
} else {
formContent.style.display = "none";
toggleBtn.textContent = "Expand News Search Dialog";
// Always use the current URL
fetchAndDisplayNews(currentNewsUrl);
refreshTimer = setInterval(() => {
fetchAndDisplayNews(currentNewsUrl);
}, 300000);
// Update ticker visibility based on mode
updateTickerVisibility();
}
}
function completelyHide() { document.getElementById("form-container").style.display = "none"; }
function updatenews() {
const start = document.getElementById("startTime").value;
const end = document.getElementById("endTime").value;
const keyTerms = document.getElementById("keyTerms").value;
const terms = keyTerms.split(',');
let arrayterms = terms.map(term => `key[]=${encodeURIComponent(term)}`).join('&');
url = `lsr.php?newsarchive=true&start=${start}&end=${end}&${arrayterms}`;
// Clear the cache to force an update
lastNewsData = null;
fetchAndDisplayNews(url);
}
function sadCheck() {
const params = new URLSearchParams(document.location.search);
if (params.has("sad") || params.has("SAD")) {
isSadMode = true;
completelyHide();
Object.assign(document.documentElement.style, {height: '100%'});
Object.assign(document.body.style, {height: '100%', overflow: 'hidden', display: 'flex', flexDirection: 'column'});
document.getElementById('sad-display-area').style.display = 'flex';
document.getElementById('full-display-container').style.display = 'none';
// Update ticker visibility and start when entering SAD mode
updateTickerVisibility();
fetchAndDisplayTickerData(true);
startTickerWatchdog();
}
}
function format_date_with_ordinal(date) {
const day = date.getDate();
const month = date.toLocaleString('default', { month: 'long' });
const get_ordinal_suffix = (day) => {
if (day > 3 && day < 21) return 'th';
switch (day % 10) {
case 1: return "st";
case 2: return "nd";
case 3: return "rd";
default: return "th";
}
};
const suffix = get_ordinal_suffix(day);
return `${month} ${day}${suffix}`;
}
function fetchWikimediaEvents() {
const now = new Date();
// Get the individual components
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed, so add 1
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
// Concatenate them into the final string
const formattedDate = `${year}${month}${day}${hours}`;
// Format today's date as YYYY-MM-DD to match the API response keys
const todayFormatted = `${year}-${month}-${day}`;
const url = 'https://wx.stoat.org/calendar/wikimedia_onthisday.json';
const cacheBustingUrl = url; //`${url}?v=${formattedDate}`;
return $.getJSON(cacheBustingUrl)
.done(function(data) {
if (data && typeof data === 'object') {
// Collect events only from today
let allEvents = [];
// Add today's events
if (data[todayFormatted] && data[todayFormatted].events && Array.isArray(data[todayFormatted].events)) {
allEvents = allEvents.concat(data[todayFormatted].events);
}
if (allEvents.length > 0) {
// Always shuffle and use all events
window.currentWikimediaEvents = [...allEvents].sort(() => 0.5 - Math.random());
} else {
console.warn("No Wikimedia events found for today.");
window.currentWikimediaEvents = [];
}
} else {
console.warn("Wikimedia JSON is empty or invalid.");
window.currentWikimediaEvents = [];
}
})
.fail(function(jqXHR, textStatus, errorThrown) {
console.error(`Failed to load from ${cacheBustingUrl}. Status: ${textStatus}, Error: ${errorThrown}`);
window.currentWikimediaEvents = [];
});
}
function fetchHolidays() {
const now = new Date();
// Format today's date as YYYY-MM-DD to match the API response keys
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const todayFormatted = `${year}-${month}-${day}`;
// Get the individual components for cache busting
const hours = String(now.getHours()).padStart(2, '0');
const formattedDate = `${year}${month}${day}${hours}`;
const url = 'https://calendar.wx4rlx.org/get_holidays.py';
const cacheBustingUrl = `${url}?time=${formattedDate}`;
return $.getJSON(cacheBustingUrl)
.done(function(data) {
if (data && data.holidays) {
// Get holidays for today using the formatted date as the key
window.currentHolidays = data.holidays[todayFormatted] || [];
// Store upcoming holidays for the next 9 days
upcomingHolidays = [];
for (let i = 1; i <= 9; i++) {
const nextDate = new Date(now);
nextDate.setDate(now.getDate() + i);
const nextYear = nextDate.getFullYear();
const nextMonth = String(nextDate.getMonth() + 1).padStart(2, '0');
const nextDay = String(nextDate.getDate()).padStart(2, '0');
const nextFormatted = `${nextYear}-${nextMonth}-${nextDay}`;
const nextHolidays = data.holidays[nextFormatted] || [];
if (nextHolidays.length > 0) {
upcomingHolidays.push({
date: nextDate,
holidays: nextHolidays
});
}
}
} else {
console.warn("Holidays JSON is empty, invalid, or does not contain 'holidays'.");
window.currentHolidays = [];
upcomingHolidays = [];
}
})
.fail(function(jqXHR, textStatus, errorThrown) {
console.error(`Failed to load holidays from ${cacheBustingUrl}. Status: ${textStatus}, Error: ${errorThrown}`);
window.currentHolidays = [];
upcomingHolidays = [];
});
}
function fetchTriviaQuestions() {
const now = Date.now();
// Check if we need to fetch new questions and respect the 5-second rate limit
if (triviaQuestions.length > 10 || now - lastTriviaFetchTime < 5000) {
return $.Deferred().resolve().promise();
}
lastTriviaFetchTime = now;
const url = 'https://opentdb.com/api.php?amount=50&type=multiple';
return $.getJSON(url)
.done(function(data) {
if (data.response_code === 0 && data.results) {
triviaQuestions = data.results;
console.log(`Fetched ${triviaQuestions.length} trivia questions`);
} else {
console.warn('Trivia API returned non-zero response code or no results');
triviaQuestions = [];
}
})
.fail(function(jqXHR, textStatus, errorThrown) {
console.error(`Failed to fetch trivia questions: ${textStatus}, ${errorThrown}`);
triviaQuestions = [];
});
}
function fetchInjection() {
const injectionApiUrl = 'https://calendar.wx4rlx.org/onetime.py?action=api';
return $.ajax({
url: injectionApiUrl,
dataType: 'text' // Treat the response as plain text
})
.done(function(data) {
if (data && data.trim().length > 0) {
injectionHtml = data;
console.log('Injection content fetched:', injectionHtml);
} else {
injectionHtml = null;
}
})
.fail(function() {
console.log('No injection content available or error fetching');
injectionHtml = null;
});
}
function fetchAndDisplayTickerData(startImmediately = true) {
// First, fetch the injection content
fetchInjection().always(function() {
// Then fetch other data
$.when(fetchWikimediaEvents(), fetchHolidays(), fetchTriviaQuestions()).always(function() {
const tickerApiUrl = 'https://calendar.wx4rlx.org/?action=api';
$.getJSON(tickerApiUrl, function(data) {
if (data.status !== 'success') return;
updateTickerLastUpdateTime();
// Always update the ticker with fresh data
const today = new Date();
const currentYear = today.getFullYear();
const formatted_date = format_date_with_ordinal(today);
const tickerContent = $('#ticker-content');
let localItems = [];
data.events.forEach(item => localItems.push({ date: item.date, text: item.event, type: 'event' }));
data.weather_reports.forEach(item => localItems.push({ date: item.date, text: item.report, type: 'report' }));
// Add xmacis records
if (data.xmacis_records) {
data.xmacis_records.forEach(item => {
// Extract year from the date field
const year = parseInt(item.date.split('-')[0]);
localItems.push({
date: item.date,
text: item.description,
type: 'report', // Use 'report' type to match weather_reports color scheme
year: year
});
});
}
// Increment cycle count
tickerCycleCount++;
// Add upcoming events based on cycle count
let upcomingEventItem = null;
if (upcomingHolidays.length > 0) {
// Every 10 cycles takes priority over every other cycle
if (tickerCycleCount % 10 === 0) {
const nextFiveDays = upcomingHolidays.slice(0, 5);
let upcomingTexts = [];
nextFiveDays.forEach(day => {
const formattedDay = format_date_with_ordinal(day.date);
upcomingTexts.push(`${formattedDay} - ${day.holidays.join(', ')}`);
});
if (upcomingTexts.length > 0) {
upcomingEventItem = {
date: today.toISOString().split('T')[0],
text: 'Upcoming Special Days: ' + upcomingTexts.join('; '),
type: 'upcoming',
year: 'Upcoming'
};
}
}
// Only show tomorrow's events if it's an even cycle AND not a multiple of 10
else if (tickerCycleCount % 2 === 0) {
const tomorrow = upcomingHolidays[0];
if (tomorrow) {
upcomingEventItem = {
date: tomorrow.date.toISOString().split('T')[0],
text: 'Upcoming Special Days: Tomorrow - ' + tomorrow.holidays.join(', '),
type: 'upcoming',
year: 'Tomorrow'
};
}
}
}
// Add today's holidays to local items
if (window.currentHolidays && window.currentHolidays.length > 0) {
localItems.push({
date: today.toISOString().split('T')[0],
text: 'Special Days: ' + window.currentHolidays.join(', '),
type: 'holiday',
year: 'Today'
});
}
// Add injection HTML at the beginning if available
// Use the injectionHtml that was fetched at the start of this function
if (injectionHtml) {
localItems.unshift({
date: today.toISOString().split('T')[0],
text: injectionHtml,
type: 'injection',
year: 'INJECTION'
});
}
// Add upcoming event at the very end if it exists
if (upcomingEventItem) {
localItems.push(upcomingEventItem);
}
// Sort items by year
localItems.sort((a, b) => {
const getYear = (item) => {
if (item.year !== undefined) {
// Handle 'INJECTION', 'Today', 'Tomorrow', 'Upcoming' etc.
if (isNaN(Number(item.year))) return Infinity;
return Number(item.year);
}
return Number(item.date.split('-')[0]);
};
const yearA = getYear(a);
const yearB = getYear(b);
if (isNaN(yearA) && isNaN(yearB)) return 0;
if (isNaN(yearA)) return 1;
if (isNaN(yearB)) return -1;
return yearA - yearB;
});
// Calculate duration of local items
const tempLocalHtml = buildTickerHtml(localItems, currentYear);
tickerContent.html(tempLocalHtml);
const localWidth = tickerContent[0].scrollWidth;
const localDuration = localWidth / PIXELS_PER_SECOND_SPEED;
// Determine number of Wikimedia items to add
let numToSprinkle = 0;
if (window.currentWikimediaEvents && window.currentWikimediaEvents.length > 0) {
// Always include at least one item
numToSprinkle = 1;
// Calculate average width per Wikimedia item using the first few events
const sampleEvents = window.currentWikimediaEvents.slice(0, Math.min(5, window.currentWikimediaEvents.length));
let totalWidth = 0;
sampleEvents.forEach(event => {
const tempWikiItem = { date: `${event.year}-01-01`, text: event.text, type: 'wikimedia', year: event.year };
tickerContent.html(buildTickerHtml([tempWikiItem], currentYear));
totalWidth += tickerContent[0].scrollWidth;
});
const avgWikiWidth = totalWidth / sampleEvents.length;
const timePerWikiItem = avgWikiWidth / PIXELS_PER_SECOND_SPEED;
// Add more items if there's time
const durationGap = TARGET_BROADCAST_SECONDS - localDuration;
if (timePerWikiItem > 0 && durationGap > timePerWikiItem) {
const additionalItems = Math.floor((durationGap - timePerWikiItem) / timePerWikiItem);
numToSprinkle += Math.max(0, additionalItems);
}
numToSprinkle = Math.min(numToSprinkle, window.currentWikimediaEvents.length);
}
// Add Wikimedia items to local items
if (numToSprinkle > 0 && window.currentWikimediaEvents && window.currentWikimediaEvents.length > 0) {
const eventsToAdd = window.currentWikimediaEvents.slice(0, numToSprinkle);
eventsToAdd.forEach(event => {
localItems.push({ date: `${event.year}-01-01`, text: event.text, type: 'wikimedia', year: event.year });
});
// Re-sort with the new items
localItems.sort((a, b) => {
const getYear = (item) => {
if (item.year !== undefined) {
if (isNaN(Number(item.year))) return Infinity;
return Number(item.year);
}
return Number(item.date.split('-')[0]);
};
const yearA = getYear(a);
const yearB = getYear(b);
if (isNaN(yearA) && isNaN(yearB)) return 0;
if (isNaN(yearA)) return 1;
if (isNaN(yearB)) return -1;
return yearA - yearB;
});
}
const finalContentHtml = buildTickerHtml(localItems, currentYear, formatted_date);
// Set the content
tickerContent.html(finalContentHtml);
nextTickerHtml = finalContentHtml; // For next update
if (startImmediately) {
if (!animationId) {
animationId = requestAnimationFrame(animateTicker);
}
// Clear injectionHtml after using it to prevent reuse in next cycle
injectionHtml = null;
// Fetch new for next cycle, which will fetch a new injection
fetchAndDisplayTickerData(false);
}
}).fail(function() {
console.error("Failed to fetch data for the horizontal ticker.");
});
});
});
}
function buildTickerHtml(items, currentYear, formatted_date) {
let contentHtml = `<span style="display: inline-block; width: 100vw;"></span>`;
// First, add injection items at the very beginning
const injectionItems = items.filter(item => item.type === 'injection');
injectionItems.forEach(item => {
contentHtml += `<span>${item.text}</span>`;
});
// Add the "On This Day" header
if (formatted_date) {
contentHtml += `<span><span class="ticker-event">On This Day, ${formatted_date}:</span></span>`;
}
// Add all other items (excluding injection items which we've already added)
items.filter(item => item.type !== 'injection').forEach(item => {
const year = item.year || parseInt(item.date.split('-')[0]);
let textClass = `ticker-${item.type}`;
// Add a specific class for holidays and upcoming events
if (item.type === 'holiday') {
textClass = 'ticker-holiday';
} else if (item.type === 'upcoming') {
textClass = 'ticker-upcoming';
}
const yearDiff = currentYear - year;
let anniversaryPrefix = '';
// Only show anniversary for positive year differences (past events)
// Skip for holiday type
if (item.type !== 'holiday' && yearDiff > 0 && yearDiff % 5 === 0) {
anniversaryPrefix = `<span style="color: yellow; font-weight: bold;">${yearDiff} Years Ago: </span>`;
}
let itemText = item.text;
let yearText = year;
const arbitraryLength = 500;
if (item.text.length > arbitraryLength) {
const mazzaImgTag = '<img src="mazza.png" alt="Mazza" style="height: 1.2em; vertical-align: middle; margin: 0 0.3em;">';
const imageCount = Math.floor((item.text.length - arbitraryLength) / 200);
const imageTags = mazzaImgTag.repeat(imageCount);
yearText = imageTags ? `${imageTags} ${year}` : year;
}
// For holidays and upcoming events, don't show the year prefix
if (item.type === 'holiday' || item.type === 'upcoming') {
contentHtml += `<span><span class="${textClass}">${itemText}</span></span>`;
} else {
contentHtml += `<span>${anniversaryPrefix}<span class="ticker-year">${yearText}:</span> <span class="${textClass}">${itemText}</span></span>`;
}
});
if (formatted_date) {
contentHtml += `<span><span class="ticker-event">Office/Local Event</span></span>`;
contentHtml += `<span><span class="ticker-wikimedia">World Event</span></span>`;
contentHtml += `<span><span class="ticker-report">Local Weather Event</span></span>`;
// Add trivia question if available
if (triviaQuestions.length > 0) {
const trivia = triviaQuestions.shift();
// Decode HTML entities in question and answers
const question = $('<div>').html(trivia.question).text();
const correctAnswer = $('<div>').html(trivia.correct_answer).text();
const allAnswers = [correctAnswer, ...trivia.incorrect_answers.map(ans => $('<div>').html(ans).text())];
// Shuffle answers
for (let i = allAnswers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[allAnswers[i], allAnswers[j]] = [allAnswers[j], allAnswers[i]];
}
// Build question with choices
let questionHtml = `<span style="color: gold;">TRIVIA: ${question} `;
const choices = ['A', 'B', 'C', 'D'];
allAnswers.forEach((answer, index) => {
questionHtml += `${choices[index]}) ${answer} `;
});
questionHtml += `</span>`;
contentHtml += `<span>${questionHtml}</span>`;
// Store the correct answer for later display
// Find which choice corresponds to the correct answer
const correctIndex = allAnswers.indexOf(correctAnswer);
const correctChoice = choices[correctIndex];
window.lastTriviaAnswer = { correctChoice, correctAnswer };
}
contentHtml += `<span><span class="ticker-event">Visit <b>calendar.wx4rlx.org</b> to make updates or see info for upcoming days!</span></span>`;
// Add trivia answer if available
if (window.lastTriviaAnswer) {
const { correctChoice, correctAnswer } = window.lastTriviaAnswer;
contentHtml += `<span><span style="color: gold;">ANSWER: ${correctChoice}) ${correctAnswer}</span></span>`;
// Clear the answer after displaying it
window.lastTriviaAnswer = null;
}
}
return contentHtml;
}
function startTickerWatchdog() {
// Clear any existing watchdog
if (tickerWatchdogTimer) {
clearInterval(tickerWatchdogTimer);
}
// Check every 30 seconds if the ticker hasn't updated in 3x the expected duration
tickerWatchdogTimer = setInterval(() => {
const timeSinceLastUpdate = Date.now() - lastTickerUpdateTime;
const maxAllowedTime = (TARGET_BROADCAST_SECONDS + 5) * 3 * 1000; // 3x expected duration in ms
if (timeSinceLastUpdate > maxAllowedTime) {
console.warn('Ticker watchdog triggered - forcing refresh');
fetchAndDisplayTickerData(true);
}
}, 30000);
}
function updateTickerLastUpdateTime() {
lastTickerUpdateTime = Date.now();
}
function updateTickerVisibility() {
const tickerContainer = document.getElementById('ticker-container');
if (isSadMode) {
tickerContainer.style.display = 'block';
} else {
tickerContainer.style.display = 'none';
// Stop animation and watchdog when hiding
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
if (tickerWatchdogTimer) {
clearInterval(tickerWatchdogTimer);
tickerWatchdogTimer = null;
}
}
}
// Debug function to print ticker info and force next cycle
window.debugTicker = function() {
console.log('=== TICKER DEBUG INFO ===');
console.log('Current cycle count:', tickerCycleCount);
console.log('Upcoming holidays:', upcomingHolidays);
console.log('Current Wikimedia events count:', window.currentWikimediaEvents ? window.currentWikimediaEvents.length : 0);
console.log('Current holidays:', window.currentHolidays);
console.log('Next ticker HTML length:', nextTickerHtml ? nextTickerHtml.length : 'null');
console.log('Current ticker content:', document.getElementById('ticker-content').innerHTML);
// Force next cycle
console.log('Forcing next ticker cycle...');
tickerCycleCount++;
fetchAndDisplayTickerData(true);
};
sadCheck();
toggleForm();
refreshTimer = setInterval(() => {
fetchAndDisplayNews(currentNewsUrl);
}, 300000);
fetchAndDisplayNews(currentNewsUrl);
// Show/hide ticker based on initial mode
updateTickerVisibility();
</script>
</body>
</html>