1658 lines
58 KiB
JavaScript
1658 lines
58 KiB
JavaScript
// 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",
|
||
},
|
||
},
|
||
});
|
||
}
|