!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