| 1 |
document.addEventListener('DOMContentLoaded', function () { |
| 2 |
document.querySelectorAll('.bkbg-ww-app').forEach(function (root) { |
| 3 |
var opts = JSON.parse(root.dataset.opts || '{}'); |
| 4 |
new WeatherWidget(root, opts); |
| 5 |
}); |
| 6 |
}); |
| 7 |
|
| 8 |
function WeatherWidget(root, opts) { |
| 9 |
var self = this; |
| 10 |
self.root = root; |
| 11 |
self.opts = opts; |
| 12 |
self.apiKey = opts.apiKey || ''; |
| 13 |
self.city = opts.city || 'London'; |
| 14 |
self.units = opts.units || 'metric'; |
| 15 |
self.lang = opts.lang || 'en'; |
| 16 |
self.refreshMs = (opts.refreshInterval || 0) * 60 * 1000; |
| 17 |
self.timer = null; |
| 18 |
self.lat = null; |
| 19 |
self.lon = null; |
| 20 |
self.current = null; |
| 21 |
|
| 22 |
if (!self.apiKey) { |
| 23 |
root.innerHTML = '<div class="bkbg-ww-loading">⚠️ No OpenWeatherMap API key set. Add it in the block settings.</div>'; |
| 24 |
return; |
| 25 |
} |
| 26 |
|
| 27 |
if (opts.autoLocation && navigator.geolocation) { |
| 28 |
navigator.geolocation.getCurrentPosition( |
| 29 |
function (pos) { |
| 30 |
self.lat = pos.coords.latitude; |
| 31 |
self.lon = pos.coords.longitude; |
| 32 |
self.fetchWeather(); |
| 33 |
}, |
| 34 |
function () { self.fetchWeather(); } |
| 35 |
); |
| 36 |
} else { |
| 37 |
self.fetchWeather(); |
| 38 |
} |
| 39 |
} |
| 40 |
|
| 41 |
WeatherWidget.prototype.fetchWeather = function () { |
| 42 |
var self = this; |
| 43 |
var base = 'https://api.openweathermap.org/data/2.5/'; |
| 44 |
var unitStr = '&units=' + self.units + '&lang=' + self.lang; |
| 45 |
var locStr = self.lat ? ('lat=' + self.lat + '&lon=' + self.lon) : ('q=' + encodeURIComponent(self.city)); |
| 46 |
var currentUrl = base + 'weather?' + locStr + unitStr + '&appid=' + self.apiKey; |
| 47 |
var forecastUrl = base + 'forecast?' + locStr + unitStr + '&cnt=40&appid=' + self.apiKey; |
| 48 |
|
| 49 |
self.root.innerHTML = '<div class="bkbg-ww-loading">🌐 Loading weather…</div>'; |
| 50 |
|
| 51 |
Promise.all([ |
| 52 |
fetch(currentUrl).then(function (r) { return r.json(); }), |
| 53 |
self.opts.showForecast ? fetch(forecastUrl).then(function (r) { return r.json(); }) : Promise.resolve(null) |
| 54 |
]).then(function (data) { |
| 55 |
var current = data[0]; |
| 56 |
var forecast = data[1]; |
| 57 |
if (current.cod && current.cod !== 200) { |
| 58 |
self.root.innerHTML = '<div class="bkbg-ww-error">❌ ' + (current.message || 'API error') + '</div>'; |
| 59 |
return; |
| 60 |
} |
| 61 |
self.current = current; |
| 62 |
self.render(current, forecast); |
| 63 |
if (self.refreshMs > 0) { |
| 64 |
clearInterval(self.timer); |
| 65 |
self.timer = setInterval(function () { self.fetchWeather(); }, self.refreshMs); |
| 66 |
} |
| 67 |
}).catch(function (err) { |
| 68 |
self.root.innerHTML = '<div class="bkbg-ww-error">❌ Failed to load weather data. Check your API key and city name.</div>'; |
| 69 |
}); |
| 70 |
}; |
| 71 |
|
| 72 |
WeatherWidget.prototype.render = function (current, forecast) { |
| 73 |
var self = this; |
| 74 |
var opts = self.opts; |
| 75 |
var a = opts; |
| 76 |
|
| 77 |
var deg = a.units === 'imperial' ? '°F' : a.units === 'standard' ? 'K' : '°C'; |
| 78 |
var temp = Math.round(current.main.temp); |
| 79 |
var feelsLike = Math.round(current.main.feels_like); |
| 80 |
var tempMax = Math.round(current.main.temp_max); |
| 81 |
var tempMin = Math.round(current.main.temp_min); |
| 82 |
var humidity = current.main.humidity; |
| 83 |
var windSpeed = Math.round(current.wind.speed * (a.units === 'metric' ? 3.6 : 1)); |
| 84 |
var windUnit = a.units === 'metric' ? 'km/h' : a.units === 'imperial' ? 'mph' : 'm/s'; |
| 85 |
var pressure = current.main.pressure; |
| 86 |
var visibility = current.visibility ? Math.round(current.visibility / 1000) + ' km' : '--'; |
| 87 |
var condition = current.weather[0].description; |
| 88 |
var iconCode = current.weather[0].icon; |
| 89 |
var cityName = current.name + (current.sys && current.sys.country ? ', ' + current.sys.country : ''); |
| 90 |
var isNight = iconCode.endsWith('n'); |
| 91 |
|
| 92 |
/* determine bg class */ |
| 93 |
var mainId = current.weather[0].id; |
| 94 |
var bgClass; |
| 95 |
if (a.backgroundStyle === 'custom') { |
| 96 |
bgClass = 'bkbg-ww-bg-custom'; |
| 97 |
} else if (a.backgroundStyle === 'transparent') { |
| 98 |
bgClass = ''; |
| 99 |
} else { |
| 100 |
if (isNight) bgClass = 'bkbg-ww-bg-night'; |
| 101 |
else if (mainId >= 200 && mainId < 300) bgClass = 'bkbg-ww-bg-thunderstorm'; |
| 102 |
else if (mainId >= 300 && mainId < 600) bgClass = 'bkbg-ww-bg-rain'; |
| 103 |
else if (mainId >= 600 && mainId < 700) bgClass = 'bkbg-ww-bg-snow'; |
| 104 |
else if (mainId >= 700 && mainId < 800) bgClass = 'bkbg-ww-bg-mist'; |
| 105 |
else if (mainId === 800) bgClass = 'bkbg-ww-bg-clear'; |
| 106 |
else bgClass = 'bkbg-ww-bg-clouds'; |
| 107 |
} |
| 108 |
|
| 109 |
var weatherEmoji = iconToEmoji(iconCode); |
| 110 |
|
| 111 |
/* CSS vars */ |
| 112 |
var wrapStyle = 'color:' + a.textColor + ';padding:' + a.padding + 'px;border-radius:' + a.borderRadius + 'px;'; |
| 113 |
if (a.backgroundStyle === 'custom') wrapStyle += '--ww-bg:' + a.customBg + ';'; |
| 114 |
if (a.layout === 'card') wrapStyle += 'max-width:' + a.maxWidth + 'px;margin:0 auto;'; |
| 115 |
|
| 116 |
var cardStyle = 'background:' + a.cardBg + ';backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-radius:' + a.cardRadius + 'px;padding:24px;border:1px solid rgba(255,255,255,0.2);'; |
| 117 |
|
| 118 |
/* forecast daily groups (pick one item per day ~noon) */ |
| 119 |
var forecastHTML = ''; |
| 120 |
if (a.showForecast && forecast && forecast.list) { |
| 121 |
var days = []; |
| 122 |
var seen = {}; |
| 123 |
forecast.list.forEach(function (item) { |
| 124 |
var d = new Date(item.dt * 1000); |
| 125 |
var dayKey = d.toLocaleDateString('en', { weekday: 'short' }); |
| 126 |
var dayDate = d.toDateString(); |
| 127 |
if (!seen[dayDate] && days.length < a.forecastDays) { |
| 128 |
seen[dayDate] = true; |
| 129 |
days.push({ |
| 130 |
day: dayKey, |
| 131 |
emoji: iconToEmoji(item.weather[0].icon), |
| 132 |
hi: Math.round(item.main.temp_max), |
| 133 |
lo: Math.round(item.main.temp_min), |
| 134 |
}); |
| 135 |
} |
| 136 |
}); |
| 137 |
var fcDays = days.map(function (f) { |
| 138 |
return '<div class="bkbg-ww-fc-day" style="background:' + a.forecastBg + ';border-radius:' + a.cardRadius + 'px;">' + |
| 139 |
'<div class="bkbg-ww-fc-name">' + f.day + '</div>' + |
| 140 |
'<div class="bkbg-ww-fc-icon">' + f.emoji + '</div>' + |
| 141 |
'<div class="bkbg-ww-fc-hi">' + f.hi + deg + '</div>' + |
| 142 |
'<div class="bkbg-ww-fc-lo">' + f.lo + deg + '</div>' + |
| 143 |
'</div>'; |
| 144 |
}).join(''); |
| 145 |
forecastHTML = '<div class="bkbg-ww-forecast-label">5-Day Forecast</div>' + |
| 146 |
'<div class="bkbg-ww-forecast">' + fcDays + '</div>'; |
| 147 |
} |
| 148 |
|
| 149 |
var statsHTML = ''; |
| 150 |
if (a.showHumidity || a.showWind || a.showPressure || a.showVisibility) { |
| 151 |
var stats = []; |
| 152 |
if (a.showHumidity) stats.push({ icon: '💧', val: humidity + '%', lbl: 'Humidity' }); |
| 153 |
if (a.showWind) stats.push({ icon: '💨', val: windSpeed + ' ' + windUnit, lbl: 'Wind' }); |
| 154 |
if (a.showPressure) stats.push({ icon: '🌡', val: pressure + ' hPa', lbl: 'Pressure' }); |
| 155 |
if (a.showVisibility) stats.push({ icon: '👁', val: visibility, lbl: 'Visibility' }); |
| 156 |
statsHTML = '<div class="bkbg-ww-stats" style="margin-bottom:' + (a.showForecast ? '24' : '0') + 'px;">' + |
| 157 |
stats.map(function (s) { |
| 158 |
return '<div class="bkbg-ww-stat">' + |
| 159 |
'<div class="bkbg-ww-stat-icon">' + s.icon + '</div>' + |
| 160 |
'<div class="bkbg-ww-stat-val">' + s.val + '</div>' + |
| 161 |
'<div class="bkbg-ww-stat-lbl">' + s.lbl + '</div>' + |
| 162 |
'</div>'; |
| 163 |
}).join('') + '</div>'; |
| 164 |
} |
| 165 |
|
| 166 |
var feelsLikeHTML = a.showFeelsLike |
| 167 |
? '<div class="bkbg-ww-feelslike">Feels like ' + feelsLike + deg + ' · H: ' + tempMax + deg + ' L: ' + tempMin + deg + '</div>' |
| 168 |
: ''; |
| 169 |
|
| 170 |
var refreshBtn = '<button class="bkbg-ww-refresh-btn" data-ww-refresh>↻ Refresh</button>'; |
| 171 |
|
| 172 |
var html = '<div class="bkbg-ww-wrap bkbg-ww-' + a.layout + ' ' + bgClass + '" style="' + wrapStyle + '">' + |
| 173 |
'<div class="bkbg-ww-card-inner" style="' + cardStyle + '">' + |
| 174 |
'<div class="bkbg-ww-top">' + |
| 175 |
'<div>' + |
| 176 |
'<div class="bkbg-ww-city-label">📍 ' + cityName + '</div>' + |
| 177 |
'<div class="bkbg-ww-temp">' + temp + deg + '</div>' + |
| 178 |
'<div class="bkbg-ww-condition">' + condition + '</div>' + |
| 179 |
'</div>' + |
| 180 |
(a.showAnimatedIcon ? '<div class="bkbg-ww-icon-lg">' + weatherEmoji + '</div>' : '') + |
| 181 |
'</div>' + |
| 182 |
feelsLikeHTML + |
| 183 |
statsHTML + |
| 184 |
forecastHTML + |
| 185 |
'<div style="text-align:right;margin-top:12px;">' + refreshBtn + '</div>' + |
| 186 |
'</div>' + |
| 187 |
'</div>'; |
| 188 |
|
| 189 |
self.root.innerHTML = html; |
| 190 |
self.root.querySelector('[data-ww-refresh]').addEventListener('click', function () { |
| 191 |
self.fetchWeather(); |
| 192 |
}); |
| 193 |
}; |
| 194 |
|
| 195 |
function iconToEmoji(code) { |
| 196 |
if (!code) return '🌤'; |
| 197 |
var c = String(code); |
| 198 |
if (c.startsWith('01d')) return '☀️'; |
| 199 |
if (c.startsWith('01n')) return '🌙'; |
| 200 |
if (c.startsWith('02')) return '� |
| 201 |
'; |
| 202 |
if (c.startsWith('03') || c.startsWith('04')) return '☁️'; |
| 203 |
if (c.startsWith('09') || c.startsWith('10')) return '🌧'; |
| 204 |
if (c.startsWith('11')) return '⛈'; |
| 205 |
if (c.startsWith('13')) return '❄️'; |
| 206 |
if (c.startsWith('50')) return '🌫'; |
| 207 |
return '🌤'; |
| 208 |
} |
| 209 |
|