1102 lines
37 KiB
JavaScript
1102 lines
37 KiB
JavaScript
// API ключ от WeatherAPI
|
||
const API_KEY = "485eff906f7d473b913104046250710";
|
||
|
||
// Глобальные переменные для темы
|
||
const themeButtons = {
|
||
light: document.getElementById("theme-light"),
|
||
auto: document.getElementById("theme-auto"),
|
||
dark: document.getElementById("theme-dark"),
|
||
};
|
||
|
||
const citySelect = document.getElementById("city");
|
||
const getWeatherBtn = document.getElementById("get-weather");
|
||
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");
|
||
|
||
// Глобальная переменная для Swiper
|
||
let weatherSwiper;
|
||
|
||
// Глобальные переменные для графиков
|
||
let temperatureChart = null;
|
||
let precipitationChart = null;
|
||
|
||
// Иконки погоды
|
||
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": "Грозы в округе",
|
||
};
|
||
|
||
// Функция для получения иконки погоды с умным 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 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 = citySelect.options[citySelect.selectedIndex].text;
|
||
document.title = `Погода в ${selectedCity}`;
|
||
}
|
||
|
||
// Автоматическая загрузка погоды при загрузке страницы
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
// Отображаем текущую дату
|
||
displayCurrentDate();
|
||
|
||
// Загружаем сохраненный город из localStorage
|
||
const savedCity = localStorage.getItem("selectedCity");
|
||
if (savedCity) {
|
||
citySelect.value = savedCity;
|
||
}
|
||
|
||
// Обновляем заголовок страницы
|
||
updatePageTitle();
|
||
|
||
// Инициализируем тему
|
||
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;
|
||
});
|
||
|
||
window.addEventListener('appinstalled', (e) => {
|
||
console.log('PWA installed successfully');
|
||
});
|
||
|
||
// Функция для проверки реального соединения
|
||
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('Оффлайн режим активирован');
|
||
} else {
|
||
banner.classList.add('hidden');
|
||
console.log('Онлайн режим восстановлен');
|
||
}
|
||
}
|
||
|
||
// Проверяем статус при загрузке
|
||
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 city = citySelect.value;
|
||
|
||
// Сохраняем выбранный город в localStorage
|
||
localStorage.setItem("selectedCity", city);
|
||
|
||
const url = `https://api.weatherapi.com/v1/forecast.json?key=${API_KEY}&q=${city}&days=7&hourly=true&lang=ru`;
|
||
|
||
console.log("Запрос к API:", url);
|
||
console.log("Выбранный город:", city);
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
console.log("Статус ответа:", response.status, response.statusText);
|
||
if (!response.ok) {
|
||
throw new Error(`Ошибка сети: ${response.status} ${response.statusText}`);
|
||
}
|
||
const data = await response.json();
|
||
console.log("Данные получены успешно");
|
||
|
||
// Отображаем текущую погоду
|
||
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);
|
||
|
||
// Обновляем заголовок страницы с выбранным городом
|
||
updatePageTitle();
|
||
} catch (error) {
|
||
console.error("Ошибка загрузки данных:", error);
|
||
console.error("Детали ошибки:", error.message);
|
||
if (error.response) {
|
||
console.error("Статус ответа:", error.response.status);
|
||
console.error("Текст ответа:", error.response.statusText);
|
||
}
|
||
alert("Не удалось загрузить данные о погоде. Проверьте API ключ.");
|
||
}
|
||
});
|
||
|
||
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`;
|
||
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} км/ч`;
|
||
|
||
// Показываем блок текущей погоды
|
||
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");
|
||
|
||
// Подготавливаем данные
|
||
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");
|
||
}
|
||
|
||
// Сохраняем выбранную тему
|
||
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();
|
||
|
||
// Подготавливаем данные
|
||
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",
|
||
},
|
||
},
|
||
});
|
||
}
|