Add html/weather.ejs

This commit is contained in:
ashley 2025-08-24 12:13:04 +02:00
parent 950c5e64bf
commit 256112ec03

421
html/weather.ejs Normal file
View File

@ -0,0 +1,421 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
<title>PokeWeather</title>
<meta name="theme-color" content="#0ea5e9" />
<meta name="color-scheme" content="dark light" />
<style>
:root{--bg:#0b0f16;--fg:#eef3ff;--muted:#a7b4cc;--card:#0f1624;--ring:#ffffff22;--accent:#0ea5e9;--good:#7ee787;--bad:#f97373;--warn:#fbbf24;--br:16px}
@media (prefers-color-scheme:light){:root{--bg:#fbfbfe;--fg:#0b1020;--muted:#5b6b87;--card:#ffffff;--ring:#0b102018}}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:radial-gradient(1200px 800px at 20% 10%,#0c1528,var(--bg));color:var(--fg);font:15px/1.4 ui-sans-serif,system-ui,Segoe UI,Roboto,Inter,Arial}
/* Improved, stable navbar (no weird pill stretching) */
header{position:sticky;top:0;z-index:40}
.navwrap{backdrop-filter:saturate(1.4) blur(10px);background:linear-gradient(180deg,rgba(0,0,0,.35),rgba(0,0,0,.15));border-bottom:1px solid var(--ring)}
.wrap{max-width:1100px;margin:0 auto;padding:12px 14px}
.top{display:flex;align-items:center;gap:12px;justify-content:space-between;min-height:56px}
.brand{display:flex;align-items:center;gap:10px}
.brand .logo{width:36px;height:36px;border-radius:12px;background:linear-gradient(135deg,#00d2ff,#3a7bd5);display:grid;place-items:center;box-shadow:0 6px 20px #00d2ff44;flex:0 0 36px}
/* removed brand text on purpose */
.chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--ring);border-radius:999px;padding:6px 10px;background:linear-gradient(180deg,transparent,rgba(255,255,255,.03));white-space:nowrap;max-width:58vw;overflow:hidden;text-overflow:ellipsis}
.controls{display:flex;gap:8px;flex-wrap:nowrap}
button,.btn{border:1px solid var(--ring);background:linear-gradient(180deg,transparent,rgba(255,255,255,.03));color:var(--fg);padding:10px 12px;border-radius:12px;cursor:pointer;white-space:nowrap}
button:active{transform:translateY(1px)}
.subnav{border-top:1px solid var(--ring)}
.subrow{display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;padding-top:8px}
.tabs{display:flex;gap:8px}
.tab{padding:8px 12px;border:1px solid var(--ring);border-radius:999px;background:linear-gradient(180deg,transparent,rgba(255,255,255,.03));font-weight:700;text-decoration:none;color:inherit}
.search{display:flex;gap:8px;position:relative;flex:1;min-width:220px}
input[type="search"]{flex:1;min-width:140px;border:1px solid var(--ring);background:var(--card);color:var(--fg);padding:12px 14px;border-radius:12px;outline:none}
main{max-width:1100px;margin:14px auto;padding:0 14px;display:grid;gap:14px}
.grid{display:grid;gap:14px;grid-template-columns:repeat(12,1fr)}
.card{background:var(--card);border:1px solid var(--ring);border-radius:var(--br);padding:14px}
.current{grid-column:span 12;display:grid;gap:12px}
.curr-top{display:flex;gap:14px;align-items:center;justify-content:space-between;flex-wrap:wrap}
.temp{font-size:52px;font-weight:900;letter-spacing:-1px}
.desc{color:var(--muted);font-weight:600}
.details{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
.stat{border:1px dashed var(--ring);border-radius:12px;padding:10px}
.stat h4{margin:0 0 6px 0;font-size:12px;color:var(--muted);font-weight:700}
.stat div{font-weight:800}
.row{display:flex;gap:10px;align-items:center}
.bigicon{font-size:42px}
.section-title{font-size:14px;color:var(--muted);margin:0 0 8px 4px;font-weight:700;text-transform:uppercase;letter-spacing:.1em}
.scrollx{display:flex;gap:10px;overflow:auto;scroll-snap-type:x mandatory;padding-bottom:4px}
.hour{min-width:84px;scroll-snap-align:start;border:1px solid var(--ring);background:linear-gradient(180deg,transparent,rgba(255,255,255,.02));border-radius:12px;padding:10px;text-align:center}
.hour .t{font-weight:800}
.day{display:grid;grid-template-columns:1.2fr 2fr .8fr;gap:10px;align-items:center;border:1px solid var(--ring);border-radius:12px;padding:10px}
.day .hi{font-weight:800}
.day .lo{color:var(--muted)}
.panel{grid-column:span 12}
canvas{width:100%;height:120px;background:linear-gradient(180deg,transparent,rgba(255,255,255,.02));border-radius:12px;border:1px solid var(--ring)}
footer{opacity:.8;text-align:center;padding:18px;color:var(--muted)}
.suggest{position:absolute;background:var(--card);border:1px solid var(--ring);border-radius:12px;margin-top:4px;overflow:hidden;display:none;z-index:30;max-height:300px;overflow:auto}
.suggest button{display:block;width:100%;text-align:left;padding:10px 12px;border:0;border-bottom:1px solid var(--ring);background:none;color:inherit}
.suggest button:last-child{border-bottom:0}
.hidden{display:none}
@media (min-width:780px){.current{grid-column:span 7}.panel{grid-column:span 5}}
/* SSR (no-JS) block */
.ssr{display:none}
.no-js .ssr{display:block}
.no-js main{display:none}
</style>
<% /* Add a no-JS class if you detect on server (e.g., via UA or query). Default leaves it blank. */ %>
</head>
<body class="<%= ssr && ssr.forceNoJS ? 'no-js' : '' %>">
<!-- NAVBAR (brand text removed, location chip compact) -->
<header>
<div class="navwrap">
<div class="wrap top">
<div class="brand">
<div class="logo" aria-hidden="true"><span>☁️</span></div>
<span class="chip" id="locChip"><%= (ssr && ssr.name) || '—' %></span>
</div>
<div class="controls">
<button id="btnUnits" title="Toggle °C/°F">°C</button>
<button id="btnTheme" title="Toggle theme">🌗</button>
<button id="btnInstall" class="hidden" title="Install">⬇️ Install</button>
<a class="btn" href="#" id="btnShare" title="Share">🔗 Share</a>
</div>
</div>
<div class="wrap subrow subnav">
<div class="tabs" role="tablist" aria-label="Sections">
<a class="tab" href="#now">Now</a>
<a class="tab" href="#hourly">Hourly</a>
<a class="tab" href="#daily">7-Day</a>
</div>
<div class="search" role="search" aria-label="Search location">
<input id="q" type="search" placeholder="Search city, place, or paste lat,lon" autocomplete="off" />
<button id="btnSearch" title="Search">Search</button>
<button id="btnGeo" title="Use my location">📍</button>
<div id="suggest" class="suggest" role="listbox" aria-label="Suggestions"></div>
</div>
</div>
</div>
</header>
<!-- ===== Server-Side Render (shown if no JS) ===== -->
<section class="ssr">
<div class="wrap" style="padding-top:14px">
<article class="card" aria-live="polite">
<h3 class="section-title">Now — <%= (ssr && ssr.name) || 'Unknown' %></h3>
<% if (ssr && ssr.current) { %>
<div class="row" style="justify-content:space-between;flex-wrap:wrap">
<div class="row">
<div class="bigicon"><%= ssr.icon %></div>
<div>
<div class="temp"><%= Math.round(ssr.current.temperature_2m) %>°</div>
<div class="desc"><%= ssr.desc %></div>
</div>
</div>
<div class="row" style="gap:12px">
<div class="stat"><h4>Feels like</h4><div><%= Math.round(ssr.current.apparent_temperature) %>°</div></div>
<div class="stat"><h4>Wind</h4><div><%= Math.round(ssr.current.wind_speed_10m) %> <%= ssr.windUnit %></div></div>
<div class="stat"><h4>Humidity</h4><div><%= Math.round(ssr.current.relative_humidity_2m) %>%</div></div>
<div class="stat"><h4>Pressure</h4><div><%= Math.round(ssr.current.pressure_msl) %> hPa</div></div>
</div>
</div>
<div class="details" style="margin-top:10px">
<div class="stat"><h4>Sunrise</h4><div><%= ssr.sunriseLocal %></div></div>
<div class="stat"><h4>Sunset</h4><div><%= ssr.sunsetLocal %></div></div>
<div class="stat"><h4>UV max</h4><div><%= ssr.daily.uv_index_max?.[0] ?? '—' %></div></div>
<div class="stat"><h4>Precip prob (next hr)</h4><div><%= ssr.popNext ?? '—' %>%</div></div>
</div>
<% } else { %>
<p style="color:var(--muted)">No data to show.</p>
<% } %>
</article>
<article class="card" style="margin-top:14px">
<h3 class="section-title">7-day forecast</h3>
<% if (ssr && ssr.daily) { %>
<div class="grid" style="grid-template-columns:repeat(1,1fr);gap:10px">
<% for (let i=0;i<ssr.daily.time.length;i++){ %>
<div class="day">
<div class="row" style="gap:10px">
<div style="font-size:22px"><%= ssr.dailyIcons[i] %></div>
<div><%= ssr.dailyLabels[i] %></div>
</div>
<div class="row">
<div class="hi"><%= Math.round(ssr.daily.temperature_2m_max[i]) %>°</div>
<div class="lo"><span style="color:var(--muted)"><%= Math.round(ssr.daily.temperature_2m_min[i]) %>°</span></div>
</div>
<div style="text-align:right;color:var(--muted)"><%= ssr.dailyTexts[i] %></div>
</div>
<% } %>
</div>
<% } %>
</article>
</div>
</section>
<!-- ===== JS APP (kept intact) ===== -->
<main>
<section id="now" class="grid">
<article class="card current" aria-live="polite">
<div class="curr-top">
<div class="row">
<div class="bigicon" id="currIcon">—</div>
<div>
<div class="temp" id="currTemp">—</div>
<div class="desc" id="currDesc">—</div>
</div>
</div>
<div class="row" style="gap:12px">
<div class="stat"><h4>Feels like</h4><div id="currFeels">—</div></div>
<div class="stat"><h4>Wind</h4><div id="currWind">—</div></div>
<div class="stat"><h4>Humidity</h4><div id="currHum">—</div></div>
<div class="stat"><h4>Pressure</h4><div id="currPress">—</div></div>
</div>
</div>
<div class="details">
<div class="stat"><h4>Sunrise</h4><div id="sunrise">—</div></div>
<div class="stat"><h4>Sunset</h4><div id="sunset">—</div></div>
<div class="stat"><h4>UV max</h4><div id="uv">—</div></div>
<div class="stat"><h4>Precip prob (next hr)</h4><div id="pop">—</div></div>
</div>
</article>
<aside id="hourly" class="panel card">
<h3 class="section-title">Next 24 hours</h3>
<div id="hours" class="scrollx" aria-label="Hourly forecast"></div>
<canvas id="chart" height="120" aria-label="Temperature chart" role="img"></canvas>
</aside>
</section>
<section id="daily" class="card">
<h3 class="section-title">7-day forecast</h3>
<div id="days" class="grid" style="grid-template-columns:repeat(1,1fr);gap:10px"></div>
</section>
<footer>
<div>Data: Open-Meteo & OpenStreetMap/Nominatim. Your settings are saved locally.</div>
</footer>
</main>
<script>
const $=s=>document.querySelector(s);const $$=s=>[...document.querySelectorAll(s)];
const units=JSON.parse(localStorage.getItem('pokeweather:units')||'{}');
let state={lat:null,lon:null,name:null,units:units.units||'metric'};
const btnUnits=$('#btnUnits'); btnUnits.textContent=state.units==='metric'?"°C":"°F";
const weatherText=(c)=>{
if([0].includes(c))return"Clear sky";
if([1].includes(c))return"Mostly clear";
if([2].includes(c))return"Partly cloudy";
if([3].includes(c))return"Overcast";
if([45,48].includes(c))return"Fog";
if([51,53,55].includes(c))return"Drizzle";
if([56,57].includes(c))return"Freezing drizzle";
if([61,63,65].includes(c))return"Rain";
if([66,67].includes(c))return"Freezing rain";
if([71,73,75].includes(c))return"Snow";
if([77].includes(c))return"Snow grains";
if([80,81,82].includes(c))return"Showers";
if([85,86].includes(c))return"Snow showers";
if([95].includes(c))return"Thunderstorm";
if([96,99].includes(c))return"Storm & hail";return"—"}
const weatherIcon=(c,isDay)=>{
if(c===0)return isDay?"☀️":"🌙";
if([1,2].includes(c))return isDay?"🌤️":"☁️";
if([3].includes(c))return"☁️";
if([45,48].includes(c))return"🌫️";
if([51,53,55,80,81,82].includes(c))return"🌦️";
if([61,63,65].includes(c))return"🌧️";
if([66,67].includes(c))return"🌧️❄️";
if([71,73,75,77,85,86].includes(c))return"❄️";
if([95,96,99].includes(c))return"⛈️";return"☁️";
};
const fmt=(n,u='')=>n==null?'—':`${Math.round(n)}${u}`;
const fmtTime=(s)=>new Date(s).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
const fmtDay=(s)=>new Date(s).toLocaleDateString([], {weekday:'short',month:'short',day:'numeric'});
async function searchPlaces(q){
if(!q) return [];
const url=`https://nominatim.openstreetmap.org/search?format=json&limit=5&addressdetails=1&q=${encodeURIComponent(q)}`;
const r=await fetch(url,{headers:{'Accept-Language':navigator.language}});
if(!r.ok) return [];
return r.json();
}
function applyUnits(){
btnUnits.textContent=state.units==='metric'?"°C":"°F";
if(state.lat&&state.lon) loadWeather(state.lat,state.lon,state.name);
localStorage.setItem('pokeweather:units',JSON.stringify({units:state.units}))
}
async function loadWeather(lat,lon,name){
state.lat=+lat;state.lon=+lon;state.name=name||state.name||`${lat.toFixed(3)},${lon.toFixed(3)}`;
$('#locChip').textContent=state.name;
const tu=state.units==='metric'?"celsius":"fahrenheit";
const wu=state.units==='metric'?"kmh":"mph";
const url=new URL('https://api.open-meteo.com/v1/forecast');
url.search={}.toString();
url.searchParams.set('latitude',lat);url.searchParams.set('longitude',lon);
url.searchParams.set('current','temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,wind_speed_10m,wind_direction_10m,pressure_msl');
url.searchParams.set('hourly','temperature_2m,apparent_temperature,precipitation_probability,precipitation,weather_code,wind_speed_10m');
url.searchParams.set('daily','weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,uv_index_max');
url.searchParams.set('timezone','auto');url.searchParams.set('forecast_days','7');
url.searchParams.set('temperature_unit',tu);url.searchParams.set('windspeed_unit',wu);
$('.current').classList.add('loading');
try{
const res=await fetch(url.toString());
if(!res.ok) throw new Error('Weather error');
const data=await res.json();
localStorage.setItem('pokeweather:last',JSON.stringify({when:Date.now(),state,data}));
render(data);
}catch(e){
const cached=JSON.parse(localStorage.getItem('pokeweather:last')||'null');
if(cached){render(cached.data);}
else alert('Could not load weather.');
}finally{$('.current').classList.remove('loading')}
}
function render(d){
const c=d.current; const daily=d.daily; const hourly=d.hourly;
$('#currIcon').textContent=weatherIcon(c.weather_code,c.is_day);
$('#currTemp').textContent=fmt(c.temperature_2m,'°');
$('#currDesc').textContent=weatherText(c.weather_code);
$('#currFeels').textContent=fmt(c.apparent_temperature,'°');
$('#currWind').textContent=`${fmt(c.wind_speed_10m)} ${state.units==='metric'?'km/h':'mph'} ↗ ${c.wind_direction_10m??'—'}°`;
$('#currHum').textContent=fmt(c.relative_humidity_2m,'%');
$('#currPress').textContent=fmt(c.pressure_msl,' hPa');
$('#sunrise').textContent=fmtTime(daily.sunrise[0]);
$('#sunset').textContent=fmtTime(daily.sunset[0]);
$('#uv').textContent=(daily.uv_index_max?.[0]??'—');
const nowIndex=hourly.time.findIndex(t=>Date.parse(t)>Date.now());
const popNext=hourly.precipitation_probability?.[nowIndex]??hourly.precipitation_probability?.[0]??null;
$('#pop').textContent=popNext==null?'—':popNext+"%";
// hourly tiles
const H=$('#hours'); H.innerHTML='';
const start=nowIndex>0?nowIndex-1:0; const end=Math.min(start+24,hourly.time.length);
for(let i=start;i<end;i++){
const el=document.createElement('div');
el.className='hour';
const t=new Date(hourly.time[i]).toLocaleTimeString([], {hour:'2-digit'});
el.innerHTML=`<div>${t}</div><div style="font-size:22px">${weatherIcon(hourly.weather_code[i],1)}</div><div class="t">${fmt(hourly.temperature_2m[i],'°')}</div><div style="color:var(--muted);font-size:12px">${(hourly.precipitation_probability?.[i]??0)}%</div>`;
H.appendChild(el);
}
// chart
drawChart($('#chart'), hourly.time.slice(start,end).map(t=>new Date(t)), hourly.temperature_2m.slice(start,end));
// 7-day
const D=$('#days'); D.innerHTML='';
for(let i=0;i<daily.time.length;i++){
const row=document.createElement('div'); row.className='day';
const dt=fmtDay(daily.time[i]);
const hi=fmt(daily.temperature_2m_max[i],'°');
const lo=fmt(daily.temperature_2m_min[i],'°');
row.innerHTML=`<div class="row" style="gap:10px"><div style="font-size:22px">${weatherIcon(daily.weather_code[i],1)}</div><div>${dt}</div></div><div class="row"><div class="hi">${hi}</div><div class="lo">${lo}</div></div><div style="text-align:right;color:var(--muted)">${weatherText(daily.weather_code[i])}</div>`;
D.appendChild(row);
}
}
function drawChart(canvas, xs, ys){
const ctx=canvas.getContext('2d');
const w=canvas.width=canvas.clientWidth*2; const h=canvas.height=canvas.clientHeight*2; ctx.scale(2,2);
const min=Math.min(...ys), max=Math.max(...ys), pad=8; const xstep=(canvas.clientWidth-2*pad)/(ys.length-1);
const ymap=v=>{if(max===min) return canvas.clientHeight/2; const t=(v-min)/(max-min); return canvas.clientHeight-pad - t*(canvas.clientHeight-2*pad)};
ctx.clearRect(0,0,canvas.clientWidth,canvas.clientHeight);
ctx.lineWidth=2; ctx.strokeStyle=getComputedStyle(document.documentElement).getPropertyValue('--accent');
ctx.beginPath(); ctx.moveTo(pad, ymap(ys[0]));
for(let i=1;i<ys.length;i++){ctx.lineTo(pad+i*xstep, ymap(ys[i]));}
ctx.stroke();
ctx.fillStyle=getComputedStyle(document.documentElement).getPropertyValue('--accent');
ys.forEach((v,i)=>{ctx.beginPath();ctx.arc(pad+i*xstep, ymap(v), 2.5, 0, Math.PI*2);ctx.fill()});
}
btnUnits.addEventListener('click',()=>{state.units=state.units==='metric'?'imperial':'metric';applyUnits()});
$('#btnTheme').addEventListener('click',()=>{
const dark=document.documentElement.style.getPropertyValue('--bg')==='#0b0f16';
if(dark){document.documentElement.style.setProperty('--bg','#fbfbfe');document.documentElement.style.setProperty('--fg','#0b1020');document.documentElement.style.setProperty('--muted','#5b6b87');document.documentElement.style.setProperty('--card','#ffffff');document.documentElement.style.setProperty('--ring','#0b102018');}
else{document.documentElement.style.setProperty('--bg','#0b0f16');document.documentElement.style.setProperty('--fg','#eef3ff');document.documentElement.style.setProperty('--muted','#a7b4cc');document.documentElement.style.setProperty('--card','#0f1624');document.documentElement.style.setProperty('--ring','#ffffff22');}
});
$('#btnGeo').addEventListener('click',()=>{
if(!navigator.geolocation){alert('Geolocation not supported');return}
navigator.geolocation.getCurrentPosition(p=>{
reverseName(p.coords.latitude,p.coords.longitude).then(n=>loadWeather(p.coords.latitude,p.coords.longitude,n));
},e=>alert('Location denied'))
});
function reverseName(lat,lon){
const u=`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lon}`;
return fetch(u).then(r=>r.ok?r.json():null).then(j=>j?.display_name?.split(',').slice(0,2).join(', ')||`${lat.toFixed(3)},${lon.toFixed(3)}`);
}
const suggest=$('#suggest');
$('#q').addEventListener('input', debounce(async(e)=>{
const v=e.target.value.trim(); if(!v){suggest.style.display='none'; return}
const list=await searchPlaces(v); suggest.innerHTML='';
list.forEach(item=>{
const b=document.createElement('button'); b.type='button';
const label=item.display_name.split(',').slice(0,3).join(', ');
b.textContent=label; b.addEventListener('click',()=>{selectPlace(item)});
suggest.appendChild(b);
});
if(list.length){const r=$('#q').getBoundingClientRect(); suggest.style.minWidth=r.width+'px'; suggest.style.display='block'} else suggest.style.display='none';
},300));
function selectPlace(item){
suggest.style.display='none';
$('#q').value=item.display_name.split(',').slice(0,2).join(', ');
loadWeather(item.lat,item.lon,item.display_name.split(',').slice(0,2).join(', '));
}
$('#btnSearch').addEventListener('click',()=>goSearch());
$('#q').addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();goSearch()}});
function goSearch(){
const v=$('#q').value.trim();
if(!v) return;
const latlon=v.match(/^\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*$/);
if(latlon){const lat=+latlon[1], lon=+latlon[2]; reverseName(lat,lon).then(n=>loadWeather(lat,lon,n)); return}
searchPlaces(v).then(list=>{if(list[0]) selectPlace(list[0]); else alert('No results')})
}
$('#btnShare').addEventListener('click',async e=>{
e.preventDefault();
const url=new URL(location.href);
if(state.lat&&state.lon){url.searchParams.set('lat',state.lat);url.searchParams.set('lon',state.lon);url.searchParams.set('name',state.name)}
try{ if(navigator.share){await navigator.share({title:'PokeWeather',text:`${state.name||'Weather'}: ${$('#currTemp').textContent} ${$('#currDesc').textContent}`,url:url.toString()});}
else{await navigator.clipboard.writeText(url.toString());alert('Link copied!')} }catch{}
});
function debounce(fn,ms){let t;return (...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms)}}
(function setupPWA(){
const manifest={name:'PokeWeather',short_name:'PokeWeather',start_url:'./',display:'standalone',background_color:'#0b0f16',theme_color:'#0ea5e9',icons:[{src:'data:image/svg+xml;utf8,'+encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#00d2ff"/><stop offset="1" stop-color="#3a7bd5"/></linearGradient></defs><rect rx="28" width="128" height="128" fill="url(#g)"/><text x="50%" y="58%" text-anchor="middle" font-size="72" font-weight="900" fill="#fff">☁️</text></svg>`),sizes:'128x128',type:'image/svg+xml'}]};
const blob=new Blob([JSON.stringify(manifest)],{type:'application/json'}); const url=URL.createObjectURL(blob);
const link=document.createElement('link'); link.rel='manifest'; link.href=url; document.head.appendChild(link);
if('serviceWorker' in navigator){
const swSrc=`self.addEventListener('install',e=>{e.waitUntil(caches.open('pokeweather-v1').then(c=>c.addAll(['./'])))});self.addEventListener('fetch',e=>{const u=new URL(e.request.url);if(u.origin===location.origin){e.respondWith(caches.match(e.request).then(r=>r||fetch(e.request)))} })`;
const swURL=URL.createObjectURL(new Blob([swSrc],{type:'application/javascript'}));
navigator.serviceWorker.register(swURL);
window.addEventListener('beforeinstallprompt',(e)=>{e.preventDefault();window.deferredPrompt=e;$('#btnInstall').classList.remove('hidden')});
$('#btnInstall').addEventListener('click',async()=>{const p=window.deferredPrompt;if(!p) return;p.prompt();await p.userChoice;$('#btnInstall').classList.add('hidden')});
}
})();
(function init(){
const sp=new URLSearchParams(location.search);
const lat=sp.get('lat'), lon=sp.get('lon'), name=sp.get('name');
if(lat&&lon){loadWeather(+lat,+lon,name);return}
const cached=JSON.parse(localStorage.getItem('pokeweather:last')||'null');
if(cached){state.units=cached.state?.units||state.units; btnUnits.textContent=state.units==='metric'?"°C":"°F"; loadWeather(cached.state.lat,cached.state.lon,cached.state.name); return}
if(navigator.geolocation){navigator.geolocation.getCurrentPosition(p=>{reverseName(p.coords.latitude,p.coords.longitude).then(n=>loadWeather(p.coords.latitude,p.coords.longitude,n))},()=>{})}
})();
</script>
</body>
</html>