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

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/ node_modules/
apikey.txt 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 дней вперед - **Недельный прогноз**: погода на 7 дней вперед
- **Интерактивные графики**: температура и осадки с помощью Chart.js - **Интерактивные графики**: температура и осадки с помощью Chart.js
- **Адаптивный дизайн**: корректное отображение на всех устройствах - **Адаптивный дизайн**: корректное отображение на всех устройствах
- **Выбор городов**: предустановленный список российских городов - **Выбор городов**: поиск городов через API WeatherAPI.com
- **Система тем**: светлая, темная и автоматическая (по системным настройкам) - **Система тем**: светлая, темная и автоматическая (по системным настройкам)
- **Анимации**: плавные переходы и hover-эффекты - **Анимации**: плавные переходы и hover-эффекты
- **PWA (Progressive Web App)**: установка как нативное приложение, оффлайн режим с кэшированием данных - **PWA (Progressive Web App)**: установка как нативное приложение, оффлайн режим с кэшированием данных
@ -24,7 +24,7 @@
### Backend ### Backend
- **Node.js** v8+ (устаревшая версия, рекомендуется обновить до LTS) - **Node.js** v8 (устаревшая версия, рекомендуется обновить до LTS 18+)
- **Express.js** v4.17.1 - веб-сервер - **Express.js** v4.17.1 - веб-сервер
- **Service Worker** - кэширование и оффлайн поддержка - **Service Worker** - кэширование и оффлайн поддержка
@ -55,7 +55,7 @@
### Системные требования ### Системные требования
- **Node.js** v8+ (⚠️ устаревшая версия, рекомендуется использовать LTS версию Node.js 18+) - **Node.js** v8 (⚠️ устаревшая версия, рекомендуется использовать актуальную LTS версию Node.js 18+)
- **npm** для управления пакетами - **npm** для управления пакетами
### Установка ### Установка
@ -157,6 +157,7 @@
- **WeatherAPI.com** - источник данных о погоде - **WeatherAPI.com** - источник данных о погоде
- **Google Fonts API** - загрузка шрифтов Inter - **Google Fonts API** - загрузка шрифтов Inter
- **Yandex Metrika** - аналитика и отслеживание событий
- **CDN сервисы** для библиотек JavaScript (jsDelivr, unpkg) - **CDN сервисы** для библиотек JavaScript (jsDelivr, unpkg)
## Разработчик ## Разработчик
@ -179,7 +180,7 @@
1. Зарегистрироваться на [WeatherAPI.com](https://weatherapi.com) 1. Зарегистрироваться на [WeatherAPI.com](https://weatherapi.com)
2. Получить бесплатный API ключ 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> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Погода</title> <title>Погода</title>
<meta name="theme-color" content="#3b82f6" /> <meta name="theme-color" content="#3b82f6" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<link <link
@ -318,27 +318,28 @@
> >
<i data-lucide="map-pin" class="w-5 h-5 text-blue-500"></i> <i data-lucide="map-pin" class="w-5 h-5 text-blue-500"></i>
<label <label
for="city" for="city-search"
class="font-medium text-lg" class="font-medium text-lg"
style="color: var(--theme-text-secondary)" style="color: var(--theme-text-secondary)"
>Выберите город:</label >Выберите город:</label
> >
</div> </div>
<select <input
id="city" 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> <i data-lucide="navigation" class="w-5 h-5 text-black dark:text-white"></i>
<option value="Saint Petersburg">Санкт-Петербург</option> </button>
<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>
<button <button
id="get-weather" 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" 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> <i data-lucide="thermometer" class="w-6 h-6 text-blue-500"></i>
Текущая погода Текущая погода
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <!-- Температура отдельно в первом ряду -->
<div class="text-center"> <div class="text-center mb-6">
<div class="flex items-center justify-center gap-2 mb-2"> <div class="flex items-center justify-center gap-2 mb-2">
<i <i
data-lucide="thermometer" data-lucide="thermometer"
class="w-5 h-5 text-blue-500" class="w-5 h-5 text-blue-500"
></i> ></i>
<p class="text-4xl font-bold text-blue-500" id="current-temp"> <p class="text-4xl font-bold text-blue-500" id="current-temp">
--°C --°C
</p>
</div>
<p
class="text-lg"
id="current-desc"
style="color: var(--theme-text-secondary)"
>
Загрузка...
</p> </p>
</div> </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="text-center">
<div class="flex items-center justify-center gap-2 mb-2"> <div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="droplets" class="w-5 h-5 text-blue-500"></i> <i data-lucide="droplets" class="w-5 h-5 text-blue-500"></i>
@ -420,6 +431,98 @@
> >
-- км/ч -- км/ч
</p> </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> </div>
</div> </div>
@ -428,7 +531,7 @@
</div> </div>
<!-- Timelapse Section --> <!-- 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 <h2
class="text-2xl font-bold mb-6 flex items-center gap-2" class="text-2xl font-bold mb-6 flex items-center gap-2"
style="color: var(--theme-text)" style="color: var(--theme-text)"
@ -442,7 +545,7 @@
</div> </div>
<!-- Weekly Weather Section --> <!-- 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 <h2
class="text-2xl font-bold mb-6 flex items-center gap-2" class="text-2xl font-bold mb-6 flex items-center gap-2"
style="color: var(--theme-text)" style="color: var(--theme-text)"
@ -458,6 +561,146 @@
</div> </div>
</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 --> <!-- Charts Section -->
<div class="charts-section weather-card rounded-2xl shadow-xl p-8 mt-8"> <div class="charts-section weather-card rounded-2xl shadow-xl p-8 mt-8">
<h2 <h2

604
script.js
View File

@ -6,8 +6,8 @@ const YM_COUNTER_ID = 104563496; // Замените на реальный ID с
// Функция для отправки событий в Yandex Metrika // Функция для отправки событий в Yandex Metrika
function ymSendEvent(eventName, params = {}) { function ymSendEvent(eventName, params = {}) {
if (typeof ym !== 'undefined' && YM_COUNTER_ID !== 'YOUR_COUNTER_ID') { if (typeof ym !== "undefined" && YM_COUNTER_ID !== "YOUR_COUNTER_ID") {
ym(YM_COUNTER_ID, 'reachGoal', eventName, params); ym(YM_COUNTER_ID, "reachGoal", eventName, params);
console.log(`Yandex Metrika event: ${eventName}`, params); console.log(`Yandex Metrika event: ${eventName}`, params);
} }
} }
@ -19,8 +19,11 @@ const themeButtons = {
dark: document.getElementById("theme-dark"), 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 getWeatherBtn = document.getElementById("get-weather");
const geolocationBtn = document.getElementById("geolocation-btn");
const timelapseContainer = document.getElementById("timelapse-container"); const timelapseContainer = document.getElementById("timelapse-container");
const currentWeatherDiv = document.getElementById("current-weather"); const currentWeatherDiv = document.getElementById("current-weather");
const currentTempEl = document.getElementById("current-temp"); const currentTempEl = document.getElementById("current-temp");
@ -28,6 +31,10 @@ const currentDescEl = document.getElementById("current-desc");
const currentHumidityEl = document.getElementById("current-humidity"); const currentHumidityEl = document.getElementById("current-humidity");
const currentWindEl = document.getElementById("current-wind"); const currentWindEl = document.getElementById("current-wind");
// Глобальные переменные для координат пользователя (для fallback)
let userLatitude = null;
let userLongitude = null;
// Глобальная переменная для Swiper // Глобальная переменная для Swiper
let weatherSwiper; let weatherSwiper;
@ -35,6 +42,73 @@ let weatherSwiper;
let temperatureChart = null; let temperatureChart = null;
let precipitationChart = 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 = { const weatherIcons = {
Sunny: "sun", Sunny: "sun",
@ -235,6 +309,24 @@ const beautifulRussianNames = {
"Thundery outbreaks in nearby": "Грозы в округе", "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 // Функция для получения иконки погоды с умным fallback
function getWeatherIcon(conditionText) { function getWeatherIcon(conditionText) {
// Сначала проверяем русские условия // Сначала проверяем русские условия
@ -318,6 +410,149 @@ function getWeatherIconColor(iconName) {
return colorMap[iconName] || (isDark ? "cloud-dark" : "cloud"); 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() { function displayCurrentDate() {
const currentDateEl = document.getElementById("current-date"); const currentDateEl = document.getElementById("current-date");
@ -356,14 +591,16 @@ function displayCurrentDate() {
// Функция для обновления заголовка страницы // Функция для обновления заголовка страницы
function updatePageTitle() { function updatePageTitle() {
const selectedCity = citySelect.options[citySelect.selectedIndex].text; const selectedCity = cityInput.value;
document.title = `Погода в ${selectedCity}`; // Извлечь только название города из "Город, Регион, Страна"
const cityName = selectedCity.split(",")[0].trim();
document.title = `Погода в ${cityName || "городе"}`;
} }
// Автоматическая загрузка погоды при загрузке страницы // Автоматическая загрузка погоды при загрузке страницы
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
// Отправляем событие загрузки страницы // Отправляем событие загрузки страницы
ymSendEvent('page_load'); ymSendEvent("page_load");
// Отображаем текущую дату // Отображаем текущую дату
displayCurrentDate(); displayCurrentDate();
@ -371,64 +608,77 @@ document.addEventListener("DOMContentLoaded", () => {
// Загружаем сохраненный город из localStorage // Загружаем сохраненный город из localStorage
const savedCity = localStorage.getItem("selectedCity"); const savedCity = localStorage.getItem("selectedCity");
if (savedCity) { if (savedCity) {
citySelect.value = savedCity; cityInput.value = savedCity;
} }
// Настраиваем поиск городов
setupCitySearch();
// Обновляем заголовок страницы // Обновляем заголовок страницы
updatePageTitle(); updatePageTitle();
// Обработчик для геолокации
geolocationBtn.addEventListener("click", getCurrentLocation);
// Инициализируем тему // Инициализируем тему
initializeTheme(); initializeTheme();
watchSystemTheme(); watchSystemTheme();
// Регистрируем Service Worker для PWA // Регистрируем Service Worker для PWA
if ('serviceWorker' in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.register('/service-worker.js') navigator.serviceWorker
.then(function(registration) { .register("/service-worker.js")
console.log('Service Worker registered successfully:', registration.scope); .then(function (registration) {
console.log(
"Service Worker registered successfully:",
registration.scope
);
}) })
.catch(function(error) { .catch(function (error) {
console.log('Service Worker registration failed:', error); console.log("Service Worker registration failed:", error);
}); });
} }
// Логи для PWA установки // Логи для PWA установки
let deferredPrompt; let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => { window.addEventListener("beforeinstallprompt", (e) => {
console.log('PWA install prompt available'); console.log("PWA install prompt available");
deferredPrompt = e; deferredPrompt = e;
ymSendEvent('pwa_install_prompt'); ymSendEvent("pwa_install_prompt");
}); });
window.addEventListener('appinstalled', (e) => { window.addEventListener("appinstalled", (e) => {
console.log('PWA installed successfully'); console.log("PWA installed successfully");
ymSendEvent('pwa_installed'); ymSendEvent("pwa_installed");
}); });
// Функция для проверки реального соединения // Функция для проверки реального соединения
async function checkOnlineStatus() { async function checkOnlineStatus() {
try { 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; return response.ok;
} catch (error) { } catch (error) {
console.log('Connection check failed:', error); console.log("Connection check failed:", error);
return false; return false;
} }
} }
// Функция для управления плашкой оффлайн // Функция для управления плашкой оффлайн
async function updateOfflineBanner() { async function updateOfflineBanner() {
const banner = document.getElementById('offline-banner'); const banner = document.getElementById("offline-banner");
const isOnline = await checkOnlineStatus(); const isOnline = await checkOnlineStatus();
if (!isOnline) { if (!isOnline) {
banner.classList.remove('hidden'); banner.classList.remove("hidden");
console.log('Оффлайн режим активирован'); console.log("Оффлайн режим активирован");
ymSendEvent('offline_mode'); ymSendEvent("offline_mode");
} else { } else {
banner.classList.add('hidden'); banner.classList.add("hidden");
console.log('Онлайн режим восстановлен'); console.log("Онлайн режим восстановлен");
ymSendEvent('online_mode'); ymSendEvent("online_mode");
} }
} }
@ -436,8 +686,8 @@ document.addEventListener("DOMContentLoaded", () => {
updateOfflineBanner(); updateOfflineBanner();
// Слушаем изменения статуса сети и проверяем реально // Слушаем изменения статуса сети и проверяем реально
window.addEventListener('online', updateOfflineBanner); window.addEventListener("online", updateOfflineBanner);
window.addEventListener('offline', updateOfflineBanner); window.addEventListener("offline", updateOfflineBanner);
// Периодическая проверка каждые 30 секунд // Периодическая проверка каждые 30 секунд
setInterval(updateOfflineBanner, 30000); setInterval(updateOfflineBanner, 30000);
@ -460,30 +710,54 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
getWeatherBtn.addEventListener("click", async () => { 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 (полное название для отображения)
localStorage.setItem("selectedCity", city); 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("Запрос к API:", url);
console.log("Выбранный город:", city); console.log("Выбранный город:", city);
console.log("API ключ используется:", API_KEY);
try { try {
const response = await fetch(url); let response = await fetch(url);
console.log("Статус ответа:", response.status, response.statusText); console.log("Статус ответа:", response.status, response.statusText);
console.log(
"Заголовки ответа:",
Object.fromEntries(response.headers.entries())
);
if (!response.ok) { 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(); const data = await response.json();
console.log("Данные получены успешно"); console.log("Данные получены успешно");
// Отправляем событие успешной загрузки данных // Отправляем событие успешной загрузки данных
ymSendEvent('weather_load_success', { city: city }); ymSendEvent("weather_load_success", { city: city });
// Отображаем текущую погоду // Отображаем текущую погоду
displayCurrentWeather(data); displayCurrentWeather(data);
@ -509,6 +783,12 @@ getWeatherBtn.addEventListener("click", async () => {
createTemperatureChart(data.forecast.forecastday[0].hour); createTemperatureChart(data.forecast.forecastday[0].hour);
createPrecipitationChart(data.forecast.forecastday[0].hour); createPrecipitationChart(data.forecast.forecastday[0].hour);
// Астрономическая информация
displayAstronomy(data.forecast.forecastday[0].astro);
// Качество воздуха
displayAirQuality(data.current.air_quality);
// Обновляем заголовок страницы с выбранным городом // Обновляем заголовок страницы с выбранным городом
updatePageTitle(); updatePageTitle();
} catch (error) { } catch (error) {
@ -519,16 +799,230 @@ getWeatherBtn.addEventListener("click", async () => {
console.error("Текст ответа:", error.response.statusText); 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, 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) { function displayCurrentWeather(data) {
const current = data.current; const current = data.current;
const iconName = getWeatherIcon(current.condition.text); const iconName = getWeatherIcon(current.condition.text);
@ -536,10 +1030,32 @@ function displayCurrentWeather(data) {
const beautifulName = getBeautifulRussianName(current.condition.text); const beautifulName = getBeautifulRussianName(current.condition.text);
currentTempEl.textContent = `${Math.round(current.temp_c)}°C`; 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}`; currentDescEl.innerHTML = `<i data-lucide="${iconName}" class="w-5 h-5 inline mr-2 weather-icon ${iconColorClass}"></i>${beautifulName}`;
currentHumidityEl.textContent = `${current.humidity}%`; currentHumidityEl.textContent = `${current.humidity}%`;
currentWindEl.textContent = `${current.wind_kph} км/ч`; 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.remove("hidden");
currentWeatherDiv.classList.add("animate-fade-in"); currentWeatherDiv.classList.add("animate-fade-in");
@ -729,7 +1245,7 @@ function createTemperatureChart(hourlyData) {
document.body.classList.contains("dark"); document.body.classList.contains("dark");
// Отправляем событие просмотра графика температуры // Отправляем событие просмотра графика температуры
ymSendEvent('chart_view', { chart_type: 'temperature' }); ymSendEvent("chart_view", { chart_type: "temperature" });
// Подготавливаем данные // Подготавливаем данные
const labels = hourlyData.map((item) => { 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); localStorage.setItem("theme", theme);
@ -975,7 +1491,7 @@ function createPrecipitationChart(hourlyData) {
const colors = getThemeColors(); const colors = getThemeColors();
// Отправляем событие просмотра графика осадков // Отправляем событие просмотра графика осадков
ymSendEvent('chart_view', { chart_type: 'precipitation' }); ymSendEvent("chart_view", { chart_type: "precipitation" });
// Подготавливаем данные // Подготавливаем данные
const labels = hourlyData.map((item) => { const labels = hourlyData.map((item) => {

View File

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