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,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";
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}¤t_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");
283Submitted on 7/11/2026