+
-
-
+
+
-
`).join('');
@@ -644,7 +551,7 @@ class ConfigManager {
try {
resultsContainer.style.display = 'block';
- resultsContainer.innerHTML = '
Searching...
';
+ resultsContainer.textContent = 'Searching...';
const response = await fetch(`/api/sites/search?q=${encodeURIComponent(query)}`);
@@ -656,35 +563,32 @@ class ConfigManager {
const data = await response.json();
if (!data.sites || data.sites.length === 0) {
- resultsContainer.innerHTML = '
No sites found. Try a different search term.
';
+ resultsContainer.textContent = 'No sites found. Try a different search term.';
return;
}
- resultsContainer.innerHTML = data.sites.map(site => `
-
-
${site.name}
-
ID: ${site.id}
-
- `).join('');
-
- // Add click handlers to search results
- resultsContainer.querySelectorAll('.site-search-result').forEach(result => {
- result.addEventListener('click', () => {
- const siteId = result.dataset.siteId;
- const siteName = result.dataset.siteName;
- this.addSiteFromSearch(siteId, siteName);
+ resultsContainer.innerHTML = '';
+ data.sites.forEach(site => {
+ const resultDiv = document.createElement('div');
+ resultDiv.className = 'site-search-result';
+ resultDiv.dataset.siteId = site.id;
+ resultDiv.dataset.siteName = site.name;
+
+ const nameDiv = document.createElement('div');
+ nameDiv.textContent = site.name;
+ const idDiv = document.createElement('div');
+ idDiv.textContent = `ID: ${site.id}`;
+
+ resultDiv.appendChild(nameDiv);
+ resultDiv.appendChild(idDiv);
+
+ resultDiv.addEventListener('click', () => {
+ this.addSiteFromSearch(site.id, site.name);
searchInput.value = '';
resultsContainer.style.display = 'none';
});
-
- result.addEventListener('mouseenter', () => {
- result.style.backgroundColor = '#f5f5f5';
- });
-
- result.addEventListener('mouseleave', () => {
- result.style.backgroundColor = 'white';
- });
+
+ resultsContainer.appendChild(resultDiv);
});
} catch (error) {
@@ -698,7 +602,7 @@ class ConfigManager {
errorMessage = `Server error: ${error.message}`;
}
- resultsContainer.innerHTML = `
Error: ${errorMessage}
`;
+ resultsContainer.textContent = `Error: ${errorMessage}`;
}
}
@@ -1159,5 +1063,8 @@ class ConfigManager {
}
}
-// Export the ConfigManager class for use in other modules
+// ES module export
+export { ConfigManager };
+
+// Keep window reference for backward compatibility
window.ConfigManager = ConfigManager;
diff --git a/public/js/components/DeparturesManager.js b/public/js/components/DeparturesManager.js
index f52147c..471d01f 100644
--- a/public/js/components/DeparturesManager.js
+++ b/public/js/components/DeparturesManager.js
@@ -424,7 +424,7 @@ class DeparturesManager {
this.updateExistingCards(departures);
}
- this.currentDepartures = JSON.parse(JSON.stringify(departures));
+ this.currentDepartures = structuredClone(departures);
}
/**
@@ -444,8 +444,8 @@ class DeparturesManager {
this.updateCardContent(existingCard, departure);
} else {
const newCard = this.createDepartureCard(departure);
- newCard.style.opacity = '0';
-
+ newCard.classList.add('card-entering');
+
if (index === 0) {
this.container.prepend(newCard);
} else if (index >= this.container.children.length) {
@@ -453,24 +453,22 @@ class DeparturesManager {
} else {
this.container.insertBefore(newCard, this.container.children[index]);
}
-
- setTimeout(() => {
- newCard.style.transition = 'opacity 0.5s ease-in';
- newCard.style.opacity = '1';
- }, 10);
+
+ requestAnimationFrame(() => {
+ newCard.classList.add('card-visible');
+ });
}
});
const newDepartureIds = newDepartures.map(d => d.journey.id.toString());
currentCards.forEach(card => {
if (!newDepartureIds.includes(card.dataset.journeyId)) {
- card.style.transition = 'opacity 0.5s ease-out';
- card.style.opacity = '0';
- setTimeout(() => {
- if (card.parentNode) {
- card.parentNode.removeChild(card);
- }
- }, 500);
+ card.classList.add('card-leaving');
+ card.addEventListener('transitionend', () => {
+ card.remove();
+ }, { once: true });
+ // Fallback removal if transitionend doesn't fire
+ setTimeout(() => card.remove(), 600);
}
});
}
@@ -505,13 +503,10 @@ class DeparturesManager {
* @param {HTMLElement} element - Element to highlight
*/
highlightElement(element) {
- element.style.transition = 'none';
- element.style.backgroundColor = 'rgba(255, 255, 0, 0.3)';
-
- setTimeout(() => {
- element.style.transition = 'background-color 1.5s ease-out';
- element.style.backgroundColor = 'transparent';
- }, 10);
+ element.classList.remove('highlight-flash');
+ // Force reflow to restart animation
+ void element.offsetWidth;
+ element.classList.add('highlight-flash');
}
/**
@@ -632,5 +627,8 @@ class DeparturesManager {
}
}
-// Export the class
+// ES module export
+export { DeparturesManager };
+
+// Keep window reference for backward compatibility
window.DeparturesManager = DeparturesManager;
diff --git a/public/js/components/WeatherManager.js b/public/js/components/WeatherManager.js
index 9f2b81a..f75e9e8 100644
--- a/public/js/components/WeatherManager.js
+++ b/public/js/components/WeatherManager.js
@@ -8,7 +8,7 @@ class WeatherManager {
constructor(options = {}) {
// Default options
// Get API key from options, window (injected by server from .env), or fallback
- const apiKey = options.apiKey || window.OPENWEATHERMAP_API_KEY || '4d8fb5b93d4af21d66a2948710284366';
+ const apiKey = options.apiKey || window.OPENWEATHERMAP_API_KEY || '';
this.options = {
latitude: options.latitude || (window.DEFAULT_LOCATION?.latitude) || 59.3293, // Stockholm latitude
@@ -24,6 +24,7 @@ class WeatherManager {
this.sunTimes = null;
this.isDarkMode = false;
this.lastUpdated = null;
+ this.daylightBarUpdateInterval = null;
// Initialize
this.init();
@@ -34,6 +35,23 @@ class WeatherManager {
*/
async init() {
try {
+ // Check for API key
+ if (!this.options.apiKey) {
+ console.warn('WeatherManager: No OpenWeatherMap API key configured. Set OPENWEATHERMAP_API_KEY in your .env file.');
+ const weatherContainer = document.getElementById('custom-weather');
+ if (weatherContainer) {
+ const warningEl = document.createElement('div');
+ warningEl.style.cssText = 'padding: 10px; color: #c41e3a; font-size: 0.9em; text-align: center;';
+ warningEl.textContent = 'Weather unavailable: No API key configured. Set OPENWEATHERMAP_API_KEY in .env';
+ weatherContainer.prepend(warningEl);
+ }
+ // Still set up sun times from calculation so dark mode works
+ await this.updateSunTimesFromCalculation();
+ this.updateDarkModeBasedOnTime();
+ this.dispatchDarkModeEvent();
+ return;
+ }
+
// Fetch weather data
await this.fetchWeatherData();
@@ -83,17 +101,17 @@ class WeatherManager {
*/
async fetchWeatherData() {
try {
- // Fetch current weather
- const currentWeatherUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${this.options.latitude}&lon=${this.options.longitude}&units=metric&appid=${this.options.apiKey}`;
+ // Fetch current weather (lang=se for Swedish descriptions)
+ const currentWeatherUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${this.options.latitude}&lon=${this.options.longitude}&units=metric&lang=se&appid=${this.options.apiKey}`;
const currentWeatherResponse = await fetch(currentWeatherUrl);
const currentWeatherData = await currentWeatherResponse.json();
-
+
if (currentWeatherData.cod !== 200) {
throw new Error(`API Error: ${currentWeatherData.message}`);
}
-
- // Fetch hourly forecast
- const forecastUrl = `https://api.openweathermap.org/data/2.5/forecast?lat=${this.options.latitude}&lon=${this.options.longitude}&units=metric&appid=${this.options.apiKey}`;
+
+ // Fetch 3-hour interval forecast (cnt=8 limits to ~24h of data)
+ const forecastUrl = `https://api.openweathermap.org/data/2.5/forecast?lat=${this.options.latitude}&lon=${this.options.longitude}&units=metric&lang=se&cnt=8&appid=${this.options.apiKey}`;
const forecastResponse = await fetch(forecastUrl);
const forecastData = await forecastResponse.json();
@@ -107,7 +125,7 @@ class WeatherManager {
this.lastUpdated = new Date();
// Extract sunrise and sunset times from the API response
- this.updateSunTimesFromApi(currentWeatherData);
+ await this.updateSunTimesFromApi(currentWeatherData);
// Update the UI with the new data
this.updateWeatherUI();
@@ -158,7 +176,7 @@ class WeatherManager {
* Process forecast data from API response
*/
processForecast(data) {
- // Get the next 7 forecasts (covering about 24 hours)
+ // Get the next 7 forecast periods (3-hour intervals, covering ~21 hours)
return data.list.slice(0, 7).map(item => {
const iconCode = item.weather[0].icon;
return {
@@ -375,6 +393,11 @@ class WeatherManager {
const sunsetTime = this.formatTime(this.sunTimes.today.sunset);
sunTimesElement.textContent = `βοΈ Sunrise: ${sunriseTime} | π Sunset: ${sunsetTime}`;
}
+
+ // Update daylight hours bar
+ if (this.sunTimes) {
+ this.updateDaylightHoursBar();
+ }
} catch (error) {
console.error('Error updating weather UI:', error);
}
@@ -383,120 +406,86 @@ class WeatherManager {
/**
* Update sunrise and sunset times from API data
*/
- updateSunTimesFromApi(data) {
+ async updateSunTimesFromApi(data) {
if (!data || !data.sys || !data.sys.sunrise || !data.sys.sunset) {
console.warn('No sunrise/sunset data in API response, using calculated times');
- this.updateSunTimesFromCalculation();
+ await this.updateSunTimesFromCalculation();
return;
}
-
+
try {
- const today = new Date();
- const tomorrow = new Date(today);
- tomorrow.setDate(tomorrow.getDate() + 1);
-
- // Create Date objects from Unix timestamps
+ // Create Date objects from Unix timestamps for today
const sunrise = new Date(data.sys.sunrise * 1000);
const sunset = new Date(data.sys.sunset * 1000);
-
- // Use calculated times for tomorrow
- const tomorrowTimes = this.calculateSunTimes(tomorrow);
-
+
+ // Fetch tomorrow's times from sunrise-sunset.org API
+ const tomorrowTimes = await this.fetchSunTimes('tomorrow');
+
this.sunTimes = {
today: { sunrise, sunset },
tomorrow: tomorrowTimes
};
-
+
console.log('Sun times updated from API:', this.sunTimes);
return this.sunTimes;
} catch (error) {
console.error('Error updating sun times from API:', error);
- this.updateSunTimesFromCalculation();
+ await this.updateSunTimesFromCalculation();
}
}
/**
- * Update sunrise and sunset times using calculation
+ * Update sunrise and sunset times using sunrise-sunset.org API
+ * Falls back to hardcoded defaults if the API is unreachable
*/
async updateSunTimesFromCalculation() {
-
try {
- // Calculate sun times based on date and location
- const today = new Date();
- const tomorrow = new Date(today);
- tomorrow.setDate(tomorrow.getDate() + 1);
-
+ const [todayData, tomorrowData] = await Promise.all([
+ this.fetchSunTimes('today'),
+ this.fetchSunTimes('tomorrow')
+ ]);
+
this.sunTimes = {
- today: this.calculateSunTimes(today),
- tomorrow: this.calculateSunTimes(tomorrow)
+ today: todayData,
+ tomorrow: tomorrowData
};
-
- console.log('Sun times updated from calculation:', this.sunTimes);
+
+ console.log('Sun times updated from sunrise-sunset.org:', this.sunTimes);
return this.sunTimes;
} catch (error) {
- console.error('Error updating sun times from calculation:', error);
- // Fallback to default times if calculation fails
+ console.error('Error fetching sun times from API, using defaults:', error);
const defaultSunrise = new Date();
- defaultSunrise.setHours(6, 45, 0, 0);
-
+ defaultSunrise.setHours(7, 0, 0, 0);
+
const defaultSunset = new Date();
- defaultSunset.setHours(17, 32, 0, 0);
-
+ defaultSunset.setHours(16, 0, 0, 0);
+
this.sunTimes = {
- today: {
- sunrise: defaultSunrise,
- sunset: defaultSunset
- },
- tomorrow: {
- sunrise: defaultSunrise,
- sunset: defaultSunset
- }
+ today: { sunrise: defaultSunrise, sunset: defaultSunset },
+ tomorrow: { sunrise: defaultSunrise, sunset: defaultSunset }
};
return this.sunTimes;
}
}
-
+
/**
- * Calculate sunrise and sunset times for a given date
- * Uses a simplified algorithm
+ * Fetch sunrise/sunset times from sunrise-sunset.org API
+ * @param {string} date - 'today', 'tomorrow', or YYYY-MM-DD
+ * @returns {Object} { sunrise: Date, sunset: Date }
*/
- calculateSunTimes(date) {
- // This is a simplified calculation
- // For more accuracy, you would use a proper astronomical calculation
-
- // Get day of year
- const start = new Date(date.getFullYear(), 0, 0);
- const diff = date - start;
- const oneDay = 1000 * 60 * 60 * 24;
- const dayOfYear = Math.floor(diff / oneDay);
-
- // Calculate sunrise and sunset times based on latitude and day of year
- // This is a very simplified model
- const latitude = this.options.latitude;
-
- // Base sunrise and sunset times (in hours)
- let baseSunrise = 6; // 6 AM
- let baseSunset = 18; // 6 PM
-
- // Adjust for latitude and season
- // Northern hemisphere seasonal adjustment
- const seasonalAdjustment = Math.sin((dayOfYear - 81) / 365 * 2 * Math.PI) * 3;
-
- // Latitude adjustment (higher latitudes have more extreme day lengths)
- const latitudeAdjustment = Math.abs(latitude) / 90 * 2;
-
- // Apply adjustments
- baseSunrise += seasonalAdjustment * latitudeAdjustment * -1;
- baseSunset += seasonalAdjustment * latitudeAdjustment;
-
- // Create Date objects
- const sunrise = new Date(date);
- sunrise.setHours(Math.floor(baseSunrise), Math.round((baseSunrise % 1) * 60), 0, 0);
-
- const sunset = new Date(date);
- sunset.setHours(Math.floor(baseSunset), Math.round((baseSunset % 1) * 60), 0, 0);
-
- return { sunrise, sunset };
+ async fetchSunTimes(date) {
+ const url = `https://api.sunrise-sunset.org/json?lat=${this.options.latitude}&lng=${this.options.longitude}&date=${date}&formatted=0`;
+ const response = await fetch(url);
+ const data = await response.json();
+
+ if (data.status !== 'OK') {
+ throw new Error(`Sunrise-sunset API returned status: ${data.status}`);
+ }
+
+ return {
+ sunrise: new Date(data.results.sunrise),
+ sunset: new Date(data.results.sunset)
+ };
}
/**
@@ -588,7 +577,136 @@ class WeatherManager {
if (!this.lastUpdated) return 'Never';
return this.formatTime(this.lastUpdated);
}
+
+ /**
+ * Render the daylight hours bar with gradient and current hour indicator
+ */
+ renderDaylightHoursBar() {
+ if (!this.sunTimes) return;
+
+ const barElement = document.getElementById('daylight-hours-bar');
+ const backgroundElement = barElement?.querySelector('.daylight-bar-background');
+ const indicatorElement = barElement?.querySelector('.daylight-bar-indicator');
+
+ if (!barElement || !backgroundElement || !indicatorElement) return;
+
+ const today = this.sunTimes.today;
+
+ // Normalize sunrise and sunset to today's date for consistent calculation
+ const now = new Date();
+ const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+
+ const sunrise = new Date(todayDate);
+ sunrise.setHours(today.sunrise.getHours(), today.sunrise.getMinutes(), 0, 0);
+
+ const sunset = new Date(todayDate);
+ sunset.setHours(today.sunset.getHours(), today.sunset.getMinutes(), 0, 0);
+
+ // Calculate positions as percentage of 24 hours (1440 minutes)
+ // Extract hours and minutes from the date objects
+ const getTimePosition = (date) => {
+ const hours = date.getHours();
+ const minutes = date.getMinutes();
+ const totalMinutes = hours * 60 + minutes;
+ return (totalMinutes / 1440) * 100;
+ };
+
+ const sunrisePosition = getTimePosition(sunrise);
+ const sunsetPosition = getTimePosition(sunset);
+ const currentPosition = getTimePosition(now);
+
+ // Ensure positions are valid (0-100)
+ const clampPosition = (pos) => Math.max(0, Math.min(100, pos));
+ const sunrisePos = clampPosition(sunrisePosition);
+ const sunsetPos = clampPosition(sunsetPosition);
+ const currentPos = clampPosition(currentPosition);
+
+ // Create modern gradient for daylight hours with smooth transitions
+ // Multiple color stops for a more sophisticated gradient effect
+ let gradient = '';
+
+ // Handle case where sunrise is before sunset (normal day)
+ if (sunrisePos < sunsetPos) {
+ // Create gradient with smooth transitions:
+ // - Midnight blue (night) -> dark blue -> orange/red (dawn) -> yellow (day) -> orange/red (dusk) -> dark blue -> midnight blue (night)
+ const dawnStart = Math.max(0, sunrisePos - 2);
+ const dawnEnd = Math.min(100, sunrisePos + 1);
+ const duskStart = Math.max(0, sunsetPos - 1);
+ const duskEnd = Math.min(100, sunsetPos + 2);
+
+ gradient = `linear-gradient(to right,
+ #191970 0%,
+ #191970 ${dawnStart}%,
+ #2E3A87 ${dawnStart}%,
+ #FF6B35 ${dawnEnd}%,
+ #FFD93D ${Math.min(100, dawnEnd + 1)}%,
+ #FFEB3B ${Math.min(100, dawnEnd + 1)}%,
+ #FFEB3B ${duskStart}%,
+ #FFD93D ${duskStart}%,
+ #FF6B35 ${Math.max(0, duskEnd - 1)}%,
+ #2E3A87 ${duskEnd}%,
+ #191970 ${duskEnd}%,
+ #191970 100%)`;
+ } else {
+ // Handle edge cases (polar day/night or sunrise after sunset near midnight)
+ // For simplicity, show all as night (midnight blue)
+ gradient = 'linear-gradient(to right, #191970 0%, #191970 100%)';
+ }
+
+ // Apply gradient to background
+ backgroundElement.style.backgroundImage = gradient;
+
+ // Determine if it's day or night for icon
+ const isDaytime = currentPos >= sunrisePos && currentPos <= sunsetPos;
+ const iconElement = indicatorElement.querySelector('.sun-icon, .moon-icon');
+ if (iconElement) {
+ iconElement.textContent = isDaytime ? 'βοΈ' : 'π';
+
+ // Update classes to match the icon for proper styling
+ if (isDaytime) {
+ iconElement.classList.remove('moon-icon');
+ iconElement.classList.add('sun-icon');
+ } else {
+ iconElement.classList.remove('sun-icon');
+ iconElement.classList.add('moon-icon');
+ }
+ }
+
+ // Position current hour indicator
+ indicatorElement.style.left = `${currentPos}%`;
+
+ // Debug logging
+ console.log('Daylight bar positions:', {
+ sunrise: `${today.sunrise.getHours()}:${today.sunrise.getMinutes().toString().padStart(2, '0')}`,
+ sunset: `${today.sunset.getHours()}:${today.sunset.getMinutes().toString().padStart(2, '0')}`,
+ current: `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`,
+ sunrisePos: `${sunrisePos.toFixed(1)}%`,
+ sunsetPos: `${sunsetPos.toFixed(1)}%`,
+ currentPos: `${currentPos.toFixed(1)}%`
+ });
+ }
+
+ /**
+ * Update daylight hours bar and set up interval for current hour updates
+ */
+ updateDaylightHoursBar() {
+ // Render the bar immediately
+ this.renderDaylightHoursBar();
+
+ // Clear existing interval if any
+ if (this.daylightBarUpdateInterval) {
+ clearInterval(this.daylightBarUpdateInterval);
+ }
+
+ // Update current hour position every minute
+ this.daylightBarUpdateInterval = setInterval(() => {
+ this.renderDaylightHoursBar();
+ }, 60000); // Update every minute
+ }
}
-// Export the WeatherManager class for use in other modules
+// ES module export
+export { WeatherManager };
+
+// Keep window reference for backward compatibility
window.WeatherManager = WeatherManager;
diff --git a/public/js/main.js b/public/js/main.js
index 3c06ac9..2f47a0c 100644
--- a/public/js/main.js
+++ b/public/js/main.js
@@ -3,22 +3,25 @@
* Initializes all components when the DOM is ready
*/
+import { Constants } from './utils/constants.js';
+import { logger } from './utils/logger.js';
+import { ConfigManager } from './components/ConfigManager.js';
+import { Clock } from './components/Clock.js';
+import { WeatherManager } from './components/WeatherManager.js';
+import { DeparturesManager } from './components/DeparturesManager.js';
+
/**
* Function to ensure content wrapper exists for rotated orientations
*/
function ensureContentWrapper() {
if (!document.getElementById('content-wrapper')) {
- if (window.logger) {
- window.logger.info('Creating content wrapper');
- } else {
- console.log('Creating content wrapper');
- }
+ logger.info('Creating content wrapper');
const wrapper = document.createElement('div');
wrapper.id = 'content-wrapper';
-
+
// Move all body children to the wrapper except excluded elements
const excludedElements = ['config-button', 'config-modal', 'background-overlay'];
-
+
// Create an array of nodes to move (can't modify while iterating)
const nodesToMove = [];
for (let i = 0; i < document.body.children.length; i++) {
@@ -27,12 +30,12 @@ function ensureContentWrapper() {
nodesToMove.push(child);
}
}
-
+
// Move the nodes to the wrapper
nodesToMove.forEach(node => {
wrapper.appendChild(node);
});
-
+
// Add the wrapper back to the body
document.body.appendChild(wrapper);
}
@@ -40,80 +43,57 @@ function ensureContentWrapper() {
// Initialize components when the DOM is loaded
document.addEventListener('DOMContentLoaded', async function() {
- if (window.logger) {
- window.logger.info('DOM fully loaded');
- } else {
- console.log('DOM fully loaded');
- }
-
+ logger.info('DOM fully loaded');
+
try {
// Initialize ConfigManager first
- if (window.logger) {
- window.logger.info('Creating ConfigManager...');
- } else {
- console.log('Creating ConfigManager...');
- }
+ logger.info('Creating ConfigManager...');
window.configManager = new ConfigManager({
defaultOrientation: 'normal',
defaultDarkMode: 'auto'
});
-
- // Note: ConfigManager already creates the config button and modal
-
+
// Initialize Clock
- const timezone = window.Constants?.TIMEZONE || 'Europe/Stockholm';
+ const timezone = Constants.TIMEZONE || 'Europe/Stockholm';
window.clock = new Clock({
elementId: 'clock',
timezone: timezone
});
-
+
// Initialize WeatherManager with location from window config or constants
- const defaultLat = window.DEFAULT_LOCATION?.latitude ||
- (window.Constants?.DEFAULT_LOCATION?.LATITUDE) || 59.3293;
- const defaultLon = window.DEFAULT_LOCATION?.longitude ||
- (window.Constants?.DEFAULT_LOCATION?.LONGITUDE) || 18.0686;
+ const defaultLat = window.DEFAULT_LOCATION?.latitude ||
+ Constants.DEFAULT_LOCATION.LATITUDE || 59.3293;
+ const defaultLon = window.DEFAULT_LOCATION?.longitude ||
+ Constants.DEFAULT_LOCATION.LONGITUDE || 18.0686;
window.weatherManager = new WeatherManager({
latitude: defaultLat,
longitude: defaultLon
});
-
- // Initialize departures - use DeparturesManager
- if (typeof DeparturesManager !== 'undefined') {
- window.departuresManager = new DeparturesManager({
- containerId: 'departures',
- statusId: 'status',
- lastUpdatedId: 'last-updated'
- });
- } else if (typeof initDepartures === 'function') {
- // Fallback to legacy function if DeparturesManager not available
- initDepartures();
- }
-
+
+ // Initialize DeparturesManager
+ window.departuresManager = new DeparturesManager({
+ containerId: 'departures',
+ statusId: 'status',
+ lastUpdatedId: 'last-updated'
+ });
+
// Set up event listeners
document.addEventListener('darkModeChanged', event => {
document.body.classList.toggle('dark-mode', event.detail.isDarkMode);
});
-
+
document.addEventListener('configChanged', event => {
if (['vertical', 'upsidedown', 'vertical-reverse'].includes(event.detail.config.orientation)) {
ensureContentWrapper();
}
});
-
+
// Ensure content wrapper exists initially
ensureContentWrapper();
-
- if (window.logger) {
- window.logger.info('All components initialized successfully');
- } else {
- console.log('All components initialized successfully');
- }
+
+ logger.info('All components initialized successfully');
} catch (error) {
- if (window.logger) {
- window.logger.error('Error during initialization:', error);
- } else {
- console.error('Error during initialization:', error);
- }
+ logger.error('Error during initialization:', error);
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = `Initialization error: ${error.message}`;
diff --git a/public/js/utils/constants.js b/public/js/utils/constants.js
index b16daf7..077668b 100644
--- a/public/js/utils/constants.js
+++ b/public/js/utils/constants.js
@@ -24,7 +24,7 @@ const Constants = {
// Refresh intervals (in milliseconds)
REFRESH: {
- DEPARTURES: 5000, // 5 seconds
+ DEPARTURES: 30000, // 30 seconds
WEATHER: 30 * 60 * 1000, // 30 minutes
DARK_MODE_CHECK: 60000 // 1 minute
},
@@ -85,5 +85,8 @@ const Constants = {
}
};
-// Export constants
+// ES module export
+export { Constants };
+
+// Keep window reference for backward compatibility with inline scripts
window.Constants = Constants;
diff --git a/public/js/utils/logger.js b/public/js/utils/logger.js
index a31f533..34ca4f0 100644
--- a/public/js/utils/logger.js
+++ b/public/js/utils/logger.js
@@ -95,6 +95,9 @@ class Logger {
// Create a singleton instance
const logger = new Logger();
-// Export both the class and the singleton instance
+// ES module export
+export { Logger, logger };
+
+// Keep window reference for backward compatibility
window.Logger = Logger;
window.logger = logger;
diff --git a/server.js b/server.js
index 9234fbe..dfc68d2 100644
--- a/server.js
+++ b/server.js
@@ -2,7 +2,6 @@
require('dotenv').config();
const http = require('http');
-const url = require('url');
const fs = require('fs');
const path = require('path');
@@ -52,7 +51,7 @@ loadSitesConfig();
// Create HTTP server
const server = http.createServer(async (req, res) => {
- const parsedUrl = url.parse(req.url, true);
+ const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
@@ -72,7 +71,7 @@ const server = http.createServer(async (req, res) => {
sitesRouter.handleSiteSearch(req, res, parsedUrl);
}
else if (parsedUrl.pathname === '/api/sites/nearby') {
- sitesRouter.handleNearbySites(req, res, parsedUrl);
+ await sitesRouter.handleNearbySites(req, res, parsedUrl);
}
else if (parsedUrl.pathname === '/api/config') {
configRouter.handleGetConfig(req, res, config);
diff --git a/server/routes/departures.js b/server/routes/departures.js
index f27b322..5ed81d3 100644
--- a/server/routes/departures.js
+++ b/server/routes/departures.js
@@ -23,62 +23,14 @@ function fetchDeparturesForSite(siteId) {
});
res.on('end', () => {
- console.log('Raw API response:', data.substring(0, 200) + '...');
-
try {
- try {
- const parsedData = JSON.parse(data);
- console.log('Successfully parsed as regular JSON');
- resolve(parsedData);
- return;
- } catch (jsonError) {
- console.log('Not valid JSON, trying to fix format...');
- }
-
- if (data.startsWith('departures":')) {
- data = '{' + data;
- } else if (data.includes('departures":')) {
- const startIndex = data.indexOf('departures":');
- if (startIndex > 0) {
- data = '{' + data.substring(startIndex);
- }
- }
-
- data = data.replace(/}{\s*"/g, '},{"');
- data = data.replace(/"([^"]+)":\s*([^,{}\[\]]+)(?=")/g, '"$1": $2,');
- data = data.replace(/,\s*}/g, '}').replace(/,\s*\]/g, ']');
-
- try {
- const parsedData = JSON.parse(data);
- console.log('Successfully parsed fixed JSON');
-
- if (parsedData && parsedData.departures && parsedData.departures.length > 0) {
- console.log('Sample departure structure:', JSON.stringify(parsedData.departures[0], null, 2));
-
- const sample = parsedData.departures[0];
- console.log('Direction fields:', {
- direction: sample.direction,
- directionText: sample.directionText,
- directionCode: sample.directionCode,
- destination: sample.destination
- });
- }
-
- resolve(parsedData);
- } catch (parseError) {
- console.error('Failed to parse even after fixing:', parseError);
- // Return empty departures array instead of rejecting to be more resilient
- resolve({
- departures: [],
- error: 'Failed to parse API response: ' + parseError.message
- });
- }
+ const parsedData = JSON.parse(data);
+ resolve(parsedData);
} catch (error) {
- console.error('Error processing API response:', error);
- // Return empty departures array instead of rejecting to be more resilient
+ console.error('Error parsing departures API response:', error);
resolve({
departures: [],
- error: 'Error processing API response: ' + error.message
+ error: 'Failed to parse API response: ' + error.message
});
}
});
diff --git a/server/routes/sites.js b/server/routes/sites.js
index f7b2947..e8fe370 100644
--- a/server/routes/sites.js
+++ b/server/routes/sites.js
@@ -1,198 +1,178 @@
/**
* Sites route handler
* Handles site search and nearby sites queries
+ *
+ * Search uses SL Journey Planner v2 Stop Finder (real server-side search)
+ * Nearby uses cached site list from SL Transport API (fetched once, filtered in-memory)
*/
const https = require('https');
-/**
- * Normalize site data from API response to consistent format
- * @param {Object} site - Raw site data from API
- * @returns {Object} - Normalized site object
- */
-function normalizeSite(site) {
- return {
- id: String(site.id || site.siteId || site.SiteId || ''),
- name: site.name || site.siteName || site.Name || site.StopPointName || 'Unknown',
- lat: site.lat || site.latitude || site.Lat || site.Latitude || null,
- lon: site.lon || site.longitude || site.Lon || site.Longitude || null
- };
-}
+// ββ Site cache for nearby lookups ββββββββββββββββββββββββββββββββββββββββββ
+let cachedSites = null;
+let cacheTimestamp = null;
+const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
/**
- * Parse sites from API response (handles multiple response formats)
- * @param {Object|Array} parsedData - Parsed JSON data from API
- * @returns {Array