Добавлены новые функции

This commit is contained in:
Fovway 2025-10-09 23:48:59 +07:00
parent d484ccf9a9
commit 38ee07e881
14 changed files with 846 additions and 85 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules/
apikey.txt
Идеи.txt

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -13,7 +13,7 @@
- **Недельный прогноз**: погода на 7 дней вперед
- **Интерактивные графики**: температура и осадки с помощью Chart.js
- **Адаптивный дизайн**: корректное отображение на всех устройствах
- **Выбор городов**: предустановленный список российских городов
- **Выбор городов**: поиск городов через API WeatherAPI.com
- **Система тем**: светлая, темная и автоматическая (по системным настройкам)
- **Анимации**: плавные переходы и hover-эффекты
- **PWA (Progressive Web App)**: установка как нативное приложение, оффлайн режим с кэшированием данных
@ -24,7 +24,7 @@
### Backend
- **Node.js** v8+ (устаревшая версия, рекомендуется обновить до LTS)
- **Node.js** v8 (устаревшая версия, рекомендуется обновить до LTS 18+)
- **Express.js** v4.17.1 - веб-сервер
- **Service Worker** - кэширование и оффлайн поддержка
@ -55,7 +55,7 @@
### Системные требования
- **Node.js** v8+ (⚠️ устаревшая версия, рекомендуется использовать LTS версию Node.js 18+)
- **Node.js** v8 (⚠️ устаревшая версия, рекомендуется использовать актуальную LTS версию Node.js 18+)
- **npm** для управления пакетами
### Установка
@ -157,6 +157,7 @@
- **WeatherAPI.com** - источник данных о погоде
- **Google Fonts API** - загрузка шрифтов Inter
- **Yandex Metrika** - аналитика и отслеживание событий
- **CDN сервисы** для библиотек JavaScript (jsDelivr, unpkg)
## Разработчик
@ -179,7 +180,7 @@
1. Зарегистрироваться на [WeatherAPI.com](https://weatherapi.com)
2. Получить бесплатный API ключ
3. Добавить ключ в переменные окружения или конфигурацию сервера(server.js - const API_KEY = "API_KEY_HERE";)
3. Добавить ключ в script.js (const API_KEY = "API_KEY_HERE";)
### Производительность

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Погода</title>
<title>Погода</title>
<meta name="theme-color" content="#3b82f6" />
<link rel="manifest" href="/manifest.json" />
<link
@ -318,27 +318,28 @@
>
<i data-lucide="map-pin" class="w-5 h-5 text-blue-500"></i>
<label
for="city"
for="city-search"
class="font-medium text-lg"
style="color: var(--theme-text-secondary)"
>Выберите город:</label
>
</div>
<select
<input
id="city"
class="flex-1 px-6 py-3 border-2 border-gray-200 rounded-xl focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all duration-200 bg-white text-gray-700 font-medium min-w-[200px]"
type="text"
list="city-list"
placeholder="Введите название города"
class="flex-1 px-6 py-3 border-2 border-gray-200 dark:border-gray-600 rounded-xl focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all duration-200 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium min-w-[200px]"
autocomplete="off"
/>
<datalist id="city-list"></datalist>
<button
id="geolocation-btn"
class="px-4 py-3 bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700 font-semibold rounded-xl transition-all duration-200 transform hover:scale-105 flex items-center gap-2"
title="Определить мое местоположение"
>
<option value="Moscow">Москва</option>
<option value="Saint Petersburg">Санкт-Петербург</option>
<option value="Novosibirsk">Новосибирск</option>
<option value="Yekaterinburg">Екатеринбург</option>
<option value="Nizhny Novgorod">Нижний Новгород</option>
<option value="Kazan">Казань</option>
<option value="Chelyabinsk">Челябинск</option>
<option value="Omsk">Омск</option>
<option value="Samara">Самара</option>
<option value="Rostov-on-Don">Ростов-на-Дону</option>
</select>
<i data-lucide="navigation" class="w-5 h-5 text-black dark:text-white"></i>
</button>
<button
id="get-weather"
class="px-8 py-3 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-xl transition-all duration-200 transform hover:scale-105 shadow-lg hover:shadow-xl flex items-center gap-2"
@ -364,25 +365,35 @@
<i data-lucide="thermometer" class="w-6 h-6 text-blue-500"></i>
Текущая погода
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i
data-lucide="thermometer"
class="w-5 h-5 text-blue-500"
></i>
<p class="text-4xl font-bold text-blue-500" id="current-temp">
--°C
</p>
</div>
<p
class="text-lg"
id="current-desc"
style="color: var(--theme-text-secondary)"
>
Загрузка...
<!-- Температура отдельно в первом ряду -->
<div class="text-center mb-6">
<div class="flex items-center justify-center gap-2 mb-2">
<i
data-lucide="thermometer"
class="w-5 h-5 text-blue-500"
></i>
<p class="text-4xl font-bold text-blue-500" id="current-temp">
--°C
</p>
</div>
<p
class="text-sm"
id="current-feels-like"
style="color: var(--theme-text-secondary)"
>
Ощущается как --°C
</p>
<p
class="text-lg"
id="current-desc"
style="color: var(--theme-text-secondary)"
>
Загрузка...
</p>
</div>
<!-- Влажность, ветер и видимость во втором ряду -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="droplets" class="w-5 h-5 text-blue-500"></i>
@ -420,6 +431,98 @@
>
-- км/ч
</p>
<p
class="text-sm"
id="current-gust"
style="color: var(--theme-text-secondary)"
>
Порывы: -- км/ч
</p>
</div>
</div>
</div>
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="eye" class="w-5 h-5 text-blue-500"></i>
<div>
<p
class="text-lg"
style="color: var(--theme-text-secondary)"
>
Видимость
</p>
<p
class="text-2xl font-semibold"
id="current-vis"
style="color: var(--theme-text)"
>
-- км
</p>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-6">
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="gauge" class="w-5 h-5 text-blue-500"></i>
<div>
<p
class="text-lg"
style="color: var(--theme-text-secondary)"
>
Давление
</p>
<p
class="text-2xl font-semibold"
id="current-pressure"
style="color: var(--theme-text)"
>
-- гПа
</p>
</div>
</div>
</div>
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="sun" class="w-5 h-5 text-blue-500"></i>
<div>
<p
class="text-lg"
style="color: var(--theme-text-secondary)"
>
UV-индекс
</p>
<p
class="text-2xl font-semibold"
id="current-uv"
style="color: var(--theme-text)"
>
--
</p>
</div>
</div>
</div>
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="compass" class="w-5 h-5 text-blue-500"></i>
<div>
<p
class="text-lg"
style="color: var(--theme-text-secondary)"
>
Направление ветра
</p>
<div class="flex items-center justify-center gap-2">
<i data-lucide="arrow-up" id="wind-direction-icon" class="w-6 h-6 text-blue-500 transform"></i>
<p
class="text-2xl font-semibold"
id="current-wind-dir"
style="color: var(--theme-text)"
>
--
</p>
</div>
</div>
</div>
</div>
@ -428,7 +531,7 @@
</div>
<!-- Timelapse Section -->
<div class="timelapse-weather weather-card rounded-2xl shadow-xl p-8">
<div class="timelapse-weather weather-card rounded-2xl shadow-xl p-8 mb-8">
<h2
class="text-2xl font-bold mb-6 flex items-center gap-2"
style="color: var(--theme-text)"
@ -442,7 +545,7 @@
</div>
<!-- Weekly Weather Section -->
<div class="weekly-weather weather-card rounded-2xl shadow-xl p-8 mt-8">
<div class="weekly-weather weather-card rounded-2xl shadow-xl p-8 mb-8">
<h2
class="text-2xl font-bold mb-6 flex items-center gap-2"
style="color: var(--theme-text)"
@ -458,6 +561,146 @@
</div>
</div>
<!-- Astronomy Section -->
<div
id="astronomy-section"
class="hidden weather-card rounded-2xl shadow-xl p-8 mb-8"
>
<h2
class="text-2xl font-bold mb-6 flex items-center justify-center gap-2"
style="color: var(--theme-text)"
>
<i data-lucide="sunrise" class="w-6 h-6 text-blue-500"></i>
Астрономическая информация
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="sunrise" class="w-5 h-5 text-orange-500"></i>
<div>
<p
class="text-lg"
style="color: var(--theme-text-secondary)"
>
Восход солнца
</p>
<p
class="text-2xl font-semibold"
id="sunrise-time"
style="color: var(--theme-text)"
>
--
</p>
</div>
</div>
</div>
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="sunset" class="w-5 h-5 text-orange-600"></i>
<div>
<p
class="text-lg"
style="color: var(--theme-text-secondary)"
>
Заход солнца
</p>
<p
class="text-2xl font-semibold"
id="sunset-time"
style="color: var(--theme-text)"
>
--
</p>
</div>
</div>
</div>
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="moon" class="w-5 h-5 text-blue-400"></i>
<div>
<p
class="text-lg"
style="color: var(--theme-text-secondary)"
>
Фаза луны
</p>
<p
class="text-2xl font-semibold"
id="moon-phase"
style="color: var(--theme-text)"
>
--
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Air Quality Section -->
<div
id="air-quality-section"
class="hidden weather-card rounded-2xl shadow-xl p-8 mb-8"
>
<h2
class="text-2xl font-bold mb-6 flex items-center justify-center gap-2"
style="color: var(--theme-text)"
>
<i data-lucide="wind" class="w-6 h-6 text-blue-500"></i>
Качество воздуха
</h2>
<div class="text-center mb-6">
<div class="flex items-center justify-center gap-4 mb-4">
<div class="text-6xl font-bold" id="aqi-value">--</div>
<div class="text-left">
<div class="text-lg font-semibold" id="aqi-status">Загрузка...</div>
<div class="text-sm" style="color: var(--theme-text-secondary)" id="aqi-description">Индекс качества воздуха</div>
</div>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-4 mb-4">
<div class="h-4 rounded-full transition-all duration-500" id="aqi-bar"></div>
</div>
<div class="flex flex-col md:flex-row justify-center gap-8 mt-6">
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="activity" class="w-5 h-5 text-blue-500"></i>
<div>
<p class="text-sm" style="color: var(--theme-text-secondary)">Мелкие частицы</p>
<p class="text-lg font-semibold" id="pm25" style="color: var(--theme-text)">--</p>
</div>
</div>
</div>
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="circle-dot" class="w-5 h-5 text-blue-500"></i>
<div>
<p class="text-sm" style="color: var(--theme-text-secondary)">Крупные частицы</p>
<p class="text-lg font-semibold" id="pm10" style="color: var(--theme-text)">--</p>
</div>
</div>
</div>
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="zap" class="w-5 h-5 text-blue-500"></i>
<div>
<p class="text-sm" style="color: var(--theme-text-secondary)">Диоксид азота</p>
<p class="text-lg font-semibold" id="no2" style="color: var(--theme-text)">--</p>
</div>
</div>
</div>
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="sun" class="w-5 h-5 text-blue-500"></i>
<div>
<p class="text-sm" style="color: var(--theme-text-secondary)">Озон</p>
<p class="text-lg font-semibold" id="o3" style="color: var(--theme-text)">--</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Charts Section -->
<div class="charts-section weather-card rounded-2xl shadow-xl p-8 mt-8">
<h2

604
script.js
View File

@ -6,8 +6,8 @@ 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);
if (typeof ym !== "undefined" && YM_COUNTER_ID !== "YOUR_COUNTER_ID") {
ym(YM_COUNTER_ID, "reachGoal", eventName, params);
console.log(`Yandex Metrika event: ${eventName}`, params);
}
}
@ -19,8 +19,11 @@ const themeButtons = {
dark: document.getElementById("theme-dark"),
};
const citySelect = document.getElementById("city");
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");
@ -28,6 +31,10 @@ 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;
@ -35,6 +42,73 @@ 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",
@ -235,6 +309,24 @@ const beautifulRussianNames = {
"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) {
// Сначала проверяем русские условия
@ -318,6 +410,149 @@ function getWeatherIconColor(iconName) {
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");
@ -356,14 +591,16 @@ function displayCurrentDate() {
// Функция для обновления заголовка страницы
function updatePageTitle() {
const selectedCity = citySelect.options[citySelect.selectedIndex].text;
document.title = `Погода в ${selectedCity}`;
const selectedCity = cityInput.value;
// Извлечь только название города из "Город, Регион, Страна"
const cityName = selectedCity.split(",")[0].trim();
document.title = `Погода в ${cityName || "городе"}`;
}
// Автоматическая загрузка погоды при загрузке страницы
document.addEventListener("DOMContentLoaded", () => {
// Отправляем событие загрузки страницы
ymSendEvent('page_load');
ymSendEvent("page_load");
// Отображаем текущую дату
displayCurrentDate();
@ -371,64 +608,77 @@ document.addEventListener("DOMContentLoaded", () => {
// Загружаем сохраненный город из localStorage
const savedCity = localStorage.getItem("selectedCity");
if (savedCity) {
citySelect.value = 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);
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);
.catch(function (error) {
console.log("Service Worker registration failed:", error);
});
}
// Логи для PWA установки
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
console.log('PWA install prompt available');
window.addEventListener("beforeinstallprompt", (e) => {
console.log("PWA install prompt available");
deferredPrompt = e;
ymSendEvent('pwa_install_prompt');
ymSendEvent("pwa_install_prompt");
});
window.addEventListener('appinstalled', (e) => {
console.log('PWA installed successfully');
ymSendEvent('pwa_installed');
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' });
const response = await fetch("/manifest.json", {
method: "HEAD",
cache: "no-cache",
});
return response.ok;
} catch (error) {
console.log('Connection check failed:', error);
console.log("Connection check failed:", error);
return false;
}
}
// Функция для управления плашкой оффлайн
async function updateOfflineBanner() {
const banner = document.getElementById('offline-banner');
const banner = document.getElementById("offline-banner");
const isOnline = await checkOnlineStatus();
if (!isOnline) {
banner.classList.remove('hidden');
console.log('Оффлайн режим активирован');
ymSendEvent('offline_mode');
banner.classList.remove("hidden");
console.log("Оффлайн режим активирован");
ymSendEvent("offline_mode");
} else {
banner.classList.add('hidden');
console.log('Онлайн режим восстановлен');
ymSendEvent('online_mode');
banner.classList.add("hidden");
console.log("Онлайн режим восстановлен");
ymSendEvent("online_mode");
}
}
@ -436,8 +686,8 @@ document.addEventListener("DOMContentLoaded", () => {
updateOfflineBanner();
// Слушаем изменения статуса сети и проверяем реально
window.addEventListener('online', updateOfflineBanner);
window.addEventListener('offline', updateOfflineBanner);
window.addEventListener("online", updateOfflineBanner);
window.addEventListener("offline", updateOfflineBanner);
// Периодическая проверка каждые 30 секунд
setInterval(updateOfflineBanner, 30000);
@ -460,30 +710,54 @@ document.addEventListener("DOMContentLoaded", () => {
});
getWeatherBtn.addEventListener("click", async () => {
const city = citySelect.value;
const cityFull = cityInput.value.trim();
if (!cityFull) {
alert("Пожалуйста, введите название города");
return;
}
// Извлечь только название города для API
const city = cityFull.split(",")[0].trim();
// Отправляем событие выбора города
ymSendEvent('city_select', { city: city });
ymSendEvent("city_select", { city: city, cityFull: cityFull });
// Сохраняем выбранный город в localStorage
localStorage.setItem("selectedCity", city);
// Сохраняем выбранный город в localStorage (полное название для отображения)
localStorage.setItem("selectedCity", cityFull);
const url = `https://api.weatherapi.com/v1/forecast.json?key=${API_KEY}&q=${city}&days=7&hourly=true&lang=ru`;
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 {
const response = await fetch(url);
let response = await fetch(url);
console.log("Статус ответа:", response.status, response.statusText);
console.log(
"Заголовки ответа:",
Object.fromEntries(response.headers.entries())
);
if (!response.ok) {
throw new Error(`Ошибка сети: ${response.status} ${response.statusText}`);
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 });
ymSendEvent("weather_load_success", { city: city });
// Отображаем текущую погоду
displayCurrentWeather(data);
@ -509,6 +783,12 @@ getWeatherBtn.addEventListener("click", async () => {
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) {
@ -519,16 +799,230 @@ getWeatherBtn.addEventListener("click", async () => {
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', {
ymSendEvent("weather_load_error", {
city: city,
error: error.message
error: error.message,
});
alert("Не удалось загрузить данные о погоде. Проверьте API ключ.");
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);
@ -536,10 +1030,32 @@ function displayCurrentWeather(data) {
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");
@ -729,7 +1245,7 @@ function createTemperatureChart(hourlyData) {
document.body.classList.contains("dark");
// Отправляем событие просмотра графика температуры
ymSendEvent('chart_view', { chart_type: 'temperature' });
ymSendEvent("chart_view", { chart_type: "temperature" });
// Подготавливаем данные
const labels = hourlyData.map((item) => {
@ -915,7 +1431,7 @@ function setTheme(theme) {
}
// Отправляем событие смены темы
ymSendEvent('theme_change', { theme: theme });
ymSendEvent("theme_change", { theme: theme });
// Сохраняем выбранную тему
localStorage.setItem("theme", theme);
@ -975,7 +1491,7 @@ function createPrecipitationChart(hourlyData) {
const colors = getThemeColors();
// Отправляем событие просмотра графика осадков
ymSendEvent('chart_view', { chart_type: 'precipitation' });
ymSendEvent("chart_view", { chart_type: "precipitation" });
// Подготавливаем данные
const labels = hourlyData.map((item) => {

View File

@ -1,5 +1,5 @@
const CACHE_NAME = 'weather-app-v1';
const API_CACHE_NAME = 'weather-api-v1';
const CACHE_NAME = 'weather-app-v2';
const API_CACHE_NAME = 'weather-api-v2';
const urlsToCache = [
'/',
'/index.html',