Root
>
Weather.html
Download
Delete
Weather.html
35,6 KB • HTML • jun. 18, 2026 06:54
Render
Source
Live Edit
Copy
Save
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Fullscreen Weather Widget</title> <style> :root { --bg: #0b1020; --panel: #111827; --panel-soft: #172033; --panel-strong: #1f2937; --text: #f9fafb; --muted: #a7b0c0; --line: rgba(255, 255, 255, 0.12); --accent: #8ab4ff; --danger: #fca5a5; --shadow: rgba(0, 0, 0, 0.35); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } * { box-sizing: border-box; } html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: var(--bg); color: var(--text); } body { min-height: 100svh; } button, input { font: inherit; } button { cursor: pointer; } .app { position: relative; width: 100vw; height: 100svh; min-height: 560px; overflow: hidden; background: var(--bg); isolation: isolate; } .weather-stage { position: relative; width: 100%; height: 100%; padding: clamp(18px, 3vw, 40px); display: grid; grid-template-rows: auto 1fr auto; gap: 22px; z-index: 1; } .particles { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: -1; } .drop { position: absolute; top: -12vh; width: 2px; height: 38px; border-radius: 999px; background: rgba(180, 205, 255, 0.48); animation: rainFall linear infinite; transform: rotate(12deg); } .snow { position: absolute; top: -8vh; width: 7px; height: 7px; border-radius: 50%; background: rgba(255, 255, 255, 0.7); animation: snowFall linear infinite; } @keyframes rainFall { to { transform: translate3d(-12vw, 125vh, 0) rotate(12deg); } } @keyframes snowFall { to { transform: translate3d(8vw, 120vh, 0); } } .topbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; z-index: 3; } .brand { display: flex; flex-direction: column; gap: 7px; } .eyebrow { color: var(--muted); font-size: 0.8rem; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 750; } .place { font-size: clamp(1.6rem, 4vw, 3.4rem); font-weight: 800; line-height: 0.95; letter-spacing: -0.05em; max-width: 900px; } .updated { color: var(--muted); font-size: 0.95rem; } .settings { position: relative; z-index: 10; } .icon-button { width: 48px; height: 48px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 16px; background: rgba(17, 24, 39, 0.82); color: var(--text); box-shadow: 0 18px 50px var(--shadow); } .icon-button svg { width: 22px; height: 22px; } .settings-panel { position: absolute; right: 0; top: 58px; width: min(360px, calc(100vw - 36px)); padding: 16px; border: 1px solid var(--line); border-radius: 22px; background: rgba(17, 24, 39, 0.96); box-shadow: 0 24px 80px var(--shadow); } .settings-panel[hidden] { display: none; } .settings-title { font-weight: 800; margin-bottom: 10px; } .settings-help { color: var(--muted); font-size: 0.88rem; line-height: 1.4; margin-bottom: 14px; } .search-row { display: flex; gap: 8px; } .search-row input { flex: 1; min-width: 0; color: var(--text); background: var(--panel-soft); border: 1px solid var(--line); border-radius: 14px; padding: 11px 12px; outline: none; } .search-row input:focus { border-color: rgba(138, 180, 255, 0.75); } .search-row button { border: 0; border-radius: 14px; padding: 0 14px; font-weight: 800; color: #0b1020; background: var(--accent); } .hero { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr); align-items: center; gap: clamp(20px, 5vw, 72px); min-height: 0; } .main-reading { min-width: 0; } .icon-wrap { width: clamp(120px, 22vw, 260px); height: clamp(120px, 22vw, 260px); display: grid; place-items: center; color: var(--accent); margin-bottom: clamp(12px, 2vw, 20px); } .icon-wrap svg { width: 100%; height: 100%; filter: drop-shadow(0 28px 45px rgba(0, 0, 0, 0.35)); } .temp-row { display: flex; align-items: flex-end; gap: 14px; flex-wrap: wrap; } .temperature { font-size: clamp(5.5rem, 15vw, 13rem); font-weight: 900; line-height: 0.78; letter-spacing: -0.1em; } .degree { letter-spacing: -0.03em; } .condition-block { padding-bottom: clamp(4px, 1.4vw, 16px); } .condition { font-size: clamp(1.3rem, 3vw, 2.5rem); font-weight: 800; letter-spacing: -0.04em; line-height: 1; } .feels { color: var(--muted); font-size: clamp(0.95rem, 1.6vw, 1.15rem); margin-top: 8px; } .stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } .stat-card { min-height: 124px; border: 1px solid var(--line); border-radius: 26px; background: rgba(17, 24, 39, 0.72); padding: 18px; box-shadow: 0 18px 50px var(--shadow); } .stat-label { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 0.86rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 12px; } .stat-value { font-size: clamp(1.6rem, 3vw, 2.4rem); font-weight: 850; line-height: 1; letter-spacing: -0.05em; } .stat-note { color: var(--muted); font-size: 0.88rem; margin-top: 9px; } .bottom-panel { display: grid; grid-template-columns: 1.3fr 0.7fr; gap: 14px; min-height: 154px; } .strip { border: 1px solid var(--line); border-radius: 28px; background: rgba(17, 24, 39, 0.78); box-shadow: 0 18px 50px var(--shadow); padding: 16px; min-width: 0; } .strip-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; } .strip-title { font-weight: 850; letter-spacing: -0.03em; } .status { color: var(--muted); font-size: 0.88rem; text-align: right; } .hourly, .daily { display: grid; gap: 10px; } .hourly { grid-template-columns: repeat(8, minmax(74px, 1fr)); overflow: auto; padding-bottom: 3px; } .hour { min-width: 74px; border: 1px solid var(--line); background: var(--panel-soft); border-radius: 20px; padding: 10px; text-align: center; } .hour-time, .day-name { color: var(--muted); font-size: 0.78rem; font-weight: 750; } .mini-icon { color: var(--accent); height: 34px; display: grid; place-items: center; margin: 7px 0; } .mini-icon svg { width: 30px; height: 30px; } .hour-temp, .day-temp { font-weight: 850; } .rain-chance { color: #b8c7e6; font-size: 0.76rem; margin-top: 5px; } .daily { grid-template-columns: 1fr; } .day { display: grid; grid-template-columns: 44px 36px 1fr auto; align-items: center; gap: 10px; border: 1px solid var(--line); background: var(--panel-soft); border-radius: 18px; padding: 9px 10px; } .day .mini-icon { margin: 0; height: 28px; } .message { color: var(--muted); } .message.error { color: var(--danger); } @media (max-width: 920px) { .app { overflow: auto; } .weather-stage { min-height: 100svh; height: auto; } .hero { grid-template-columns: 1fr; align-items: start; } .stats { grid-template-columns: repeat(4, minmax(150px, 1fr)); overflow: auto; padding-bottom: 4px; } .bottom-panel { grid-template-columns: 1fr; } } @media (max-width: 640px) { .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } .stat-card { min-height: 108px; padding: 14px; } .hourly { grid-template-columns: repeat(8, 78px); } .temperature { font-size: clamp(5rem, 27vw, 8rem); } .place { font-size: 1.8rem; } .app { overflow: hidden !important; } } </style> </head> <body> <main class="app" style="width: 100%"> <section class="weather-stage" aria-live="polite"> <div id="particles" class="particles"></div> <header class="topbar"> <div class="brand"> <div class="eyebrow">Live weather</div> <div id="locationName" class="place">Copenhagen, Denmark</div> <div id="updated" class="updated">Loading current conditions...</div> </div> <div class="settings"> <button id="settingsToggle" class="icon-button" type="button" aria-label="Open city settings" aria-expanded="false" aria-controls="settingsPanel"> <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"> <path d="M12 15.5A3.5 3.5 0 1 0 12 8a3.5 3.5 0 0 0 0 7.5Z" stroke="currentColor" stroke-width="1.8" /> <path d="M19.4 15a1.8 1.8 0 0 0 .36 1.98l.05.05a2.1 2.1 0 0 1-2.97 2.97l-.05-.05a1.8 1.8 0 0 0-1.98-.36 1.8 1.8 0 0 0-1.1 1.65V21.4a2.1 2.1 0 0 1-4.2 0v-.08a1.8 1.8 0 0 0-1.1-1.65 1.8 1.8 0 0 0-1.98.36l-.05.05a2.1 2.1 0 0 1-2.97-2.97l.05-.05A1.8 1.8 0 0 0 4.6 15a1.8 1.8 0 0 0-1.65-1.1H2.9a2.1 2.1 0 0 1 0-4.2h.08A1.8 1.8 0 0 0 4.6 8a1.8 1.8 0 0 0-.36-1.98l-.05-.05A2.1 2.1 0 0 1 7.16 3l.05.05A1.8 1.8 0 0 0 9.2 3.4 1.8 1.8 0 0 0 10.3 1.75V1.7a2.1 2.1 0 0 1 4.2 0v.08A1.8 1.8 0 0 0 15.6 3.4a1.8 1.8 0 0 0 1.98-.36l.05-.05A2.1 2.1 0 0 1 20.6 6l-.05.05A1.8 1.8 0 0 0 20.2 8c.26.66.9 1.1 1.6 1.1h.1a2.1 2.1 0 0 1 0 4.2h-.08A1.8 1.8 0 0 0 19.4 15Z" stroke="currentColor" stroke-width="1.25" /> </svg> </button> <div id="settingsPanel" class="settings-panel" hidden> <div class="settings-title">Change city</div> <div class="settings-help"> Search uses Open-Meteo geocoding. Nothing is stored locally. </div> <div class="search-row"> <input id="cityInput" type="search" placeholder="Search city" autocomplete="off" inputmode="search" /> <button id="citySearchButton" type="button">Go</button> </div> </div> </div> </header> <section class="hero"> <div class="main-reading"> <div id="weatherIcon" class="icon-wrap"></div> <div class="temp-row"> <div id="temperature" class="temperature">--<span class="degree">°</span></div> <div class="condition-block"> <div id="condition" class="condition">Loading</div> <div id="feelsLike" class="feels">Feels like --°</div> </div> </div> </div> <div class="stats"> <article class="stat-card"> <div class="stat-label">Humidity</div> <div id="humidity" class="stat-value">--%</div> <div class="stat-note">Relative humidity</div> </article> <article class="stat-card"> <div class="stat-label">Wind</div> <div id="wind" class="stat-value">--</div> <div id="windNote" class="stat-note">10 m wind speed</div> </article> <article class="stat-card"> <div class="stat-label">Rain now</div> <div id="rainNow" class="stat-value">--</div> <div id="rainNote" class="stat-note">Current precipitation</div> </article> <article class="stat-card"> <div class="stat-label">Clouds</div> <div id="cloudCover" class="stat-value">--%</div> <div class="stat-note">Current cloud cover</div> </article> </div> </section> <section class="bottom-panel"> <article class="strip"> <div class="strip-head"> <div class="strip-title">Next hours</div> <div id="hourlyStatus" class="status">--</div> </div> <div id="hourly" class="hourly"></div> </article> <article class="strip"> <div class="strip-head"> <div class="strip-title">5 days</div> <div id="dailyStatus" class="status">--</div> </div> <div id="daily" class="daily"></div> </article> </section> </section> </main> <script> // No localStorage, sessionStorage, IndexedDB, cookies, or geolocation. // All state exists only in memory while this page is open. const DEFAULT_CITY = { name: "Copenhagen, Denmark", latitude: 55.6761, longitude: 12.5683 }; const els = { particles: document.getElementById("particles"), locationName: document.getElementById("locationName"), updated: document.getElementById("updated"), weatherIcon: document.getElementById("weatherIcon"), temperature: document.getElementById("temperature"), condition: document.getElementById("condition"), feelsLike: document.getElementById("feelsLike"), humidity: document.getElementById("humidity"), wind: document.getElementById("wind"), windNote: document.getElementById("windNote"), rainNow: document.getElementById("rainNow"), rainNote: document.getElementById("rainNote"), cloudCover: document.getElementById("cloudCover"), hourly: document.getElementById("hourly"), daily: document.getElementById("daily"), hourlyStatus: document.getElementById("hourlyStatus"), dailyStatus: document.getElementById("dailyStatus"), settingsToggle: document.getElementById("settingsToggle"), settingsPanel: document.getElementById("settingsPanel"), cityInput: document.getElementById("cityInput"), citySearchButton: document.getElementById("citySearchButton") }; const weatherMeta = { 0: { label: "Clear sky", type: "clear" }, 1: { label: "Mainly clear", type: "clear" }, 2: { label: "Partly cloudy", type: "partly" }, 3: { label: "Overcast", type: "cloud" }, 45: { label: "Fog", type: "fog" }, 48: { label: "Rime fog", type: "fog" }, 51: { label: "Light drizzle", type: "rain" }, 53: { label: "Drizzle", type: "rain" }, 55: { label: "Dense drizzle", type: "rain" }, 56: { label: "Freezing drizzle", type: "rain" }, 57: { label: "Dense freezing drizzle", type: "rain" }, 61: { label: "Slight rain", type: "rain" }, 63: { label: "Rain", type: "rain" }, 65: { label: "Heavy rain", type: "rain" }, 66: { label: "Freezing rain", type: "rain" }, 67: { label: "Heavy freezing rain", type: "rain" }, 71: { label: "Slight snow", type: "snow" }, 73: { label: "Snow", type: "snow" }, 75: { label: "Heavy snow", type: "snow" }, 77: { label: "Snow grains", type: "snow" }, 80: { label: "Light showers", type: "rain" }, 81: { label: "Showers", type: "rain" }, 82: { label: "Heavy showers", type: "rain" }, 85: { label: "Snow showers", type: "snow" }, 86: { label: "Heavy snow showers", type: "snow" }, 95: { label: "Thunderstorm", type: "storm" }, 96: { label: "Thunderstorm with hail", type: "storm" }, 99: { label: "Heavy thunderstorm with hail", type: "storm" } }; function iconSvg(type, isDay = true) { const sun = ` <circle cx="50" cy="50" r="18" fill="none" stroke="currentColor" stroke-width="7"/> <path d="M50 9v12M50 79v12M9 50h12M79 50h12M21 21l9 9M70 70l9 9M79 21l-9 9M30 70l-9 9" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round"/> `; const moon = ` <path d="M66 72c-20 7-42-8-42-30 0-16 10-29 24-34-5 22 9 43 31 47-3 8-7 13-13 17Z" fill="none" stroke="currentColor" stroke-width="7" stroke-linejoin="round"/> `; const cloud = ` <path d="M31 68h39c11 0 19-8 19-18s-8-18-18-18c-4-15-18-24-34-20-13 3-23 15-23 29C5 44 0 51 0 59c0 10 8 9 31 9Z" transform="translate(6 12)" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/> `; const rain = ` ${cloud} <path d="M34 82l-5 12M52 82l-5 12M70 82l-5 12" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round"/> `; const snow = ` ${cloud} <path d="M34 84v13M27 90l14 8M41 90l-14 8M62 84v13M55 90l14 8M69 90l-14 8" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/> `; const storm = ` ${cloud} <path d="M54 77 43 99h15l-9 20 24-32H58l9-10H54Z" fill="currentColor"/> `; const fog = ` ${cloud} <path d="M18 86h64M26 98h52M16 110h68" fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round"/> `; const partly = ` <g transform="translate(2 0)">${isDay ? sun : moon}</g> <g transform="translate(4 20) scale(.9)">${cloud}</g> `; const map = { clear: isDay ? sun : moon, partly, cloud, rain, snow, storm, fog }; return ` <svg viewBox="0 0 120 120" fill="none" aria-hidden="true"> ${map[type] || cloud} </svg> `; } function formatTemp(value) { return typeof value === "number" ? `${Math.round(value)}°` : "--°"; } function formatKm(value) { return typeof value === "number" ? `${Math.round(value)} km/h` : "--"; } function formatPercent(value) { return typeof value === "number" ? `${Math.round(value)}%` : "--%"; } function formatMm(value) { return typeof value === "number" ? `${value.toFixed(value < 1 ? 1 : 0)} mm` : "--"; } function formatHour(timeString) { const date = new Date(timeString); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function formatDay(timeString) { const date = new Date(timeString); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleDateString([], { weekday: "short" }); } function setLoading(isLoading) { els.citySearchButton.disabled = isLoading; els.settingsToggle.disabled = false; } function setStatus(text, isError = false) { els.updated.textContent = text; els.updated.classList.toggle("error", isError); } function getMeta(code) { return weatherMeta[code] || { label: "Weather data", type: "cloud" }; } function renderParticles(type, intensity) { els.particles.textContent = ""; if (type !== "rain" && type !== "storm" && type !== "snow") return; const count = type === "snow" ? Math.min(55, 22 + intensity * 5) : Math.min(95, 36 + intensity * 8); for (let i = 0; i < count; i += 1) { const item = document.createElement("span"); item.className = type === "snow" ? "snow" : "drop"; item.style.left = `${Math.random() * 112}%`; item.style.animationDuration = type === "snow" ? `${5 + Math.random() * 8}s` : `${0.55 + Math.random() * 0.85}s`; item.style.animationDelay = `${Math.random() * 5}s`; item.style.opacity = `${0.25 + Math.random() * 0.6}`; if (type !== "snow") { item.style.height = `${24 + Math.random() * 34}px`; } els.particles.appendChild(item); } } function renderHourly(data) { const hourly = data.hourly; els.hourly.textContent = ""; if (!hourly || !hourly.time || !hourly.time.length) { els.hourlyStatus.textContent = "Unavailable"; return; } const currentTime = data.current?.time || hourly.time[0]; let startIndex = hourly.time.findIndex(time => time >= currentTime); if (startIndex < 0) startIndex = 0; const endIndex = Math.min(startIndex + 8, hourly.time.length); const times = hourly.time.slice(startIndex, endIndex); times.forEach((time, offset) => { const index = startIndex + offset; const code = hourly.weather_code?.[index]; const meta = getMeta(code); const temp = hourly.temperature_2m?.[index]; const rainChance = hourly.precipitation_probability?.[index]; const card = document.createElement("div"); card.className = "hour"; card.innerHTML = ` <div class="hour-time">${offset === 0 ? "Now" : formatHour(time)}</div> <div class="mini-icon">${iconSvg(meta.type, true)}</div> <div class="hour-temp">${formatTemp(temp)}</div> <div class="rain-chance">${formatPercent(rainChance)}</div> `; els.hourly.appendChild(card); }); els.hourlyStatus.textContent = "Rain chance shown"; } function renderDaily(data) { const daily = data.daily; els.daily.textContent = ""; if (!daily || !daily.time || !daily.time.length) { els.dailyStatus.textContent = "Unavailable"; return; } daily.time.slice(0, 5).forEach((time, index) => { const code = daily.weather_code?.[index]; const meta = getMeta(code); const max = daily.temperature_2m_max?.[index]; const min = daily.temperature_2m_min?.[index]; const rainChance = daily.precipitation_probability_max?.[index]; const row = document.createElement("div"); row.className = "day"; row.innerHTML = ` <div class="day-name">${index === 0 ? "Today" : formatDay(time)}</div> <div class="mini-icon">${iconSvg(meta.type, true)}</div> <div class="rain-chance">${meta.label}</div> <div class="day-temp">${formatTemp(max)} / ${formatTemp(min)}</div> `; els.daily.appendChild(row); }); els.dailyStatus.textContent = "High / low"; } async function fetchWeather(place) { setLoading(true); setStatus(`Loading ${place.name}...`); try { const params = new URLSearchParams({ latitude: String(place.latitude), longitude: String(place.longitude), current: [ "temperature_2m", "relative_humidity_2m", "apparent_temperature", "is_day", "precipitation", "rain", "showers", "snowfall", "weather_code", "cloud_cover", "wind_speed_10m", "wind_direction_10m" ].join(","), hourly: [ "temperature_2m", "precipitation_probability", "precipitation", "weather_code" ].join(","), daily: [ "weather_code", "temperature_2m_max", "temperature_2m_min", "precipitation_probability_max" ].join(","), timezone: "auto", forecast_days: "5" }); const response = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`); if (!response.ok) { throw new Error("Weather request failed."); } const data = await response.json(); if (!data.current) { throw new Error("Weather data unavailable."); } const current = data.current; const meta = getMeta(current.weather_code); const isDay = current.is_day !== 0; const precipitation = Number(current.precipitation || 0) + Number(current.rain || 0) + Number(current.showers || 0) + Number(current.snowfall || 0); els.locationName.textContent = place.name; els.weatherIcon.innerHTML = iconSvg(meta.type, isDay); els.temperature.innerHTML = `${Math.round(current.temperature_2m)}<span class="degree">°</span>`; els.condition.textContent = meta.label; els.feelsLike.textContent = `Feels like ${formatTemp(current.apparent_temperature)}`; els.humidity.textContent = formatPercent(current.relative_humidity_2m); els.wind.textContent = formatKm(current.wind_speed_10m); els.windNote.textContent = typeof current.wind_direction_10m === "number" ? `${Math.round(current.wind_direction_10m)}° direction` : "10 m wind speed"; els.rainNow.textContent = formatMm(precipitation); els.rainNote.textContent = meta.type === "rain" || meta.type === "storm" ? "Rain effect enabled" : meta.type === "snow" ? "Snow effect enabled" : "Current precipitation"; els.cloudCover.textContent = formatPercent(current.cloud_cover); renderHourly(data); renderDaily(data); const intensity = Math.max( 1, Math.round(precipitation * 3), meta.type === "storm" ? 8 : 0, current.weather_code === 65 || current.weather_code === 82 ? 7 : 0 ); renderParticles(meta.type, intensity); const updated = current.time ? `Updated ${formatHour(current.time)}` : "Updated just now"; setStatus(updated); } catch (error) { setStatus(error.message || "Could not load weather.", true); } finally { setLoading(false); } } async function searchCity() { const query = els.cityInput.value.trim(); if (!query) { setStatus("Enter a city name.", true); return; } setLoading(true); setStatus(`Searching ${query}...`); try { const params = new URLSearchParams({ name: query, count: "1", language: "en", format: "json" }); const response = await fetch(`https://geocoding-api.open-meteo.com/v1/search?${params}`); if (!response.ok) { throw new Error("City search failed."); } const data = await response.json(); const result = data.results && data.results[0]; if (!result) { throw new Error("No matching city found."); } const place = { name: [result.name, result.admin1, result.country].filter(Boolean).join(", "), latitude: result.latitude, longitude: result.longitude }; els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); await fetchWeather(place); } catch (error) { setStatus(error.message || "Could not search city.", true); } finally { setLoading(false); } } els.settingsToggle.addEventListener("click", () => { const isOpen = !els.settingsPanel.hidden; els.settingsPanel.hidden = isOpen; els.settingsToggle.setAttribute("aria-expanded", String(!isOpen)); if (isOpen) return; setTimeout(() => { els.cityInput.focus(); els.cityInput.select(); }, 0); }); els.citySearchButton.addEventListener("click", searchCity); els.cityInput.addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); searchCity(); } if (event.key === "Escape") { els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); } }); document.addEventListener("click", event => { const clickedInsideSettings = event.target.closest(".settings"); if (clickedInsideSettings) return; els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); }); fetchWeather(DEFAULT_CITY); </script> </body> </html>
Copy
Save
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Fullscreen Weather Widget</title> <style> :root { --bg: #0b1020; --panel: #111827; --panel-soft: #172033; --panel-strong: #1f2937; --text: #f9fafb; --muted: #a7b0c0; --line: rgba(255, 255, 255, 0.12); --accent: #8ab4ff; --danger: #fca5a5; --shadow: rgba(0, 0, 0, 0.35); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } * { box-sizing: border-box; } html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: var(--bg); color: var(--text); } body { min-height: 100svh; } button, input { font: inherit; } button { cursor: pointer; } .app { position: relative; width: 100vw; height: 100svh; min-height: 560px; overflow: hidden; background: var(--bg); isolation: isolate; } .weather-stage { position: relative; width: 100%; height: 100%; padding: clamp(18px, 3vw, 40px); display: grid; grid-template-rows: auto 1fr auto; gap: 22px; z-index: 1; } .particles { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: -1; } .drop { position: absolute; top: -12vh; width: 2px; height: 38px; border-radius: 999px; background: rgba(180, 205, 255, 0.48); animation: rainFall linear infinite; transform: rotate(12deg); } .snow { position: absolute; top: -8vh; width: 7px; height: 7px; border-radius: 50%; background: rgba(255, 255, 255, 0.7); animation: snowFall linear infinite; } @keyframes rainFall { to { transform: translate3d(-12vw, 125vh, 0) rotate(12deg); } } @keyframes snowFall { to { transform: translate3d(8vw, 120vh, 0); } } .topbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; z-index: 3; } .brand { display: flex; flex-direction: column; gap: 7px; } .eyebrow { color: var(--muted); font-size: 0.8rem; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 750; } .place { font-size: clamp(1.6rem, 4vw, 3.4rem); font-weight: 800; line-height: 0.95; letter-spacing: -0.05em; max-width: 900px; } .updated { color: var(--muted); font-size: 0.95rem; } .settings { position: relative; z-index: 10; } .icon-button { width: 48px; height: 48px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 16px; background: rgba(17, 24, 39, 0.82); color: var(--text); box-shadow: 0 18px 50px var(--shadow); } .icon-button svg { width: 22px; height: 22px; } .settings-panel { position: absolute; right: 0; top: 58px; width: min(360px, calc(100vw - 36px)); padding: 16px; border: 1px solid var(--line); border-radius: 22px; background: rgba(17, 24, 39, 0.96); box-shadow: 0 24px 80px var(--shadow); } .settings-panel[hidden] { display: none; } .settings-title { font-weight: 800; margin-bottom: 10px; } .settings-help { color: var(--muted); font-size: 0.88rem; line-height: 1.4; margin-bottom: 14px; } .search-row { display: flex; gap: 8px; } .search-row input { flex: 1; min-width: 0; color: var(--text); background: var(--panel-soft); border: 1px solid var(--line); border-radius: 14px; padding: 11px 12px; outline: none; } .search-row input:focus { border-color: rgba(138, 180, 255, 0.75); } .search-row button { border: 0; border-radius: 14px; padding: 0 14px; font-weight: 800; color: #0b1020; background: var(--accent); } .hero { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr); align-items: center; gap: clamp(20px, 5vw, 72px); min-height: 0; } .main-reading { min-width: 0; } .icon-wrap { width: clamp(120px, 22vw, 260px); height: clamp(120px, 22vw, 260px); display: grid; place-items: center; color: var(--accent); margin-bottom: clamp(12px, 2vw, 20px); } .icon-wrap svg { width: 100%; height: 100%; filter: drop-shadow(0 28px 45px rgba(0, 0, 0, 0.35)); } .temp-row { display: flex; align-items: flex-end; gap: 14px; flex-wrap: wrap; } .temperature { font-size: clamp(5.5rem, 15vw, 13rem); font-weight: 900; line-height: 0.78; letter-spacing: -0.1em; } .degree { letter-spacing: -0.03em; } .condition-block { padding-bottom: clamp(4px, 1.4vw, 16px); } .condition { font-size: clamp(1.3rem, 3vw, 2.5rem); font-weight: 800; letter-spacing: -0.04em; line-height: 1; } .feels { color: var(--muted); font-size: clamp(0.95rem, 1.6vw, 1.15rem); margin-top: 8px; } .stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } .stat-card { min-height: 124px; border: 1px solid var(--line); border-radius: 26px; background: rgba(17, 24, 39, 0.72); padding: 18px; box-shadow: 0 18px 50px var(--shadow); } .stat-label { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 0.86rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 12px; } .stat-value { font-size: clamp(1.6rem, 3vw, 2.4rem); font-weight: 850; line-height: 1; letter-spacing: -0.05em; } .stat-note { color: var(--muted); font-size: 0.88rem; margin-top: 9px; } .bottom-panel { display: grid; grid-template-columns: 1.3fr 0.7fr; gap: 14px; min-height: 154px; } .strip { border: 1px solid var(--line); border-radius: 28px; background: rgba(17, 24, 39, 0.78); box-shadow: 0 18px 50px var(--shadow); padding: 16px; min-width: 0; } .strip-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; } .strip-title { font-weight: 850; letter-spacing: -0.03em; } .status { color: var(--muted); font-size: 0.88rem; text-align: right; } .hourly, .daily { display: grid; gap: 10px; } .hourly { grid-template-columns: repeat(8, minmax(74px, 1fr)); overflow: auto; padding-bottom: 3px; } .hour { min-width: 74px; border: 1px solid var(--line); background: var(--panel-soft); border-radius: 20px; padding: 10px; text-align: center; } .hour-time, .day-name { color: var(--muted); font-size: 0.78rem; font-weight: 750; } .mini-icon { color: var(--accent); height: 34px; display: grid; place-items: center; margin: 7px 0; } .mini-icon svg { width: 30px; height: 30px; } .hour-temp, .day-temp { font-weight: 850; } .rain-chance { color: #b8c7e6; font-size: 0.76rem; margin-top: 5px; } .daily { grid-template-columns: 1fr; } .day { display: grid; grid-template-columns: 44px 36px 1fr auto; align-items: center; gap: 10px; border: 1px solid var(--line); background: var(--panel-soft); border-radius: 18px; padding: 9px 10px; } .day .mini-icon { margin: 0; height: 28px; } .message { color: var(--muted); } .message.error { color: var(--danger); } @media (max-width: 920px) { .app { overflow: auto; } .weather-stage { min-height: 100svh; height: auto; } .hero { grid-template-columns: 1fr; align-items: start; } .stats { grid-template-columns: repeat(4, minmax(150px, 1fr)); overflow: auto; padding-bottom: 4px; } .bottom-panel { grid-template-columns: 1fr; } } @media (max-width: 640px) { .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } .stat-card { min-height: 108px; padding: 14px; } .hourly { grid-template-columns: repeat(8, 78px); } .temperature { font-size: clamp(5rem, 27vw, 8rem); } .place { font-size: 1.8rem; } .app { overflow: hidden !important; } } </style> </head> <body> <main class="app" style="width: 100%"> <section class="weather-stage" aria-live="polite"> <div id="particles" class="particles"></div> <header class="topbar"> <div class="brand"> <div class="eyebrow">Live weather</div> <div id="locationName" class="place">Copenhagen, Denmark</div> <div id="updated" class="updated">Loading current conditions...</div> </div> <div class="settings"> <button id="settingsToggle" class="icon-button" type="button" aria-label="Open city settings" aria-expanded="false" aria-controls="settingsPanel"> <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"> <path d="M12 15.5A3.5 3.5 0 1 0 12 8a3.5 3.5 0 0 0 0 7.5Z" stroke="currentColor" stroke-width="1.8" /> <path d="M19.4 15a1.8 1.8 0 0 0 .36 1.98l.05.05a2.1 2.1 0 0 1-2.97 2.97l-.05-.05a1.8 1.8 0 0 0-1.98-.36 1.8 1.8 0 0 0-1.1 1.65V21.4a2.1 2.1 0 0 1-4.2 0v-.08a1.8 1.8 0 0 0-1.1-1.65 1.8 1.8 0 0 0-1.98.36l-.05.05a2.1 2.1 0 0 1-2.97-2.97l.05-.05A1.8 1.8 0 0 0 4.6 15a1.8 1.8 0 0 0-1.65-1.1H2.9a2.1 2.1 0 0 1 0-4.2h.08A1.8 1.8 0 0 0 4.6 8a1.8 1.8 0 0 0-.36-1.98l-.05-.05A2.1 2.1 0 0 1 7.16 3l.05.05A1.8 1.8 0 0 0 9.2 3.4 1.8 1.8 0 0 0 10.3 1.75V1.7a2.1 2.1 0 0 1 4.2 0v.08A1.8 1.8 0 0 0 15.6 3.4a1.8 1.8 0 0 0 1.98-.36l.05-.05A2.1 2.1 0 0 1 20.6 6l-.05.05A1.8 1.8 0 0 0 20.2 8c.26.66.9 1.1 1.6 1.1h.1a2.1 2.1 0 0 1 0 4.2h-.08A1.8 1.8 0 0 0 19.4 15Z" stroke="currentColor" stroke-width="1.25" /> </svg> </button> <div id="settingsPanel" class="settings-panel" hidden> <div class="settings-title">Change city</div> <div class="settings-help"> Search uses Open-Meteo geocoding. Nothing is stored locally. </div> <div class="search-row"> <input id="cityInput" type="search" placeholder="Search city" autocomplete="off" inputmode="search" /> <button id="citySearchButton" type="button">Go</button> </div> </div> </div> </header> <section class="hero"> <div class="main-reading"> <div id="weatherIcon" class="icon-wrap"></div> <div class="temp-row"> <div id="temperature" class="temperature">--<span class="degree">°</span></div> <div class="condition-block"> <div id="condition" class="condition">Loading</div> <div id="feelsLike" class="feels">Feels like --°</div> </div> </div> </div> <div class="stats"> <article class="stat-card"> <div class="stat-label">Humidity</div> <div id="humidity" class="stat-value">--%</div> <div class="stat-note">Relative humidity</div> </article> <article class="stat-card"> <div class="stat-label">Wind</div> <div id="wind" class="stat-value">--</div> <div id="windNote" class="stat-note">10 m wind speed</div> </article> <article class="stat-card"> <div class="stat-label">Rain now</div> <div id="rainNow" class="stat-value">--</div> <div id="rainNote" class="stat-note">Current precipitation</div> </article> <article class="stat-card"> <div class="stat-label">Clouds</div> <div id="cloudCover" class="stat-value">--%</div> <div class="stat-note">Current cloud cover</div> </article> </div> </section> <section class="bottom-panel"> <article class="strip"> <div class="strip-head"> <div class="strip-title">Next hours</div> <div id="hourlyStatus" class="status">--</div> </div> <div id="hourly" class="hourly"></div> </article> <article class="strip"> <div class="strip-head"> <div class="strip-title">5 days</div> <div id="dailyStatus" class="status">--</div> </div> <div id="daily" class="daily"></div> </article> </section> </section> </main> <script> // No localStorage, sessionStorage, IndexedDB, cookies, or geolocation. // All state exists only in memory while this page is open. const DEFAULT_CITY = { name: "Copenhagen, Denmark", latitude: 55.6761, longitude: 12.5683 }; const els = { particles: document.getElementById("particles"), locationName: document.getElementById("locationName"), updated: document.getElementById("updated"), weatherIcon: document.getElementById("weatherIcon"), temperature: document.getElementById("temperature"), condition: document.getElementById("condition"), feelsLike: document.getElementById("feelsLike"), humidity: document.getElementById("humidity"), wind: document.getElementById("wind"), windNote: document.getElementById("windNote"), rainNow: document.getElementById("rainNow"), rainNote: document.getElementById("rainNote"), cloudCover: document.getElementById("cloudCover"), hourly: document.getElementById("hourly"), daily: document.getElementById("daily"), hourlyStatus: document.getElementById("hourlyStatus"), dailyStatus: document.getElementById("dailyStatus"), settingsToggle: document.getElementById("settingsToggle"), settingsPanel: document.getElementById("settingsPanel"), cityInput: document.getElementById("cityInput"), citySearchButton: document.getElementById("citySearchButton") }; const weatherMeta = { 0: { label: "Clear sky", type: "clear" }, 1: { label: "Mainly clear", type: "clear" }, 2: { label: "Partly cloudy", type: "partly" }, 3: { label: "Overcast", type: "cloud" }, 45: { label: "Fog", type: "fog" }, 48: { label: "Rime fog", type: "fog" }, 51: { label: "Light drizzle", type: "rain" }, 53: { label: "Drizzle", type: "rain" }, 55: { label: "Dense drizzle", type: "rain" }, 56: { label: "Freezing drizzle", type: "rain" }, 57: { label: "Dense freezing drizzle", type: "rain" }, 61: { label: "Slight rain", type: "rain" }, 63: { label: "Rain", type: "rain" }, 65: { label: "Heavy rain", type: "rain" }, 66: { label: "Freezing rain", type: "rain" }, 67: { label: "Heavy freezing rain", type: "rain" }, 71: { label: "Slight snow", type: "snow" }, 73: { label: "Snow", type: "snow" }, 75: { label: "Heavy snow", type: "snow" }, 77: { label: "Snow grains", type: "snow" }, 80: { label: "Light showers", type: "rain" }, 81: { label: "Showers", type: "rain" }, 82: { label: "Heavy showers", type: "rain" }, 85: { label: "Snow showers", type: "snow" }, 86: { label: "Heavy snow showers", type: "snow" }, 95: { label: "Thunderstorm", type: "storm" }, 96: { label: "Thunderstorm with hail", type: "storm" }, 99: { label: "Heavy thunderstorm with hail", type: "storm" } }; function iconSvg(type, isDay = true) { const sun = ` <circle cx="50" cy="50" r="18" fill="none" stroke="currentColor" stroke-width="7"/> <path d="M50 9v12M50 79v12M9 50h12M79 50h12M21 21l9 9M70 70l9 9M79 21l-9 9M30 70l-9 9" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round"/> `; const moon = ` <path d="M66 72c-20 7-42-8-42-30 0-16 10-29 24-34-5 22 9 43 31 47-3 8-7 13-13 17Z" fill="none" stroke="currentColor" stroke-width="7" stroke-linejoin="round"/> `; const cloud = ` <path d="M31 68h39c11 0 19-8 19-18s-8-18-18-18c-4-15-18-24-34-20-13 3-23 15-23 29C5 44 0 51 0 59c0 10 8 9 31 9Z" transform="translate(6 12)" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/> `; const rain = ` ${cloud} <path d="M34 82l-5 12M52 82l-5 12M70 82l-5 12" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round"/> `; const snow = ` ${cloud} <path d="M34 84v13M27 90l14 8M41 90l-14 8M62 84v13M55 90l14 8M69 90l-14 8" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/> `; const storm = ` ${cloud} <path d="M54 77 43 99h15l-9 20 24-32H58l9-10H54Z" fill="currentColor"/> `; const fog = ` ${cloud} <path d="M18 86h64M26 98h52M16 110h68" fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round"/> `; const partly = ` <g transform="translate(2 0)">${isDay ? sun : moon}</g> <g transform="translate(4 20) scale(.9)">${cloud}</g> `; const map = { clear: isDay ? sun : moon, partly, cloud, rain, snow, storm, fog }; return ` <svg viewBox="0 0 120 120" fill="none" aria-hidden="true"> ${map[type] || cloud} </svg> `; } function formatTemp(value) { return typeof value === "number" ? `${Math.round(value)}°` : "--°"; } function formatKm(value) { return typeof value === "number" ? `${Math.round(value)} km/h` : "--"; } function formatPercent(value) { return typeof value === "number" ? `${Math.round(value)}%` : "--%"; } function formatMm(value) { return typeof value === "number" ? `${value.toFixed(value < 1 ? 1 : 0)} mm` : "--"; } function formatHour(timeString) { const date = new Date(timeString); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function formatDay(timeString) { const date = new Date(timeString); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleDateString([], { weekday: "short" }); } function setLoading(isLoading) { els.citySearchButton.disabled = isLoading; els.settingsToggle.disabled = false; } function setStatus(text, isError = false) { els.updated.textContent = text; els.updated.classList.toggle("error", isError); } function getMeta(code) { return weatherMeta[code] || { label: "Weather data", type: "cloud" }; } function renderParticles(type, intensity) { els.particles.textContent = ""; if (type !== "rain" && type !== "storm" && type !== "snow") return; const count = type === "snow" ? Math.min(55, 22 + intensity * 5) : Math.min(95, 36 + intensity * 8); for (let i = 0; i < count; i += 1) { const item = document.createElement("span"); item.className = type === "snow" ? "snow" : "drop"; item.style.left = `${Math.random() * 112}%`; item.style.animationDuration = type === "snow" ? `${5 + Math.random() * 8}s` : `${0.55 + Math.random() * 0.85}s`; item.style.animationDelay = `${Math.random() * 5}s`; item.style.opacity = `${0.25 + Math.random() * 0.6}`; if (type !== "snow") { item.style.height = `${24 + Math.random() * 34}px`; } els.particles.appendChild(item); } } function renderHourly(data) { const hourly = data.hourly; els.hourly.textContent = ""; if (!hourly || !hourly.time || !hourly.time.length) { els.hourlyStatus.textContent = "Unavailable"; return; } const currentTime = data.current?.time || hourly.time[0]; let startIndex = hourly.time.findIndex(time => time >= currentTime); if (startIndex < 0) startIndex = 0; const endIndex = Math.min(startIndex + 8, hourly.time.length); const times = hourly.time.slice(startIndex, endIndex); times.forEach((time, offset) => { const index = startIndex + offset; const code = hourly.weather_code?.[index]; const meta = getMeta(code); const temp = hourly.temperature_2m?.[index]; const rainChance = hourly.precipitation_probability?.[index]; const card = document.createElement("div"); card.className = "hour"; card.innerHTML = ` <div class="hour-time">${offset === 0 ? "Now" : formatHour(time)}</div> <div class="mini-icon">${iconSvg(meta.type, true)}</div> <div class="hour-temp">${formatTemp(temp)}</div> <div class="rain-chance">${formatPercent(rainChance)}</div> `; els.hourly.appendChild(card); }); els.hourlyStatus.textContent = "Rain chance shown"; } function renderDaily(data) { const daily = data.daily; els.daily.textContent = ""; if (!daily || !daily.time || !daily.time.length) { els.dailyStatus.textContent = "Unavailable"; return; } daily.time.slice(0, 5).forEach((time, index) => { const code = daily.weather_code?.[index]; const meta = getMeta(code); const max = daily.temperature_2m_max?.[index]; const min = daily.temperature_2m_min?.[index]; const rainChance = daily.precipitation_probability_max?.[index]; const row = document.createElement("div"); row.className = "day"; row.innerHTML = ` <div class="day-name">${index === 0 ? "Today" : formatDay(time)}</div> <div class="mini-icon">${iconSvg(meta.type, true)}</div> <div class="rain-chance">${meta.label}</div> <div class="day-temp">${formatTemp(max)} / ${formatTemp(min)}</div> `; els.daily.appendChild(row); }); els.dailyStatus.textContent = "High / low"; } async function fetchWeather(place) { setLoading(true); setStatus(`Loading ${place.name}...`); try { const params = new URLSearchParams({ latitude: String(place.latitude), longitude: String(place.longitude), current: [ "temperature_2m", "relative_humidity_2m", "apparent_temperature", "is_day", "precipitation", "rain", "showers", "snowfall", "weather_code", "cloud_cover", "wind_speed_10m", "wind_direction_10m" ].join(","), hourly: [ "temperature_2m", "precipitation_probability", "precipitation", "weather_code" ].join(","), daily: [ "weather_code", "temperature_2m_max", "temperature_2m_min", "precipitation_probability_max" ].join(","), timezone: "auto", forecast_days: "5" }); const response = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`); if (!response.ok) { throw new Error("Weather request failed."); } const data = await response.json(); if (!data.current) { throw new Error("Weather data unavailable."); } const current = data.current; const meta = getMeta(current.weather_code); const isDay = current.is_day !== 0; const precipitation = Number(current.precipitation || 0) + Number(current.rain || 0) + Number(current.showers || 0) + Number(current.snowfall || 0); els.locationName.textContent = place.name; els.weatherIcon.innerHTML = iconSvg(meta.type, isDay); els.temperature.innerHTML = `${Math.round(current.temperature_2m)}<span class="degree">°</span>`; els.condition.textContent = meta.label; els.feelsLike.textContent = `Feels like ${formatTemp(current.apparent_temperature)}`; els.humidity.textContent = formatPercent(current.relative_humidity_2m); els.wind.textContent = formatKm(current.wind_speed_10m); els.windNote.textContent = typeof current.wind_direction_10m === "number" ? `${Math.round(current.wind_direction_10m)}° direction` : "10 m wind speed"; els.rainNow.textContent = formatMm(precipitation); els.rainNote.textContent = meta.type === "rain" || meta.type === "storm" ? "Rain effect enabled" : meta.type === "snow" ? "Snow effect enabled" : "Current precipitation"; els.cloudCover.textContent = formatPercent(current.cloud_cover); renderHourly(data); renderDaily(data); const intensity = Math.max( 1, Math.round(precipitation * 3), meta.type === "storm" ? 8 : 0, current.weather_code === 65 || current.weather_code === 82 ? 7 : 0 ); renderParticles(meta.type, intensity); const updated = current.time ? `Updated ${formatHour(current.time)}` : "Updated just now"; setStatus(updated); } catch (error) { setStatus(error.message || "Could not load weather.", true); } finally { setLoading(false); } } async function searchCity() { const query = els.cityInput.value.trim(); if (!query) { setStatus("Enter a city name.", true); return; } setLoading(true); setStatus(`Searching ${query}...`); try { const params = new URLSearchParams({ name: query, count: "1", language: "en", format: "json" }); const response = await fetch(`https://geocoding-api.open-meteo.com/v1/search?${params}`); if (!response.ok) { throw new Error("City search failed."); } const data = await response.json(); const result = data.results && data.results[0]; if (!result) { throw new Error("No matching city found."); } const place = { name: [result.name, result.admin1, result.country].filter(Boolean).join(", "), latitude: result.latitude, longitude: result.longitude }; els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); await fetchWeather(place); } catch (error) { setStatus(error.message || "Could not search city.", true); } finally { setLoading(false); } } els.settingsToggle.addEventListener("click", () => { const isOpen = !els.settingsPanel.hidden; els.settingsPanel.hidden = isOpen; els.settingsToggle.setAttribute("aria-expanded", String(!isOpen)); if (isOpen) return; setTimeout(() => { els.cityInput.focus(); els.cityInput.select(); }, 0); }); els.citySearchButton.addEventListener("click", searchCity); els.cityInput.addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); searchCity(); } if (event.key === "Escape") { els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); } }); document.addEventListener("click", event => { const clickedInsideSettings = event.target.closest(".settings"); if (clickedInsideSettings) return; els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); }); fetchWeather(DEFAULT_CITY); </script> </body> </html>
Move to Deleted?
will be moved to Deleted.
Cancel
Delete
Rename
Name
Cancel
Rename