document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const cityInput = document.getElementById('city-input');
const searchBtn = document.getElementById('search-btn');
const loadingElement = document.getElementById('loading');
const errorElement = document.getElementById('error-message');
const errorText = document.getElementById('error-text');
const currentWeatherElement = document.getElementById('current-weather');
const forecastElement = document.getElementById('forecast');
const mapCard = document.getElementById('map-card');
const recentSearchesContainer = document.getElementById('recent-searches');
const celsiusBtn = document.getElementById('celsius-btn');
const fahrenheitBtn = document.getElementById('fahrenheit-btn');
// State
let recentSearches = JSON.parse(localStorage.getItem('recentSearches')) || [];
let currentUnit = 'celsius';
let currentWeatherData = null;
let map = null;
let mapInitialized = false;
// Weather code to icon mapping
const weatherIcons = {
0: 'sun', 1: 'cloud-sun', 2: 'cloud', 3: 'cloud', 45: 'smog', 48: 'smog',
51: 'cloud-rain', 53: 'cloud-rain', 55: 'cloud-rain', 56: 'cloud-rain',
57: 'cloud-rain', 61: 'cloud-rain', 63: 'cloud-rain', 65: 'cloud-showers-heavy',
66: 'cloud-rain', 67: 'cloud-showers-heavy', 71: 'snowflake', 73: 'snowflake',
75: 'snowflake', 77: 'snowflake', 80: 'cloud-showers-heavy', 81: 'cloud-showers-heavy',
82: 'cloud-showers-heavy', 85: 'snowflake', 86: 'snowflake', 95: 'bolt',
96: 'bolt', 99: 'bolt'
};
// AQI categories
const aqiCategories = {
1: { text: 'Good', color: 'aqi-good' },
2: { text: 'Moderate', color: 'aqi-moderate' },
3: { text: 'Unhealthy', color: 'aqi-unhealthy' },
4: { text: 'Very Unhealthy', color: 'aqi-unhealthy' },
5: { text: 'Hazardous', color: 'aqi-unhealthy' }
};
// Event Listeners
searchBtn.addEventListener('click', fetchWeatherData);
let debounceTimeout;
cityInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
clearTimeout(debounceTimeout);
fetchWeatherData();
} else {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(fetchWeatherData, 500);
}
});
celsiusBtn.addEventListener('click', () => toggleTemperatureUnit('celsius'));
fahrenheitBtn.addEventListener('click', () => toggleTemperatureUnit('fahrenheit'));
// Initialize
updateRecentSearches();
loadMapLibrary();
// Functions
function sanitizeInput(input) {
const div = document.createElement('div');
div.textContent = input;
return div.innerHTML;
}
function getWeatherDescription(code, isDay) {
const descriptions = {
0: isDay ? 'Clear sky' : 'Clear night',
1: isDay ? 'Mainly clear' : 'Mainly clear night',
2: 'Partly cloudy', 3: 'Overcast', 45: 'Fog', 48: 'Depositing rime fog',
51: 'Light drizzle', 53: 'Moderate drizzle', 55: 'Dense drizzle',
56: 'Light freezing drizzle', 57: 'Dense freezing drizzle',
61: 'Slight rain', 63: 'Moderate rain', 65: 'Heavy rain',
66: 'Light freezing rain', 67: 'Heavy freezing rain',
71: 'Slight snow', 73: 'Moderate snow', 75: 'Heavy snow',
77: 'Snow grains', 80: 'Slight rain showers', 81: 'Moderate rain showers',
82: 'Violent rain showers', 85: 'Slight snow showers', 86: 'Heavy snow showers',
95: 'Slight or moderate thunderstorm', 96: 'Thunderstorm with slight hail',
99: 'Thunderstorm with heavy hail'
};
return descriptions[code] || 'Unknown';
}
function formatTime(isoString) {
return new Date(isoString).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}
function addToRecentSearches(city) {
city = city.toLowerCase();
recentSearches = recentSearches.filter(c => c !== city);
recentSearches.unshift(city);
if (recentSearches.length > 5) recentSearches.pop();
localStorage.setItem('recentSearches', JSON.stringify(recentSearches));
updateRecentSearches();
}
function updateRecentSearches() {
recentSearchesContainer.innerHTML = '';
recentSearches.forEach(city => {
const button = document.createElement('button');
button.className = 'recent-city';
button.textContent = city.charAt(0).toUpperCase() + city.slice(1);
button.addEventListener('click', () => {
cityInput.value = city;
fetchWeatherData();
});
recentSearchesContainer.appendChild(button);
});
}
function loadMapLibrary() {
const script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.onload = () => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(link);
mapInitialized = true;
initializeMap();
};
document.head.appendChild(script);
}
function initializeMap() {
if (!map) {
map = L.map('map-container').setView([0, 0], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
}
}
function updateMap(latitude, longitude, cityName) {
if (!mapInitialized) return;
map.setView([latitude, longitude], 10);
L.marker([latitude, longitude]).addTo(map)
.bindPopup(cityName)
.openPopup();
}
function updateTemperatureDisplay(tempCelsius, feelsLikeCelsius) {
const tempElement = document.getElementById('current-temp');
const feelsLikeElement = document.getElementById('feels-like');
const tempSup = document.querySelector('.temperature sup');
let temp, feelsLike;
if (currentUnit === 'fahrenheit') {
temp = Math.round((tempCelsius * 9/5) + 32);
feelsLike = Math.round((feelsLikeCelsius * 9/5) + 32);
tempSup.textContent = '°F';
} else {
temp = Math.round(tempCelsius);
feelsLike = Math.round(feelsLikeCelsius);
tempSup.textContent = '°C';
}
tempElement.textContent = temp;
feelsLikeElement.textContent = `${feelsLike}°${currentUnit === 'fahrenheit' ? 'F' : 'C'}`;
}
function updateAirQuality(airQualityData) {
const aqiValue = airQualityData.us_aqi;
const aqiCategory = aqiCategories[Math.ceil(aqiValue / 100)] || aqiCategories[1];
const aqiElement = document.getElementById('aqi-value');
const aqiTextElement = document.getElementById('aqi-text');
const pm25Element = document.getElementById('pm25');
const pm10Element = document.getElementById('pm10');
aqiElement.textContent = aqiValue;
aqiElement.className = `aqi-value ${aqiCategory.color}`;
aqiTextElement.textContent = aqiCategory.text;
pm25Element.textContent = `${airQualityData.pm2_5} μg/m³`;
pm10Element.textContent = `${airQualityData.pm10} μg/m³`;
}
function fetchWeatherData() {
const city = cityInput.value.trim();
if (!city) return;
loadingElement.style.display = 'flex';
errorElement.style.display = 'none';
currentWeatherElement.style.display = 'none';
forecastElement.style.display = 'none';
mapCard.style.display = 'none';
addToRecentSearches(city);
fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en&format=json`)
.then(response => response.json())
.then(geoData => {
if (!geoData.results || geoData.results.length === 0) {
throw new Error('City not found');
}
const { latitude, longitude, name, country, admin1 } = geoData.results[0];
return Promise.all([
fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m&daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,uv_index_max,precipitation_sum,rain_sum,showers_sum,snowfall_sum,precipitation_hours,precipitation_probability_max,wind_speed_10m_max,wind_gusts_10m_max,wind_direction_10m_dominant&hourly=temperature_2m,weather_code,precipitation_probability,relative_humidity_2m,wind_speed_10m&timezone=auto&forecast_days=5`),
fetch(`https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${latitude}&longitude=${longitude}¤t=us_aqi,pm10,pm2_5`)
]).then(responses =>
Promise.all(responses.map(res => res.json()))
).then(([weatherData, airQualityData]) => {
currentWeatherData = {
geoData: { latitude, longitude, name, country, admin1 },
weatherData,
airQualityData
};
updateCurrentWeather(weatherData.current, name, country, admin1);
updateForecast(weatherData.daily);
updateHourlyForecast(weatherData.hourly);
updateAirQuality(airQualityData.current);
if (mapInitialized) {
updateMap(latitude, longitude, name);
}
currentWeatherElement.style.display = 'block';
forecastElement.style.display = 'block';
mapCard.style.display = 'block';
});
})
.catch(error => {
console.error('Error fetching weather data:', error);
errorText.textContent = error.message === 'City not found'
? 'City not found. Please try another location.'
: 'Failed to fetch weather data. Check your connection or try again.';
errorElement.style.display = 'block';
})
.finally(() => {
loadingElement.style.display = 'none';
});
}
function updateCurrentWeather(currentData, cityName, country, region) {
document.getElementById('city-name').textContent = sanitizeInput(`${cityName}, ${country}${region && region !== cityName ? `, ${region}` : ''}`);
const now = new Date();
document.getElementById('current-date').textContent = now.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
updateTemperatureDisplay(currentData.temperature_2m, currentData.apparent_temperature);
const weatherDescription = getWeatherDescription(currentData.weather_code, currentData.is_day);
document.getElementById('weather-description').textContent = weatherDescription;
const iconName = weatherIcons[currentData.weather_code] || 'cloud';
const iconUrl = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/svgs/solid/${iconName}.svg`;
document.getElementById('weather-icon').src = iconUrl;
document.getElementById('weather-icon').alt = weatherDescription;
document.getElementById('humidity').textContent = `${currentData.relative_humidity_2m}%`;
document.getElementById('wind-speed').textContent = `${currentData.wind_speed_10m} km/h`;
document.getElementById('pressure').textContent = `${currentData.pressure_msl} hPa`;
document.getElementById('precipitation').textContent = `${currentData.precipitation} mm`;
}
function updateForecast(dailyData) {
const forecastContainer = document.getElementById('forecast-container');
forecastContainer.innerHTML = '';
document.getElementById('sunrise').textContent = formatTime(dailyData.sunrise[0]);
document.getElementById('sunset').textContent = formatTime(dailyData.sunset[0]);
document.getElementById('uv-index').textContent = dailyData.uv_index_max[0].toFixed(1);
for (let i = 1; i <= 5; i++) {
const date = new Date(dailyData.time[i]);
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' });
const weatherCode = dailyData.weather_code[i];
const iconName = weatherIcons[weatherCode] || 'cloud';
const iconUrl = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/svgs/solid/${iconName}.svg`;
const maxTemp = currentUnit === 'fahrenheit'
? Math.round((dailyData.temperature_2m_max[i] * 9/5) + 32)
: Math.round(dailyData.temperature_2m_max[i]);
const minTemp = currentUnit === 'fahrenheit'
? Math.round((dailyData.temperature_2m_min[i] * 9/5) + 32)
: Math.round(dailyData.temperature_2m_min[i]);
const forecastDay = document.createElement('div');
forecastDay.className = 'forecast-day';
forecastDay.innerHTML = `
${dayName}
${maxTemp}°
${minTemp}°
${dailyData.precipitation_probability_max[i]}%
${dailyData.wind_speed_10m_max[i]} km/h
`;
forecastContainer.appendChild(forecastDay);
}
}
function updateHourlyForecast(hourlyData) {
const hourlyContainer = document.getElementById('hourly-container');
hourlyContainer.innerHTML = '';
for (let i = 0; i < 24; i++) {
const time = new Date(hourlyData.time[i]);
const hour = time.toLocaleTimeString('en-US', { hour: 'numeric', hour12: true });
const weatherCode = hourlyData.weather_code[i];
const iconName = weatherIcons[weatherCode] || 'cloud';
const iconUrl = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/svgs/solid/${iconName}.svg`;
const temp = currentUnit === 'fahrenheit'
? Math.round((hourlyData.temperature_2m[i] * 9/5) + 32)
: Math.round(hourlyData.temperature_2m[i]);
const hourlyItem = document.createElement('div');
hourlyItem.className = 'hourly-item';
hourlyItem.innerHTML = `
${hour}
${temp}°
${hourlyData.precipitation_probability[i]}%
`;
hourlyContainer.appendChild(hourlyItem);
}
}
function toggleTemperatureUnit(unit) {
if (unit === currentUnit) return;
currentUnit = unit;
celsiusBtn.classList.toggle('active', unit === 'celsius');
fahrenheitBtn.classList.toggle('active', unit === 'fahrenheit');
if (currentWeatherData) {
const { temperature_2m, apparent_temperature } = currentWeatherData.weatherData.current;
updateTemperatureDisplay(temperature_2m, apparent_temperature);
updateForecast(currentWeatherData.weatherData.daily);
updateHourlyForecast(currentWeatherData.weatherData.hourly);
}
}
});