From 956d6938fc1df0a0a7b1c88517cb49c4b83f6307 Mon Sep 17 00:00:00 2001 From: ashley Date: Sun, 28 Sep 2025 17:00:19 +0200 Subject: [PATCH] add to js --- css/weather-client.js | 262 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 css/weather-client.js diff --git a/css/weather-client.js b/css/weather-client.js new file mode 100644 index 00000000..293ba335 --- /dev/null +++ b/css/weather-client.js @@ -0,0 +1,262 @@ + 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"; + // Keep SSR search form in sync so non-JS round-trips preserve units + const uHidden=document.getElementById('unitsHidden'); + if(uHidden) uHidden.value = state.units === 'metric' ? 'metric' : 'imperial'; + 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("/api/weather", location.origin); + 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${t}
${weatherIcon(hourly.weather_code[i],1)}
${fmt(hourly.temperature_2m[i],'°')}
${(hourly.precipitation_probability?.[i]??0)}%
`; + 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
${weatherIcon(daily.weather_code[i],1)}
${dt}
${hi}
${lo}
${weatherText(daily.weather_code[i])}
`; + 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{ctx.beginPath();ctx.arc(pad+i*xstep, ymap(v), 2.5, 0, Math.PI*2);ctx.fill()}); + } + + // Keep units in sync for SSR form and client + 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(', ')); + } + + // Intercept form submit in JS mode and route smartly. + const form = document.getElementById('searchForm'); + form.addEventListener('submit',(e)=>{ + e.preventDefault(); + goSearch(); + }); + + $('#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; + + // If user typed "lat,lon", stay fully client-side + 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; + } + + // Prefer SSR round-trip when available + if (window.__SSR_ROUTE__) { + const units = (state.units==='metric') ? 'metric' : 'imperial'; + const url = new URL(window.__SSR_ROUTE__, location.origin); + url.searchParams.set('q', v); + url.searchParams.set('units', units); + location.href = url.toString(); + return; + } + + // Fallback: pure client search (for static build) + 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 init(){ + // Sync hidden units field at boot + const uHidden=document.getElementById('unitsHidden'); + if(uHidden) uHidden.value = state.units; + + // If server already gave us a place, hydrate immediately so UI & search are “awake” + if (window.__SSR__ && typeof window.__SSR__.lat === "number" && typeof window.__SSR__.lon === "number") { + try { + localStorage.setItem('pokeweather:last', JSON.stringify({ + when: Date.now(), + state: { lat: window.__SSR__.lat, lon: window.__SSR__.lon, name: window.__SSR__.name, units: state.units }, + data: { current: window.__SSR__.current || {}, daily: window.__SSR__.daily || {}, hourly: window.__SSR__.hourly || {} } + })); + } catch {} + loadWeather(window.__SSR__.lat, window.__SSR__.lon, window.__SSR__.name); + return; + } + + 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"; if(uHidden) uHidden.value=state.units; 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))}, + ()=>{} + )} + })(); \ No newline at end of file