Weather/script.js

1658 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// API ключ от WeatherAPI
const API_KEY = "485eff906f7d473b913104046250710";
// Yandex Metrika counter ID (замените на свой)
const YM_COUNTER_ID = 104563496; // Замените на реальный ID счетчика
// Функция для отправки событий в Yandex Metrika
function ymSendEvent(eventName, params = {}) {
if (typeof ym !== "undefined" && YM_COUNTER_ID !== "YOUR_COUNTER_ID") {
ym(YM_COUNTER_ID, "reachGoal", eventName, params);
console.log(`Yandex Metrika event: ${eventName}`, params);
}
}
// Глобальные переменные для темы
const themeButtons = {
light: document.getElementById("theme-light"),
auto: document.getElementById("theme-auto"),
dark: document.getElementById("theme-dark"),
};
const cityInput = document.getElementById("city");
const cityList = document.getElementById("city-list");
let searchTimeout = null;
const getWeatherBtn = document.getElementById("get-weather");
const geolocationBtn = document.getElementById("geolocation-btn");
const timelapseContainer = document.getElementById("timelapse-container");
const currentWeatherDiv = document.getElementById("current-weather");
const currentTempEl = document.getElementById("current-temp");
const currentDescEl = document.getElementById("current-desc");
const currentHumidityEl = document.getElementById("current-humidity");
const currentWindEl = document.getElementById("current-wind");
// Глобальные переменные для координат пользователя (для fallback)
let userLatitude = null;
let userLongitude = null;
// Глобальная переменная для Swiper
let weatherSwiper;
// Глобальные переменные для графиков
let temperatureChart = null;
let precipitationChart = null;
// Функция поиска городов через API
async function searchCities(query) {
if (!query || query.length < 3) {
cityList.innerHTML = "";
return;
}
try {
const url = `https://api.weatherapi.com/v1/search.json?key=${API_KEY}&q=${encodeURIComponent(
query
)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const cities = await response.json();
// Ограничить до 10 результатов
const limitedCities = cities.slice(0, 10);
// Заполнить datalist
cityList.innerHTML = limitedCities
.map((city) => {
const displayName = `${city.name}, ${city.region}, ${city.country}`;
return `<option value="${displayName}">`;
})
.join("");
console.log(`Найдено ${limitedCities.length} городов для "${query}"`);
} catch (error) {
console.error("Ошибка поиска городов:", error);
cityList.innerHTML = "";
// Можно добавить уведомление пользователю
}
}
// Обработчик ввода для поиска
function setupCitySearch() {
cityInput.addEventListener("input", (e) => {
const query = e.target.value.trim();
// Очистить предыдущий таймаут
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Установить новый таймаут для дебаунсинга (500ms)
searchTimeout = setTimeout(() => {
searchCities(query);
}, 500);
});
// Обработчик выбора города из списка
cityInput.addEventListener("change", () => {
const selectedCity = cityInput.value.trim();
if (selectedCity) {
// Сохранить выбранный город
localStorage.setItem("selectedCity", selectedCity);
// Обновить заголовок страницы
updatePageTitle();
console.log("Город выбран:", selectedCity);
}
});
}
// Иконки погоды
const weatherIcons = {
Sunny: "sun",
Clear: "sun",
"Partly cloudy": "cloud-sun",
Cloudy: "cloud",
Overcast: "cloud",
Mist: "cloud-fog",
Fog: "cloud-fog",
"Light rain": "cloud-rain",
"Moderate rain": "cloud-rain",
"Heavy rain": "cloud-rain",
"Light snow": "cloud-snow",
"Moderate snow": "cloud-snow",
"Heavy snow": "cloud-snow",
Thunderstorm: "cloud-lightning",
"Patchy rain possible": "cloud-drizzle",
"Patchy snow possible": "cloud-snow",
"Patchy sleet possible": "cloud-snow",
"Patchy freezing drizzle possible": "cloud-drizzle",
"Blowing snow": "wind",
Blizzard: "wind",
"Freezing fog": "cloud-fog",
"Patchy light drizzle": "cloud-drizzle",
"Light drizzle": "cloud-drizzle",
"Freezing drizzle": "cloud-drizzle",
"Heavy freezing drizzle": "cloud-drizzle",
"Patchy light rain": "cloud-rain",
"Light rain shower": "cloud-rain",
"Moderate or heavy rain shower": "cloud-rain",
"Torrential rain shower": "cloud-rain",
"Patchy light snow": "cloud-snow",
"Light snow showers": "cloud-snow",
"Moderate or heavy snow showers": "cloud-snow",
"Patchy light rain with thunder": "cloud-lightning",
"Moderate or heavy rain with thunder": "cloud-lightning",
"Patchy light snow with thunder": "cloud-lightning",
"Moderate or heavy snow with thunder": "cloud-lightning",
// Ночные условия
"Clear night": "moon",
"Partly cloudy night": "cloud-moon",
"Cloudy night": "cloud",
"Sunny night": "moon",
"Light rain night": "cloud-rain",
"Moderate rain night": "cloud-rain",
"Heavy rain night": "cloud-rain",
"Light snow night": "cloud-snow",
"Moderate snow night": "cloud-snow",
"Heavy snow night": "cloud-snow",
"Thunderstorm night": "cloud-lightning",
"Patchy rain possible night": "cloud-drizzle",
"Patchy snow possible night": "cloud-snow",
"Mist night": "cloud-fog",
"Fog night": "cloud-fog",
// Дополнительные типы погоды
"Light sleet": "cloud-snow",
"Moderate or heavy sleet": "cloud-snow",
"Ice pellets": "cloud-hail",
"Light showers of ice pellets": "cloud-hail",
"Moderate or heavy showers of ice pellets": "cloud-hail",
Hail: "cloud-hail",
"Light hail": "cloud-hail",
"Moderate or heavy hail": "cloud-hail",
Tornado: "tornado",
Hurricane: "hurricane",
Squall: "wind",
Dust: "wind",
Sand: "wind",
"Volcanic ash": "wind",
"Thundery outbreaks possible": "cloud-lightning",
"Thundery outbreaks in nearby": "cloud-lightning",
};
// Маппинг русских погодных условий на английские ключи для иконок
const russianToEnglishWeather = {
Солнечно: "Sunny",
Ясно: "Clear",
"Частично облачно": "Partly cloudy",
Облачно: "Cloudy",
Пасмурно: "Overcast",
Туман: "Mist",
"Слабый туман": "Mist",
"Легкий дождь": "Light rain",
"Умеренный дождь": "Moderate rain",
"Сильный дождь": "Heavy rain",
"Легкий снег": "Light snow",
"Умеренный снег": "Moderate snow",
"Сильный снег": "Heavy snow",
Гроза: "Thunderstorm",
"Возможен небольшой дождь": "Patchy rain possible",
"Возможен небольшой снег": "Patchy snow possible",
"Местами дождь": "Patchy rain possible",
"Местами снег": "Patchy snow possible",
Морось: "Patchy light drizzle",
"Легкая морось": "Light drizzle",
"Замерзающая морось": "Freezing drizzle",
Ливень: "Light rain shower",
"Умеренный или сильный ливень": "Moderate or heavy rain shower",
"Снег с дождем": "Light sleet",
"Умеренный или сильный снег с дождем": "Moderate or heavy sleet",
Град: "Hail",
"Легкий град": "Light hail",
"Умеренный или сильный град": "Moderate or heavy hail",
Торнадо: "Tornado",
Ураган: "Hurricane",
Шквал: "Squall",
Пыль: "Dust",
Песок: "Sand",
"Вулканический пепел": "Volcanic ash",
"Возможны грозы": "Thundery outbreaks possible",
"Грозы в округе": "Thundery outbreaks in nearby",
// Ночные условия
"Ясно ночь": "Clear night",
"Частично облачно ночь": "Partly cloudy night",
"Облачно ночь": "Cloudy night",
"Солнечно ночь": "Sunny night",
"Легкий дождь ночь": "Light rain night",
"Умеренный дождь ночь": "Moderate rain night",
"Сильный дождь ночь": "Heavy rain night",
"Легкий снег ночь": "Light snow night",
"Умеренный снег ночь": "Moderate snow night",
"Сильный снег ночь": "Heavy snow night",
"Гроза ночь": "Thunderstorm night",
"Возможен небольшой дождь ночь": "Patchy rain possible night",
"Возможен небольшой снег ночь": "Patchy snow possible night",
"Туман ночь": "Mist night",
"Слабый туман ночь": "Fog night",
};
// Красивые русские названия для отображения
const beautifulRussianNames = {
Sunny: "Солнечно",
Clear: "Ясно",
"Partly cloudy": "Переменная облачность",
Cloudy: "Облачно",
Overcast: "Пасмурно",
Mist: "Туман",
Fog: "Густой туман",
"Light rain": "Небольшой дождь",
"Moderate rain": "Дождь",
"Heavy rain": "Сильный дождь",
"Light snow": "Небольшой снег",
"Moderate snow": "Снег",
"Heavy snow": "Сильный снег",
Thunderstorm: "Гроза",
"Patchy rain possible": "Местами дождь",
"Patchy snow possible": "Местами снег",
"Patchy sleet possible": "Местами мокрый снег",
"Patchy freezing drizzle possible": "Местами изморось",
"Blowing snow": "Поземок",
Blizzard: "Метель",
"Freezing fog": "Ледяной туман",
"Patchy light drizzle": "Морось",
"Light drizzle": "Легкая морось",
"Freezing drizzle": "Замерзающая морось",
"Heavy freezing drizzle": "Сильная изморось",
"Patchy light rain": "Кратковременный дождь",
"Light rain shower": "Ливень",
"Moderate or heavy rain shower": "Сильный ливень",
"Torrential rain shower": "Ливневой дождь",
"Patchy light snow": "Кратковременный снег",
"Light snow showers": "Снеговые заряды",
"Moderate or heavy snow showers": "Сильные снегопады",
"Patchy light rain with thunder": "Гроза с дождем",
"Moderate or heavy rain with thunder": "Гроза с сильным дождем",
"Patchy light snow with thunder": "Гроза со снегом",
"Moderate or heavy snow with thunder": "Гроза с сильным снегом",
"Clear night": "Ясная ночь",
"Partly cloudy night": "Переменная облачность ночью",
"Cloudy night": "Облачная ночь",
"Sunny night": "Солнечная ночь",
"Light rain night": "Дождь ночью",
"Moderate rain night": "Дождь ночью",
"Heavy rain night": "Сильный дождь ночью",
"Light snow night": "Снег ночью",
"Moderate snow night": "Снег ночью",
"Heavy snow night": "Сильный снег ночью",
"Thunderstorm night": "Гроза ночью",
"Patchy rain possible night": "Местами дождь ночью",
"Patchy snow possible night": "Местами снег ночью",
"Mist night": "Туман ночью",
"Fog night": "Густой туман ночью",
"Light sleet": "Мокрый снег",
"Moderate or heavy sleet": "Сильный мокрый снег",
"Ice pellets": "Ледяная крупа",
"Light showers of ice pellets": "Ледяная крупа",
"Moderate or heavy showers of ice pellets": "Сильная ледяная крупа",
Hail: "Град",
"Light hail": "Мелкий град",
"Moderate or heavy hail": "Крупный град",
Tornado: "Торнадо",
Hurricane: "Ураган",
Squall: "Шквал",
Dust: "Пыль",
Sand: "Песок",
"Volcanic ash": "Вулканический пепел",
"Thundery outbreaks possible": "Возможны грозы",
"Thundery outbreaks in nearby": "Грозы в округе",
};
// Функция для конвертации времени в 24-часовой формат
function convertTo24Hour(timeStr) {
if (!timeStr.includes(" ")) {
// Уже в 24-часовом формате или без AM/PM
return timeStr;
}
const [time, period] = timeStr.split(" ");
let [hours, minutes] = time.split(":").map(Number);
if (period === "PM" && hours !== 12) {
hours += 12;
} else if (period === "AM" && hours === 12) {
hours = 0;
}
return `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}`;
}
// Функция для получения иконки погоды с умным fallback
function getWeatherIcon(conditionText) {
// Сначала проверяем русские условия
const englishCondition =
russianToEnglishWeather[conditionText] || conditionText;
// Прямой маппинг
if (weatherIcons[englishCondition]) {
return weatherIcons[englishCondition];
}
// Умный fallback на основе ключевых слов
const lowerCondition = englishCondition.toLowerCase();
if (
lowerCondition.includes("rain") ||
lowerCondition.includes("drizzle") ||
lowerCondition.includes("shower")
) {
return "cloud-rain";
}
if (
lowerCondition.includes("snow") ||
lowerCondition.includes("sleet") ||
lowerCondition.includes("hail") ||
lowerCondition.includes("ice")
) {
return "cloud-snow";
}
if (lowerCondition.includes("thunder") || lowerCondition.includes("storm")) {
return "cloud-lightning";
}
if (lowerCondition.includes("fog") || lowerCondition.includes("mist")) {
return "cloud-fog";
}
if (lowerCondition.includes("cloud") || lowerCondition.includes("overcast")) {
return "cloud";
}
if (lowerCondition.includes("sun") || lowerCondition.includes("clear")) {
return "sun";
}
if (
lowerCondition.includes("wind") ||
lowerCondition.includes("blowing") ||
lowerCondition.includes("blizzard")
) {
return "wind";
}
// Общий fallback
return "cloud";
}
// Функция для получения красивого русского названия погоды
function getBeautifulRussianName(conditionText) {
return beautifulRussianNames[conditionText] || conditionText;
}
// Функция для получения цвета иконки погоды
function getWeatherIconColor(iconName) {
const isDark =
document.documentElement.classList.contains("dark") ||
document.body.classList.contains("dark");
const colorMap = {
sun: isDark ? "sun-dark" : "sun",
moon: isDark ? "sun-dark" : "sun",
"cloud-sun": isDark ? "sun-dark" : "sun",
"cloud-moon": isDark ? "sun-dark" : "sun",
cloud: isDark ? "cloud-dark" : "cloud",
"cloud-rain": isDark ? "rain-dark" : "rain",
"cloud-drizzle": isDark ? "drizzle-dark" : "drizzle",
"cloud-snow": isDark ? "snow-dark" : "snow",
"cloud-lightning": isDark ? "thunder-dark" : "thunder",
"cloud-fog": isDark ? "fog-dark" : "fog",
wind: isDark ? "wind-dark" : "wind",
"cloud-hail": isDark ? "hail-dark" : "hail",
tornado: isDark ? "thunder-dark" : "thunder",
hurricane: isDark ? "thunder-dark" : "thunder",
};
return colorMap[iconName] || (isDark ? "cloud-dark" : "cloud");
}
// Функция для перевода направления ветра на русский
function getRussianWindDirection(windDir) {
const windDirMap = {
N: "Север",
NNE: "Северо-северо-восток",
NE: "Северо-восток",
ENE: "Востоко-северо-восток",
E: "Восток",
ESE: "Востоко-юго-восток",
SE: "Юго-восток",
SSE: "Юго-юго-восток",
S: "Юг",
SSW: "Юго-юго-запад",
SW: "Юго-запад",
WSW: "Западо-юго-запад",
W: "Запад",
WNW: "Западо-северо-запад",
NW: "Северо-запад",
NNW: "Северо-северо-запад",
};
return windDirMap[windDir] || windDir; // Если направление не найдено, вернуть оригинал
}
// Функция для геолокации
async function getCurrentLocation() {
console.log("Начало функции getCurrentLocation");
if (!navigator.geolocation) {
console.error("Геолокация не поддерживается браузером");
alert("Геолокация не поддерживается вашим браузером");
ymSendEvent("geolocation_error", { error: "not_supported" });
return;
}
// Определяем классы иконки на основе текущей темы
const isDark =
document.documentElement.classList.contains("dark") ||
document.body.classList.contains("dark");
const iconClasses = `w-5 h-5 ${isDark ? "text-white" : "text-black"}`;
console.log("Классы иконки для текущей темы:", iconClasses);
console.log("Текущий innerHTML кнопки:", geolocationBtn.innerHTML);
geolocationBtn.disabled = true;
geolocationBtn.innerHTML =
'<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i>';
console.log("Вызов navigator.geolocation.getCurrentPosition");
navigator.geolocation.getCurrentPosition(
async (position) => {
const { latitude, longitude } = position.coords;
console.log("Получены координаты:", { latitude, longitude });
ymSendEvent("geolocation_success");
// Сохраняем координаты для возможного fallback
userLatitude = latitude;
userLongitude = longitude;
try {
// Получаем ближайшие города по координатам через WeatherAPI search endpoint
const locationUrl = `https://api.weatherapi.com/v1/search.json?key=${API_KEY}&q=${latitude},${longitude}`;
console.log("Запрос ближайших городов к WeatherAPI:", locationUrl);
const response = await fetch(locationUrl);
console.log("Статус ответа:", response.status);
const cities = await response.json();
console.log("Найденные города:", cities);
if (cities.length > 0) {
const cityName = cities[0].name;
console.log("Определенный город:", cityName);
// Установить город в input поле
cityInput.value = cityName;
console.log(
"Установленное значение cityInput.value:",
cityInput.value
);
localStorage.setItem("selectedCity", cityName);
console.log("Сохранено в localStorage: selectedCity =", cityName);
updatePageTitle();
// Автоматически загружаем погоду
console.log("Автоматический клик на кнопку погоды");
getWeatherBtn.click();
} else {
console.error("Не найдено городов по координатам");
alert(
"Не удалось определить ближайший город по вашему местоположению"
);
ymSendEvent("geolocation_city_error", { error: "no_cities_found" });
}
} catch (error) {
console.error("Ошибка получения города по координатам:", error);
alert("Не удалось определить город по вашему местоположению");
ymSendEvent("geolocation_city_error", { error: error.message });
}
geolocationBtn.disabled = false;
geolocationBtn.innerHTML = `<i data-lucide="navigation" class="${iconClasses}"></i>`;
console.log(
"Восстановленный innerHTML кнопки после ошибки:",
geolocationBtn.innerHTML
);
lucide.createIcons();
},
(error) => {
console.error("Ошибка геолокации:", error);
let errorMessage = "Не удалось получить ваше местоположение";
switch (error.code) {
case error.PERMISSION_DENIED:
errorMessage =
"Доступ к геолокации запрещен. Разрешите доступ в настройках браузера.";
break;
case error.POSITION_UNAVAILABLE:
errorMessage = "Информация о местоположении недоступна.";
break;
case error.TIMEOUT:
errorMessage = "Превышено время ожидания геолокации.";
break;
}
alert(errorMessage);
ymSendEvent("geolocation_error", {
code: error.code,
message: error.message,
});
geolocationBtn.disabled = false;
geolocationBtn.innerHTML = `<i data-lucide="navigation" class="${iconClasses}"></i>`;
console.log(
"Восстановленный innerHTML кнопки:",
geolocationBtn.innerHTML
);
lucide.createIcons();
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 300000, // 5 минут
}
);
}
// Функция для отображения текущей даты
function displayCurrentDate() {
const currentDateEl = document.getElementById("current-date");
const now = new Date();
const daysOfWeek = [
"воскресенье",
"понедельник",
"вторник",
"среда",
"четверг",
"пятница",
"суббота",
];
const months = [
"января",
"февраля",
"марта",
"апреля",
"мая",
"июня",
"июля",
"августа",
"сентября",
"октября",
"ноября",
"декабря",
];
const dayOfWeek = daysOfWeek[now.getDay()];
const day = now.getDate();
const month = months[now.getMonth()];
currentDateEl.textContent = `${dayOfWeek}, ${day} ${month}`;
}
// Функция для обновления заголовка страницы
function updatePageTitle() {
const selectedCity = cityInput.value;
// Извлечь только название города из "Город, Регион, Страна"
const cityName = selectedCity.split(",")[0].trim();
document.title = `Погода в ${cityName || "городе"}`;
}
// Автоматическая загрузка погоды при загрузке страницы
document.addEventListener("DOMContentLoaded", () => {
// Отправляем событие загрузки страницы
ymSendEvent("page_load");
// Отображаем текущую дату
displayCurrentDate();
// Загружаем сохраненный город из localStorage
const savedCity = localStorage.getItem("selectedCity");
if (savedCity) {
cityInput.value = savedCity;
}
// Настраиваем поиск городов
setupCitySearch();
// Обновляем заголовок страницы
updatePageTitle();
// Обработчик для геолокации
geolocationBtn.addEventListener("click", getCurrentLocation);
// Инициализируем тему
initializeTheme();
watchSystemTheme();
// Регистрируем Service Worker для PWA
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/service-worker.js")
.then(function (registration) {
console.log(
"Service Worker registered successfully:",
registration.scope
);
})
.catch(function (error) {
console.log("Service Worker registration failed:", error);
});
}
// Логи для PWA установки
let deferredPrompt;
window.addEventListener("beforeinstallprompt", (e) => {
console.log("PWA install prompt available");
deferredPrompt = e;
ymSendEvent("pwa_install_prompt");
});
window.addEventListener("appinstalled", (e) => {
console.log("PWA installed successfully");
ymSendEvent("pwa_installed");
});
// Функция для проверки реального соединения
async function checkOnlineStatus() {
try {
// Пингуем небольшой ресурс
const response = await fetch("/manifest.json", {
method: "HEAD",
cache: "no-cache",
});
return response.ok;
} catch (error) {
console.log("Connection check failed:", error);
return false;
}
}
// Функция для управления плашкой оффлайн
async function updateOfflineBanner() {
const banner = document.getElementById("offline-banner");
const isOnline = await checkOnlineStatus();
if (!isOnline) {
banner.classList.remove("hidden");
console.log("Оффлайн режим активирован");
ymSendEvent("offline_mode");
} else {
banner.classList.add("hidden");
console.log("Онлайн режим восстановлен");
ymSendEvent("online_mode");
}
}
// Проверяем статус при загрузке
updateOfflineBanner();
// Слушаем изменения статуса сети и проверяем реально
window.addEventListener("online", updateOfflineBanner);
window.addEventListener("offline", updateOfflineBanner);
// Периодическая проверка каждые 30 секунд
setInterval(updateOfflineBanner, 30000);
// Сохраняем данные для графиков
window.currentHourlyData = null;
// Ждем загрузки Swiper
if (typeof Swiper !== "undefined") {
getWeatherBtn.click();
} else {
// Если Swiper еще не загрузился, ждем его
const checkSwiper = setInterval(() => {
if (typeof Swiper !== "undefined") {
clearInterval(checkSwiper);
getWeatherBtn.click();
}
}, 100);
}
});
getWeatherBtn.addEventListener("click", async () => {
const cityFull = cityInput.value.trim();
if (!cityFull) {
alert("Пожалуйста, введите название города");
return;
}
// Извлечь только название города для API
const city = cityFull.split(",")[0].trim();
// Отправляем событие выбора города
ymSendEvent("city_select", { city: city, cityFull: cityFull });
// Сохраняем выбранный город в localStorage (полное название для отображения)
localStorage.setItem("selectedCity", cityFull);
let url = `https://api.weatherapi.com/v1/forecast.json?key=${API_KEY}&q=${city}&days=7&hourly=true&aqi=yes&lang=ru`;
console.log("Запрос к API:", url);
console.log("Выбранный город:", city);
console.log("API ключ используется:", API_KEY);
try {
let response = await fetch(url);
console.log("Статус ответа:", response.status, response.statusText);
console.log(
"Заголовки ответа:",
Object.fromEntries(response.headers.entries())
);
if (!response.ok) {
let errorText = "";
try {
errorText = await response.text();
console.log("Текст ошибки от API:", errorText);
} catch (e) {
console.log("Не удалось прочитать текст ошибки");
}
throw new Error(
`Ошибка сети: ${response.status} ${response.statusText}${
errorText ? ` - ${errorText}` : ""
}`
);
}
const data = await response.json();
console.log("Данные получены успешно");
// Отправляем событие успешной загрузки данных
ymSendEvent("weather_load_success", { city: city });
// Отображаем текущую погоду
displayCurrentWeather(data);
// Пересоздаем иконки после обновления DOM
setTimeout(() => {
lucide.createIcons();
}, 100);
// Сохраняем данные для графиков
window.currentHourlyData = data.forecast.forecastday[0].hour;
// Таймлапс на сегодня
displayTimelapse(
data.forecast.forecastday[0].hour,
data.location.localtime
);
// Погода на неделю
displayWeeklyWeather(data.forecast.forecastday);
// Графики температуры и осадков
createTemperatureChart(data.forecast.forecastday[0].hour);
createPrecipitationChart(data.forecast.forecastday[0].hour);
// Астрономическая информация
displayAstronomy(data.forecast.forecastday[0].astro);
// Качество воздуха
displayAirQuality(data.current.air_quality);
// Обновляем заголовок страницы с выбранным городом
updatePageTitle();
} catch (error) {
console.error("Ошибка загрузки данных:", error);
console.error("Детали ошибки:", error.message);
if (error.response) {
console.error("Статус ответа:", error.response.status);
console.error("Текст ответа:", error.response.statusText);
}
// Попытка загрузить из кэша для оффлайн режима
try {
const cache = await caches.open(API_CACHE_NAME);
const cachedResponse = await cache.match(url);
if (cachedResponse) {
console.log("Используем кэшированные данные погоды для оффлайн режима");
const cachedData = await cachedResponse.json();
ymSendEvent("weather_load_from_cache");
// Отображаем кэшированные данные
displayCurrentWeather(cachedData);
setTimeout(() => {
lucide.createIcons();
}, 100);
window.currentHourlyData = cachedData.forecast.forecastday[0].hour;
displayTimelapse(
cachedData.forecast.forecastday[0].hour,
cachedData.location.localtime
);
displayWeeklyWeather(cachedData.forecast.forecastday);
createTemperatureChart(cachedData.forecast.forecastday[0].hour);
createPrecipitationChart(cachedData.forecast.forecastday[0].hour);
displayAstronomy(cachedData.forecast.forecastday[0].astro);
displayAirQuality(cachedData.current.air_quality);
updatePageTitle();
return; // Успешно загружено из кэша
} else {
console.log("Кэш пуст, данные недоступны");
}
} catch (cacheError) {
console.error("Ошибка чтения кэша:", cacheError);
}
// Fallback: если город не найден и есть координаты, используем их
if (userLatitude !== null && userLongitude !== null) {
console.log("Пытаемся загрузить погоду по координатам:", {
userLatitude,
userLongitude,
});
try {
const fallbackUrl = `https://api.weatherapi.com/v1/forecast.json?key=${API_KEY}&q=${userLatitude},${userLongitude}&days=7&hourly=true&aqi=yes&lang=ru`;
console.log("Fallback запрос к API:", fallbackUrl);
const fallbackResponse = await fetch(fallbackUrl);
if (fallbackResponse.ok) {
const fallbackData = await fallbackResponse.json();
console.log("Fallback данные получены успешно");
// Обновляем город в интерфейсе на реальный из данных
const realCityName = fallbackData.location.name;
cityInput.value = realCityName;
localStorage.setItem("selectedCity", realCityName);
updatePageTitle();
// Отправляем событие успешной fallback загрузки
ymSendEvent("weather_load_success_fallback", { city: realCityName });
// Повторяем отображение с fallback данными
displayCurrentWeather(fallbackData);
setTimeout(() => {
lucide.createIcons();
}, 100);
window.currentHourlyData = fallbackData.forecast.forecastday[0].hour;
displayTimelapse(
fallbackData.forecast.forecastday[0].hour,
fallbackData.location.localtime
);
displayWeeklyWeather(fallbackData.forecast.forecastday);
createTemperatureChart(fallbackData.forecast.forecastday[0].hour);
createPrecipitationChart(fallbackData.forecast.forecastday[0].hour);
displayAstronomy(fallbackData.forecast.forecastday[0].astro);
displayAirQuality(fallbackData.current.air_quality);
return; // Успешно, выходим
} else {
console.error("Fallback также не удался");
}
} catch (fallbackError) {
console.error("Ошибка fallback:", fallbackError);
}
}
// Отправляем событие ошибки загрузки
ymSendEvent("weather_load_error", {
city: city,
error: error.message,
});
alert(
`Не удалось загрузить данные о погоде для "${city}". Попробуйте другой город или проверьте API ключ. Детали в консоли браузера.`
);
}
});
function displayAstronomy(astroData) {
const sunriseEl = document.getElementById("sunrise-time");
const sunsetEl = document.getElementById("sunset-time");
const moonPhaseEl = document.getElementById("moon-phase");
const astronomySection = document.getElementById("astronomy-section");
// Перевод фаз луны на русский
const moonPhases = {
"New Moon": "Новолуние",
"Waxing Crescent": "Растущий серп",
"First Quarter": "Первая четверть",
"Waxing Gibbous": "Растущая луна",
"Full Moon": "Полнолуние",
"Waning Gibbous": "Убывающая луна",
"Last Quarter": "Последняя четверть",
"Waning Crescent": "Убывающий серп",
};
sunriseEl.textContent = convertTo24Hour(astroData.sunrise);
sunsetEl.textContent = convertTo24Hour(astroData.sunset);
moonPhaseEl.textContent =
moonPhases[astroData.moon_phase] || astroData.moon_phase;
// Показываем секцию
astronomySection.classList.remove("hidden");
astronomySection.classList.add("animate-fade-in");
// Отправляем событие просмотра астрономии
ymSendEvent("astronomy_view");
// Пересоздаем иконки
setTimeout(() => {
lucide.createIcons();
}, 100);
}
function displayAirQuality(airQualityData) {
console.log("Полученные данные качества воздуха:", airQualityData);
const aqiValueEl = document.getElementById("aqi-value");
const aqiStatusEl = document.getElementById("aqi-status");
const aqiDescriptionEl = document.getElementById("aqi-description");
const aqiBarEl = document.getElementById("aqi-bar");
const airQualitySection = document.getElementById("air-quality-section");
const aqi = airQualityData["us-epa-index"]; // Используем US EPA индекс
console.log("AQI индекс качества воздуха:", aqi);
// Определяем статус и цвет
let status, color, description, percentage;
switch (aqi) {
case 1:
status = "Хороший";
color = "#10b981";
description = "Качество воздуха хорошее";
percentage = 20;
break;
case 2:
status = "Умеренный";
color = "#f59e0b";
description = "Качество воздуха умеренное";
percentage = 40;
break;
case 3:
status = "Плохое";
color = "#f97316";
description = "Качество воздуха плохое";
percentage = 60;
break;
case 4:
status = "Очень плохое";
color = "#ef4444";
description = "Качество воздуха очень плохое";
percentage = 80;
break;
case 5:
status = "Критическое";
color = "#7c2d12";
description = "Качество воздуха критическое";
percentage = 100;
break;
default:
status = "Неизвестно";
color = "#6b7280";
description = "Данные недоступны";
percentage = 0;
}
// Убираем отображение цифры AQI
aqiValueEl.style.display = "none";
aqiStatusEl.textContent = status;
aqiStatusEl.style.color = color;
aqiDescriptionEl.textContent = description;
aqiBarEl.style.width = `${percentage}%`;
aqiBarEl.style.backgroundColor = color;
// Детальные показатели
document.getElementById("pm25").textContent = airQualityData.pm2_5
? airQualityData.pm2_5.toFixed(1)
: "--";
document.getElementById("pm10").textContent = airQualityData.pm10
? airQualityData.pm10.toFixed(1)
: "--";
document.getElementById("no2").textContent = airQualityData.no2
? airQualityData.no2.toFixed(1)
: "--";
document.getElementById("o3").textContent = airQualityData.o3
? airQualityData.o3.toFixed(1)
: "--";
// Показываем секцию
airQualitySection.classList.remove("hidden");
airQualitySection.classList.add("animate-fade-in");
// Отправляем событие просмотра качества воздуха
ymSendEvent("air_quality_view", { aqi: aqi });
// Пересоздаем иконки
setTimeout(() => {
lucide.createIcons();
}, 100);
}
function displayCurrentWeather(data) {
const current = data.current;
const iconName = getWeatherIcon(current.condition.text);
const iconColorClass = getWeatherIconColor(iconName);
const beautifulName = getBeautifulRussianName(current.condition.text);
currentTempEl.textContent = `${Math.round(current.temp_c)}°C`;
const feelsLikeEl = document.getElementById("current-feels-like");
if (feelsLikeEl && current.feelslike_c !== undefined) {
feelsLikeEl.textContent = `Ощущается как ${Math.round(
current.feelslike_c
)}°C`;
}
currentDescEl.innerHTML = `<i data-lucide="${iconName}" class="w-5 h-5 inline mr-2 weather-icon ${iconColorClass}"></i>${beautifulName}`;
currentHumidityEl.textContent = `${current.humidity}%`;
currentWindEl.textContent = `${current.wind_kph} км/ч`;
// Новые поля
document.getElementById(
"current-gust"
).textContent = `Порывы: ${current.gust_kph} км/ч`;
document.getElementById("current-vis").textContent = `${current.vis_km} км`;
document.getElementById(
"current-pressure"
).textContent = `${current.pressure_mb} гПа`;
document.getElementById("current-uv").textContent = current.uv;
// Направление ветра
const windDirectionIcon = document.getElementById("wind-direction-icon");
const windDirEl = document.getElementById("current-wind-dir");
windDirEl.textContent = getRussianWindDirection(current.wind_dir);
windDirectionIcon.style.transform = `rotate(${current.wind_degree}deg)`;
// Показываем блок текущей погоды
currentWeatherDiv.classList.remove("hidden");
currentWeatherDiv.classList.add("animate-fade-in");
// Пересоздаем иконки
setTimeout(() => {
lucide.createIcons();
}, 100);
}
function displayTimelapse(hourlyData, localtime) {
timelapseContainer.innerHTML = "";
const currentHour = new Date(localtime).getHours();
let currentSlideIndex = -1;
hourlyData.forEach((item, index) => {
const slide = document.createElement("div");
const hour = new Date(item.time).getHours();
const isCurrent = hour === currentHour;
if (isCurrent) currentSlideIndex = index;
slide.className = `swiper-slide timelapse-item bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-700 dark:to-gray-800 p-3 rounded-xl shadow-md border ${
isCurrent
? "border-blue-500 bg-gradient-to-br from-blue-100 to-indigo-100 dark:from-blue-900/30 dark:to-indigo-900/30 ring-2 ring-blue-300 dark:ring-blue-600"
: "border-gray-100 dark:border-gray-600"
}`;
slide.style.width = "140px";
const iconName = getWeatherIcon(item.condition.text);
const iconColorClass = getWeatherIconColor(iconName);
// Отладочная информация
console.log(
`Timelapse: ${hour}:00 - Условие: "${item.condition.text}" - Иконка: "${iconName}" - Цвет: "${iconColorClass}"`
);
slide.innerHTML = `
<div class="text-center">
<p class="text-xs font-medium ${
isCurrent
? "text-blue-700 dark:text-blue-300 font-semibold"
: "text-gray-600 dark:text-gray-300"
} mb-1">${hour}:00 ${isCurrent ? "(сейчас)" : ""}</p>
<div class="flex justify-center mb-2">
<i data-lucide="${iconName}" class="w-6 h-6 weather-icon ${iconColorClass} ${
isCurrent
? "ring-2 ring-blue-300 dark:ring-blue-600 rounded-full p-1"
: ""
}"></i>
</div>
<p class="text-lg font-bold ${
isCurrent
? "text-blue-700 dark:text-blue-300"
: "text-blue-600 dark:text-blue-400"
} mb-1">${Math.round(item.temp_c)}°C</p>
<p class="text-xs ${
isCurrent
? "text-blue-600 dark:text-blue-400"
: "text-gray-600 dark:text-gray-300"
} mb-1">${getBeautifulRussianName(item.condition.text)}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">💧 ${
item.precip_mm
} мм</p>
<p class="text-xs text-gray-500 dark:text-gray-400">☔ ${
item.chance_of_rain
}%</p>
</div>
`;
timelapseContainer.appendChild(slide);
});
// Пересоздаем иконки
setTimeout(() => {
lucide.createIcons();
}, 100);
// Инициализируем Swiper если он еще не создан
setTimeout(() => {
if (!weatherSwiper) {
weatherSwiper = new Swiper(".swiper", {
slidesPerView: window.innerWidth < 768 ? 3 : 6,
spaceBetween: 12,
grabCursor: true,
freeMode: true,
mousewheel: {
enabled: true,
sensitivity: 1,
},
slidesOffsetBefore: 0,
slidesOffsetAfter: 0,
centeredSlides: false,
watchSlidesProgress: true,
});
} else {
weatherSwiper.update();
weatherSwiper.updateSlides();
}
// Фокус на текущем времени города
if (currentSlideIndex !== -1) {
setTimeout(() => {
weatherSwiper.slideTo(currentSlideIndex, 800);
}, 200);
}
}, 50);
}
function displayWeeklyWeather(forecastData) {
const weeklyContainer = document.getElementById("weekly-container");
weeklyContainer.innerHTML = "";
const daysOfWeek = ["вс", "пн", "вт", "ср", "чт", "пт", "сб"];
forecastData.forEach((day, index) => {
const date = new Date(day.date);
const dayOfWeek = daysOfWeek[date.getDay()];
const dayNumber = date.getDate();
const month = date.toLocaleDateString("ru-RU", { month: "short" });
const iconName = getWeatherIcon(day.day.condition.text);
const iconColorClass = getWeatherIconColor(iconName);
const isToday = index === 0;
// Отладочная информация
console.log(
`Weekly: ${isToday ? "Сегодня" : dayOfWeek} - Условие: "${
day.day.condition.text
}" - Иконка: "${iconName}" - Цвет: "${iconColorClass}"`
);
const card = document.createElement("div");
card.className = `text-center p-4 rounded-xl border transition-all duration-200 hover:shadow-lg ${
isToday
? "bg-blue-50 dark:bg-blue-900/20 border-blue-300 dark:border-blue-700 ring-2 ring-blue-200 dark:ring-blue-600"
: "bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-600 hover:border-blue-300 dark:hover:border-blue-600"
}`;
card.innerHTML = `
<div class="mb-2">
<p class="text-sm font-medium text-gray-600 dark:text-gray-300 ${
isToday ? "text-blue-700 dark:text-blue-300" : ""
}">
${isToday ? "Сегодня" : dayOfWeek}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">${dayNumber} ${month}</p>
</div>
<div class="flex justify-center mb-3">
<i data-lucide="${iconName}" class="w-8 h-8 weather-icon ${iconColorClass} ${
isToday ? "ring-2 ring-blue-300 dark:ring-blue-600 rounded-full p-1" : ""
}"></i>
</div>
<div class="mb-2">
<p class="text-lg font-bold ${
isToday
? "text-blue-700 dark:text-blue-300"
: "text-gray-800 dark:text-gray-200"
}">
${Math.round(day.day.maxtemp_c)}°
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
${Math.round(day.day.mintemp_c)}°
</p>
</div>
<p class="text-xs ${
isToday
? "text-blue-600 dark:text-blue-400"
: "text-gray-600 dark:text-gray-300"
}">
${getBeautifulRussianName(day.day.condition.text)}
</p>
`;
weeklyContainer.appendChild(card);
});
// Пересоздаем иконки для недельного прогноза
setTimeout(() => {
lucide.createIcons();
}, 100);
}
// Функция создания графика температуры
function createTemperatureChart(hourlyData) {
const ctx = document.getElementById("temperatureChart").getContext("2d");
const colors = getThemeColors();
const isDark =
document.documentElement.classList.contains("dark") ||
document.body.classList.contains("dark");
// Отправляем событие просмотра графика температуры
ymSendEvent("chart_view", { chart_type: "temperature" });
// Подготавливаем данные
const labels = hourlyData.map((item) => {
const hour = new Date(item.time).getHours();
return `${hour}:00`;
});
const temperatures = hourlyData.map((item) => Math.round(item.temp_c));
// Уничтожаем предыдущий график если он существует
if (temperatureChart) {
temperatureChart.destroy();
}
// Создаем новый график
temperatureChart = new Chart(ctx, {
type: "line",
data: {
labels: labels,
datasets: [
{
label: "Температура (°C)",
data: temperatures,
borderColor: "#3b82f6",
backgroundColor: "rgba(59, 130, 246, 0.1)",
borderWidth: 3,
fill: true,
tension: 0.4,
pointBackgroundColor: "#3b82f6",
pointBorderColor: "#ffffff",
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6,
pointHoverBackgroundColor: "#2563eb",
pointHoverBorderColor: "#ffffff",
pointHoverBorderWidth: 2,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
tooltip: {
backgroundColor: colors.backgroundColor,
titleColor: colors.titleColor,
bodyColor: colors.bodyColor,
cornerRadius: 8,
displayColors: false,
callbacks: {
title: function (context) {
return `Время: ${context[0].label}`;
},
label: function (context) {
return `Температура: ${context.parsed.y}°C`;
},
},
},
},
scales: {
x: {
grid: {
display: false,
},
ticks: {
color: colors.textColor,
font: {
size: 12,
color: colors.textColor,
},
},
},
y: {
grid: {
color: colors.gridColor,
lineWidth: 1,
},
ticks: {
color: colors.textColor,
font: {
size: 12,
color: colors.textColor,
},
callback: function (value) {
return value + "°C";
},
},
},
},
animation: {
duration: 2000,
easing: "easeInOutQuart",
},
interaction: {
intersect: false,
mode: "index",
},
},
});
}
// Функция для получения цветов в зависимости от темы
function getThemeColors() {
const isDark =
document.documentElement.classList.contains("dark") ||
document.body.classList.contains("dark");
return {
textColor: isDark ? "#d1d5db" : "#ffffff",
gridColor: isDark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)",
backgroundColor: isDark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.8)",
titleColor: isDark ? "#ffffff" : "#ffffff",
bodyColor: isDark ? "#ffffff" : "#ffffff",
};
}
// Функции для работы с темами
function initializeTheme() {
const savedTheme = localStorage.getItem("theme") || "auto";
setTheme(savedTheme);
updateThemeButtons(savedTheme);
// Если выбрана авто-тема, проверяем системную тему при загрузке
if (savedTheme === "auto") {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
updateThemeButtons(prefersDark ? "dark" : "light");
}
// Добавляем обработчики событий для кнопок темы
if (themeButtons.light) {
themeButtons.light.addEventListener("click", () => {
setTheme("light");
updateThemeButtons("light");
});
}
if (themeButtons.auto) {
themeButtons.auto.addEventListener("click", () => {
setTheme("auto");
updateThemeButtons("auto");
});
}
if (themeButtons.dark) {
themeButtons.dark.addEventListener("click", () => {
setTheme("dark");
updateThemeButtons("dark");
});
}
}
function setTheme(theme) {
const html = document.documentElement;
// Удаляем предыдущие настройки темы
html.removeAttribute("data-theme");
html.classList.remove("dark");
document.body.classList.remove("dark");
if (theme === "dark") {
html.setAttribute("data-theme", "dark");
html.classList.add("dark");
document.body.classList.add("dark");
} else if (theme === "auto") {
// Определяем системную тему
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (prefersDark) {
html.setAttribute("data-theme", "dark");
html.classList.add("dark");
document.body.classList.add("dark");
} else {
html.setAttribute("data-theme", "light");
}
} else {
// Светлая тема
html.setAttribute("data-theme", "light");
}
// Отправляем событие смены темы
ymSendEvent("theme_change", { theme: theme });
// Сохраняем выбранную тему
localStorage.setItem("theme", theme);
// Обновляем иконки после смены темы
setTimeout(() => {
if (typeof lucide !== "undefined") {
lucide.createIcons();
}
}, 100);
// Принудительно перестраиваем Swiper если он существует
if (window.weatherSwiper) {
setTimeout(() => {
window.weatherSwiper.update();
window.weatherSwiper.updateSlides();
}, 150);
}
// Обновляем графики если они существуют
if (temperatureChart) {
temperatureChart.destroy();
createTemperatureChart(window.currentHourlyData || []);
}
if (precipitationChart) {
precipitationChart.destroy();
createPrecipitationChart(window.currentHourlyData || []);
}
}
function updateThemeButtons(activeTheme) {
Object.keys(themeButtons).forEach((theme) => {
if (themeButtons[theme]) {
if (theme === activeTheme) {
themeButtons[theme].classList.add("active");
} else {
themeButtons[theme].classList.remove("active");
}
}
});
}
// Слушаем изменения системной темы
function watchSystemTheme() {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", (e) => {
const currentTheme = localStorage.getItem("theme");
if (currentTheme === "auto") {
setTheme("auto");
}
});
}
// Функция создания графика осадков
function createPrecipitationChart(hourlyData) {
const ctx = document.getElementById("precipitationChart").getContext("2d");
const colors = getThemeColors();
// Отправляем событие просмотра графика осадков
ymSendEvent("chart_view", { chart_type: "precipitation" });
// Подготавливаем данные
const labels = hourlyData.map((item) => {
const hour = new Date(item.time).getHours();
return `${hour}:00`;
});
const precipitations = hourlyData.map((item) => item.precip_mm);
const chances = hourlyData.map((item) => item.chance_of_rain);
// Уничтожаем предыдущий график если он существует
if (precipitationChart) {
precipitationChart.destroy();
}
// Создаем новый график
precipitationChart = new Chart(ctx, {
type: "bar",
data: {
labels: labels,
datasets: [
{
label: "Осадки (мм)",
data: precipitations,
backgroundColor: "rgba(59, 130, 246, 0.6)",
borderColor: "#3b82f6",
borderWidth: 1,
borderRadius: 4,
borderSkipped: false,
barThickness: "flex",
maxBarThickness: 30,
},
{
label: "Вероятность (%)",
data: chances,
type: "line",
borderColor: "#ef4444",
backgroundColor: "rgba(239, 68, 68, 0.1)",
borderWidth: 2,
fill: false,
tension: 0.4,
pointBackgroundColor: "#ef4444",
pointBorderColor: "#ffffff",
pointBorderWidth: 2,
pointRadius: 3,
pointHoverRadius: 5,
pointHoverBackgroundColor: "#dc2626",
pointHoverBorderColor: "#ffffff",
pointHoverBorderWidth: 2,
yAxisID: "y1",
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
tooltip: {
backgroundColor: colors.backgroundColor,
titleColor: colors.titleColor,
bodyColor: colors.bodyColor,
cornerRadius: 8,
displayColors: true,
callbacks: {
title: function (context) {
return `Время: ${context[0].label}`;
},
label: function (context) {
if (context.datasetIndex === 0) {
return `Осадки: ${context.parsed.y} мм`;
} else {
return `Вероятность: ${context.parsed.y}%`;
}
},
},
},
},
scales: {
x: {
grid: {
display: false,
},
ticks: {
color: colors.textColor,
font: {
size: 12,
color: colors.textColor,
},
},
},
y: {
type: "linear",
display: true,
position: "left",
grid: {
color: colors.gridColor,
lineWidth: 1,
},
ticks: {
color: colors.textColor,
font: {
size: 12,
color: colors.textColor,
},
callback: function (value) {
return value + " мм";
},
},
title: {
display: true,
text: "Осадки (мм)",
color: "#3b82f6",
font: {
size: 14,
weight: "bold",
},
},
},
y1: {
type: "linear",
display: true,
position: "right",
grid: {
drawOnChartArea: false,
},
ticks: {
color: "#ef4444",
font: {
size: 12,
color: "#ef4444",
},
callback: function (value) {
return value + "%";
},
},
title: {
display: true,
text: "Вероятность (%)",
color: "#ef4444",
font: {
size: 14,
weight: "bold",
},
},
},
},
animation: {
duration: 2000,
easing: "easeInOutQuart",
delay: function (context) {
return context.dataIndex * 100;
},
},
interaction: {
intersect: false,
mode: "index",
},
},
});
}