Compare commits
No commits in common. "38ee07e881cb1707a3402256cdd1f35ea8c40a07" and "ec415de7240b4584fe028fb738a987e8e0577034" have entirely different histories.
38ee07e881
...
ec415de724
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
apikey.txt
|
apikey.txt
|
||||||
Идеи.txt
|
|
||||||
BIN
@eaDir/README.md@SynoEAStream
Normal file
BIN
@eaDir/README.md@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/index.html@SynoEAStream
Normal file
BIN
@eaDir/index.html@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/input.css@SynoEAStream
Normal file
BIN
@eaDir/input.css@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/output.css@SynoEAStream
Normal file
BIN
@eaDir/output.css@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/package.json@SynoEAStream
Normal file
BIN
@eaDir/package.json@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/postcss.config.js@SynoEAStream
Normal file
BIN
@eaDir/postcss.config.js@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/script.js@SynoEAStream
Normal file
BIN
@eaDir/script.js@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/server.js@SynoEAStream
Normal file
BIN
@eaDir/server.js@SynoEAStream
Normal file
Binary file not shown.
BIN
@eaDir/tailwind.config.js@SynoEAStream
Normal file
BIN
@eaDir/tailwind.config.js@SynoEAStream
Normal file
Binary file not shown.
@ -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";)
|
||||||
|
|
||||||
### Производительность
|
### Производительность
|
||||||
|
|
||||||
|
|||||||
330
index.html
330
index.html
@ -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
|
||||||
@ -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,35 +345,25 @@
|
|||||||
<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"
|
||||||
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>
|
||||||
@ -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
616
script.js
@ -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();
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
25
Идеи.txt
25
Идеи.txt
@ -1,25 +0,0 @@
|
|||||||
Сайт уже имеет хорошую функциональность: текущая погода, часовой таймлапс, недельный прогноз, графики, темы и PWA-поддержку. Вот идеи для дополнения:
|
|
||||||
|
|
||||||
Дополнительные погодные данные
|
|
||||||
Давление, UV-индекс, видимость — в блоке текущей погоды или отдельной карточке.
|
|
||||||
Восход/заход солнца, фаза луны — новая секция с астрономическими данными.
|
|
||||||
Направление ветра, порывы — расширить блок ветра стрелкой и скоростью порывов.
|
|
||||||
Качество воздуха — добавить индекс AQI с цветовой шкалой.
|
|
||||||
Функциональные улучшения
|
|
||||||
Геолокация — кнопка "Определить мое местоположение" для авто-выбора города.
|
|
||||||
Поиск городов — поле ввода вместо селекта, с автодополнением.
|
|
||||||
Единицы измерения — переключатели (°C/°F, км/ч/м/с, мм/дюймы).
|
|
||||||
Сравнение погоды — вкладка для сравнения с вчерашним днём или прошлой неделей.
|
|
||||||
Прогноз на месяц — расширить API-запрос до 30 дней.
|
|
||||||
Визуальные и интерактивные элементы
|
|
||||||
Анимированные иконки — использовать CSS-анимации или Lottie для иконок погоды.
|
|
||||||
Карта погоды — интегрировать OpenWeatherMap или Google Maps с наложением.
|
|
||||||
Тематические фоны — фон сайта меняется в зависимости от погоды (дождь — капли, снег — снежинки).
|
|
||||||
Виджеты для дашборда — мини-версии графиков или карты для главной страницы.
|
|
||||||
Уведомления и интеграции
|
|
||||||
Push-уведомления — предупреждения о дожде, снеге или экстремальной погоде.
|
|
||||||
Экспорт данных — кнопки для скачивания прогноза в PDF/CSV.
|
|
||||||
Социальные функции — поделиться прогнозом в соцсетях.
|
|
||||||
Расширения для PWA
|
|
||||||
Оффлайн-кэширование — хранить последние данные для просмотра без интернета.
|
|
||||||
Виджеты на домашнем экране — мини-версии PWA для iOS/Android.
|
|
||||||
Loading…
x
Reference in New Issue
Block a user