diff --git a/index.html b/index.html index 60c86a6..8504ac4 100755 --- a/index.html +++ b/index.html @@ -376,6 +376,38 @@

Lên kế hoạch cho chuyến đi của bạn với thông tin thời tiết chính xác.

+
+
+

Thời tiết hiện tại

+
+ +
+
--°C
+
Đang tải thời tiết...
+
+
+
+ Cao nhất: --° + Thấp nhất: --° +
+
+ +
+
+

Ngày mai

+ +
+
+ +
Đang tải thời tiết...
+
+
+ Cao nhất: --° + Thấp nhất: --° +
+
+
+
diff --git a/js/main.js b/js/main.js index b8d4aea..2e05f8c 100644 --- a/js/main.js +++ b/js/main.js @@ -11,6 +11,7 @@ const languageLocales = { let currentLang = 'vi'; let currentWeatherCondition = 'weather_loading'; +let tomorrowWeatherCondition = 'weather_loading'; function getWeatherPresentation(code) { if (code === 0) return { key: 'weather_clear', icon: 'fa-sun', color: 'text-amber-500' }; @@ -29,25 +30,104 @@ function getWeatherPresentation(code) { function renderWeatherCondition() { const condition = document.getElementById('weather-condition'); if (condition) condition.textContent = translations[currentLang][currentWeatherCondition] || translations.en[currentWeatherCondition]; + + const currentForecastCondition = document.getElementById('weather-current-condition'); + if (currentForecastCondition) { + currentForecastCondition.textContent = translations[currentLang][currentWeatherCondition] || translations.en[currentWeatherCondition]; + } + + const tomorrowForecastCondition = document.getElementById('weather-tomorrow-condition'); + if (tomorrowForecastCondition) { + tomorrowForecastCondition.textContent = translations[currentLang][tomorrowWeatherCondition] || translations.en[tomorrowWeatherCondition]; + } } -async function loadCurrentWeather() { +function setWeatherRange(id, value) { + const element = document.getElementById(id); + if (element) element.textContent = Number.isFinite(value) ? `${Math.round(value)}°` : '--°'; +} + +function setWeatherIcon(id, presentation) { + const icon = document.getElementById(id); + if (icon) icon.className = `fa-solid ${presentation.icon} text-4xl ${presentation.color}`; +} + +async function loadWeatherForecast() { const temperature = document.getElementById('weather-temperature'); const icon = document.getElementById('weather-icon'); + const currentForecastTemperature = document.getElementById('weather-current-temperature'); + const tomorrowDate = document.getElementById('weather-tomorrow-date'); try { - const response = await fetch('https://api.open-meteo.com/v1/forecast?latitude=12.2388&longitude=109.1967¤t=temperature_2m,weather_code&timezone=Asia%2FHo_Chi_Minh'); + const response = await fetch('https://api.open-meteo.com/v1/forecast?latitude=12.2388&longitude=109.1967¤t=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=Asia%2FHo_Chi_Minh&forecast_days=2'); if (!response.ok) throw new Error('Weather request failed'); const data = await response.json(); - const presentation = getWeatherPresentation(data.current.weather_code); - temperature.textContent = `${Math.round(data.current.temperature_2m)}°C`; - icon.className = `fa-solid ${presentation.icon} text-2xl ${presentation.color}`; - currentWeatherCondition = presentation.key; + const current = data.current; + const daily = data.daily; + if ( + !Number.isFinite(current?.temperature_2m) + || !Number.isFinite(current?.weather_code) + || !Array.isArray(daily?.time) + || !Array.isArray(daily?.weather_code) + || !Array.isArray(daily?.temperature_2m_max) + || !Array.isArray(daily?.temperature_2m_min) + || daily.time.length < 2 + || daily.weather_code.length < 2 + || daily.temperature_2m_max.length < 2 + || daily.temperature_2m_min.length < 2 + || !Number.isFinite(daily.weather_code[1]) + || !Number.isFinite(daily.temperature_2m_max[0]) + || !Number.isFinite(daily.temperature_2m_min[0]) + || !Number.isFinite(daily.temperature_2m_max[1]) + || !Number.isFinite(daily.temperature_2m_min[1]) + || !/^\d{4}-\d{2}-\d{2}$/.test(daily.time[1]) + ) { + throw new Error('Weather response is incomplete'); + } + + const currentPresentation = getWeatherPresentation(current.weather_code); + const tomorrowPresentation = getWeatherPresentation(daily.weather_code[1]); + const currentTemperature = `${Math.round(current.temperature_2m)}°C`; + if (temperature) temperature.textContent = currentTemperature; + if (currentForecastTemperature) currentForecastTemperature.textContent = currentTemperature; + if (icon) icon.className = `fa-solid ${currentPresentation.icon} text-2xl ${currentPresentation.color}`; + setWeatherIcon('weather-current-icon', currentPresentation); + setWeatherIcon('weather-tomorrow-icon', tomorrowPresentation); + setWeatherRange('weather-today-high', daily.temperature_2m_max[0]); + setWeatherRange('weather-today-low', daily.temperature_2m_min[0]); + setWeatherRange('weather-tomorrow-high', daily.temperature_2m_max[1]); + setWeatherRange('weather-tomorrow-low', daily.temperature_2m_min[1]); + + if (tomorrowDate) { + const [year, month, day] = daily.time[1].split('-').map(Number); + const date = new Date(year, month - 1, day, 12); + tomorrowDate.dateTime = daily.time[1]; + tomorrowDate.textContent = new Intl.DateTimeFormat(languageLocales[currentLang].replace('_', '-'), { + weekday: 'short', + month: 'short', + day: 'numeric' + }).format(date); + } + + currentWeatherCondition = currentPresentation.key; + tomorrowWeatherCondition = tomorrowPresentation.key; } catch { - temperature.textContent = '--°C'; - icon.className = 'fa-solid fa-cloud text-2xl text-gray-400'; + if (temperature) temperature.textContent = '--°C'; + if (currentForecastTemperature) currentForecastTemperature.textContent = '--°C'; + if (icon) icon.className = 'fa-solid fa-cloud text-2xl text-gray-400'; + setWeatherIcon('weather-current-icon', { icon: 'fa-cloud', color: 'text-gray-400' }); + setWeatherIcon('weather-tomorrow-icon', { icon: 'fa-cloud', color: 'text-gray-400' }); + setWeatherRange('weather-today-high', NaN); + setWeatherRange('weather-today-low', NaN); + setWeatherRange('weather-tomorrow-high', NaN); + setWeatherRange('weather-tomorrow-low', NaN); + if (tomorrowDate) { + tomorrowDate.textContent = ''; + tomorrowDate.removeAttribute('datetime'); + } currentWeatherCondition = 'weather_unavailable'; + tomorrowWeatherCondition = 'weather_unavailable'; } renderWeatherCondition(); @@ -108,6 +188,16 @@ function setLanguage(lang) { document.getElementById('og-url').content = canonicalUrl; document.getElementById('og-locale').content = languageLocales[lang]; renderWeatherCondition(); + + const tomorrowDate = document.getElementById('weather-tomorrow-date'); + if (tomorrowDate.dateTime) { + const [year, month, day] = tomorrowDate.dateTime.split('-').map(Number); + tomorrowDate.textContent = new Intl.DateTimeFormat(languageLocales[lang].replace('_', '-'), { + weekday: 'short', + month: 'short', + day: 'numeric' + }).format(new Date(year, month - 1, day, 12)); + } } function toggleMobileMenu() { @@ -161,5 +251,5 @@ function closeModal() { } setLanguage(getRequestedLanguage() || getPreferredLanguage()); -loadCurrentWeather(); -setInterval(loadCurrentWeather, 30 * 60 * 1000); +loadWeatherForecast(); +setInterval(loadWeatherForecast, 30 * 60 * 1000); diff --git a/js/translations.js b/js/translations.js index 6b63c21..221dce9 100644 --- a/js/translations.js +++ b/js/translations.js @@ -61,6 +61,10 @@ window.translations = { attr_9_loc: "Vĩnh Hy, Ninh Hải", weather_title: "Thời điểm lý tưởng du lịch", weather_desc: "Lên kế hoạch cho chuyến đi của bạn với thông tin thời tiết chính xác.", + weather_current: "Thời tiết hiện tại", + weather_tomorrow: "Ngày mai", + weather_high: "Cao nhất", + weather_low: "Thấp nhất", weather_loading: "Đang tải thời tiết...", weather_unavailable: "Không có dữ liệu", weather_clear: "Trời quang", @@ -166,6 +170,10 @@ window.translations = { attr_9_loc: "Виньхи, Ниньхай", weather_title: "Лучшее время для поездки", weather_desc: "Спланируйте свою поездку на основе точных метеорологических данных.", + weather_current: "Сейчас", + weather_tomorrow: "Завтра", + weather_high: "Макс.", + weather_low: "Мин.", weather_loading: "Загрузка погоды...", weather_unavailable: "Нет данных", weather_clear: "Ясно", @@ -271,6 +279,10 @@ window.translations = { attr_9_loc: "Vĩnh Hy, Ninh Hải", weather_title: "Best Time to Visit", weather_desc: "Plan your trip with accurate seasonal weather insights.", + weather_current: "Current weather", + weather_tomorrow: "Tomorrow", + weather_high: "High", + weather_low: "Low", weather_loading: "Loading weather...", weather_unavailable: "Unavailable", weather_clear: "Clear", @@ -376,6 +388,10 @@ window.translations = { attr_9_loc: "宁海县永希", weather_title: "最佳旅游季节", weather_desc: "根据精准气候指南,轻松规划您的完美行程。", + weather_current: "当前天气", + weather_tomorrow: "明天", + weather_high: "最高", + weather_low: "最低", weather_loading: "正在加载天气...", weather_unavailable: "暂无数据", weather_clear: "晴朗", @@ -481,6 +497,10 @@ window.translations = { attr_9_loc: "Vĩnh Hy, Ninh Hải", weather_title: "Beste Reisezeit", weather_desc: "Planen Sie Ihre Reise mit verlässlichen Informationen zum saisonalen Wetter.", + weather_current: "Aktuelles Wetter", + weather_tomorrow: "Morgen", + weather_high: "Höchstwert", + weather_low: "Tiefstwert", weather_loading: "Wetter wird geladen...", weather_unavailable: "Keine Daten", weather_clear: "Klar", @@ -586,6 +606,10 @@ window.translations = { attr_9_loc: "Vĩnh Hy, Ninh Hải", weather_title: "Quand partir ?", weather_desc: "Planifiez votre séjour grâce à nos informations sur le climat selon les saisons.", + weather_current: "Météo actuelle", + weather_tomorrow: "Demain", + weather_high: "Max.", + weather_low: "Min.", weather_loading: "Chargement de la météo...", weather_unavailable: "Données indisponibles", weather_clear: "Ciel dégagé",