IP Location Weather Widget

This highly responsive, dark-themed weather widget provides a compact yet comprehensive snapshot of local atmospheric conditions, making it perfect for embedding into websites or blogs.

At its core, the widget automatically detects a user's location via their IP address to instantly display the current temperature alongside a detailed, real-time summary of expected highs, lows, wind speeds, and precipitation.

To keep the interface sleek and highly readable, it utilises a balanced grid layout that tracks barometric pressure, current moon phases, and a cleanly aligned 7-day forecast featuring smart labels that intuitively switch to "Today" and "Tomorrow" for immediate clarity.

Beyond standard meteorology, the widget incorporates robust quality-of-life enhancements and an advanced, geolocation-aware Aurora Alert system. By pulling live space weather data directly from NOAA, the widget dynamically evaluates the planetary K-index against the user’s specific geographic latitude to accurately calculate aurora visibility anywhere in the world, rather than relying on a single fixed region.

Coupled with seamless user experience upgrades - such as error-handling for unrecognised cities and a reactive loading state that prevents button spamming during searches - this widget serves as an intelligent, universally accurate companion for everyday tracking and stargazing alike.

Detecting...

--
BAROMETER
--
MOON
--
AURORA ALERT
Checking...
7-DAY FORECAST
IP Location Weather Widget
<div id="sd-weather-widget" style="font-family: sans-serif; background: #0f172a; color: #ffffff; border-radius: 16px; padding: 20px; max-width: 400px; margin: 0 auto; box-sizing: border-box;">
    <style>
        #sd-search-btn { transition: background-color 0.2s ease; }
        #sd-search-btn:hover:not(:disabled) { background-color: #2563eb !important; }
        #sd-search-btn:disabled { background-color: #475569 !important; cursor: not-allowed !important; }
    </style>

    <div style="margin-bottom: 0px; text-align: center;">
        <div style="display: flex; gap: 8px;">
            <input type="text" id="sd-loc-input" placeholder="Enter town or city..." style="flex: 1; padding: 10px; border-radius: 8px; border: none; background: #1e293b; color: #fff;">
            <button id="sd-search-btn" style="padding: 10px 15px; border-radius: 8px; border: none; background: #3b82f6; color: white; cursor: pointer; font-weight: bold;">Search</button>
        </div>
        <h2 id="sd-location-name" style="margin: 15px 0 0 0; font-size: 1.2rem;">Detecting...</h2>
    </div>

    <div id="sd-warning-box" style="display:none; background: #991b1b; color: #fee2e2; padding: 10px; border-radius: 8px; margin-bottom: 15px; font-size: 0.8rem; font-weight: bold; text-align: center;"></div>

    <div style="position: relative; text-align: center; margin-bottom: 20px; min-height: 120px; display: flex; flex-direction: column; justify-content: center; align-items: center;">
        <div id="sd-current-icon" style="position: absolute; opacity: 0.15; pointer-events: none;"></div>
        <div style="position: relative; z-index: 1;">
            <div id="sd-current-temp" style="font-size: 3.5rem; font-weight: bold; margin-top: 0px;">--</div>
            <div id="sd-today-summary" style="font-size: 0.95rem; color: #cbd5e1; margin-top: 5px; line-height: 1.4; padding: 0 10px; font-weight: 500;"></div>
        </div>
    </div>

    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 20px;">
        <div style="background: #1e293b; padding: 12px; border-radius: 8px; text-align: center;">
            <div style="font-size: 0.6rem; color: #94a3b8; letter-spacing: 0.1em;">BAROMETER</div>
            <div id="sd-pressure-val" style="font-weight: bold; margin-top: 4px;">--</div>
        </div>
        <div style="background: #1e293b; padding: 12px; border-radius: 8px; text-align: center;">
            <div style="font-size: 0.6rem; color: #94a3b8; letter-spacing: 0.1em;">MOON</div>
            <div id="sd-moon-text" style="font-weight: bold; margin-top: 4px;">--</div>
        </div>
        <div style="grid-column: span 2; background: #1e293b; padding: 12px; border-radius: 8px; text-align: center;">
            <div style="font-size: 0.6rem; color: #94a3b8; letter-spacing: 0.1em;">AURORA ALERT</div>
            <div id="sd-aurora-text" style="font-weight: bold; margin-top: 4px; transition: color 0.3s ease;">Checking...</div>
        </div>
    </div>

    <div style="border-top: 1px solid #1e293b; padding-top: 15px;">
        <div style="margin-bottom: 10px; color: #94a3b8; font-size: 0.7rem; font-weight: bold; text-transform: uppercase;">7-DAY FORECAST</div>
        <div id="sd-forecast-list"></div>
    </div>
</div>

