Items 10-15: ES modules, inline style cleanup, template modal, code modernization
- Item 10: Convert to ES modules with import/export, single module entry point - Item 11: Replace inline styles with CSS classes (background overlay, card animations, highlight effect, config modal form elements) - Item 12: Move ConfigManager modal HTML from JS template literal to <template> element in index.html - Item 13: Replace deprecated url.parse() with new URL() in server.js and update route handlers to use searchParams - Item 14: Replace JSON.parse/stringify deep clone with structuredClone() - Item 15: Remove dead JSON-fixing regex code from departures.js route Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user