!DOCTYPE html
html lang=ru
head
meta charset=UTF-8
meta name=viewport content=width=device-width, initial-scale=1.0
titleGidron Weathertitle
link rel=manifest href=manifest.json
link rel=icon href=icon.svg
meta name=theme-color content=#0a0a0a
link rel=stylesheet href=httpsunpkg.comleaflet@1.9.4distleaflet.css
script src=httpsunpkg.comleaflet@1.9.4distleaflet.jsscript
style
root {
--bg #0a0a0a; --surface rgba(255,255,255,0.05); --border rgba(255,255,255,0.1);
--text #fff; --text-dim #a1a1aa; --accent #8b5cf6; --accent-2 #06b6d4;
--grad linear-gradient(135deg, #8b5cf6, #06b6d4);
}
[data-theme=light] {
--bg #f3f4f6; --surface rgba(0,0,0,0.03); --border rgba(0,0,0,0.08);
--text #111827; --text-dim #4b5563;
}
{ box-sizing border-box; margin 0; padding 0; font-family -apple-system, 'Inter', sans-serif; }
body { background var(--bg); color var(--text); min-height 100vh; padding 20px; transition 0.3s; }
.container { max-width 900px; margin 0 auto; }
header { display flex; justify-content space-between; align-items center; margin-bottom 30px; }
.logo { display flex; align-items center; gap 12px; font-weight 700; font-size 20px; }
.logo-icon { width 40px; height 40px; border-radius 12px; background var(--grad); display flex; align-items center; justify-content center; font-size 20px; }
.theme-btn { background var(--surface); border 1px solid var(--border); color var(--text); padding 10px 16px; border-radius 10px; cursor pointer; font-size 14px; }
.search-box { display flex; gap 10px; margin-bottom 20px; }
.search-box input { flex 1; padding 16px 20px; background var(--surface); border 1px solid var(--border); border-radius 14px; color var(--text); font-size 16px; outline none; }
.search-box inputfocus { border-color var(--accent); }
.search-box button { padding 16px 24px; background var(--grad); border none; border-radius 14px; color white; font-weight 600; cursor pointer; font-size 16px; }
.current-card { background var(--surface); border 1px solid var(--border); border-radius 20px; padding 30px; margin-bottom 20px; display flex; justify-content space-between; align-items center; flex-wrap wrap; gap 20px; }
.current-temp { font-size 72px; font-weight 200; line-height 1; background var(--grad); -webkit-background-clip text; -webkit-text-fill-color transparent; }
.current-info { display flex; flex-direction column; gap 6px; }
.current-city { font-size 24px; font-weight 600; }
.current-desc { color var(--text-dim); font-size 16px; }
.current-meta { display flex; gap 20px; margin-top 10px; flex-wrap wrap; }
.current-meta span { font-size 14px; color var(--text-dim); }
.grid { display grid; grid-template-columns 1fr 1fr; gap 20px; margin-bottom 20px; }
@media (max-width 700px) { .grid { grid-template-columns 1fr; } }
.card { background var(--surface); border 1px solid var(--border); border-radius 20px; padding 24px; }
.card-title { font-size 14px; font-weight 600; color var(--text-dim); text-transform uppercase; letter-spacing 1px; margin-bottom 16px; }
.hourly-list { display flex; gap 12px; overflow-x auto; padding-bottom 10px; }
.hourly-item { min-width 80px; text-align center; padding 12px; background var(--bg); border-radius 12px; }
.hourly-time { font-size 12px; color var(--text-dim); margin-bottom 6px; }
.hourly-temp { font-size 18px; font-weight 600; }
.daily-item { display flex; justify-content space-between; align-items center; padding 12px 0; border-bottom 1px solid var(--border); }
.daily-itemlast-child { border-bottom none; }
.daily-day { font-weight 500; width 100px; }
.daily-temps { font-size 16px; }
.daily-desc { color var(--text-dim); font-size 13px; text-align right; }
#map { height 300px; border-radius 16px; z-index 1; }
.loading { text-align center; padding 40px; color var(--text-dim); }
.spinner { width 40px; height 40px; border 3px solid var(--border); border-top-color var(--accent); border-radius 50%; animation spin 1s linear infinite; margin 0 auto 16px; }
@keyframes spin { to { transform rotate(360deg); } }
.source-badge { display inline-block; padding 4px 10px; background rgba(139,92,246,0.2); color var(--accent); border-radius 6px; font-size 11px; font-weight 600; margin-left 8px; }
style
head
body data-theme=dark
div class=container
header
div class=logo
div class=logo-icon🌤️div
spanGidron Weatherspan
div
button class=theme-btn onclick=toggleTheme()🌓 Темаbutton
header
div class=search-box
input type=text id=cityInput placeholder=Введите город (например, Минск) onkeypress=if(event.key==='Enter') searchCity()
button onclick=searchCity()Найтиbutton
div
div id=content
div class=loading
div class=spinnerdiv
pЗагрузка погоды...p
div
div
div
script
=== ЛОГИКА GIDRON WEATHER ===
let currentCity = { name 'Минск', lat 53.9, lon 27.5667 };
let currentWeather = null;
let map = null;
Определение геолокации при старте
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(pos) = {
currentCity = { name 'Ваше местоположение', lat pos.coords.latitude, lon pos.coords.longitude };
loadWeather();
},
() = loadWeather() Если отклонил — грузим Минск
);
} else {
loadWeather();
}
function toggleTheme() {
const body = document.body;
body.dataset.theme = body.dataset.theme === 'dark' 'light' 'dark';
}
async function searchCity() {
const query = document.getElementById('cityInput').value.trim();
if (!query) return;
document.getElementById('content').innerHTML = 'div class=loadingdiv class=spinnerdivpПоиск города...pdiv';
try {
const res = await fetch(`httpsgeocoding-api.open-meteo.comv1searchname=${encodeURIComponent(query)}&count=1&language=ru&format=json`);
const data = await res.json();
if (data.results && data.results.length 0) {
const city = data.results[0];
currentCity = { name city.name, lat city.latitude, lon city.longitude };
loadWeather();
} else {
alert('Город не найден');
loadWeather();
}
} catch (e) {
alert('Ошибка поиска ' + e.message);
}
}
async function loadWeather() {
document.getElementById('content').innerHTML = 'div class=loadingdiv class=spinnerdivpЗагрузка погоды...pdiv';
try {
Источник 1 Open-Meteo (основной)
const weatherUrl = `httpsapi.open-meteo.comv1forecastlatitude=${currentCity.lat}&longitude=${currentCity.lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,weather_code,wind_speed_10m&hourly=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto&forecast_days=7`;
const res = await fetch(weatherUrl);
const data = await res.json();
currentWeather = data;
renderWeather(data);
Источник 2 wttr.in (резервный, для сравнения)
fetchBackupWeather();
Инициализация карты осадков
initMap();
} catch (e) {
document.getElementById('content').innerHTML = `div class=loadingp style=colorvar(--danger)Ошибка загрузки ${e.message}pdiv`;
}
}
async function fetchBackupWeather() {
try {
const res = await fetch(`httpswttr.in${currentCity.lat},${currentCity.lon}format=j1`);
const data = await res.json();
const backupTemp = data.current_condition[0].temp_C;
document.getElementById('backupSource').textContent = `Резерв (wttr.in) ${backupTemp}°C`;
} catch (e) {
console.log('Резервный источник недоступен');
}
}
function renderWeather(data) {
const current = data.current;
const hourly = data.hourly;
const daily = data.daily;
const weatherDesc = getWeatherDesc(current.weather_code);
Текущая погода
let html = `
div class=current-card
div class=current-info
div class=current-city${currentCity.name}div
div class=current-desc${weatherDesc}div
div class=current-meta
span💧 Влажность ${current.relative_humidity_2m}%span
span💨 Ветер ${current.wind_speed_10m} кмчspan
span🌡️ Ощущается ${current.apparent_temperature}°Cspan
div
div
div class=current-temp${Math.round(current.temperature_2m)}°div
div
div class=grid
div class=card
div class=card-titleПрогноз на 24 часа span class=source-badgeOpen-Meteospandiv
div class=hourly-list
`;
Почасовой прогноз (первые 24 часа)
for (let i = 0; i 24; i++) {
const time = new Date(hourly.time[i]).toLocaleTimeString('ru-RU', { hour '2-digit', minute '2-digit' });
const temp = Math.round(hourly.temperature_2m[i]);
const icon = getWeatherEmoji(hourly.weather_code[i]);
html += `div class=hourly-item
div class=hourly-time${time}div
div style=font-size20px${icon}div
div class=hourly-temp${temp}°div
div`;
}
html += `divdiv`;
Прогноз на 7 дней
html += `
div class=card
div class=card-titleПрогноз на 7 дней span class=source-badgeOpen-Meteospandiv
div class=daily-list
`;
for (let i = 0; i 7; i++) {
const date = new Date(daily.time[i]).toLocaleDateString('ru-RU', { weekday 'short', day 'numeric' });
const max = Math.round(daily.temperature_2m_max[i]);
const min = Math.round(daily.temperature_2m_min[i]);
const icon = getWeatherEmoji(daily.weather_code[i]);
html += `div class=daily-item
div class=daily-day${date}div
div style=font-size20px${icon}div
div class=daily-temps${max}° ${min}°div
div`;
}
html += `divdivdiv`;
Карта осадков
html += `
div class=card style=margin-bottom20px
div class=card-titleКарта осадков span class=source-badgeRainViewerspandiv
div id=mapdiv
div
div class=card
div class=card-titleСравнение источниковdiv
div style=displayflex;gap20px;flex-wrapwrap
divOpen-Meteo strong id=openMeteoSource${Math.round(current.temperature_2m)}°Cstrongdiv
divwttr.in strong id=backupSourceЗагрузка...strongdiv
div
div
`;
document.getElementById('content').innerHTML = html;
}
function initMap() {
if (map) map.remove();
map = L.map('map').setView([currentCity.lat, currentCity.lon], 8);
L.tileLayer('https{s}.basemaps.cartocdn.comdark_all{z}{x}{y}{r}.png', {
attribution '© OpenStreetMap, CartoDB',
subdomains 'abcd',
maxZoom 19
}).addTo(map);
Слой осадков RainViewer
fetch('httpsapi.rainviewer.compublicweather-maps.json')
.then(res = res.json())
.then(data = {
const radar = data.radar.past;
if (radar && radar.length 0) {
const lastRadar = radar[radar.length - 1];
const tileUrl = `httpstilecache.rainviewer.com${lastRadar.path}256{z}{x}{y}21_1.png`;
L.tileLayer(tileUrl, {
opacity 0.6,
attribution 'RainViewer'
}).addTo(map);
}
})
.catch(() = console.log('Карта осадков недоступна'));
Маркер города
L.marker([currentCity.lat, currentCity.lon]).addTo(map)
.bindPopup(currentCity.name);
}
function getWeatherDesc(code) {
const descs = {
0 'Ясно', 1 'В основном ясно', 2 'Переменная облачность', 3 'Пасмурно',
45 'Туман', 48 'Изморозь', 51 'Лёгкая морось', 53 'Морось', 55 'Сильная морось',
61 'Небольшой дождь', 63 'Дождь', 65 'Сильный дождь',
71 'Небольшой снег', 73 'Снег', 75 'Сильный снег',
80 'Ливень', 81 'Сильный ливень', 82 'Очень сильный ливень',
95 'Гроза', 96 'Гроза с градом', 99 'Сильная гроза с градом'
};
return descs[code] 'Неизвестно';
}
function getWeatherEmoji(code) {
if (code === 0) return '☀️';
if (code = 3) return '⛅';
if (code = 48) return '🌫️';
if (code = 55) return '🌦️';
if (code = 65) return '🌧️';
if (code = 75) return '❄️';
if (code = 82) return '🌧️';
return '⛈️';
}
=== PWA Регистрация Service Worker ===
if ('serviceWorker' in navigator) {
window.addEventListener('load', () = {
navigator.serviceWorker.register('sw.js')
.then(reg = console.log('✅ SW зарегистрирован'))
.catch(err = console.log('⚠️ SW не зарегистрирован', err));
});
}
script
body
html