Compare commits

..

No commits in common. "38ee07e881cb1707a3402256cdd1f35ea8c40a07" and "ec415de7240b4584fe028fb738a987e8e0577034" have entirely different histories.

15 changed files with 71 additions and 916 deletions

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
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.

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 18+) - **Node.js** v8+ (устаревшая версия, рекомендуется обновить до LTS)
- **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,7 +157,6 @@
- **WeatherAPI.com** - источник данных о погоде - **WeatherAPI.com** - источник данных о погоде
- **Google Fonts API** - загрузка шрифтов Inter - **Google Fonts API** - загрузка шрифтов Inter
- **Yandex Metrika** - аналитика и отслеживание событий
- **CDN сервисы** для библиотек JavaScript (jsDelivr, unpkg) - **CDN сервисы** для библиотек JavaScript (jsDelivr, unpkg)
## Разработчик ## Разработчик
@ -180,7 +179,7 @@
1. Зарегистрироваться на [WeatherAPI.com](https://weatherapi.com) 1. Зарегистрироваться на [WeatherAPI.com](https://weatherapi.com)
2. Получить бесплатный API ключ 2. Получить бесплатный API ключ
3. Добавить ключ в script.js (const API_KEY = "API_KEY_HERE";) 3. Добавить ключ в переменные окружения или конфигурацию сервера(server.js - const API_KEY = "API_KEY_HERE";)
### Производительность ### Производительность

View File

@ -24,25 +24,6 @@
/> />
<!-- Chart.js --> <!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(104563496, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true,
trackHash:true,
ut:"noindex"
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/104563496" style="position:absolute; left:-9999px;" alt="" /></div></noscript></search>
<!-- /Yandex.Metrika counter -->
<style> <style>
:root { :root {
--theme-bg: linear-gradient(to bottom right, #dbeafe, #ffffff, #e0e7ff); --theme-bg: linear-gradient(to bottom right, #dbeafe, #ffffff, #e0e7ff);
@ -318,28 +299,27 @@
> >
<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-search" for="city"
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>
<input <select
id="city" id="city"
type="text" 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]"
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="Определить мое местоположение"
> >
<i data-lucide="navigation" class="w-5 h-5 text-black dark:text-white"></i> <option value="Moscow">Москва</option>
</button> <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>
<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"
@ -365,8 +345,8 @@
<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 mb-6"> <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 <i
data-lucide="thermometer" data-lucide="thermometer"
@ -376,13 +356,6 @@
--°C --°C
</p> </p>
</div> </div>
<p
class="text-sm"
id="current-feels-like"
style="color: var(--theme-text-secondary)"
>
Ощущается как --°C
</p>
<p <p
class="text-lg" class="text-lg"
id="current-desc" id="current-desc"
@ -391,9 +364,6 @@
Загрузка... Загрузка...
</p> </p>
</div> </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>
@ -431,98 +401,6 @@
> >
-- км/ч -- км/ч
</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>
@ -531,7 +409,7 @@
</div> </div>
<!-- Timelapse Section --> <!-- Timelapse Section -->
<div class="timelapse-weather weather-card rounded-2xl shadow-xl p-8 mb-8"> <div class="timelapse-weather weather-card rounded-2xl shadow-xl p-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)"
@ -545,7 +423,7 @@
</div> </div>
<!-- Weekly Weather Section --> <!-- Weekly Weather Section -->
<div class="weekly-weather weather-card rounded-2xl shadow-xl p-8 mb-8"> <div class="weekly-weather weather-card rounded-2xl shadow-xl p-8 mt-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)"
@ -561,146 +439,6 @@
</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

616
script.js
View File

@ -1,17 +1,6 @@
// API ключ от WeatherAPI // API ключ от WeatherAPI
const API_KEY = "485eff906f7d473b913104046250710"; const API_KEY = "485eff906f7d473b913104046250710";
// Yandex Metrika counter ID (замените на свой)
const YM_COUNTER_ID = 104563496; // Замените на реальный ID счетчика
// Функция для отправки событий в Yandex Metrika
function ymSendEvent(eventName, params = {}) {
if (typeof ym !== "undefined" && YM_COUNTER_ID !== "YOUR_COUNTER_ID") {
ym(YM_COUNTER_ID, "reachGoal", eventName, params);
console.log(`Yandex Metrika event: ${eventName}`, params);
}
}
// Глобальные переменные для темы // Глобальные переменные для темы
const themeButtons = { const themeButtons = {
light: document.getElementById("theme-light"), light: document.getElementById("theme-light"),
@ -19,11 +8,8 @@ const themeButtons = {
dark: document.getElementById("theme-dark"), dark: document.getElementById("theme-dark"),
}; };
const cityInput = document.getElementById("city"); const citySelect = 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");
@ -31,10 +17,6 @@ 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;
@ -42,73 +24,6 @@ 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",
@ -309,24 +224,6 @@ 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) {
// Сначала проверяем русские условия // Сначала проверяем русские условия
@ -410,149 +307,6 @@ 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");
@ -591,94 +345,72 @@ function displayCurrentDate() {
// Функция для обновления заголовка страницы // Функция для обновления заголовка страницы
function updatePageTitle() { function updatePageTitle() {
const selectedCity = cityInput.value; const selectedCity = citySelect.options[citySelect.selectedIndex].text;
// Извлечь только название города из "Город, Регион, Страна" document.title = `Погода в ${selectedCity}`;
const cityName = selectedCity.split(",")[0].trim();
document.title = `Погода в ${cityName || "городе"}`;
} }
// Автоматическая загрузка погоды при загрузке страницы // Автоматическая загрузка погоды при загрузке страницы
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
// Отправляем событие загрузки страницы
ymSendEvent("page_load");
// Отображаем текущую дату // Отображаем текущую дату
displayCurrentDate(); displayCurrentDate();
// Загружаем сохраненный город из localStorage // Загружаем сохраненный город из localStorage
const savedCity = localStorage.getItem("selectedCity"); const savedCity = localStorage.getItem("selectedCity");
if (savedCity) { if (savedCity) {
cityInput.value = savedCity; citySelect.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 navigator.serviceWorker.register('/service-worker.js')
.register("/service-worker.js") .then(function(registration) {
.then(function (registration) { console.log('Service Worker registered successfully:', registration.scope);
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");
}); });
window.addEventListener("appinstalled", (e) => { window.addEventListener('appinstalled', (e) => {
console.log("PWA installed successfully"); console.log('PWA installed successfully');
ymSendEvent("pwa_installed");
}); });
// Функция для проверки реального соединения // Функция для проверки реального соединения
async function checkOnlineStatus() { async function checkOnlineStatus() {
try { try {
// Пингуем небольшой ресурс // Пингуем небольшой ресурс
const response = await fetch("/manifest.json", { const response = await fetch('/manifest.json', { method: 'HEAD', cache: 'no-cache' });
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");
} else { } else {
banner.classList.add("hidden"); banner.classList.add('hidden');
console.log("Онлайн режим восстановлен"); console.log('Онлайн режим восстановлен');
ymSendEvent("online_mode");
} }
} }
@ -686,8 +418,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);
@ -710,55 +442,25 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
getWeatherBtn.addEventListener("click", async () => { getWeatherBtn.addEventListener("click", async () => {
const cityFull = cityInput.value.trim(); const city = citySelect.value;
if (!cityFull) {
alert("Пожалуйста, введите название города");
return;
}
// Извлечь только название города для API // Сохраняем выбранный город в localStorage
const city = cityFull.split(",")[0].trim(); localStorage.setItem("selectedCity", city);
// Отправляем событие выбора города const url = `https://api.weatherapi.com/v1/forecast.json?key=${API_KEY}&q=${city}&days=7&hourly=true&lang=ru`;
ymSendEvent("city_select", { city: city, cityFull: cityFull });
// Сохраняем выбранный город в localStorage (полное название для отображения)
localStorage.setItem("selectedCity", cityFull);
let url = `https://api.weatherapi.com/v1/forecast.json?key=${API_KEY}&q=${city}&days=7&hourly=true&aqi=yes&lang=ru`;
console.log("Запрос к API:", url); console.log("Запрос к API:", url);
console.log("Выбранный город:", city); console.log("Выбранный город:", city);
console.log("API ключ используется:", API_KEY);
try { try {
let response = await fetch(url); const 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) {
let errorText = ""; throw new Error(`Ошибка сети: ${response.status} ${response.statusText}`);
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 });
// Отображаем текущую погоду // Отображаем текущую погоду
displayCurrentWeather(data); displayCurrentWeather(data);
@ -783,12 +485,6 @@ 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) {
@ -798,231 +494,10 @@ getWeatherBtn.addEventListener("click", async () => {
console.error("Статус ответа:", error.response.status); console.error("Статус ответа:", error.response.status);
console.error("Текст ответа:", error.response.statusText); console.error("Текст ответа:", error.response.statusText);
} }
alert("Не удалось загрузить данные о погоде. Проверьте API ключ.");
// Попытка загрузить из кэша для оффлайн режима
try {
const cache = await caches.open(API_CACHE_NAME);
const cachedResponse = await cache.match(url);
if (cachedResponse) {
console.log("Используем кэшированные данные погоды для оффлайн режима");
const cachedData = await cachedResponse.json();
ymSendEvent("weather_load_from_cache");
// Отображаем кэшированные данные
displayCurrentWeather(cachedData);
setTimeout(() => {
lucide.createIcons();
}, 100);
window.currentHourlyData = cachedData.forecast.forecastday[0].hour;
displayTimelapse(
cachedData.forecast.forecastday[0].hour,
cachedData.location.localtime
);
displayWeeklyWeather(cachedData.forecast.forecastday);
createTemperatureChart(cachedData.forecast.forecastday[0].hour);
createPrecipitationChart(cachedData.forecast.forecastday[0].hour);
displayAstronomy(cachedData.forecast.forecastday[0].astro);
displayAirQuality(cachedData.current.air_quality);
updatePageTitle();
return; // Успешно загружено из кэша
} else {
console.log("Кэш пуст, данные недоступны");
}
} catch (cacheError) {
console.error("Ошибка чтения кэша:", cacheError);
}
// Fallback: если город не найден и есть координаты, используем их
if (userLatitude !== null && userLongitude !== null) {
console.log("Пытаемся загрузить погоду по координатам:", {
userLatitude,
userLongitude,
});
try {
const fallbackUrl = `https://api.weatherapi.com/v1/forecast.json?key=${API_KEY}&q=${userLatitude},${userLongitude}&days=7&hourly=true&aqi=yes&lang=ru`;
console.log("Fallback запрос к API:", fallbackUrl);
const fallbackResponse = await fetch(fallbackUrl);
if (fallbackResponse.ok) {
const fallbackData = await fallbackResponse.json();
console.log("Fallback данные получены успешно");
// Обновляем город в интерфейсе на реальный из данных
const realCityName = fallbackData.location.name;
cityInput.value = realCityName;
localStorage.setItem("selectedCity", realCityName);
updatePageTitle();
// Отправляем событие успешной fallback загрузки
ymSendEvent("weather_load_success_fallback", { city: realCityName });
// Повторяем отображение с fallback данными
displayCurrentWeather(fallbackData);
setTimeout(() => {
lucide.createIcons();
}, 100);
window.currentHourlyData = fallbackData.forecast.forecastday[0].hour;
displayTimelapse(
fallbackData.forecast.forecastday[0].hour,
fallbackData.location.localtime
);
displayWeeklyWeather(fallbackData.forecast.forecastday);
createTemperatureChart(fallbackData.forecast.forecastday[0].hour);
createPrecipitationChart(fallbackData.forecast.forecastday[0].hour);
displayAstronomy(fallbackData.forecast.forecastday[0].astro);
displayAirQuality(fallbackData.current.air_quality);
return; // Успешно, выходим
} else {
console.error("Fallback также не удался");
}
} catch (fallbackError) {
console.error("Ошибка fallback:", fallbackError);
}
}
// Отправляем событие ошибки загрузки
ymSendEvent("weather_load_error", {
city: city,
error: error.message,
});
alert(
`Не удалось загрузить данные о погоде для "${city}". Попробуйте другой город или проверьте API ключ. Детали в консоли браузера.`
);
} }
}); });
function displayAstronomy(astroData) {
const sunriseEl = document.getElementById("sunrise-time");
const sunsetEl = document.getElementById("sunset-time");
const moonPhaseEl = document.getElementById("moon-phase");
const astronomySection = document.getElementById("astronomy-section");
// Перевод фаз луны на русский
const moonPhases = {
"New Moon": "Новолуние",
"Waxing Crescent": "Растущий серп",
"First Quarter": "Первая четверть",
"Waxing Gibbous": "Растущая луна",
"Full Moon": "Полнолуние",
"Waning Gibbous": "Убывающая луна",
"Last Quarter": "Последняя четверть",
"Waning Crescent": "Убывающий серп",
};
sunriseEl.textContent = convertTo24Hour(astroData.sunrise);
sunsetEl.textContent = convertTo24Hour(astroData.sunset);
moonPhaseEl.textContent =
moonPhases[astroData.moon_phase] || astroData.moon_phase;
// Показываем секцию
astronomySection.classList.remove("hidden");
astronomySection.classList.add("animate-fade-in");
// Отправляем событие просмотра астрономии
ymSendEvent("astronomy_view");
// Пересоздаем иконки
setTimeout(() => {
lucide.createIcons();
}, 100);
}
function displayAirQuality(airQualityData) {
console.log("Полученные данные качества воздуха:", airQualityData);
const aqiValueEl = document.getElementById("aqi-value");
const aqiStatusEl = document.getElementById("aqi-status");
const aqiDescriptionEl = document.getElementById("aqi-description");
const aqiBarEl = document.getElementById("aqi-bar");
const airQualitySection = document.getElementById("air-quality-section");
const aqi = airQualityData["us-epa-index"]; // Используем US EPA индекс
console.log("AQI индекс качества воздуха:", aqi);
// Определяем статус и цвет
let status, color, description, percentage;
switch (aqi) {
case 1:
status = "Хороший";
color = "#10b981";
description = "Качество воздуха хорошее";
percentage = 20;
break;
case 2:
status = "Умеренный";
color = "#f59e0b";
description = "Качество воздуха умеренное";
percentage = 40;
break;
case 3:
status = "Плохое";
color = "#f97316";
description = "Качество воздуха плохое";
percentage = 60;
break;
case 4:
status = "Очень плохое";
color = "#ef4444";
description = "Качество воздуха очень плохое";
percentage = 80;
break;
case 5:
status = "Критическое";
color = "#7c2d12";
description = "Качество воздуха критическое";
percentage = 100;
break;
default:
status = "Неизвестно";
color = "#6b7280";
description = "Данные недоступны";
percentage = 0;
}
// Убираем отображение цифры AQI
aqiValueEl.style.display = "none";
aqiStatusEl.textContent = status;
aqiStatusEl.style.color = color;
aqiDescriptionEl.textContent = description;
aqiBarEl.style.width = `${percentage}%`;
aqiBarEl.style.backgroundColor = color;
// Детальные показатели
document.getElementById("pm25").textContent = airQualityData.pm2_5
? airQualityData.pm2_5.toFixed(1)
: "--";
document.getElementById("pm10").textContent = airQualityData.pm10
? airQualityData.pm10.toFixed(1)
: "--";
document.getElementById("no2").textContent = airQualityData.no2
? airQualityData.no2.toFixed(1)
: "--";
document.getElementById("o3").textContent = airQualityData.o3
? airQualityData.o3.toFixed(1)
: "--";
// Показываем секцию
airQualitySection.classList.remove("hidden");
airQualitySection.classList.add("animate-fade-in");
// Отправляем событие просмотра качества воздуха
ymSendEvent("air_quality_view", { aqi: aqi });
// Пересоздаем иконки
setTimeout(() => {
lucide.createIcons();
}, 100);
}
function displayCurrentWeather(data) { function displayCurrentWeather(data) {
const current = data.current; const current = data.current;
const iconName = getWeatherIcon(current.condition.text); const iconName = getWeatherIcon(current.condition.text);
@ -1030,32 +505,10 @@ 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");
@ -1244,9 +697,6 @@ function createTemperatureChart(hourlyData) {
document.documentElement.classList.contains("dark") || document.documentElement.classList.contains("dark") ||
document.body.classList.contains("dark"); document.body.classList.contains("dark");
// Отправляем событие просмотра графика температуры
ymSendEvent("chart_view", { chart_type: "temperature" });
// Подготавливаем данные // Подготавливаем данные
const labels = hourlyData.map((item) => { const labels = hourlyData.map((item) => {
const hour = new Date(item.time).getHours(); const hour = new Date(item.time).getHours();
@ -1430,9 +880,6 @@ function setTheme(theme) {
html.setAttribute("data-theme", "light"); html.setAttribute("data-theme", "light");
} }
// Отправляем событие смены темы
ymSendEvent("theme_change", { theme: theme });
// Сохраняем выбранную тему // Сохраняем выбранную тему
localStorage.setItem("theme", theme); localStorage.setItem("theme", theme);
@ -1490,9 +937,6 @@ function createPrecipitationChart(hourlyData) {
const ctx = document.getElementById("precipitationChart").getContext("2d"); const ctx = document.getElementById("precipitationChart").getContext("2d");
const colors = getThemeColors(); const colors = getThemeColors();
// Отправляем событие просмотра графика осадков
ymSendEvent("chart_view", { chart_type: "precipitation" });
// Подготавливаем данные // Подготавливаем данные
const labels = hourlyData.map((item) => { const labels = hourlyData.map((item) => {
const hour = new Date(item.time).getHours(); const hour = new Date(item.time).getHours();

View File

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

View File

@ -1,25 +0,0 @@
Сайт уже имеет хорошую функциональность: текущая погода, часовой таймлапс, недельный прогноз, графики, темы и PWA-поддержку. Вот идеи для дополнения:
Дополнительные погодные данные
Давление, UV-индекс, видимость — в блоке текущей погоды или отдельной карточке.
Восход/заход солнца, фаза луны — новая секция с астрономическими данными.
Направление ветра, порывы — расширить блок ветра стрелкой и скоростью порывов.
Качество воздуха — добавить индекс AQI с цветовой шкалой.
Функциональные улучшения
Геолокация — кнопка "Определить мое местоположение" для авто-выбора города.
Поиск городов — поле ввода вместо селекта, с автодополнением.
Единицы измерения — переключатели (°C/°F, км/ч/м/с, мм/дюймы).
Сравнение погоды — вкладка для сравнения с вчерашним днём или прошлой неделей.
Прогноз на месяц — расширить API-запрос до 30 дней.
Визуальные и интерактивные элементы
Анимированные иконки — использовать CSS-анимации или Lottie для иконок погоды.
Карта погоды — интегрировать OpenWeatherMap или Google Maps с наложением.
Тематические фоны — фон сайта меняется в зависимости от погоды (дождь — капли, снег — снежинки).
Виджеты для дашборда — мини-версии графиков или карты для главной страницы.
Уведомления и интеграции
Push-уведомления — предупреждения о дожде, снеге или экстремальной погоде.
Экспорт данных — кнопки для скачивания прогноза в PDF/CSV.
Социальные функции — поделиться прогнозом в соцсетях.
Расширения для PWA
Оффлайн-кэширование — хранить последние данные для просмотра без интернета.
Виджеты на домашнем экране — мини-версии PWA для iOS/Android.