<script>
(function() {
    const deg = String.fromCharCode(176);
    const bigIcons = {
        sun: '<svg width="140" height="140" viewBox="0 0 24 24" fill="#fbbf24"><circle cx="12" cy="12" r="5"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>',
        cloud: '<svg width="140" height="140" viewBox="0 0 24 24" fill="#94a3b8"><path d="M18 10h-1.26A7 7 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg>'
    };
    const smallIcons = {
        sun: '<svg width="24" height="24" viewBox="0 0 24 24" fill="#fbbf24"><circle cx="12" cy="12" r="5"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>',
        cloud: '<svg width="24" height="24" viewBox="0 0 24 24" fill="#94a3b8"><path d="M18 10h-1.26A7 7 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg>'
    };

    async function updateWidget(lat, lon, name) {
        document.getElementById('sd-location-name').innerText = name;
        const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,weather_code,surface_pressure&daily=weather_code,temperature_2m_max,temperature_2m_min,wind_speed_10m_max,precipitation_sum&wind_speed_unit=mph&timezone=auto`;
        
        try {
            const res = await fetch(url);
            const data = await res.json();
            
            document.getElementById('sd-current-temp').innerText = Math.round(data.current.temperature_2m) + deg + "C";
            document.getElementById('sd-current-icon').innerHTML = data.current.weather_code < 3 ? bigIcons.sun : bigIcons.cloud;
            document.getElementById('sd-today-summary').innerText = `Conditions are ${data.current.weather_code < 3 ? 'clear' : 'cloudy'}. Expect a high of ${Math.round(data.daily.temperature_2m_max[0])}${deg}C and low of ${Math.round(data.daily.temperature_2m_min[0])}${deg}C. Wind gusts up to ${Math.round(data.daily.wind_speed_10m_max[0])} mph, with ${data.daily.precipitation_sum[0]}mm of rain.`;
            
            const warn = document.getElementById('sd-warning-box');
            warn.style.display = data.daily.wind_speed_10m_max[0] > 40 ? "block" : "none";
            warn.innerText = "High Wind Warning: Exercise Caution";
            
            document.getElementById('sd-pressure-val').innerText = Math.round(data.current.surface_pressure) + " hPa";
            
            const list = document.getElementById('sd-forecast-list');
            list.innerHTML = "";
            data.daily.time.forEach((t, i) => {
                const row = document.createElement('div');
                row.style = "display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid #1e293b;";
                
                let dayLabel = new Date(t).toLocaleDateString('en-GB', {weekday:'short'});
                if (i === 0) dayLabel = "Today";
                if (i === 1) dayLabel = "Tmw";

                row.innerHTML = `<div style="width: 55px; font-size: 0.9rem;">${dayLabel}</div><div style="flex:1;">${data.daily.weather_code[i] < 3 ? smallIcons.sun : smallIcons.cloud}</div><div style="font-weight:bold; width: 35px; text-align: right;">${Math.round(data.daily.temperature_2m_max[i])}${deg}</div><div style="color:#94a3b8; width: 35px; text-align: right; margin-left: 10px;">${Math.round(data.daily.temperature_2m_min[i])}${deg}</div>`;
                list.appendChild(row);
            });
        } catch(err) { console.error(err); }

        fetch("https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json")
            .then(r => r.json())
            .then(d => {
                const latest = d.filter(item => Array.isArray(item)).pop();
                const auroraEl = document.getElementById('sd-aurora-text');
                
                if (latest && latest[1]) {
                    const kClean = Math.round(parseFloat(latest[1]) * 10) / 10;
                    const absLat = Math.abs(lat);
                    
                    let requiredKp = 9;
                    if (absLat >= 65) requiredKp = 1;
                    else if (absLat >= 60) requiredKp = 2;
                    else if (absLat >= 55) requiredKp = 3;
                    else if (absLat >= 50) requiredKp = 4;
                    else if (absLat >= 45) requiredKp = 5;
                    else if (absLat >= 40) requiredKp = 7;

                    if (kClean >= requiredKp) {
                        auroraEl.innerText = `Visible (Kp ${kClean})`;
                        auroraEl.style.color = "#fbbf24"; 
                    } else if (kClean >= requiredKp - 1.5 && requiredKp <= 7) {
                        auroraEl.innerText = `Possible (Kp ${kClean})`;
                        auroraEl.style.color = "#93c5fd"; 
                    } else {
                        auroraEl.innerText = `Quiet (Kp ${kClean})`;
                        auroraEl.style.color = "#ffffff";
                    }
                } else {
                    auroraEl.innerText = "Quiet";
                    auroraEl.style.color = "#ffffff";
                }
            })
            .catch(() => {
                const auroraEl = document.getElementById('sd-aurora-text');
                auroraEl.innerText = "Quiet";
                auroraEl.style.color = "#ffffff";
            });

        const p = ((new Date() - new Date(2000,0,6)) / 86400000) % 29.53;
        const phases = ["New Moon", "Waxing", "First Qtr", "Waxing Gibbous", "Full Moon", "Waning Gibbous", "Last Qtr", "Waning"];
        document.getElementById('sd-moon-text').innerText = phases[Math.floor(p/4)];
    }

    document.getElementById('sd-search-btn').onclick = async () => {
        const input = document.getElementById('sd-loc-input');
        const btn = document.getElementById('sd-search-btn');
        const locName = document.getElementById('sd-location-name');
        const q = input.value;
        
        if(!q) return;

        btn.innerText = "Searching...";
        btn.disabled = true;

        try {
            const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(q)}&count=1`);
            const d = await res.json();
            
            if(d.results && d.results.length > 0) {
                updateWidget(d.results[0].latitude, d.results[0].longitude, d.results[0].name);
            } else {
                locName.innerText = "Location not found";
            }
        } catch(err) {
            locName.innerText = "Error searching";
        } finally {
            btn.innerText = "Search";
            btn.disabled = false;
        }
    };

    fetch("https://ipapi.co/json/").then(r=>r.json()).then(d=>updateWidget(d.latitude, d.longitude, d.city)).catch(()=>updateWidget(54.5497, -3.5847, "Whitehaven"));
})();
</script>

Post a Comment

0 Comments

Notice:
Comments are moderated and may not appear immediately. Please keep your comments respectful, and relevant to the post. Spam will not be tolerated. My site. My rules.

Post a Comment (0)