Weather Box
Widget to track weather across cities

Code Snippet

1let view = "dashboard";
2let savedCities = await storage.get("weather_cities_v3") || [
3    { name: "London", lat: 51.5085, lon: -0.1257, state: "England", country: "United Kingdom" },
4    { name: "New York", lat: 40.7143, lon: -74.006, state: "New York", country: "United States" }
5];
6
7let searchResults = [];
8let isSearching = false;
9let currentQuery = "";
10let searchTimeout = null;
11let weatherCache = {};
12
13// Highly accurate, Day/Night aware Emoji mapping!
14function getWeatherEmoji(code, isDay = 1) {
15    const clear = isDay ? "☀️" : "🌙";
16    
17    if (code === 0) return clear; // Clear sky
18    if (code === 1) return isDay ? "🌤️" : "☁️"; // Mainly clear
19    if (code === 2) return isDay ? "⛅" : "☁️"; // Partly cloudy
20    if (code === 3) return "☁️"; // Overcast
21    
22    if (code >= 45 && code <= 48) return "🌫️"; // Fog
23    
24    if (code === 51 || code === 53 || code === 55) return "🌧️"; // Drizzle
25    if (code === 56 || code === 57) return "🌨️"; // Freezing Drizzle
26    
27    if (code === 61 || code === 63 || code === 65) return "🌧️"; // Rain
28    if (code === 66 || code === 67) return "🌨️"; // Freezing Rain
29    
30    if (code === 71 || code === 73 || code === 75) return "❄️"; // Snow fall
31    if (code === 77) return "❄️"; // Snow grains
32    
33    if (code === 80 || code === 81 || code === 82) return isDay ? "🌦️" : "🌧️"; // Rain showers
34    if (code === 85 || code === 86) return "🌨️"; // Snow showers
35    
36    if (code >= 95 && code <= 99) return "⛈️"; // Thunderstorm
37    
38    return "🌡️";
39}
40
41async function renderDashboard(isUpdate = false) {
42    const cityCards = savedCities.map((city, index) => {
43        const cached = weatherCache[city.name];
44        const temp = cached ? `${cached.temp}°C` : "-";
45        const emoji = cached ? cached.emoji : "-";
46        
47        return {
48            type: "row",
49            bg: "card2",
50            rounded: true,
51            padding: "md",
52            align: "center",
53            justify: "between", 
54            components: [
55                {
56                    type: "column",
57                    gap: "sm",
58                    align: "start",
59                    components: [
60                        { type: "row", gap: "md", padding: "sm", align: "center", justify: "start", components: [
61                            { type: "text", text: emoji, size: "md" },
62                            { type: "text", text: city.name, weight: "bold", size: "md" },
63                        ]},
64                        
65                    ]
66                },
67                {
68                    type: "row",
69                    gap: "md",
70                    align: "center",
71                    justify: "end",
72                    padding: "sm",
73                    components: [
74                        { type: "text", text: temp, weight: "bold", size: "md" },
75                        { type: "button", id: `del_${index}`, icon: "X", text: "", variant: "ghost", size: "md" }
76                    ]
77                }
78            ]
79        };
80    });
81
82
83    const logoBase64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKKklEQVR4nO1caZBU1RX+YNgXRRiMAokVSYBKZWIQCIioRIwbYS1N1AoVUsZoitKAVDABk7BJuSWKG0wGsArFCMkYsAxuMTNhYN6bcTQkoZJUKcNiVRJWw2ICZvlSx9cwM0y/7vu63+v7ut+5Vd8vpm+f+52vzz333HMBQh41RCeHGO0Qc12iyiG2OESzSxx2iA9dggr4cuAQJ1NcNQt3KQ7nCqfCLeI4dhBdGohpDvGCSxxVByMSkTvEEZeodompwrltv2ML0d8lljrEQXU6ChrZUpwvaSLKC+74GqKXS9zvEMfV8bC6pYkPHGLZdqJnQZwv4cch9qrjEatcxiH2uMSUyBxfQ3RzieW2F6pANiGsbSJ6hOp8lxjkENuVfBSLAN/eSgwIxflbicEu8W4MFqVgIA52NxJD83L+NmKYS+xX8lGs4tvXQAzJyfn1xECX2BWDRSiYOwcO8V4j8fHACZ/u+Sgl4b21mehqLACXqIyB0QqGx4FDPGHkfCnpKvkoSfHVE5MyOl/Oj7rvo6SLRRkrhlLetW2kAlFzsNQv9PdziWPqAJT83UHaCySHuM+2cQoUSgSL2jhf7pb1SheJEaBDHGjTT+AQ020bpUChRTC5tQCkk0edwORw4BAbWvfwHYmDUQoUUgDvbyDK5Og3RolHIsXXQIxEqnvXujEK2OBgjghglZKPpAqwUraAuhgYoqAVDmpFAHrnz2QKUB6fiAAO2TZEAVsCOIDUUyR1AhPJwQnEwAgF7XGgAmCyBagCoH0nqABiQISbUGgEoH0nqABiQISbUGgEoH0nqABiQISbUGgEoH0nqABiQISbUGgEoH0nqABiQISbUGgEoH0nqABiQISbUGgEoH0nqABiQISbUGgEoH0nqABiQISbUGgEoH0nqABiQISbUGgEoH0nlKwAvnIXOPBCM6z9nX0y3AQiUgF86SYQMMMqxz4ZbgKhAqB9J6gANAJQI4BuAdQtIAYh0U0YNAegfSeoADQHoEaAiHKAmmPeEXPJz8B7VoKz7gfnV4EP/BJcuQX8zdHoyX/jCFi1DVy0Dpy3Apz1ADh/FfjgRvCnW8Ga4yUggE17wWEj2uKsvuZ1gAuGtf/8KSx8Jpgt1e+C31wIDhkOdizL/L1lncCKS7y/3/z38Ihd/xfwG/eCgyvAjh0z29CpM/j5y8A77gNfPVikAvjFO+bODoo7HzInffy07IT7oVsP8KbZ+Tnh2d+DY68HO3TIzYYevcEZ94C//ocKwFgA9f8Bb1/i/ZLCEFz/gcErk1s/BL/+/ewRxxQDPgk+s10jQFYB1H4Ajvty+FGne09zEcgeP/LK8G3odbYXUXQL8BFA3Qlw1ITotp5zB4Gv7M/sABGg5BBR2TDoU9FuB0WdA0y+NbrvRAoSXTKte8KN0dtwzS0qgHbEyxEqauKRwprG9A740drCfL8ktev/rBGgTeiXJCloKL1sMnjJdeA55wb77Pjp6esL5ecHm0eOupdPAcdcA57dL9hnJ86MuQAKWQf4wdPm8/buAz78YvtTw3efND81yN+dWay5e7m5DX36g4+/3v7UIFub6amhey9w279jLIBCNoR85gvm8z6y2X+e2Y+Yz/PYa20/K4I1+ZzUAyrr/G24bZG5DatdFQA37jYvslSMzUxY3QnzKDVzQcvn1v3B3Gljrs1sg5SipQBlMpeUkBMfAYKE/1t/mJ2wSyeazXX1zbmF/7sezm6DlIFN5pp2uwqAN8wyJ18ufap3ZsZVXzWba/TVLeRfN8PcBrmEymaDqQivvEEF8FEGbUp+mBg2ooX8KAs/mTDiiyoAfvoiO+QPrmghP+gRNCx87lIVwEfneRvkV7RKKIPWEcJCtoQyEcfATwyxQ/7Y61ts6HeeHRuEz8QLQJo8bJA//Y4W8gcOtmPDjHkqgEBXv527gr3PCQdzHm0hf/gV5jZ06RaeDQtWqwB4y1xz8odeHM3WNuU2cxsuHh/dFhv7HCDIefnMUqsflv08WNh89OXw17Vgtfn3S9VSGj8TKYCb54S/v0m/XpDWL2nq2Lgn3HVt2hus71BOLr/6WwIFIF2upiR17d7+xswP4yYFiwIiAtO53RTk5m1Ng/+/B8kDBPIEPtOlUDrUnQSffrOIBSA3cUFIkl+V3NmLcOQKWK5s5R78hea28z5VE2zeU5AKntTmxRHyi6z9wLuWff2w11Es9sqFyxVTvaRLwrdcPqVb20ObcrNh+OVeQllV77Wh//afnqPFhuf/BP74JW/9wkPPs7y29VcOFKkAhGDJxHMhqjXkMYWtkvB3fpJ+bc7/vOJQIWyQRyRFKQCBXGDkS4D0+p85r1yiSNeszfLrczvMr3LzgXQxFa0A1v0x94capyCOTtcNI50+YfXhwwdi+0t/9V/f4udyfwRiis5dousMLsh/EhXkNOAHeVuXbm55byf7ZJQOmPdU5vV9rzJ/kWdD0OdxsRKA9ODl+3hD3u75zb+iFuz7sejIHzUh+xolgQza6JnvNlg0AhBIpivv7nINl9nau1475FXooogGZYaZ+Mv7vOJXFNFAco0oXhEX/P8JfOKN3F7ziBNM9kF5GXzjncE6kuEDOQrKDdyiZ4ORL8c5ad8KI0ntUw5e+zVw6fPgln+VgABaJ4dyJpczt9yuyZm3dQv0eReAnx0DTv82eO8aL+Ou/2+wiCNimznfa+cqH+AffTp08B6EXjTOe20ke7o8zJStK581isOWv+pVOUdd5V0j+9kgUUMKVlInmPot7+gnHAVZc1EJIB0k0xfHRTW/FH1efM87Qkpnb/VOL7RH8cvKJEwpJbe2Qcrb0qFsg/NYCUABFYCKABoBVATQLUBFAM0BVATQJFBFAD0FqAigx0AVAbQOoCJA+IUghzipxCKp4johlcBDMTBEwcJz4BAHRAC71AFIpAAdolkEUGfbEAVscVArAqhSByCRInSIlZIE3m3bEAVscTBbBDBaHYCkinAEaohODnEkBsYoWDgOHOL9DUQZZLhEtToASRPgepwaDcS0GBikYOE4qCcmnRbADqKLQxxUJyApItzfRHQ+LYDUNrAkBoYpGD0HDrGwjfNT20A/lzimTkBJi9AhjjcR5Ug3HGKZbQMViJqDxfAbTUQPvRtAyYrQIfZsJ3r6CiCVC0yxbagCUXEwMaPzW20FK9QJKDUhPgbTUUN0c4m3Y2C0gvlz4BBvbia6IsjYSgzQfAAlceffRJyPXEYjMdQl9tlehAK5crCvgRiSk/NbieBCl3hHnYBiE+Iu+QEjjJHaDt6KwaIUNNvzcw77fkOSCJdYrk5A3EVYGTjhCzLkFskldsdgoQq2+dU3G5/z8x2piuFSvTtAHER4VMq79UR3FHqkLpAWS395DIhIGvbLrV490Re2R6qfYLJDbJBWoxiQU6o4LJ08sg23u8+Py5A+s0ZilEvMkYREes8dYqe8QNJnaNmdnOJIuBLOalIczm4gRp7u4Qtx/B8Ap+oZKzN8owAAAABJRU5ErkJggg==";
84
85    const componentTree = [
86        {
87            type: "column",
88            gap: "sm",
89            bg: "card",
90            padding: "md",
91            rounded: true,
92            components: [
93                { type: "row", justify: "between", align: "center", components: [
94                    { type: "image", src: logoBase64, width: "24px", height: "24px", rounded: true },
95                    { type: "text", text: "Weatherbox", size: "md", weight: "bold" }
96                ]},
97                { type: "divider" },
98                {
99                    type: "column",
100                    gap: "sm",
101                    maxHeight: "250px",
102                    scroll: true, 
103                    components: cityCards.length > 0 ? cityCards : [{ type: "text", text: "No cities added.", color: "muted", size: "sm", align: "center" }]
104                },
105                { type: "button", id: "open_search", icon: "Plus", text: "New City", variant: "primary", size: "md" }
106            ]
107        }
108    ];
109
110    if (isUpdate) {
111        await widget.update(componentTree);
112    } else {
113        await widget.show(componentTree, "bottom-right");
114    }
115
116    await widget.on("open_search", "click", async () => {
117        view = "search";
118        searchResults = [];
119        currentQuery = "";
120        await renderSearch(false);
121    });
122
123    for (let i = 0; i < savedCities.length; i++) {
124        await widget.on(`del_${i}`, "click", async () => {
125            savedCities.splice(i, 1);
126            await storage.set("weather_cities_v3", savedCities);
127            await renderDashboard(true);
128        });
129    }
130
131    const needsFetch = savedCities.some(c => !weatherCache[c.name]);
132    if (needsFetch) {
133        await Promise.all(savedCities.map(async (city) => {
134            if (weatherCache[city.name]) return;
135            try {
136                const data = await http.fetch(`https://api.open-meteo.com/v1/forecast?latitude=${city.lat}&longitude=${city.lon}&current_weather=true`);
137                if (!data.error) {
138                    const current = data.current_weather;
139                    weatherCache[city.name] = {
140                        temp: Math.round(current.temperature),
141                        // Pass the is_day parameter into the emoji generator!
142                        emoji: getWeatherEmoji(current.weathercode, current.is_day)
143                    };
144                } else {
145                    weatherCache[city.name] = { temp: "?", emoji: "❌" };
146                }
147            } catch(e) {
148                weatherCache[city.name] = { temp: "?", emoji: "❌" };
149            }
150        }));
151
152        if (view === "dashboard") {
153            await renderDashboard(true);
154        }
155    }
156}
157
158async function renderSearch(isUpdate = true) {
159    let resultCards = [];
160    
161    if (isSearching) {
162        resultCards = [
163            { type: "row", justify: "center", gap: "sm", padding: "md", components: [
164                { type: "spinner" }, 
165                { type: "text", text: "Searching...", color: "muted", size: "sm" }
166            ]}
167        ];
168    } else if (searchResults.length > 0) {
169        resultCards = searchResults.map((r, index) => ({
170            type: "row",
171            bg: "card2",
172            rounded: true,
173            padding: "md",
174            align: "center",
175            justify: "between",
176            components: [
177                {
178                    type: "column",
179                    gap: "sm",
180                    align: "start",
181                    components: [
182                        { type: "text", text: r.name, weight: "bold", size: "sm" },
183                        { type: "text", text: `${r.admin1 ? r.admin1 + ', ' : ''}${r.country}`, color: "muted", size: "xs" }
184                    ]
185                },
186                { type: "button", id: `add_res_${index}`, icon: "Plus", variant: "outline", size: "sm" }
187            ]
188        }));
189    } else if (currentQuery.length > 0) {
190        resultCards = [{ type: "text", text: "No results found.", color: "muted", size: "sm", align: "center" }];
191    }
192
193    const componentTree = [
194        {
195            type: "column",
196            gap: "sm",
197            bg: "card",
198            padding: "md",
199            rounded: true,
200            components: [
201                {
202                    type: "row",
203                    align: "center",
204                    justify: "between",
205                    components: [
206                        { type: "button", id: "go_back", icon: "ArrowLeft", text: "", variant: "ghost", size: "sm" },
207                        { type: "text", text: "Search Cities", size: "md", weight: "bold" }
208                    ]
209                },
210                { type: "divider" },
211                {
212                    type: "input", id: "search_input", placeholder: "Start typing (e.g. Paris)...", value: currentQuery, size: "sm" 
213                },
214                {
215                    type: "column",
216                    gap: "sm",
217                    maxHeight: "200px",
218                    scroll: true,
219                    components: resultCards
220                }
221            ]
222        }
223    ];
224
225    if (isUpdate) {
226        await widget.update(componentTree);
227    } else {
228        await widget.show(componentTree, "bottom-right");
229    }
230
231    await widget.on("go_back", "click", async () => {
232        view = "dashboard";
233        await renderDashboard(true);
234    });
235
236    await widget.on("search_input", "change", async (val) => {
237        currentQuery = val;
238        
239        if (searchTimeout) clearTimeout(searchTimeout);
240        
241        if (currentQuery.trim().length === 0) {
242            searchResults = [];
243            isSearching = false;
244            await renderSearch(true);
245            return;
246        }
247
248        isSearching = true;
249        await renderSearch(true); 
250
251        searchTimeout = setTimeout(async () => {
252            try {
253                const data = await http.fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(currentQuery)}&count=5&language=en&format=json`);
254                searchResults = data.results || [];
255            } catch(err) {
256                searchResults = [];
257            }
258            
259            isSearching = false;
260            await renderSearch(true);
261        }, 400); 
262    });
263
264    for (let i = 0; i < searchResults.length; i++) {
265        await widget.on(`add_res_${i}`, "click", async () => {
266            const cityToAdd = searchResults[i];
267            savedCities.push({
268                name: cityToAdd.name,
269                lat: cityToAdd.latitude,
270                lon: cityToAdd.longitude,
271                state: cityToAdd.admin1 || "",
272                country: cityToAdd.country || "Unknown"
273            });
274            await storage.set("weather_cities_v3", savedCities);
275            view = "dashboard";
276            await renderDashboard(true);
277        });
278    }
279}
280
281await renderDashboard();
282await widget.waitForEvent("__widget_closed__", "close");
283
Submitted on 7/11/2026