Changeset 3371900
- Timestamp:
- 10/02/2025 03:55:46 PM (5 months ago)
- Location:
- under-the-weather
- Files:
-
- 12 added
- 25 edited
- 1 copied
-
assets/screenshot-5.png (modified) (previous)
-
assets/screenshot-8.png (added)
-
assets/screenshot-9.png (added)
-
tags/2.2 (copied) (copied from under-the-weather/trunk)
-
tags/2.2/README.md (modified) (11 diffs)
-
tags/2.2/assets/screenshot-5.png (modified) (previous)
-
tags/2.2/assets/screenshot-8.png (added)
-
tags/2.2/assets/screenshot-9.png (added)
-
tags/2.2/build/block.json (modified) (1 diff)
-
tags/2.2/css/admin-styles.css (modified) (3 diffs)
-
tags/2.2/css/admin-styles.min.css (modified) (1 diff)
-
tags/2.2/css/under-the-weather.css (modified) (9 diffs)
-
tags/2.2/css/under-the-weather.min.css (modified) (1 diff)
-
tags/2.2/images/seths--weather-images-sunrise.png (added)
-
tags/2.2/images/seths--weather-images-sunset.png (added)
-
tags/2.2/images/seths--weather-images-warning.png (added)
-
tags/2.2/js/admin-geocoder.js (modified) (9 diffs)
-
tags/2.2/js/under-the-weather.js (modified) (8 diffs)
-
tags/2.2/js/under-the-weather.min.js (modified) (1 diff)
-
tags/2.2/readme.txt (modified) (11 diffs)
-
tags/2.2/under-the-weather.php (modified) (16 diffs)
-
trunk/README.md (modified) (11 diffs)
-
trunk/assets/screenshot-5.png (modified) (previous)
-
trunk/assets/screenshot-8.png (added)
-
trunk/assets/screenshot-9.png (added)
-
trunk/build/block.json (modified) (1 diff)
-
trunk/css/admin-styles.css (modified) (3 diffs)
-
trunk/css/admin-styles.min.css (modified) (1 diff)
-
trunk/css/under-the-weather.css (modified) (9 diffs)
-
trunk/css/under-the-weather.min.css (modified) (1 diff)
-
trunk/images/seths--weather-images-sunrise.png (added)
-
trunk/images/seths--weather-images-sunset.png (added)
-
trunk/images/seths--weather-images-warning.png (added)
-
trunk/js/admin-geocoder.js (modified) (9 diffs)
-
trunk/js/under-the-weather.js (modified) (8 diffs)
-
trunk/js/under-the-weather.min.js (modified) (1 diff)
-
trunk/readme.txt (modified) (11 diffs)
-
trunk/under-the-weather.php (modified) (16 diffs)
Legend:
- Unmodified
- Added
- Removed
-
under-the-weather/tags/2.2/README.md
r3369072 r3371900 8 8 * **Requires at least:** 5.0 9 9 * **Tested up to:** 6.8 10 * **Stable tag:** 2. 110 * **Stable tag:** 2.2 11 11 * **Requires PHP:** 7.2 12 12 * **License:** GPLv2 or later … … 31 31 * **Server-Side Caching:** All API calls are cached on your server, dramatically reducing calls to the OpenWeather API and speeding up page loads for all users. 32 32 * **Visual Performance Report:** Monitor your site's API usage with a bar chart that displays a 7-day history of cached requests versus new calls to the OpenWeather API - a clear look at how the caching system is working to keep your site fast and your API calls low. 33 * **Highly Customizable:** Use the detailed settings page to control everything from cache duration to the number of forecast days. 34 * **Flexible Display:** Show either the current live weather or the high/low forecast for the current day. 33 * **Customizable Display:** Use the main display to show either the current live weather or the high/low forecast for the current day, and set the number of days to include in the forecast ahead. 35 34 * **Imperial & Metric Units:** Display weather in Fahrenheit/mph or Celsius/kph on a per-widget basis. 36 35 * **Extra Details:** Optionally display "Feels Like" temperature and detailed wind information. 36 * **Weather Alerts:** Display official severe weather alerts directly in the widget to keep visitors informed. 37 * **Sunrise & Sunset Times:** Optionally show daily sunrise and sunset times, with 12-hour and 24-hour format options. 37 38 * **Lightweight:** Enqueues assets only when needed and does not rely on heavy JavaScript libraries. 38 39 * **Settings Page Coordinate Finder:** An easy-to-use tool on the settings page retrieves coordinates by location name and generates ready-to-use widget `<div>` code. … … 112 113 The plugin's JavaScript will automatically find this element and populate it with the forecast. 113 114 115 ### Using the Shortcode (Classic Editor & Widgets) 116 117 You can also display the weather by using the `[under_the_weather]` shortcode. This is ideal for the Classic Editor, text widgets, or other page builders. 118 119 **Available attributes:** 120 * `lat`: (Required) The latitude for the forecast. 121 * `lon`: (Required) The longitude for the forecast. 122 * `location_name`: (Required) The name to display for the location. 123 * `unit`: (Optional) The unit system. Accepts `metric` or `imperial`. Defaults to `imperial`. 124 125 **Example:** 126 `[under_the_weather lat="48.8566" lon="2.3522" location_name="Paris, France" unit="metric"]` 127 114 128 --- 115 129 … … 121 135 122 136 **Cache Expiration Time:** 123 This setting controls how long the weather data is stored on your server before fetching a new forecast. 137 Use the slider to set the maximum time weather data is stored before fetching a new forecast, from 30 minutes to 8 hours. 138 139 The plugin also features a **smart caching** system that automatically ensures the cache expires after midnight in the location's local timezone. This prevents showing a stale forecast from the previous day, regardless of your slider setting. 140 124 141 For displaying live conditions (using the **Primary Display** or **Extra Details** options), a shorter cache time of 1 or 2 hours is recommended. 125 For displaying only the daily high/low, a longer cache time of 3 or 6hours is effective at reducing API calls.142 For displaying only the daily high/low, a longer cache time of 3 or 8 hours is effective at reducing API calls. 126 143 127 144 **Widget Display & Style** … … 137 154 138 155 **Extra Details:** 139 Selecting this option will **display 'Feels Like' and wind** (direction and speed) information beneath the primary display. This setting adds nuance to the current weather conditions display. 156 Selecting this option will **display 'Feels Like' and wind** (direction and speed) information beneath the primary display. This setting adds nuance to the current weather conditions display. 157 158 **Sunrise & Sunset:** This setting allows you to display the local sunrise and sunset times for the location, which is useful for planning outdoor activities. Choose to show the times in a 12-hour (e.g., 6:30 AM) or 24-hour (e.g., 18:30) format. 159 160 **Weather Alerts:** When enabled, the widget will display any active severe weather alerts (e.g., thunderstorm warnings, flood advisories) issued by official authorities for the specified location. This provides critical, at-a-glance information for your visitors. 161 140 162 141 163 **Display Timestamp:** … … 235 257 No. To retrieve fresh weather data every time a widget page loads, you can uncheck "Enable Cache" under the plugin's advanced settings. The caching system provides a great benefit for reducing API hits, but turning off this function during your initial widget setup may be useful. 236 258 259 ### Will my website ever show yesterday's weather If I set a long cache time? 260 261 Cinderella's magic disappears at midnight, and weather caches expire at midnight too. Visitors should never see a cache of the previous day's forecast. 262 263 For example, if you set the cache expiration time to 8 hours and a weather cache is created at 10 p.m. on a Friday (using the weather location's time), that cache will expire at midnight, and someone visiting the site the next day at 5 a.m. will not see the previous day's cache even though fewer than 8 hours have passed. 264 265 The plugin uses whichever expiration time is **shorter** to provide the most effective caching. You control the maximum cache duration with the "Cache Expiration Time" slider. However, to ensure your visitors never see yesterday's weather, the plugin also calculates the time until midnight in the widget's local timezone. If the time until midnight is shorter than your slider setting, the cache will expire at midnight. 266 237 267 ### The weather isn't updating. Why? 238 268 … … 261 291 ### Can I still use the manual div method if I prefer it? 262 292 263 Absolutely! The traditional method of adding `<div class="weather-widget">` with data attributes still works perfectly. The new block is simply an additional, more user-friendly option for those using the WordPress block editor. 264 265 The `<div>` method is particularly useful for theme developers and sites that dynamically populate widget attributes from post meta or custom fields. 293 Absolutely! While the **block** is the recommended, user-friendly method for the modern WordPress editor, the plugin fully supports traditional methods for maximum flexibility. 294 295 You can use the `[under_the_weather]` shortcode to easily place the widget in the Classic Editor, text widgets, or with various page builders. 296 297 Additionally, the manual `<div>` method still works perfectly. It is particularly useful for theme developers who need to integrate the widget directly into template files or dynamically populate its data from custom fields. 298 299 The traditional method of adding `<div class="weather-widget">` with data attributes still works perfectly and is particularly useful for theme developers and sites that dynamically populate widget attributes from post meta or custom fields. 266 300 267 301 ### What coordinate format should I use? … … 274 308 275 309 If you're unsure what coordinates to use, the **Coordinate Finder** tool is the best way to retrieve accurate coordinates in the correct format. 310 311 ### Where do the weather alerts come from? 312 313 The alerts are provided directly by the OpenWeather API, which sources them from official meteorological agencies in each country. This ensures the information is timely and authoritative. 276 314 277 315 ### What does the "Enable Rate Limiting" setting do? … … 326 364 _The weather widget displaying current conditions with default icons (in Celsius) and extra details enabled._ 327 365 366  367 368 _The weather widget with Weather Alerts shown._ 369 370  371 372 _The weather widget with Sunrise and Sunset times shown._ 373 328 374 --- 329 375 … … 357 403 ## Changelog 358 404 405 ### 2.2 406 * **NEW:** Introduced a `[under_the_weather]` shortcode to allow for easy placement of the weather widget in the Classic Editor, text widgets, and other page builders. 407 * **NEW:** Added a display option to show the day's sunrise time and sunset time, helpful in scheduling outdoor activities. 408 * **NEW:** Added an option to display severe weather alerts from official authorities directly within the widget. This feature can be enabled on the plugin's settings page. 409 * **IMPROVEMENT:** Incorporated clear warning icons for severe weather alerts. 410 * **IMPROVEMENT:** The plugin can now handle multiple weather widgets on a single page 411 * **IMPROVEMENT:** The front-end widget now loads its data asynchronously (AJAX). This improves perceived page load performance and allows multiple widgets on the same page to load their data independently. 412 * **IMPROVEMENT:** The settings page now features a Cache Expiration Time slider that allows greater flexibility and provides users with a visual way to select how long cached weather should be saved. 413 * **NEW:** Midnight expiration is now built into the Cache Expiration logic, so you never have to worry about displaying a cached copy of yesterday's forecast. 414 359 415 ### 2.1 360 416 * **NEW:** The block editor can now parse and automatically convert coordinates from common formats like DMS (Degrees, Minutes, Seconds) and DDM (Degrees, Decimal Minutes) into the required decimal format. … … 457 513 ### 2.0 458 514 This version includes a "Under The Weather Forecast" block for the WordPress block editor. 515 516 ### 2.2 517 This version introduces a new `[under_the_weather]` shortcode for easy widget placement and adds options to display severe weather alerts and daily sunrise/sunset times. -
under-the-weather/tags/2.2/build/block.json
r3369072 r3371900 3 3 "apiVersion": 3, 4 4 "name": "under-the-weather/widget", 5 "version": "2. 1.0",5 "version": "2.2.0", 6 6 "title": "Under The Weather Forecast", 7 7 "category": "widgets", -
under-the-weather/tags/2.2/css/admin-styles.css
r3365104 r3371900 23 23 max-width: 100px; 24 24 } 25 .under-the-weather-visual-reference-default-image{filter:drop-shadow(1px 2px 3px #555);} 26 27 28 29 /* Usage Report Chart Styling */ 25 .under-the-weather-visual-reference-default-image{ 26 filter:drop-shadow(1px 2px 3px #555); 27 } 28 29 /* --- Usage Report Chart Styling --- */ 30 30 31 .under-the-weather-usage-report { 31 32 display: flex; … … 133 134 margin: 15px 0; 134 135 } 136 137 /* --- Coordinate Finder --- */ 138 135 139 .weather-widget-coordinate-finder-pre{ 136 140 background: #f1f1f1; … … 142 146 color: red; 143 147 } 148 .utw-result-wrapper{ 149 margin-top:15px 150 } 151 152 /* --- Slider Control Styling --- */ 153 154 .utw-slider-container { 155 display: flex; 156 align-items: center; 157 margin-bottom: 10px; 158 } 159 160 .utw-slider-wrapper { 161 width: 350px; 162 } 163 164 #utw-expiration-slider { 165 width: 100%; 166 margin: 0; 167 } 168 169 /* Styles for the datalist tick marks */ 170 .utw-slider-wrapper datalist { 171 display: flex; 172 justify-content: space-between; 173 width: 100%; 174 padding: 0; 175 margin-top: 5px; 176 color: #50575e; 177 } 178 179 .utw-slider-value svg{ 180 margin-bottom:-6px 181 } 182 183 /* Saved Message */ 184 .utw-section-save-wrapper { 185 padding: 10px 10px 10px 0; 186 width: 200px; 187 line-height: 1.3; 188 font-weight: 600; 189 display: inline-block; 190 vertical-align: middle; 191 } 192 .utw-unsaved-message { 193 display: none; 194 font-weight: 600; 195 display: none; 196 color: #d63638; 197 } 198 .utw-expiration-save-wrapper { 199 display: flex; 200 align-items: center; 201 } 202 203 #utw-min-cache-notice, 204 #utw-expiration-save-btn-wrap th { 205 display: none; 206 } 207 208 #utw-expiration-save-btn-wrap td { 209 padding-left: 0; 210 } -
under-the-weather/tags/2.2/css/admin-styles.min.css
r3365104 r3371900 1 .nav-tab-wrapper{margin-bottom:20px}.under-the-weather-visual-reference-container{display:flex;gap:50px;align-items:flex-start;background:#fff;border:1px solid #999;border-radius:5px;padding:10px 24px 15px 37px;width:288px}.under-the-weather-visual-reference-item{text-align:center}.under-the-weather-visual-reference-item img{max-width:100px}.under-the-weather-visual-reference-default-image{filter:drop-shadow(1px 2px 3px #555)}.under-the-weather-usage-report{display:flex;flex-direction:column;max-width:800px}.under-the-weather-chart-container{display:flex;justify-content:space-around;align-items:flex-end;height:250px;border:1px solid #c3c4c7;padding:10px 0;gap:10px;background:#f9f9f9 ;}.under-the-weather-chart-day{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;text-align:center;position:relative;height:100%}.under-the-weather-chart-bars{display:flex;justify-content:center;align-items:flex-end;height:100%;gap:5px}.under-the-weather-chart-bar{width:30px;background-color:#9ec2e6;border-radius:3px 3px 0 0;transition:height 0.3s ease-in-out;position:relative}.under-the-weather-chart-bar.api,.under-the-weather-legend-color.api{background-color:#f0ad4e}.under-the-weather-chart-bar.cache,.under-the-weather-legend-color.cache{background-color:#5cb85c}.under-the-weather-chart-bar.value{position:absolute;top:-22px;left:50%;transform:translateX(-50%);font-size:12px;font-weight:bold;color:#333}.under-the-weather-chart-day-label{margin-top:12px;font-size:13px;color:#555}.under-the-weather-chart-legend{display:flex;justify-content:center;gap:20px;background:#f9f9f9;border:1px solid #c3c4c7;padding:8px;}.under-the-weather-legend-item{display:flex;align-items:center;gap:8px;font-size:13px}.under-the-weather-legend-color{width:15px;height:15px;border-radius:3px}.under-the-weather-data-table{width:100%;}.under-the-weather-status-box{background:#f9f9f9;padding:15px;border-left:4px solid #0073aa;border-top:1px solid #c3c4c7;border-right:1px solid #c3c4c7;border-bottom:1px solid #c3c4c7;margin:15px 0;}.weather-widget-coordinate-finder-pre{background:#f1f1f1;border-radius:4px;padding:10px;white-space:pre-wrap;}.coordinate-finder-red-message{color:red;}1 .nav-tab-wrapper{margin-bottom:20px}.under-the-weather-visual-reference-container{display:flex;gap:50px;align-items:flex-start;background:#fff;border:1px solid #999;border-radius:5px;padding:10px 24px 15px 37px;width:288px}.under-the-weather-visual-reference-item{text-align:center}.under-the-weather-visual-reference-item img{max-width:100px}.under-the-weather-visual-reference-default-image{filter:drop-shadow(1px 2px 3px #555)}.under-the-weather-usage-report{display:flex;flex-direction:column;max-width:800px}.under-the-weather-chart-container{display:flex;justify-content:space-around;align-items:flex-end;height:250px;border:1px solid #c3c4c7;padding:10px 0;gap:10px;background:#f9f9f9}.under-the-weather-chart-day{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;text-align:center;position:relative;height:100%}.under-the-weather-chart-bars{display:flex;justify-content:center;align-items:flex-end;height:100%;gap:5px}.under-the-weather-chart-bar{width:30px;background-color:#9ec2e6;border-radius:3px 3px 0 0;transition:height .3s ease-in-out;position:relative}.under-the-weather-chart-bar.api,.under-the-weather-legend-color.api{background-color:#f0ad4e}.under-the-weather-chart-bar.cache,.under-the-weather-legend-color.cache{background-color:#5cb85c}.under-the-weather-chart-bar .value{position:absolute;top:-22px;left:50%;transform:translateX(-50%);font-size:12px;font-weight:700;color:#333}.under-the-weather-chart-day-label{margin-top:12px;font-size:13px;color:#555}.under-the-weather-chart-legend{display:flex;justify-content:center;gap:20px;background:#f9f9f9;border:1px solid #c3c4c7;padding:8px}.under-the-weather-legend-item{display:flex;align-items:center;gap:8px;font-size:13px}.under-the-weather-legend-color{width:15px;height:15px;border-radius:3px}.under-the-weather-data-table{width:100%}.under-the-weather-status-box{background:#f9f9f9;padding:15px;border:1px solid #c3c4c7;border-left:4px solid #0073aa;margin:15px 0}.weather-widget-coordinate-finder-pre{background:#f1f1f1;border-radius:4px;padding:10px;white-space:pre-wrap}.coordinate-finder-red-message{color:red}.utw-result-wrapper{margin-top:15px}.utw-slider-container{display:flex;align-items:center;margin-bottom:10px}.utw-slider-wrapper{width:350px}#utw-expiration-slider{width:100%;margin:0}.utw-slider-wrapper datalist{display:flex;justify-content:space-between;width:100%;padding:0;margin-top:5px;color:#50575e}.utw-slider-value svg{margin-bottom:-6px}.utw-section-save-wrapper{padding:10px 10px 10px 0;width:200px;line-height:1.3;font-weight:600;display:inline-block;vertical-align:middle}.utw-unsaved-message{font-weight:600;display:none;color:#d63638}.utw-expiration-save-wrapper{display:flex;align-items:center}#utw-expiration-save-btn-wrap th,#utw-min-cache-notice{display:none}#utw-expiration-save-btn-wrap td{padding-left:0} -
under-the-weather/tags/2.2/css/under-the-weather.css
r3342551 r3371900 1 /* Weather Widget Styling*/1 /* --- Weather Widget Styling --- */ 2 2 .weather-widget { 3 3 min-height: 285px; 4 /*this setting can be removed. However, etting a minimum height helps to reduce browser repaints */4 /*this setting can be removed. However, setting a minimum height helps to reduce browser repaints */ 5 5 } 6 6 7 7 .weather-location-name { 8 8 border-bottom: 2px solid #eee; 9 font-size: 18px; 9 font-size: 1.375em; 10 font-weight: bold; 10 11 margin-bottom: 15px; 11 12 padding-bottom: 7px; … … 54 55 .forecast-day-name { 55 56 color: #444; 56 font-size: 14px;57 font-size: 0.65em; 57 58 margin-bottom: 8px; 58 59 } … … 64 65 } 65 66 66 .forecast-temps {67 color: #fff;68 font-size: 6px;67 .forecast-temps .slash { 68 color: transparent; 69 font-size: 0.375em; 69 70 /*Change the color and size to see the / between high and low */ 70 71 } … … 72 73 .forecast-temps .high { 73 74 color: #000; 74 font-size: 1 7px;75 font-size: 1.06em; /* Converted from 17px */ 75 76 font-weight: 700; 76 77 } 77 78 78 79 .forecast-temps .low { 79 color: # 666;80 font-size: 14px;80 color: #555; 81 font-size: 0.875em; /* Converted from 14px */ 81 82 } 82 83 … … 89 90 90 91 .today-forecast-label { 91 font-size: 1 8px;92 font-size: 1.125em; /* Converted from 18px */ 92 93 margin: 6px 0 11px; 93 94 } 94 95 95 .today-forecast-temps .temps-wrapper {96 color: #fff;97 font-size: 9px;96 .today-forecast-temps .temps-wrapper .slash{ 97 color: transparent; 98 font-size: 0.55em; 98 99 /*Change the color and size to see the / between high and low */ 99 100 } … … 101 102 .today-forecast-temps .high { 102 103 color: #000; 103 font-size: 36px;104 font-size: 2.25em; /* Converted from 36px */ 104 105 font-weight: 700; 105 106 } 106 107 107 108 .today-forecast-temps .low { 109 color: #555; 110 font-size: 1.625em; /* Converted from 26px */ 111 font-weight: 400; 112 } 113 114 .today-forecast-temps .temp-unit { 108 115 color: #666; 109 font-size: 26px; 110 font-weight: 400; 111 } 112 113 .today-forecast-temps .temp-unit { 114 color: #777; 115 font-size: 15px; 116 font-size: 0.94em; /* Converted from 15px */ 116 117 font-weight: 400; 117 118 position: relative; … … 120 121 121 122 .weather-widget .wi { 122 color: # 666;123 color: #555; 123 124 font-size: 45px; 124 125 line-height: 1; … … 131 132 132 133 .weather-extra-details { 133 border-top: 1px solid #eee;134 134 border-bottom: 1px solid #eee; 135 135 color: #555; … … 155 155 color: #999; 156 156 font-size: .8em; 157 margin-top: 1 5px;157 margin-top: 10px; 158 158 text-align: center; 159 } 159 border-top: 1px solid #eee; 160 padding-top: 5px; 161 } 162 163 /* --- Weather Alert Styling --- */ 164 165 .weather-alert { 166 display: flex; 167 align-items: center; /* Vertically aligns the icon with the text block */ 168 background-color: #f0f8ff; 169 border: 1px solid #add8e6; 170 border-radius: 4px; 171 padding: 10px 12px; 172 margin-bottom: 15px; 173 color: #333; 174 } 175 176 /* Left column for the icon */ 177 .weather-alert-icon-left { 178 margin-right: 12px; 179 } 180 181 /* Right column for the text messages */ 182 .weather-alert-message { 183 flex-grow: 1; /* Allows the message area to take up remaining space */ 184 } 185 186 .weather-alert-event { 187 font-weight: bold; 188 margin-bottom: 4px; 189 } 190 191 .weather-alert-sender { 192 font-size: 0.9em; 193 opacity: 0.8; 194 } 195 196 /* Styles for the PNG icon */ 197 .weather-alert-icon-left .weather-alert-icon { 198 width: 32px; /* Slightly larger for better visibility */ 199 height: 32px; 200 vertical-align: middle; 201 } 202 203 /* Styles for the Font Icon */ 204 .weather-alert-icon-left .wi { 205 font-size: 2em; /* Make the icon a bit bigger */ 206 line-height: 1; 207 } 208 209 /* --- Sunrise & Sunset Styling --- */ 210 211 .sunrise-sunset-container { 212 display: flex; 213 justify-content: space-around; 214 text-align: center; 215 padding: 10px 5px; 216 border-bottom: 1px solid #eee; 217 margin-bottom: 10px; 218 color: #555; 219 } 220 221 .sunrise-time, .sunset-time { 222 flex-basis: 50%; 223 } 224 225 .sunrise-sunset-label { 226 font-size: 0.8em; 227 margin: 0 0 0 10px; 228 display: inline; 229 } 230 231 .sunrise-sunset-value { 232 font-weight: bold; 233 font-size: 1.25em; 234 } 235 236 /* Icon sizing */ 237 .sunrise-sunset-container .wi { 238 font-size: 30px; /* Smaller icon size */ 239 line-height: 1; 240 color: #f28c1f; /* A nice sun color. Delete this to match forecast icons */ 241 display: inline; 242 } 243 244 .sunrise-sunset-container .sunrise-sunset-icon { 245 width: 25px; /* Visually Match the forecast day icon size */ 246 height: 25px; 247 filter: drop-shadow(1px 2px 2px #888); 248 display: inline; 249 } -
under-the-weather/tags/2.2/css/under-the-weather.min.css
r3342551 r3371900 1 .weather-widget{min-height:285px}.weather-location-name{border-bottom:2px solid #eee;font-size:1 8px;margin-bottom:15px;padding-bottom:7px;text-align:center}.current-weather{align-items:center;border-bottom:2px solid #eee;display:flex;justify-content:space-around;margin-bottom:8px;padding:10px}.current-temp{font-size:2.5em;font-weight:700}.current-conditions{align-items:center;display:flex;flex-direction:column;font-size:17px;text-transform:capitalize}.current-conditions img{filter: drop-shadow(1px 2px 3px #555);height:50px;width:50px}.forecast-container{display:flex;justify-content:space-between;text-align:center}.forecast-day{flex:1;padding:5px}.forecast-day-name{color:#444;font-size:14px;margin-bottom:8px}.forecast-day img{filter: drop-shadow(1px 2px 3px #666);height:40px;width:40px}.forecast-temps{color:#fff;font-size:6px}.forecast-temps .high{color:#000;font-size:17px;font-weight:700}.forecast-temps .low{color:#666;font-size:14px}.today-forecast-temps{align-items:center;display:flex;flex-direction:column;justify-content:center}.today-forecast-label{font-size:18px;margin:6px 0 11px}.today-forecast-temps .temps-wrapper{color:#fff;font-size:9px}.today-forecast-temps .high{color:#000;font-size:36px;font-weight:700}.today-forecast-temps .low{color:#666;font-size:26px;font-weight:400}.today-forecast-temps .temp-unit{color:#777;font-size:15px;font-weight:400;position:relative;top:-8px}.weather-widget .wi{color:#666;font-size:45px;line-height:1;margin-bottom:5px}.forecast-day .wi{font-size:40px}.weather-extra-details{border-top:1px solid #eee;border-bottom:1px solid #eee;color:#555;display:flex;font-size:.9em;justify-content:space-between;margin-bottom:10px;padding:10px 5px}.wind-details{align-items:center;display:flex}.wind-details .wi{font-size:1.5em;line-height:1;margin-right:5px}.last-updated{color:#999;font-size:.8em;margin-top:15px;text-align:center}1 .weather-widget{min-height:285px}.weather-location-name{border-bottom:2px solid #eee;font-size:1.375em;font-weight:700;margin-bottom:15px;padding-bottom:7px;text-align:center}.current-weather{align-items:center;border-bottom:2px solid #eee;display:flex;justify-content:space-around;margin-bottom:8px;padding:10px}.current-temp{font-size:2.5em;font-weight:700}.current-conditions{align-items:center;display:flex;flex-direction:column;font-size:17px;text-transform:capitalize}.current-conditions img{filter:drop-shadow(1px 2px 3px #555);height:50px;width:50px}.forecast-container{display:flex;justify-content:space-between;text-align:center}.forecast-day{flex:1;padding:5px}.forecast-day-name{color:#444;font-size:.65em;margin-bottom:8px}.forecast-day img{filter:drop-shadow(1px 2px 3px #666);height:40px;width:40px}.forecast-temps .slash{color:transparent;font-size:.375em}.forecast-temps .high{color:#000;font-size:1.06em;font-weight:700}.forecast-temps .low{color:#555;font-size:.875em}.today-forecast-temps{align-items:center;display:flex;flex-direction:column;justify-content:center}.today-forecast-label{font-size:1.125em;margin:6px 0 11px}.today-forecast-temps .temps-wrapper .slash{color:transparent;font-size:.55em}.today-forecast-temps .high{color:#000;font-size:2.25em;font-weight:700}.today-forecast-temps .low{color:#555;font-size:1.625em;font-weight:400}.today-forecast-temps .temp-unit{color:#666;font-size:.94em;font-weight:400;position:relative;top:-8px}.weather-widget .wi{color:#555;font-size:45px;line-height:1;margin-bottom:5px}.forecast-day .wi{font-size:40px}.weather-extra-details{border-bottom:1px solid #eee;color:#555;display:flex;font-size:.9em;justify-content:space-between;margin-bottom:10px;padding:10px 5px}.wind-details{align-items:center;display:flex}.wind-details .wi{font-size:1.5em;line-height:1;margin-right:5px}.last-updated{color:#999;font-size:.8em;margin-top:10px;text-align:center;border-top:1px solid #eee;padding-top:5px}.weather-alert{display:flex;align-items:center;background-color:#f0f8ff;border:1px solid #add8e6;border-radius:4px;padding:10px 12px;margin-bottom:15px;color:#333}.weather-alert-icon-left{margin-right:12px}.weather-alert-message{flex-grow:1}.weather-alert-event{font-weight:700;margin-bottom:4px}.weather-alert-sender{font-size:.9em;opacity:.8}.weather-alert-icon-left .weather-alert-icon{width:32px;height:32px;vertical-align:middle}.weather-alert-icon-left .wi{font-size:2em;line-height:1}.sunrise-sunset-container{display:flex;justify-content:space-around;text-align:center;padding:10px 5px;border-bottom:1px solid #eee;margin-bottom:10px;color:#555}.sunrise-time,.sunset-time{flex-basis:50%}.sunrise-sunset-label{font-size:.8em;margin:0 0 0 10px;display:inline}.sunrise-sunset-value{font-weight:700;font-size:1.25em}.sunrise-sunset-container .wi{font-size:30px;line-height:1;color:#f28c1f;display:inline}.sunrise-sunset-container .sunrise-sunset-icon{width:25px;height:25px;filter:drop-shadow(1px 2px 2px #888);display:inline} -
under-the-weather/tags/2.2/js/admin-geocoder.js
r3365104 r3371900 1 1 // js/admin-geocoder.js 2 2 document.addEventListener('DOMContentLoaded', function() { 3 4 5 // =================================================================== 6 // EXPIRATION SLIDER - Runs on all admin pages 7 // =================================================================== 8 9 // --- Logic to prepare the top save button for styling --- 10 const topSaveBtn = document.getElementById('utw-expiration-save-btn'); 11 if (topSaveBtn) { 12 // Find the parent table cell (td) and table row (tr) 13 const parentTd = topSaveBtn.closest('td'); 14 const parentTr = topSaveBtn.closest('tr'); 15 16 if (parentTd && parentTr) { 17 // 1. Add an ID to the table row for easy CSS targeting 18 parentTr.id = 'utw-expiration-save-btn-wrap'; 19 20 // 2. Find and remove the empty label cell (th) 21 const thLabel = parentTr.querySelector('th'); 22 if (thLabel) { 23 thLabel.remove(); 24 } 25 26 // 3. Add the colspan="2" attribute to the button's cell 27 parentTd.setAttribute('colspan', '2'); 28 } 29 } 30 31 32 const expirationSlider = document.getElementById('utw-expiration-slider'); 33 const expirationValueDisplay = document.getElementById('utw-expiration-value'); 34 const unsavedChangesMessage = document.getElementById('utw-unsaved-changes'); 35 const expirationSaveBtn = document.getElementById('utw-expiration-save-btn'); 36 const minCacheNotice = document.getElementById('utw-min-cache-notice'); 37 38 if (expirationSlider && expirationValueDisplay) { 39 const originalValue = expirationSlider.getAttribute('data-original-value'); 40 41 // Update display value in real-time as slider moves 42 expirationSlider.addEventListener('input', function() { 43 44 // Show the special notice if the user drags the slider to 0 45 if (parseFloat(this.value) === 0) { 46 minCacheNotice.style.display = 'block'; 47 } else { 48 minCacheNotice.style.display = 'none'; 49 } 50 51 // Enforce minimum of 0.5 52 let value = parseFloat(this.value); 53 if (value < 0.5) { 54 value = 0.5; 55 this.value = 0.5; 56 } 57 58 expirationValueDisplay.textContent = value; 59 60 // Show/hide unsaved changes message based on whether value changed 61 if (unsavedChangesMessage) { 62 if (String(value) !== originalValue) { 63 unsavedChangesMessage.style.display = 'inline'; 64 } else { 65 unsavedChangesMessage.style.display = 'none'; 66 } 67 } 68 }); 69 70 // Additional validation on change (when user releases the slider) 71 expirationSlider.addEventListener('change', function() { 72 let value = parseFloat(this.value); 73 if (value < 0.5) { 74 this.value = 0.5; 75 expirationValueDisplay.textContent = '0.5'; 76 } 77 }); 78 79 // Hide unsaved changes message when the upper save button is clicked 80 if (expirationSaveBtn) { 81 expirationSaveBtn.addEventListener('click', function() { 82 if (unsavedChangesMessage) { 83 unsavedChangesMessage.style.display = 'none'; 84 } 85 }); 86 } 87 88 // Also hide message if the main (bottom) form submit button is clicked 89 const mainSubmitBtn = document.querySelector('input[type="submit"][name="submit"]'); 90 if (mainSubmitBtn && mainSubmitBtn.id !== 'utw-expiration-save-btn') { 91 mainSubmitBtn.addEventListener('click', function() { 92 if (unsavedChangesMessage) { 93 unsavedChangesMessage.style.display = 'none'; 94 } 95 }); 96 } 97 } 98 99 // =================================================================== 100 // GEOCODER TOOL - Only runs when the tool exists on the page 101 // =================================================================== 102 3 103 const geocoderTool = document.getElementById('utw-geocoder-tool'); 4 104 if (!geocoderTool) { 5 return; 6 } 105 return; // Exit only AFTER checking for the expiration slider 106 } 107 7 108 const locationInput = document.getElementById('utw-location-input'); 8 109 const findButton = document.getElementById('utw-find-coords'); … … 12 113 13 114 // --- HISTORY FUNCTIONS --- 14 let searchHistory = []; // Local cache of the history115 let searchHistory = []; 15 116 const MAX_HISTORY = 5; 16 117 … … 18 119 function formatWidgetHtml(html) { 19 120 return html 20 //.replace('<div class="weather-widget"', '<div class="weather-widget"\n ')21 121 .replace(' data-lat=', '\n data-lat=') 22 122 .replace(' data-lon=', '\n data-lon=') … … 51 151 52 152 function saveToHistoryAndServer(newItem) { 53 54 // Add to local history first55 153 searchHistory.unshift(newItem); 56 154 searchHistory = searchHistory.slice(0, MAX_HISTORY); 57 155 58 // Update the UI immediately with local data 59 console.log('UTW: Updated history:', searchHistory); 156 console.log('UTW: Updated history:', searchHistory); 60 157 renderHistory(); 61 158 62 // Save to server63 159 fetch(ajaxurl, { 64 160 method: 'POST', … … 135 231 const locationName = result.display_name; 136 232 137 // Generate the HTML div for the user138 233 const widgetHtml = `<div class="weather-widget" data-lat="${lat}" data-lon="${lon}" data-location-name="${locationQuery}"></div>`; 139 234 … … 145 240 `; 146 241 147 // Add to history and save to server148 242 saveToHistoryAndServer({ locationName: locationQuery, widgetHtml }); 149 243 150 // Add click listener to the new copy button151 244 document.getElementById('utw-copy-button').addEventListener('click', function() { 152 245 copyToClipboard(formatWidgetHtml(widgetHtml), this); … … 165 258 166 259 if (historyList) { 167 // Use event delegation for copy buttons in the history list168 260 historyList.addEventListener('click', function(event) { 169 261 if (event.target.classList.contains('copy-history-btn')) { … … 173 265 }); 174 266 175 // Load history from server on page load176 267 loadHistoryFromServer(); 177 268 } 178 269 179 // Copy searches to clipboard180 270 function copyToClipboard(text, button) { 181 271 if (navigator.clipboard && window.isSecureContext) { … … 219 309 } 220 310 221 // Helper function to escape HTML for display in a <pre> tag222 311 function escapeHtml(unsafe) { 223 312 return unsafe -
under-the-weather/tags/2.2/js/under-the-weather.js
r3369072 r3371900 1 // Validate Coordinates 2 function validateCoordinates(lat, lon) { 3 const latitude = parseFloat(lat); 4 const longitude = parseFloat(lon); 5 return (latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180); 6 } 7 8 // Validate data 9 function validateWeatherData(data) { 10 return data && 11 data.current && 12 data.daily && 13 Array.isArray(data.daily) && 14 data.daily.length > 0; 15 } 16 17 // Convert a DMS (Degrees, Minutes, Seconds) coordinate, or a DDM string to the desired Decimal Degrees (DD) format. 18 // This frontend parsing serves as a helpful handler to catch malformed coordinates when possible. 19 // Decimal Degrees coordinates should be used at all time (e.g., 34.1195, -118.3005). 20 // The quotation marks included in DMS coordinates will break the HTML structure of the weather widget. 21 // DMS coordinates (e.g., 34°07'10.2"N 118°18'01.8"W) should therefore be avoided. 22 function parseCoordinate(coordString) { 23 if (!coordString || typeof coordString !== 'string') { 24 return null; 25 } 26 27 // 1. Clean the input 28 let str = coordString.trim(); 29 if (str.endsWith(',')) { 30 str = str.slice(0, -1).trim(); 31 } 32 33 // 2. Try to parse as DDM (Degrees Decimal Minutes) 34 const ddmRegex = /([0-9]{1,3})[°\s]+([0-9]+(?:\.[0-9]+)?)['\s]+([NSEW])/i; 35 let parts = str.match(ddmRegex); 36 if (parts) { 37 const degrees = parseFloat(parts[1]); 38 const minutes = parseFloat(parts[2]); 39 const hemisphere = parts[3].toUpperCase(); 40 if (isNaN(degrees) || isNaN(minutes)) return null; 41 42 let decimal = degrees + (minutes / 60); 43 if (hemisphere === 'S' || hemisphere === 'W') decimal = -decimal; 44 return parseFloat(decimal.toFixed(4)); 45 } 46 47 // 3. Try to parse as DMS (Degrees, Minutes, Seconds) 48 const dmsRegex = /([0-9]{1,3})[°\s]+([0-9]{1,2})['\s]+([0-9]{1,2}(?:\.[0-9]+)?)["\s]+([NSEW])/i; 49 parts = str.match(dmsRegex); 50 if (parts) { 51 const degrees = parseFloat(parts[1]); 52 const minutes = parseFloat(parts[2]); 53 const seconds = parseFloat(parts[3]); 54 const hemisphere = parts[4].toUpperCase(); 55 if (isNaN(degrees) || isNaN(minutes) || isNaN(seconds)) return null; 56 57 let decimal = degrees + (minutes / 60) + (seconds / 3600); 58 if (hemisphere === 'S' || hemisphere === 'W') decimal = -decimal; 59 return parseFloat(decimal.toFixed(4)); 60 } 61 62 // 4. Try to parse as simple Decimal Degrees 63 const dd = parseFloat(str); 64 if (!isNaN(dd)) { 65 return dd; 66 } 67 68 // 5. If all formats fail 69 return null; 70 } 71 1 // Check webpage for Weather Widgets and load widget data. 72 2 document.addEventListener('DOMContentLoaded', function() { 73 const widget = document.querySelector('.weather-widget'); 74 if (!widget) { 75 return; 76 } 77 3 // 1. Find ALL weather widgets on the page 4 const weatherWidgets = document.querySelectorAll('.weather-widget'); 5 6 // 2. If no widgets are found, do nothing. 7 if (weatherWidgets.length === 0) { 8 return; 9 } 10 11 // 3. Loop through each widget and load its data 12 weatherWidgets.forEach(widget => { 13 loadWeatherData(widget); 14 }); 15 }); 16 17 /** 18 * Fetches and displays weather data for a single widget element. 19 * @param {HTMLElement} widget The widget's div element. 20 */ 21 function loadWeatherData(widget) { 78 22 const locationName = widget.dataset.locationName; 79 const unit = widget.dataset.unit ? widget.dataset.unit.toLowerCase() : 'imperial'; 80 const controller = new AbortController(); 81 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout 82 83 // Get the lat/lon from the data attributes 23 // Get the lat/lon from the data attributes 84 24 let lat = widget.dataset.lat; 85 25 let lon = widget.dataset.lon; 86 26 87 27 // Attempt to parse/convert them (this handles DD, DDM, and DMS formats) 88 28 const parsedLat = parseCoordinate(lat); … … 109 49 110 50 widget.innerHTML = '<div class="weather-loading">Loading weather data...</div>'; 111 51 52 const unit = widget.dataset.unit ? widget.dataset.unit.toLowerCase() : 'imperial'; 53 const controller = new AbortController(); 54 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout 112 55 const apiUrl = `/wp-json/under-the-weather/v1/forecast?lat=${lat}&lon=${lon}&location_name=${encodeURIComponent(locationName)}&unit=${unit}`; 113 56 … … 139 82 widget.innerHTML = `<p>Unable to load weather data. Please try again later.</p>`; 140 83 }); 141 }); 84 } 85 86 // Validate Coordinates 87 function validateCoordinates(lat, lon) { 88 const latitude = parseFloat(lat); 89 const longitude = parseFloat(lon); 90 return (latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180); 91 } 92 93 // Validate Weather Data 94 function validateWeatherData(data) { 95 return data && 96 data.current && 97 data.daily && 98 Array.isArray(data.daily) && 99 data.daily.length > 0; 100 } 101 102 // Convert a DMS (Degrees, Minutes, Seconds) coordinate, or a DDM string to the desired Decimal Degrees (DD) format. 103 // This frontend parsing serves as a helpful handler to catch malformed coordinates when possible. 104 // Decimal Degrees coordinates should be used at all time (e.g., 34.1195, -118.3005). 105 // The quotation marks included in DMS coordinates will break the HTML structure of the weather widget. 106 // DMS coordinates (e.g., 34°07'10.2"N 118°18'01.8"W) should therefore be avoided. 107 function parseCoordinate(coordString) { 108 if (!coordString || typeof coordString !== 'string') { 109 return null; 110 } 111 112 // 1. Clean the input 113 let str = coordString.trim(); 114 if (str.endsWith(',')) { 115 str = str.slice(0, -1).trim(); 116 } 117 118 // 2. Try to parse as DDM (Degrees Decimal Minutes) 119 const ddmRegex = /([0-9]{1,3})[°\s]+([0-9]+(?:\.[0-9]+)?)['\s]+([NSEW])/i; 120 let parts = str.match(ddmRegex); 121 if (parts) { 122 const degrees = parseFloat(parts[1]); 123 const minutes = parseFloat(parts[2]); 124 const hemisphere = parts[3].toUpperCase(); 125 if (isNaN(degrees) || isNaN(minutes)) return null; 126 127 let decimal = degrees + (minutes / 60); 128 if (hemisphere === 'S' || hemisphere === 'W') decimal = -decimal; 129 return parseFloat(decimal.toFixed(4)); 130 } 131 132 // 3. Try to parse as DMS (Degrees, Minutes, Seconds) 133 const dmsRegex = /([0-9]{1,3})[°\s]+([0-9]{1,2})['\s]+([0-9]{1,2}(?:\.[0-9]+)?)["\s]+([NSEW])/i; 134 parts = str.match(dmsRegex); 135 if (parts) { 136 const degrees = parseFloat(parts[1]); 137 const minutes = parseFloat(parts[2]); 138 const seconds = parseFloat(parts[3]); 139 const hemisphere = parts[4].toUpperCase(); 140 if (isNaN(degrees) || isNaN(minutes) || isNaN(seconds)) return null; 141 142 let decimal = degrees + (minutes / 60) + (seconds / 3600); 143 if (hemisphere === 'S' || hemisphere === 'W') decimal = -decimal; 144 return parseFloat(decimal.toFixed(4)); 145 } 146 147 // 4. Try to parse as simple Decimal Degrees 148 const dd = parseFloat(str); 149 if (!isNaN(dd)) { 150 return dd; 151 } 152 153 // 5. If all formats fail 154 return null; 155 } 156 157 /** 158 * Selects a weather icon based on the alert event text. 159 * @param {string} eventText The text of the weather alert (e.g., "Tornado Warning"). 160 * @returns {string} The corresponding Weather Icons class name. 161 */ 162 function getAlertIconClass(eventText) { 163 const text = eventText.toLowerCase(); 164 165 // Catastrophic Events 166 if (text.includes('tornado')) return 'wi-tornado'; 167 if (text.includes('hurricane')) return 'wi-hurricane-warning'; 168 if (text.includes('tsunami')) return 'wi-tsunami'; 169 if (text.includes('earthquake')) return 'wi-earthquake'; 170 171 // Storms & Precipitation 172 if (text.includes('thunderstorm') || text.includes('lightning')) return 'wi-thunderstorm'; 173 if (text.includes('gale')) return 'wi-gale-warning'; 174 if (text.includes('hail')) return 'wi-hail'; 175 if (text.includes('rain') || text.includes('showers') || text.includes('drizzle')) return 'wi-rain'; 176 if (text.includes('flood')) return 'wi-flood'; 177 178 // Winter Weather 179 if (text.includes('winter') || text.includes('snow') || text.includes('blizzard')) return 'wi-snow'; 180 if (text.includes('ice') || text.includes('frost') || text.includes('freeze') || text.includes('cold') || text.includes('chill')) return 'wi-snowflake-cold'; 181 182 // Temperature & Wind 183 if (text.includes('heat') || text.includes('hot')) return 'wi-hot'; 184 if (text.includes('wind')) return 'wi-strong-wind'; 185 186 // Atmospheric & Air Quality 187 if (text.includes('fog')) return 'wi-fog'; 188 if (text.includes('fire')) return 'wi-fire'; 189 if (text.includes('smoke')) return 'wi-smoke'; 190 if (text.includes('smog') || text.includes('air quality')) return 'wi-smog'; 191 if (text.includes('dust')) return 'wi-dust'; 192 if (text.includes('sandstorm') || text.includes('sand')) return 'wi-sandstorm'; 193 194 // A good fallback for any other severe weather 195 return 'wi-storm-warning'; 196 } 142 197 143 198 function displayWeather(data, widget) { 144 const { style_set, display_mode, forecast_days, show_details, show_ timestamp, show_unit } = under_the_weather_settings;199 const { style_set, display_mode, forecast_days, show_details, show_unit, show_alerts, show_timestamp, sunrise_sunset_format } = under_the_weather_settings; 145 200 const locationName = widget.dataset.locationName || ''; 146 201 … … 185 240 return "a minute ago"; 186 241 } 242 243 // START: New Sunrise/Sunset Logic 244 let sunriseSunsetHtml = ''; 245 // Check if the setting is enabled and the data exists 246 if (sunrise_sunset_format !== 'off' && data.current.sunrise && data.current.sunset) { 247 248 // Define formatting options based on the setting 249 const timeOptions = { 250 timeZone: data.timezone, 251 hour: 'numeric', 252 minute: '2-digit', 253 hour12: sunrise_sunset_format === '12' // Use 12-hour format if setting is '12' 254 }; 255 256 // Convert timestamps to readable times 257 const sunriseTime = new Date(data.current.sunrise * 1000).toLocaleTimeString('en-US', timeOptions); 258 const sunsetTime = new Date(data.current.sunset * 1000).toLocaleTimeString('en-US', timeOptions); 259 260 // Get icons based on the style set 261 let sunriseIcon = ''; 262 let sunsetIcon = ''; 263 if (style_set === 'weather_icons_font') { 264 sunriseIcon = '<i class="wi wi-sunrise"></i>'; 265 sunsetIcon = '<i class="wi wi-sunset"></i>'; 266 } else { 267 // Using generic day/night icons as a fallback for the default image set 268 const sunriseImgUrl = `${under_the_weather_plugin_url.url}images/seths--weather-images-sunrise.png`; 269 const sunsetImgUrl = `${under_the_weather_plugin_url.url}images/seths--weather-images-sunset.png`; 270 sunriseIcon = `<img src="${sunriseImgUrl}" class="sunrise-sunset-icon" alt="Sunrise Time">`; 271 sunsetIcon = `<img src="${sunsetImgUrl}" class="sunrise-sunset-icon" alt="Sunset Time">`; 272 } 273 274 sunriseSunsetHtml = ` 275 <div class="sunrise-sunset-container"> 276 <div class="sunrise-time"> 277 ${sunriseIcon} 278 <div class="sunrise-sunset-label">Sunrise</div> 279 <div class="sunrise-sunset-value">${sunriseTime}</div> 280 </div> 281 <div class="sunset-time"> 282 ${sunsetIcon} 283 <div class="sunrise-sunset-label">Sunset</div> 284 <div class="sunrise-sunset-value">${sunsetTime}</div> 285 </div> 286 </div> 287 `; 288 } 289 // END: New Sunrise/Sunset Logic 187 290 188 291 let primaryDisplayHtml = ''; … … 197 300 <div class="today-forecast-label">Today</div> 198 301 <div class="temps-wrapper"> 199 <span class="high">${highTemp}${tempSymbol}</span> /<span class="low">${lowTemp}${tempSymbol}</span>${displayUnitString}302 <span class="high">${highTemp}${tempSymbol}</span><span class="slash"> / </span><span class="low">${lowTemp}${tempSymbol}</span>${displayUnitString} 200 303 </div> 201 304 </div> … … 237 340 `; 238 341 } 342 343 let alertHtml = ''; 344 if (show_alerts && data.alerts && data.alerts.length > 0) { 345 data.alerts.forEach(alert => { 346 347 // Handle custom alert icons 348 let iconHtml = '';// This will hold the icon's HTML 349 if (style_set === 'weather_icons_font') { 350 // Use the dynamic font icon logic 351 const iconClass = getAlertIconClass(alert.event); 352 iconHtml = `<i class="wi ${iconClass}"></i>`; 353 } else { 354 // Use the new PNG fallback icon 355 const imageUrl = `${under_the_weather_plugin_url.url}images/seths--weather-images-warning.png`; 356 iconHtml = `<img src="${imageUrl}" class="weather-alert-icon" alt="Weather Alert">`; 357 } 358 359 alertHtml += ` 360 <div class="weather-alert"> 361 <div class="weather-alert-icon-left"> 362 ${iconHtml} 363 </div> 364 <div class="weather-alert-message"> 365 <div class="weather-alert-event">${alert.event}</div> 366 <div class="weather-alert-sender">Issued by: ${alert.sender_name}</div> 367 </div> 368 </div> 369 `; 370 }); 371 } 239 372 240 373 const forecastDaysToShow = parseInt(forecast_days, 10) || 5; … … 252 385 ${getIconHtml(day.weather[0])} 253 386 <div class="forecast-temps"> 254 <span class="high">${highTemp}${tempSymbol}</span> /<span class="low">${lowTemp}${tempSymbol}</span>387 <span class="high">${highTemp}${tempSymbol}</span><span class="slash"> / </span><span class="low">${lowTemp}${tempSymbol}</span> 255 388 </div> 256 389 </div> … … 265 398 const finalHtml = ` 266 399 <div class="weather-location-name">${locationName}</div> 267 ${primaryDisplayHtml} 400 ${alertHtml} 401 ${primaryDisplayHtml} 268 402 ${extraDetailsHtml} 403 ${sunriseSunsetHtml} 269 404 <div class="forecast-container"> 270 405 ${forecastHtml} -
under-the-weather/tags/2.2/js/under-the-weather.min.js
r3369072 r3371900 1 function validateCoordinates(e,t){const a=parseFloat(e),n=parseFloat(t);return a>=-90&&a<=90&&n>=-180&&n<=180}function validateWeatherData(e){return e&&e.current&&e.daily&&Array.isArray(e.daily)&&e.daily.length>0}function parseCoordinate(e){if(!e||"string"!=typeof e)return null;let t=e.trim();t.endsWith(",")&&(t=t.slice(0,-1).trim());let a=t.match(/([0-9]{1,3})[°\s]+([0-9]+(?:\.[0-9]+)?)['\s]+([NSEW])/i);if(a){const e=parseFloat(a[1]),t=parseFloat(a[2]),n=a[3].toUpperCase();if(isNaN(e)||isNaN(t))return null;let r=e+t/60;return"S"!==n&&"W"!==n||(r=-r),parseFloat(r.toFixed(4))}if(a=t.match(/([0-9]{1,3})[°\s]+([0-9]{1,2})['\s]+([0-9]{1,2}(?:\.[0-9]+)?)["\s]+([NSEW])/i),a){const e=parseFloat(a[1]),t=parseFloat(a[2]),n=parseFloat(a[3]),r=a[4].toUpperCase();if(isNaN(e)||isNaN(t)||isNaN(n))return null;let s=e+t/60+n/3600;return"S"!==r&&"W"!==r||(s=-s),parseFloat(s.toFixed(4))}const n=parseFloat(t);return isNaN(n)?null:n}function displayWeather(e,t){const{style_set:a,display_mode:n,forecast_days:r,show_details:s,show_timestamp:o,show_unit:i}=under_the_weather_settings,d=t.dataset.locationName||"",l="°",c="metric"===e.units?"C":"F",u="metric"===e.units?"kph":"mph",h=i?`<span class="temp-unit">${c}</span>`:"";function p(e){return"weather_icons_font"===a?`<i class="wi ${e.icon_class}"></i>`:`<img src="${under_the_weather_plugin_url.url}images/default-weather-images-${e.icon}.png" alt="${e.description}">`}let w="";if("today_forecast"===n){const t=e.daily[0],a=Math.round(t.temp.max),n=Math.round(t.temp.min);w=`<div class="current-weather"><div class="today-forecast-temps"><div class="today-forecast-label">Today</div><div class="temps-wrapper"><span class="high">${a}${l}</span> / <span class="low">${n}${l}</span>${h}</div></div><div class="current-conditions">${p(t.weather[0])}<span>${t.weather[0].description}</span></div></div>`}else{const t=e.current;w=`<div class="current-weather"><div class="current-temp">${Math.round(t.temp)}${l}${h}</div><div class="current-conditions">${p(t.weather[0])}<span>${t.weather[0].description}</span></div></div>`}let f="";if(s){const t=Math.round(e.current.feels_like),a=Math.round(e.current.wind_speed),n=(v=e.current.wind_deg,["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"][Math.round(v/22.5)%16]),r=function(e){return`wi wi-wind from-${Math.round(e)}-deg`}(e.current.wind_deg);f=`<div class="weather-extra-details"><span>Feels like: ${t}${l}${h}</span><span class="wind-details"><i class="${r}"></i> ${n} ${a} ${u}</span></div>`}var v;const m=parseInt(r,10)||5,$=e.daily.slice(1,1+m);let g="";$.forEach((e=>{const t=new Date(1e3*e.dt).toLocaleDateString("en-US",{weekday:"short"}),a=Math.round(e.temp.max),n=Math.round(e.temp.min);g+=`<div class="forecast-day"><div class="forecast-day-name">${t}</div>${p(e.weather[0])}<div class="forecast-temps"><span class="high">${a}${l}</span> / <span class="low">${n}${l}</span></div></div>`}));let N="";o&&e.fetched_at&&(N=`<div class="last-updated">Updated ${function(e){const t=(new Date).getTime()/1e3,a=Math.floor(t-e);if(a<60)return"just now";let n=a/31536e3;return n>1?Math.floor(n)+" years ago":(n=a/2592e3,n>1?Math.floor(n)+" months ago":(n=a/86400,n>1?Math.floor(n)+" days ago":(n=a/3600,n>1?Math.floor(n)+" hours ago":(n=a/60,n>1?Math.floor(n)+" minutes ago":"a minute ago"))))}(e.fetched_at)}</div>`);const _=`<div class="weather-location-name">${d}</div>${w}${f}<div class="forecast-container">${g}</div>${N}`;t.innerHTML=_}document.addEventListener("DOMContentLoaded",(function(){const e=document.querySelector(".weather-widget");if(!e)return;const t=e.dataset.locationName,a=e.dataset.unit?e.dataset.unit.toLowerCase():"imperial",n=new AbortController,r=setTimeout((()=>n.abort()),1e4);let s=e.dataset.lat,o=e.dataset.lon;const i=parseCoordinate(s),d=parseCoordinate(o);if(null!==i&&(s=i),null!==d&&(o=d),!s||!o||!t)return void(e.innerHTML="Location data is missing.");if(!validateCoordinates(s,o))return void(e.innerHTML="Invalid location coordinates.");e.innerHTML='<div class="weather-loading">Loading weather data...</div>';const l=`/wp-json/under-the-weather/v1/forecast?lat=${s}&lon=${o}&location_name=${encodeURIComponent(t)}&unit=${a}`;fetch(l,{signal:n.signal,headers:{"X-WP-Nonce":under_the_weather_settings.nonce}}).then((t=>{if(clearTimeout(r),!t.ok)throw t.text().then((t=>{console.error("Error fetching weather data:",t),e.innerHTML="<p>Could not retrieve forecast. Server error.</p>"})),new Error("Network response was not ok");return t.json()})).then((t=>{if(!validateWeatherData(t))throw new Error("The weather data structure is invalid");displayWeather(t,e)})).catch((t=>{console.error("Network Error:",t),e.innerHTML="<p>Unable to load weather data. Please try again later.</p>"}))}));1 function loadWeatherData(e){const t=e.dataset.locationName;let n=e.dataset.lat,s=e.dataset.lon;const a=parseCoordinate(n),i=parseCoordinate(s);if(null!==a&&(n=a),null!==i&&(s=i),!n||!s||!t)return void(e.innerHTML="Location data is missing.");if(!validateCoordinates(n,s))return void(e.innerHTML="Invalid location coordinates.");e.innerHTML='<div class="weather-loading">Loading weather data...</div>';const r=e.dataset.unit?e.dataset.unit.toLowerCase():"imperial",l=new AbortController,o=setTimeout((()=>l.abort()),1e4),d=`/wp-json/under-the-weather/v1/forecast?lat=${n}&lon=${s}&location_name=${encodeURIComponent(t)}&unit=${r}`;fetch(d,{signal:l.signal,headers:{"X-WP-Nonce":under_the_weather_settings.nonce}}).then((t=>{if(clearTimeout(o),!t.ok)throw t.text().then((t=>{console.error("Error fetching weather data:",t),e.innerHTML="<p>Could not retrieve forecast. Server error.</p>"})),new Error("Network response was not ok");return t.json()})).then((t=>{if(!validateWeatherData(t))throw new Error("The weather data structure is invalid");displayWeather(t,e)})).catch((t=>{console.error("Network Error:",t),e.innerHTML="<p>Unable to load weather data. Please try again later.</p>"}))}function validateCoordinates(e,t){const n=parseFloat(e),s=parseFloat(t);return n>=-90&&n<=90&&s>=-180&&s<=180}function validateWeatherData(e){return e&&e.current&&e.daily&&Array.isArray(e.daily)&&e.daily.length>0}function parseCoordinate(e){if(!e||"string"!=typeof e)return null;let t=e.trim();t.endsWith(",")&&(t=t.slice(0,-1).trim());let n=t.match(/([0-9]{1,3})[°\s]+([0-9]+(?:\.[0-9]+)?)['\s]+([NSEW])/i);if(n){const e=parseFloat(n[1]),t=parseFloat(n[2]),s=n[3].toUpperCase();if(isNaN(e)||isNaN(t))return null;let a=e+t/60;return"S"!==s&&"W"!==s||(a=-a),parseFloat(a.toFixed(4))}if(n=t.match(/([0-9]{1,3})[°\s]+([0-9]{1,2})['\s]+([0-9]{1,2}(?:\.[0-9]+)?)["\s]+([NSEW])/i),n){const e=parseFloat(n[1]),t=parseFloat(n[2]),s=parseFloat(n[3]),a=n[4].toUpperCase();if(isNaN(e)||isNaN(t)||isNaN(s))return null;let i=e+t/60+s/3600;return"S"!==a&&"W"!==a||(i=-i),parseFloat(i.toFixed(4))}const s=parseFloat(t);return isNaN(s)?null:s}function getAlertIconClass(e){const t=e.toLowerCase();return t.includes("tornado")?"wi-tornado":t.includes("hurricane")?"wi-hurricane-warning":t.includes("tsunami")?"wi-tsunami":t.includes("earthquake")?"wi-earthquake":t.includes("thunderstorm")||t.includes("lightning")?"wi-thunderstorm":t.includes("gale")?"wi-gale-warning":t.includes("hail")?"wi-hail":t.includes("rain")||t.includes("showers")||t.includes("drizzle")?"wi-rain":t.includes("flood")?"wi-flood":t.includes("winter")||t.includes("snow")||t.includes("blizzard")?"wi-snow":t.includes("ice")||t.includes("frost")||t.includes("freeze")||t.includes("cold")||t.includes("chill")?"wi-snowflake-cold":t.includes("heat")||t.includes("hot")?"wi-hot":t.includes("wind")?"wi-strong-wind":t.includes("fog")?"wi-fog":t.includes("fire")?"wi-fire":t.includes("smoke")?"wi-smoke":t.includes("smog")||t.includes("air quality")?"wi-smog":t.includes("dust")?"wi-dust":t.includes("sandstorm")||t.includes("sand")?"wi-sandstorm":"wi-storm-warning"}function displayWeather(e,t){const{style_set:n,display_mode:s,forecast_days:a,show_details:i,show_unit:r,show_alerts:l,show_timestamp:o,sunrise_sunset_format:d}=under_the_weather_settings,c=t.dataset.locationName||"",u="°",h="metric"===e.units?"C":"F",w="metric"===e.units?"kph":"mph",p=r?`<span class="temp-unit">${h}</span>`:"";function m(e){return"weather_icons_font"===n?`<i class="wi ${e.icon_class}"></i>`:`<img src="${under_the_weather_plugin_url.url}images/default-weather-images-${e.icon}.png" alt="${e.description}">`}let v="";if("off"!==d&&e.current.sunrise&&e.current.sunset){const t={timeZone:e.timezone,hour:"numeric",minute:"2-digit",hour12:"12"===d},s=new Date(1e3*e.current.sunrise).toLocaleTimeString("en-US",t),a=new Date(1e3*e.current.sunset).toLocaleTimeString("en-US",t);let i="",r="";if("weather_icons_font"===n)i='<i class="wi wi-sunrise"></i>',r='<i class="wi wi-sunset"></i>';else{i=`<img src="${`${under_the_weather_plugin_url.url}images/seths--weather-images-sunrise.png`}" class="sunrise-sunset-icon" alt="Sunrise Time">`,r=`<img src="${`${under_the_weather_plugin_url.url}images/seths--weather-images-sunset.png`}" class="sunrise-sunset-icon" alt="Sunset Time">`}v=`<div class="sunrise-sunset-container"><div class="sunrise-time">${i}<div class="sunrise-sunset-label">Sunrise</div><div class="sunrise-sunset-value">${s}</div></div><div class="sunset-time">${r}<div class="sunrise-sunset-label">Sunset</div><div class="sunrise-sunset-value">${a}</div></div></div>`}let f="";if("today_forecast"===s){const t=e.daily[0],n=Math.round(t.temp.max),s=Math.round(t.temp.min);f=`<div class="current-weather"><div class="today-forecast-temps"><div class="today-forecast-label">Today</div><div class="temps-wrapper"> <span class="high">${n}${u}</span><span class="slash"> / </span><span class="low">${s}${u}</span>${p}</div></div><div class="current-conditions">${m(t.weather[0])}<span>${t.weather[0].description}</span></div></div>`}else{const t=e.current;f=`<div class="current-weather"><div class="current-temp">${Math.round(t.temp)}${u}${p}</div><div class="current-conditions">${m(t.weather[0])}<span>${t.weather[0].description}</span></div></div>`}let g="";if(i){const t=Math.round(e.current.feels_like),n=Math.round(e.current.wind_speed),s=($=e.current.wind_deg,["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"][Math.round($/22.5)%16]),a=function(e){return`wi wi-wind from-${Math.round(e)}-deg`}(e.current.wind_deg);g=`<div class="weather-extra-details"><span>Feels like: ${t}${u}${p}</span><span class="wind-details"><i class="${a}"></i> ${s} ${n} ${w}</span></div>`}var $;let _="";l&&e.alerts&&e.alerts.length>0&&e.alerts.forEach((e=>{let t="";if("weather_icons_font"===n){t=`<i class="wi ${getAlertIconClass(e.event)}"></i>`}else{t=`<img src="${`${under_the_weather_plugin_url.url}images/seths--weather-images-warning.png`}" class="weather-alert-icon" alt="Weather Alert">`}_+=`\n\t\t\t\t<div class="weather-alert">\n\t\t\t\t\t<div class="weather-alert-icon-left">\n\t\t\t\t\t\t${t}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class="weather-alert-message">\n\t\t\t\t\t\t<div class="weather-alert-event">${e.event}</div>\n\t\t\t\t\t\t<div class="weather-alert-sender">Issued by: ${e.sender_name}</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>`}));const N=parseInt(a,10)||5,y=e.daily.slice(1,1+N);let S="";y.forEach((e=>{const t=new Date(1e3*e.dt).toLocaleDateString("en-US",{weekday:"short"}),n=Math.round(e.temp.max),s=Math.round(e.temp.min);S+=` <div class="forecast-day"><div class="forecast-day-name">${t}</div>${m(e.weather[0])}<div class="forecast-temps"> <span class="high">${n}${u}</span><span class="slash"> / </span><span class="low">${s}${u}</span></div> </div>`}));let M="";o&&e.fetched_at&&(M=`<div class="last-updated">Updated ${function(e){const t=(new Date).getTime()/1e3,n=Math.floor(t-e);if(n<60)return"just now";let s=n/31536e3;return s>1?Math.floor(s)+" years ago":(s=n/2592e3,s>1?Math.floor(s)+" months ago":(s=n/86400,s>1?Math.floor(s)+" days ago":(s=n/3600,s>1?Math.floor(s)+" hours ago":(s=n/60,s>1?Math.floor(s)+" minutes ago":"a minute ago"))))}(e.fetched_at)}</div>`);const W=`<div class="weather-location-name">${c}</div>${_}\n\t\t${f}${g}\n\t\t${v}<div class="forecast-container">${S}</div>${M}\n`;t.innerHTML=W}document.addEventListener("DOMContentLoaded",(function(){const e=document.querySelectorAll(".weather-widget");0!==e.length&&e.forEach((e=>{loadWeatherData(e)}))})); -
under-the-weather/tags/2.2/readme.txt
r3369072 r3371900 4 4 Requires at least: 5.0 5 5 Tested up to: 6.8 6 Stable tag: 2. 16 Stable tag: 2.2 7 7 Requires PHP: 7.2 8 8 License: GPLv2 or later … … 27 27 * **Easy to Use:** Add weather widgets using the WordPress block editor or by placing a simple `<div>` with data attributes anywhere on your site. 28 28 * **Server-Side Caching:** All API calls are cached on your server, dramatically reducing calls to the OpenWeather API and speeding up page loads for all users. 29 * **Smart Caching:** In addition to a configurable cache duration, the plugin automatically resets the forecast after midnight in the location's timezone, ensuring your visitors always see the current day's weather. 29 30 * **Visual Performance Report:** Monitor your site's API usage with a bar chart that displays a 7-day history of cached requests versus new calls to the OpenWeather API - a clear look at how the caching system is working to keep your site fast and your API calls low. 30 * **Highly Customizable:** Use the detailed settings page to control everything from cache duration to the number of forecast days. 31 * **Flexible Display:** Show either the current live weather or the high/low forecast for the current day. 31 * **Customizable Display:** Use the main display to show either the current live weather or the high/low forecast for the current day, and set the number of days to include in the forecast ahead. 32 32 * **Imperial & Metric Units:** Display weather in Fahrenheit/mph or Celsius/kph on a per-widget basis. 33 33 * **Extra Details:** Optionally display "Feels Like" temperature and detailed wind information. 34 * **Weather Alerts:** Display official severe weather alerts directly in the widget to keep visitors informed. 35 * **Sunrise & Sunset Times:** Optionally show daily sunrise and sunset times, with 12-hour and 24-hour format options. 34 36 * **Lightweight:** Enqueues assets only when needed and does not rely on heavy JavaScript libraries. 35 37 * **Settings Page Coordinate Finder:** An easy-to-use tool on the settings page retrieves coordinates by location name and generates ready-to-use widget `<div>` code. … … 85 87 The plugin's JavaScript will automatically find this element and populate it with the forecast. 86 88 89 **Using the Shortcode (Classic Editor & Widgets)** 90 91 You can also display the weather by using the `[under_the_weather]` shortcode. This is ideal for the Classic Editor, text widgets, or other page builders. 92 93 **Available attributes:** 94 * `lat`: (Required) The latitude for the forecast. 95 * `lon`: (Required) The longitude for the forecast. 96 * `location_name`: (Required) The name to display for the location. 97 * `unit`: (Optional) The unit system. Accepts `metric` or `imperial`. Defaults to `imperial`. 98 99 **Example:** 100 [under_the_weather lat="48.8566" lon="2.3522" location_name="Paris, France" unit="metric"] 101 87 102 == Configuration == 88 103 … … 92 107 93 108 **Cache Expiration Time:** 94 This setting controls how long the weather data is stored on your server before fetching a new forecast. 109 Use the slider to set the maximum time weather data is stored before fetching a new forecast, from 30 minutes to 8 hours. 110 111 The plugin also features a **smart caching** system that automatically ensures the cache expires after midnight in the location's local timezone. This prevents showing a stale forecast from the previous day, regardless of your slider setting. 112 95 113 For displaying live conditions (using the **Primary Display** or **Extra Details** options), a shorter cache time of 1 or 2 hours is recommended. 96 For displaying only the daily high/low, a longer cache time of 3 or 6hours is effective at reducing API calls.114 For displaying only the daily high/low, a longer cache time of 3 or 8 hours is effective at reducing API calls. 97 115 98 116 **Widget Display & Style** … … 108 126 109 127 **Extra Details:** 110 Selecting this option will **display 'Feels Like' and wind** (direction and speed) information beneath the primary display. This setting adds nuance to the current weather conditions display. 128 Selecting this option will **display 'Feels Like' and wind** (direction and speed) information beneath the primary display. This setting adds nuance to the current weather conditions display. 129 130 **Sunrise & Sunset:** This setting allows you to display the local sunrise and sunset times for the location, which is useful for planning outdoor activities. Choose to show the times in a 12-hour (e.g., 6:30 AM) or 24-hour (e.g., 18:30) format. 131 132 **Weather Alerts:** When enabled, the widget will display any active severe weather alerts (e.g., thunderstorm warnings, flood advisories) issued by official authorities for the specified location. This provides critical, at-a-glance information for your visitors. 111 133 112 134 **Display Timestamp:** … … 181 203 No. To retrieve fresh weather data every time a widget page loads, you can uncheck "Enable Cache" under the plugin's advanced settings. The caching system provides a great benefit for reducing API hits, but turning off this function during your initial widget setup may be useful. 182 204 205 = Will my website ever show yesterday's weather If I set a long cache time? = 206 207 Cinderella's magic disappears at midnight and weather caches expire at midnight too. Visitors should never see a cache of the previous day's forecast. 208 209 For example, if you set the cache expiration time to 8 hours and a weather cache is created at 10 p.m. on a Friday (using the weather location's time), that cache will expire at midnight, and someone visiting the site the next day at 5 a.m. will not see the previous day's cache even though fewer than 8 hours have passed. 210 211 The plugin uses whichever expiration time is **shorter** to provide the most effective caching. You control the maximum cache duration with the "Cache Expiration Time" slider. However, to ensure your visitors never see yesterday's weather, the plugin also calculates the time until midnight in the widget's local timezone. If the time until midnight is shorter than your slider setting, the cache will expire at midnight. 212 183 213 = The weather isn't updating. Why? = 184 214 … … 207 237 = Can I still use the manual div method if I prefer it? = 208 238 209 Absolutely! The traditional method of adding `<div class="weather-widget">` with data attributes still works perfectly. The new block is simply an additional, more user-friendly option for those using the WordPress block editor. 210 211 The `<div>` method is particularly useful for theme developers and sites that dynamically populate widget attributes from post meta or custom fields. 239 Absolutely! While the **block** is the recommended, user-friendly method for the modern WordPress editor, the plugin fully supports traditional methods for maximum flexibility. 240 241 You can use the `[under_the_weather]` shortcode to easily place the widget in the Classic Editor, text widgets, or with various page builders. 242 243 Additionally, the manual `<div>` method still works perfectly. It is particularly useful for theme developers who need to integrate the widget directly into template files or dynamically populate its data from custom fields. 244 245 The traditional method of adding `<div class="weather-widget">` with data attributes still works perfectly and is particularly useful for theme developers and sites that dynamically populate widget attributes from post meta or custom fields. 212 246 213 247 = What coordinate format should I use? = … … 220 254 221 255 If you're unsure what coordinates to use, the **Coordinate Finder** tool is the best way to retrieve accurate coordinates in the correct format. 256 257 = Where do the weather alerts come from? = 258 259 The alerts are provided directly by the OpenWeather API, which sources them from official meteorological agencies in each country. This ensures the information is timely and authoritative. 222 260 223 261 = What does the "Enable Rate Limiting" setting do? = … … 263 301 6. The Coordinate Finder tool, which generates widget code from a location name. 264 302 7. The "Under The Weather Forecast" block in the WordPress editor. 303 8. The weather widget with Weather Alerts shown 304 9. The weather widget with Sunrise and Sunset times shown 265 305 266 306 == Credits == … … 288 328 289 329 == Changelog == 330 331 = 2.2 = 332 * NEW: Introduced a `[under_the_weather]` shortcode to allow for easy placement of the weather widget in the Classic Editor, text widgets, and other page builders. 333 * NEW: Added a display option to show the day's sunrise time and sunset time, helpful in scheduling outdoor activities. 334 * NEW: Added an option to display severe weather alerts from official authorities directly within the widget. This feature can be enabled on the plugin's settings page. 335 * IMPROVEMENT: Incorporated clear warning icons for severe weather alerts. 336 * IMPROVEMENT: The plugin can now handle multiple weather widgets on a single page 337 * IMPROVEMENT: The front-end widget now loads its data asynchronously (AJAX). This improves perceived page load performance and allows multiple widgets on the same page to load their data independently. 338 * IMPROVEMENT: The settings page now features a Cache Expiration Time slider that allows greater flexibility and provides users with a visual way to select how long cached weather should be saved. 339 * NEW: Midnight expiration is now built into the Cache Expiration logic, so you never have to worry about displaying a cached copy of yesterday's forecast. 290 340 291 341 = 2.1 = … … 387 437 = 2.0 = 388 438 This version includes a "Under The Weather Forecast" block for the WordPress block editor. 439 440 = 2.2 = 441 This version introduces a new `[under_the_weather]` shortcode for easy widget placement and adds options to display severe weather alerts and daily sunrise/sunset times. -
under-the-weather/tags/2.2/under-the-weather.php
r3369072 r3371900 4 4 * Plugin URI: https://www.sethcreates.com/plugins-for-wordpress/under-the-weather/ 5 5 * Description: A lightweight weather widget that caches OpenWeather API data and offers multiple style options. 6 * Version: 2. 16 * Version: 2.2 7 7 * Author: Seth Smigelski 8 8 * Author URI: https://www.sethcreates.com/plugins-for-wordpress/ … … 15 15 16 16 // Define a constant for the plugin version for easy maintenance. 17 define( 'UNDER_THE_WEATHER_VERSION', '2.1.0' ); 18 17 define( 'UNDER_THE_WEATHER_VERSION', '2.2.0' ); 18 19 // Add the Under The Weather Forecast block. 19 20 add_action('init', 'under_the_weather_register_widget_block'); 20 21 function under_the_weather_register_widget_block() { 21 22 register_block_type( __DIR__ . '/build' ); 23 } 24 25 // Add shortcode support. 26 add_action( 'init', 'under_the_weather_register_shortcode' ); 27 function under_the_weather_register_shortcode() { 28 add_shortcode( 'under_the_weather', 'under_the_weather_shortcode_callback' ); 22 29 } 23 30 … … 51 58 add_settings_section('under_the_weather_settings_section', __('API & Cache Settings', 'under-the-weather'), 'under_the_weather_settings_section_callback', $page_slug); 52 59 add_settings_field('under_the_weather_api_key', __('OpenWeather API Key', 'under-the-weather'), 'under_the_weather_api_key_field_html', $page_slug, 'under_the_weather_settings_section'); 53 add_settings_field('under_the_weather_expiration', __('Cache Expiration Time', 'under-the-weather'), 'under_the_weather_expiration_field_html', $page_slug, 'under_the_weather_settings_section'); 60 add_settings_field('under_the_weather_expiration', __('Cache Expiration Time (Hours)', 'under-the-weather'), 'under_the_weather_expiration_field_html', $page_slug, 'under_the_weather_settings_section'); 61 62 // Extra save button field (with empty label) 63 add_settings_field('under_the_weather_section_save', '', 'under_the_weather_save_button_callback', $page_slug, 'under_the_weather_settings_section'); 54 64 55 65 // Section for controlling widget display and style … … 59 69 add_settings_field('under_the_weather_display_mode', __('Primary Display', 'under-the-weather'), 'under_the_weather_display_mode_field_html', $page_slug, 'under_the_weather_display_section'); 60 70 add_settings_field('under_the_weather_forecast_days', __('Number of Forecast Days', 'under-the-weather'), 'under_the_weather_forecast_days_field_html', $page_slug, 'under_the_weather_display_section'); 71 add_settings_field('under_the_weather_show_unit', __('Unit Symbol', 'under-the-weather'), 'under_the_weather_show_unit_field_html', $page_slug, 'under_the_weather_display_section'); 61 72 add_settings_field('under_the_weather_show_details', __('Extra Details', 'under-the-weather'), 'under_the_weather_show_details_field_html', $page_slug, 'under_the_weather_display_section'); 62 add_settings_field('under_the_weather_show_unit', __('Display Unit Symbol', 'under-the-weather'), 'under_the_weather_show_unit_field_html', $page_slug, 'under_the_weather_display_section'); 63 add_settings_field('under_the_weather_show_timestamp', __('Display Timestamp', 'under-the-weather'), 'under_the_weather_show_timestamp_field_html', $page_slug, 'under_the_weather_display_section'); 73 add_settings_field('under_the_weather_sunrise_sunset', __('Sunrise & Sunset', 'under-the-weather'), 'under_the_weather_sunrise_sunset_field_html', $page_slug, 'under_the_weather_display_section'); 74 add_settings_field('under_the_weather_show_alerts', __('Weather Alerts', 'under-the-weather'), 'under_the_weather_show_alerts_field_html', $page_slug, 'under_the_weather_display_section'); 75 add_settings_field('under_the_weather_show_timestamp', __('Timestamps', 'under-the-weather'), 'under_the_weather_show_timestamp_field_html', $page_slug, 'under_the_weather_display_section'); 64 76 65 77 // Section for "Advanced Settings" … … 92 104 * Sanitize and validate all settings before saving to the database. 93 105 */ 106 107 // Sanitize the API key 94 108 function under_the_weather_sanitize_settings($input) { 95 109 $new_input = []; 110 111 // Sanitize the API key 96 112 if (isset($input['api_key'])) { 97 113 $api_key = sanitize_text_field($input['api_key']); 98 if (empty($api_key) || under_the_weather_validate_api_key($api_key)) { 114 if (empty($api_key)) { 115 $new_input['api_key'] = ''; // Allow clearing 116 } elseif (under_the_weather_validate_api_key($api_key)) { 99 117 $new_input['api_key'] = $api_key; 100 118 } else { 101 119 add_settings_error('under_the_weather_settings', 'invalid_api_key', 102 120 __('Invalid API key format. Please check your OpenWeather API key.', 'under-the-weather')); 121 // Keep the old value if new one is invalid 122 $old_options = get_option('under_the_weather_settings'); 123 $new_input['api_key'] = isset($old_options['api_key']) ? $old_options['api_key'] : ''; 103 124 } 104 125 } 105 if (isset($input['expiration']) && in_array($input['expiration'], ['1','2','3','6'])) { $new_input['expiration'] = $input['expiration']; } 106 if (isset($input['style_set']) && in_array($input['style_set'], ['default_images', 'weather_icons_font'])) { $new_input['style_set'] = $input['style_set']; } 107 if (isset($input['display_mode']) && in_array($input['display_mode'], ['current', 'today_forecast'])) { $new_input['display_mode'] = $input['display_mode']; } 108 if (isset($input['forecast_days']) && in_array($input['forecast_days'], ['2','3','4','5','6'])) { $new_input['forecast_days'] = $input['forecast_days']; } 109 110 // sanitization for Rate Limit options 126 127 // Sanitize the expiration time from the range slider 128 if (isset($input['expiration']) && is_numeric($input['expiration'])) { 129 $expiration = floatval($input['expiration']); 130 // Ensure the value is within the allowed range (0.5 to 8) 131 if ($expiration >= 0.5 && $expiration <= 8) { 132 $new_input['expiration'] = $expiration; 133 } else { 134 // If out of range, enforce minimum of 0.5 135 $new_input['expiration'] = max(0.5, min(8, $expiration)); 136 add_settings_error('under_the_weather_settings', 'expiration_adjusted', 137 __('Cache expiration time adjusted to minimum of 0.5 hours (30 minutes).', 'under-the-weather'), 'updated'); 138 } 139 } else { 140 $new_input['expiration'] = 4; // Default to 4 if not numeric 141 } 142 143 // Sanitize the Rate Limit options 111 144 $new_input['enable_rate_limit'] = isset($input['enable_rate_limit']) ? '1' : '0'; 112 145 if (isset($input['rate_limit_count']) && is_numeric($input['rate_limit_count'])) { … … 117 150 } 118 151 152 // Sanitize the Sunrise and Sunset preference with 12 hour time format and 24 hour time format 153 if (isset($input['sunrise_sunset']) && in_array($input['sunrise_sunset'], ['off', '12', '24'])) { 154 $new_input['sunrise_sunset'] = $input['sunrise_sunset']; 155 } else { 156 $new_input['sunrise_sunset'] = 'off'; 157 } 158 159 // Sanitize the major display options 160 if (isset($input['style_set']) && in_array($input['style_set'], ['default_images', 'weather_icons_font'])) { $new_input['style_set'] = $input['style_set']; } 161 if (isset($input['display_mode']) && in_array($input['display_mode'], ['current', 'today_forecast'])) { $new_input['display_mode'] = $input['display_mode']; } 162 if (isset($input['forecast_days']) && in_array($input['forecast_days'], ['2','3','4','5','6'])) { $new_input['forecast_days'] = $input['forecast_days']; } 163 119 164 $new_input['show_details'] = isset($input['show_details']) ? '1' : '0'; 120 165 $new_input['show_unit'] = isset($input['show_unit']) ? '1' : '0'; 166 $new_input['show_alerts'] = isset($input['show_alerts']) ? '1' : '0'; 121 167 $new_input['show_timestamp'] = isset($input['show_timestamp']) ? '1' : '0'; 122 168 $new_input['enable_cache'] = isset($input['enable_cache']) ? '1' : '0'; … … 148 194 <p><em><?php esc_html_e('Default Images', 'under-the-weather'); ?></em></p> 149 195 </div> 150 <div style="text-align: center;">196 <div class="under-the-weather-visual-reference-item"> 151 197 <img src="<?php echo esc_url($plugin_assets_url . 'font-style-example.svg'); ?>" alt="Weather Icons Font Style Example"> 152 198 <p><em><?php esc_html_e('Weather Icons Font', 'under-the-weather'); ?></em></p> … … 159 205 $options = get_option('under_the_weather_settings'); $value = isset($options['api_key']) ? $options['api_key'] : ''; echo '<input type="text" name="under_the_weather_settings[api_key]" value="' . esc_attr($value) . '" class="regular-text" placeholder="' . esc_attr__('Enter your API key', 'under-the-weather') . '">'; 160 206 } 161 function under_the_weather_expiration_field_html() { 162 $options = get_option('under_the_weather_settings'); $value = isset($options['expiration']) ? $options['expiration'] : '2'; echo '<select name="under_the_weather_settings[expiration]"><option value="1" '.selected($value, '1', false).'>' . esc_html__('1 Hour', 'under-the-weather') . '</option><option value="2" '.selected($value, '2', false).'>' . esc_html__('2 Hours', 'under-the-weather') . '</option><option value="3" '.selected($value, '3', false).'>' . esc_html__('3 Hours', 'under-the-weather') . '</option><option value="6" '.selected($value, '6', false).'>' . esc_html__('6 Hours', 'under-the-weather') . '</option></select>'; 207 208 // Use a slider to set the cache expiration time. Default: 4 hours. 209 function under_the_weather_expiration_field_html() { 210 $options = get_option('under_the_weather_settings'); 211 $value = isset($options['expiration']) ? $options['expiration'] : '4'; 212 ?> 213 <div class="utw-slider-container"> 214 <div class="utw-slider-wrapper"> 215 <input 216 type="range" 217 id="utw-expiration-slider" 218 name="under_the_weather_settings[expiration]" 219 value="<?php echo esc_attr($value); ?>" 220 min="0" 221 max="8" 222 step="0.5" 223 list="expiration-markers" 224 data-original-value="<?php echo esc_attr($value); ?>" 225 > 226 <datalist id="expiration-markers"> 227 <option value="0" label="0"></option> 228 <option value="1"></option> <option value="2" label="2"></option> 229 <option value="3"></option> <option value="4" label="4"></option> 230 <option value="5"></option> <option value="6" label="6"></option> 231 <option value="7"></option> <option value="8" label="8"></option> 232 </datalist> 233 </div> 234 </div> 235 <div class="utw-expiration-display"> 236 <span class="utw-slider-value"> 237 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 256 256"><path fill="#48484A" d="M128 44a96 96 0 1 0 96 96a96.11 96.11 0 0 0-96-96m0 168a72 72 0 1 1 72-72a72.08 72.08 0 0 1-72 72m36.49-112.49a12 12 0 0 1 0 17l-28 28a12 12 0 0 1-17-17l28-28a12 12 0 0 1 17 0M92 16a12 12 0 0 1 12-12h48a12 12 0 0 1 0 24h-48a12 12 0 0 1-12-12"/></svg> Cached weather will expire after <strong id="utw-expiration-value"><?php echo esc_html($value); ?></strong> hours. 238 </span> 239 </div> 240 <p id="utw-min-cache-notice" class="description"> 241 <?php esc_html_e('Minimum cache time is 30 minutes. To disable caching, use the advanced settings below.', 'under-the-weather'); ?> 242 </p> 243 <?php 244 } 245 // Add an extra save settigns button, just below the expiration slider. 246 function under_the_weather_save_button_callback() { 247 ?> 248 <div class="utw-section-save-wrapper"> 249 <?php submit_button( 250 __('Save Settings', 'under-the-weather'), 251 'primary', 252 'submit', 253 false, 254 ['id' => 'utw-expiration-save-btn'] 255 ); ?> 256 </div> 257 <div id="utw-unsaved-changes" class="utw-unsaved-message"> 258 <?php esc_html_e('You have unsaved changes', 'under-the-weather'); ?> 259 260 </div> 261 <?php 163 262 } 164 263 function under_the_weather_style_set_field_html() { … … 175 274 } 176 275 function under_the_weather_show_unit_field_html() { 177 $options = get_option('under_the_weather_settings'); $value = isset($options['show_unit']) ? $options['show_unit'] : '0'; echo "<input type='checkbox' name='under_the_weather_settings[show_unit]' value='1' " . checked($value, '1', false) . "> " . esc_html__('Show the temperature unit symbol (F or C) in the primary display.', 'under-the-weather'); 276 $options = get_option('under_the_weather_settings'); $value = isset($options['show_unit']) ? $options['show_unit'] : '0'; echo "<input type='checkbox' name='under_the_weather_settings[show_unit]' value='1' " . checked($value, '1', false) . "> " . esc_html__('Show the temperature unit symbol (F or C) in the primary display.', 'under-the-weather'); 277 } 278 function under_the_weather_show_alerts_field_html() { 279 $options = get_option('under_the_weather_settings'); 280 // Default to '1' (checked) to make the new feature visible 281 $value = isset($options['show_alerts']) ? $options['show_alerts'] : '1'; 282 echo "<input type='checkbox' name='under_the_weather_settings[show_alerts]' value='1' " . checked($value, '1', false) . "> " . esc_html__('Show active weather alerts from reporting authorities.', 'under-the-weather'); 283 } 284 function under_the_weather_sunrise_sunset_field_html() { 285 $options = get_option('under_the_weather_settings'); 286 $value = isset($options['sunrise_sunset']) ? $options['sunrise_sunset'] : 'off'; // Default to 'off' 287 ?> 288 <fieldset> 289 <label><input type="radio" name="under_the_weather_settings[sunrise_sunset]" value="off" <?php checked($value, 'off'); ?>> <?php esc_html_e('Off', 'under-the-weather'); ?></label><br> 290 <label><input type="radio" name="under_the_weather_settings[sunrise_sunset]" value="12" <?php checked($value, '12'); ?>> <?php esc_html_e('Show in 12-hour format (e.g., 6:30 PM)', 'under-the-weather'); ?></label><br> 291 <label><input type="radio" name="under_the_weather_settings[sunrise_sunset]" value="24" <?php checked($value, '24'); ?>> <?php esc_html_e('Show in 24-hour format (e.g., 18:30)', 'under-the-weather'); ?></label> 292 </fieldset> 293 <?php 178 294 } 179 295 function under_the_weather_show_timestamp_field_html() { … … 211 327 <?php esc_html_e('Find Coordinates', 'under-the-weather'); ?> 212 328 </button> 213 <div id="utw-result-wrapper" style="margin-top: 15px;">329 <div id="utw-result-wrapper"> 214 330 </div> 215 331 </div> … … 361 477 function under_the_weather_enqueue_assets() { 362 478 $options = get_option('under_the_weather_settings'); 363 if (empty($options)) return; 364 479 if (empty($options)) return; 480 481 // Register the main style so WordPress knows about it. 482 wp_register_style('under-the-weather-styles', plugins_url('css/under-the-weather.min.css', __FILE__), [], UNDER_THE_WEATHER_VERSION); 483 484 // Register dependent icon styles. 485 if (isset($options['style_set']) && $options['style_set'] === 'weather_icons_font') { 486 wp_register_style('under-the-weather-icons', plugins_url('css/weather-icons.min.css', __FILE__), [], '2.0'); 487 if (!empty($options['show_details'])) { 488 wp_register_style('under-the-weather-wind-icons', plugins_url('css/weather-icons-wind.min.css', __FILE__), [], '2.0'); 489 } 490 } 491 492 // Conditionally ENQUEUE based on the global setting. 365 493 if (!empty($options['enqueue_style'])) { 366 wp_enqueue_style('under-the-weather-styles', plugins_url('css/under-the-weather.min.css', __FILE__), [], UNDER_THE_WEATHER_VERSION);494 wp_enqueue_style('under-the-weather-styles'); 367 495 if (isset($options['style_set']) && $options['style_set'] === 'weather_icons_font') { 368 wp_enqueue_style('under-the-weather-icons' , plugins_url('css/weather-icons.min.css', __FILE__), [], '2.0');496 wp_enqueue_style('under-the-weather-icons'); 369 497 if (!empty($options['show_details'])) { 370 wp_enqueue_style('under-the-weather-wind-icons' , plugins_url('css/weather-icons-wind.min.css', __FILE__), [], '2.0');498 wp_enqueue_style('under-the-weather-wind-icons'); 371 499 } 372 500 } … … 467 595 'forecast_days' => isset($options['forecast_days']) ? intval($options['forecast_days']) : 5, 468 596 'show_details' => !empty($options['show_details']), 597 'show_alerts' => !empty($options['show_alerts']), 598 'sunrise_sunset_format' => isset($options['sunrise_sunset']) ? $options['sunrise_sunset'] : 'off', 469 599 'show_timestamp' => !empty($options['show_timestamp']), 470 600 'show_unit' => !empty($options['show_unit']), … … 477 607 wp_localize_script('under-the-weather-script', 'under_the_weather_plugin_url', $plugin_url_data); 478 608 } 479 480 609 481 610 // ============================================================================= … … 787 916 $api_url = "https://api.openweathermap.org/data/3.0/onecall?lat={$lat}&lon={$lon}&appid={$api_key}&units={$unit}"; 788 917 789 // Check API Response918 // Check API Response 790 919 $response_body = under_the_weather_safe_api_call($api_url); 791 920 if ($response_body === false) { … … 793 922 } 794 923 795 $weather_data = json_decode($response_body); 924 $weather_data = json_decode($response_body); 925 796 926 under_the_weather_update_usage_stats('api'); 797 927 … … 811 941 $weather_data->daily[array_search($day, $weather_data->daily)]->weather[0]->icon_class = under_the_weather_get_icon_class($day->weather[0]->icon); 812 942 } 813 } 814 815 if ($caching_enabled) { 816 set_transient($transient_key, $weather_data, $expiration_hours * HOUR_IN_SECONDS); 817 } 943 } 944 945 // Establish midnight cache expiration logic, so the previous day's weather is not shown from the cache 946 // Incorporate midnight cache expiration with timed cache expiration preference 947 // Add a 10-minute buffer to avoid caching the previous day's forecast at midnight due to service clock differences. 948 // This is like treating 12:10 a.m. as midnight as a precaution 949 950 if ($caching_enabled) { 951 // Enforce minimum cache time of 30 minutes 952 $expiration_hours = isset($options['expiration']) ? floatval($options['expiration']) : 4; 953 $expiration_hours = max(0.5, $expiration_hours); 954 955 $midnight_expiration_seconds = 0; 956 957 if (isset($weather_data->timezone) && is_string($weather_data->timezone)) { 958 try { 959 $timezone_obj = new DateTimeZone($weather_data->timezone); 960 $now = new DateTime('now', $timezone_obj); 961 $midnight = new DateTime('tomorrow midnight', $timezone_obj); 962 $seconds_until_midnight = $midnight->getTimestamp() - $now->getTimestamp(); 963 $midnight_expiration_seconds = $seconds_until_midnight + (10 * 60); 964 } catch (Exception $e) { 965 under_the_weather_log('Invalid timezone from API: ' . $weather_data->timezone); 966 $midnight_expiration_seconds = 0; 967 } 968 } 969 970 // Calculate fixed duration 971 $fixed_duration_seconds = $expiration_hours * HOUR_IN_SECONDS; 972 973 // Use the shorter of: fixed duration or midnight expiration 974 if ($midnight_expiration_seconds > 0) { 975 $final_expiration_seconds = min($fixed_duration_seconds, $midnight_expiration_seconds); 976 } else { 977 $final_expiration_seconds = $fixed_duration_seconds; 978 } 979 980 // Final safety check: ensure at least 30 minutes 981 $final_expiration_seconds = max(1800, $final_expiration_seconds); 982 983 set_transient($transient_key, $weather_data, $final_expiration_seconds); 984 } 985 818 986 819 987 return new WP_REST_Response($weather_data, 200); 988 } 989 990 /** 991 * 992 * Callback function for the [under_the_weather] shortcode. 993 * @param array $atts Shortcode attributes. 994 * @return string HTML output for the weather widget. 995 * 996 */ 997 function under_the_weather_shortcode_callback( $atts ) { 998 // 1. Define attributes and set default values. 999 $atts = shortcode_atts( 1000 array( 1001 'lat' => '', 1002 'lon' => '', 1003 'location_name' => '', 1004 'unit' => 'imperial', // Default to imperial 1005 ), 1006 $atts, 1007 'under_the_weather' 1008 ); 1009 1010 // 2. Validate that essential attributes are present. 1011 if ( empty( $atts['lat'] ) || empty( $atts['lon'] ) || empty( $atts['location_name'] ) ) { 1012 // Return an error message or an empty string if essential data is missing. 1013 return ''; 1014 } 1015 1016 // 3. Enqueue the necessary scripts and styles. 1017 // This ensures they are only loaded on pages where the shortcode is used. 1018 wp_enqueue_style( 'under-the-weather-styles' ); 1019 // We also need to enqueue the icon styles if they are selected in the settings. 1020 $options = get_option('under_the_weather_settings'); 1021 if (isset($options['style_set']) && $options['style_set'] === 'weather_icons_font') { 1022 wp_enqueue_style('under-the-weather-icons'); 1023 if (!empty($options['show_details'])) { 1024 wp_enqueue_style('under-the-weather-wind-icons'); 1025 } 1026 } 1027 under_the_weather_load_scripts_manually(); 1028 1029 // 4. Build and return the HTML div. 1030 return sprintf( 1031 '<div class="weather-widget" data-lat="%s" data-lon="%s" data-location-name="%s" data-unit="%s"></div>', 1032 esc_attr( $atts['lat'] ), 1033 esc_attr( $atts['lon'] ), 1034 esc_attr( $atts['location_name'] ), 1035 esc_attr( $atts['unit'] ) 1036 ); 820 1037 } 821 1038 -
under-the-weather/trunk/README.md
r3369072 r3371900 8 8 * **Requires at least:** 5.0 9 9 * **Tested up to:** 6.8 10 * **Stable tag:** 2. 110 * **Stable tag:** 2.2 11 11 * **Requires PHP:** 7.2 12 12 * **License:** GPLv2 or later … … 31 31 * **Server-Side Caching:** All API calls are cached on your server, dramatically reducing calls to the OpenWeather API and speeding up page loads for all users. 32 32 * **Visual Performance Report:** Monitor your site's API usage with a bar chart that displays a 7-day history of cached requests versus new calls to the OpenWeather API - a clear look at how the caching system is working to keep your site fast and your API calls low. 33 * **Highly Customizable:** Use the detailed settings page to control everything from cache duration to the number of forecast days. 34 * **Flexible Display:** Show either the current live weather or the high/low forecast for the current day. 33 * **Customizable Display:** Use the main display to show either the current live weather or the high/low forecast for the current day, and set the number of days to include in the forecast ahead. 35 34 * **Imperial & Metric Units:** Display weather in Fahrenheit/mph or Celsius/kph on a per-widget basis. 36 35 * **Extra Details:** Optionally display "Feels Like" temperature and detailed wind information. 36 * **Weather Alerts:** Display official severe weather alerts directly in the widget to keep visitors informed. 37 * **Sunrise & Sunset Times:** Optionally show daily sunrise and sunset times, with 12-hour and 24-hour format options. 37 38 * **Lightweight:** Enqueues assets only when needed and does not rely on heavy JavaScript libraries. 38 39 * **Settings Page Coordinate Finder:** An easy-to-use tool on the settings page retrieves coordinates by location name and generates ready-to-use widget `<div>` code. … … 112 113 The plugin's JavaScript will automatically find this element and populate it with the forecast. 113 114 115 ### Using the Shortcode (Classic Editor & Widgets) 116 117 You can also display the weather by using the `[under_the_weather]` shortcode. This is ideal for the Classic Editor, text widgets, or other page builders. 118 119 **Available attributes:** 120 * `lat`: (Required) The latitude for the forecast. 121 * `lon`: (Required) The longitude for the forecast. 122 * `location_name`: (Required) The name to display for the location. 123 * `unit`: (Optional) The unit system. Accepts `metric` or `imperial`. Defaults to `imperial`. 124 125 **Example:** 126 `[under_the_weather lat="48.8566" lon="2.3522" location_name="Paris, France" unit="metric"]` 127 114 128 --- 115 129 … … 121 135 122 136 **Cache Expiration Time:** 123 This setting controls how long the weather data is stored on your server before fetching a new forecast. 137 Use the slider to set the maximum time weather data is stored before fetching a new forecast, from 30 minutes to 8 hours. 138 139 The plugin also features a **smart caching** system that automatically ensures the cache expires after midnight in the location's local timezone. This prevents showing a stale forecast from the previous day, regardless of your slider setting. 140 124 141 For displaying live conditions (using the **Primary Display** or **Extra Details** options), a shorter cache time of 1 or 2 hours is recommended. 125 For displaying only the daily high/low, a longer cache time of 3 or 6hours is effective at reducing API calls.142 For displaying only the daily high/low, a longer cache time of 3 or 8 hours is effective at reducing API calls. 126 143 127 144 **Widget Display & Style** … … 137 154 138 155 **Extra Details:** 139 Selecting this option will **display 'Feels Like' and wind** (direction and speed) information beneath the primary display. This setting adds nuance to the current weather conditions display. 156 Selecting this option will **display 'Feels Like' and wind** (direction and speed) information beneath the primary display. This setting adds nuance to the current weather conditions display. 157 158 **Sunrise & Sunset:** This setting allows you to display the local sunrise and sunset times for the location, which is useful for planning outdoor activities. Choose to show the times in a 12-hour (e.g., 6:30 AM) or 24-hour (e.g., 18:30) format. 159 160 **Weather Alerts:** When enabled, the widget will display any active severe weather alerts (e.g., thunderstorm warnings, flood advisories) issued by official authorities for the specified location. This provides critical, at-a-glance information for your visitors. 161 140 162 141 163 **Display Timestamp:** … … 235 257 No. To retrieve fresh weather data every time a widget page loads, you can uncheck "Enable Cache" under the plugin's advanced settings. The caching system provides a great benefit for reducing API hits, but turning off this function during your initial widget setup may be useful. 236 258 259 ### Will my website ever show yesterday's weather If I set a long cache time? 260 261 Cinderella's magic disappears at midnight, and weather caches expire at midnight too. Visitors should never see a cache of the previous day's forecast. 262 263 For example, if you set the cache expiration time to 8 hours and a weather cache is created at 10 p.m. on a Friday (using the weather location's time), that cache will expire at midnight, and someone visiting the site the next day at 5 a.m. will not see the previous day's cache even though fewer than 8 hours have passed. 264 265 The plugin uses whichever expiration time is **shorter** to provide the most effective caching. You control the maximum cache duration with the "Cache Expiration Time" slider. However, to ensure your visitors never see yesterday's weather, the plugin also calculates the time until midnight in the widget's local timezone. If the time until midnight is shorter than your slider setting, the cache will expire at midnight. 266 237 267 ### The weather isn't updating. Why? 238 268 … … 261 291 ### Can I still use the manual div method if I prefer it? 262 292 263 Absolutely! The traditional method of adding `<div class="weather-widget">` with data attributes still works perfectly. The new block is simply an additional, more user-friendly option for those using the WordPress block editor. 264 265 The `<div>` method is particularly useful for theme developers and sites that dynamically populate widget attributes from post meta or custom fields. 293 Absolutely! While the **block** is the recommended, user-friendly method for the modern WordPress editor, the plugin fully supports traditional methods for maximum flexibility. 294 295 You can use the `[under_the_weather]` shortcode to easily place the widget in the Classic Editor, text widgets, or with various page builders. 296 297 Additionally, the manual `<div>` method still works perfectly. It is particularly useful for theme developers who need to integrate the widget directly into template files or dynamically populate its data from custom fields. 298 299 The traditional method of adding `<div class="weather-widget">` with data attributes still works perfectly and is particularly useful for theme developers and sites that dynamically populate widget attributes from post meta or custom fields. 266 300 267 301 ### What coordinate format should I use? … … 274 308 275 309 If you're unsure what coordinates to use, the **Coordinate Finder** tool is the best way to retrieve accurate coordinates in the correct format. 310 311 ### Where do the weather alerts come from? 312 313 The alerts are provided directly by the OpenWeather API, which sources them from official meteorological agencies in each country. This ensures the information is timely and authoritative. 276 314 277 315 ### What does the "Enable Rate Limiting" setting do? … … 326 364 _The weather widget displaying current conditions with default icons (in Celsius) and extra details enabled._ 327 365 366  367 368 _The weather widget with Weather Alerts shown._ 369 370  371 372 _The weather widget with Sunrise and Sunset times shown._ 373 328 374 --- 329 375 … … 357 403 ## Changelog 358 404 405 ### 2.2 406 * **NEW:** Introduced a `[under_the_weather]` shortcode to allow for easy placement of the weather widget in the Classic Editor, text widgets, and other page builders. 407 * **NEW:** Added a display option to show the day's sunrise time and sunset time, helpful in scheduling outdoor activities. 408 * **NEW:** Added an option to display severe weather alerts from official authorities directly within the widget. This feature can be enabled on the plugin's settings page. 409 * **IMPROVEMENT:** Incorporated clear warning icons for severe weather alerts. 410 * **IMPROVEMENT:** The plugin can now handle multiple weather widgets on a single page 411 * **IMPROVEMENT:** The front-end widget now loads its data asynchronously (AJAX). This improves perceived page load performance and allows multiple widgets on the same page to load their data independently. 412 * **IMPROVEMENT:** The settings page now features a Cache Expiration Time slider that allows greater flexibility and provides users with a visual way to select how long cached weather should be saved. 413 * **NEW:** Midnight expiration is now built into the Cache Expiration logic, so you never have to worry about displaying a cached copy of yesterday's forecast. 414 359 415 ### 2.1 360 416 * **NEW:** The block editor can now parse and automatically convert coordinates from common formats like DMS (Degrees, Minutes, Seconds) and DDM (Degrees, Decimal Minutes) into the required decimal format. … … 457 513 ### 2.0 458 514 This version includes a "Under The Weather Forecast" block for the WordPress block editor. 515 516 ### 2.2 517 This version introduces a new `[under_the_weather]` shortcode for easy widget placement and adds options to display severe weather alerts and daily sunrise/sunset times. -
under-the-weather/trunk/build/block.json
r3369072 r3371900 3 3 "apiVersion": 3, 4 4 "name": "under-the-weather/widget", 5 "version": "2. 1.0",5 "version": "2.2.0", 6 6 "title": "Under The Weather Forecast", 7 7 "category": "widgets", -
under-the-weather/trunk/css/admin-styles.css
r3365104 r3371900 23 23 max-width: 100px; 24 24 } 25 .under-the-weather-visual-reference-default-image{filter:drop-shadow(1px 2px 3px #555);} 26 27 28 29 /* Usage Report Chart Styling */ 25 .under-the-weather-visual-reference-default-image{ 26 filter:drop-shadow(1px 2px 3px #555); 27 } 28 29 /* --- Usage Report Chart Styling --- */ 30 30 31 .under-the-weather-usage-report { 31 32 display: flex; … … 133 134 margin: 15px 0; 134 135 } 136 137 /* --- Coordinate Finder --- */ 138 135 139 .weather-widget-coordinate-finder-pre{ 136 140 background: #f1f1f1; … … 142 146 color: red; 143 147 } 148 .utw-result-wrapper{ 149 margin-top:15px 150 } 151 152 /* --- Slider Control Styling --- */ 153 154 .utw-slider-container { 155 display: flex; 156 align-items: center; 157 margin-bottom: 10px; 158 } 159 160 .utw-slider-wrapper { 161 width: 350px; 162 } 163 164 #utw-expiration-slider { 165 width: 100%; 166 margin: 0; 167 } 168 169 /* Styles for the datalist tick marks */ 170 .utw-slider-wrapper datalist { 171 display: flex; 172 justify-content: space-between; 173 width: 100%; 174 padding: 0; 175 margin-top: 5px; 176 color: #50575e; 177 } 178 179 .utw-slider-value svg{ 180 margin-bottom:-6px 181 } 182 183 /* Saved Message */ 184 .utw-section-save-wrapper { 185 padding: 10px 10px 10px 0; 186 width: 200px; 187 line-height: 1.3; 188 font-weight: 600; 189 display: inline-block; 190 vertical-align: middle; 191 } 192 .utw-unsaved-message { 193 display: none; 194 font-weight: 600; 195 display: none; 196 color: #d63638; 197 } 198 .utw-expiration-save-wrapper { 199 display: flex; 200 align-items: center; 201 } 202 203 #utw-min-cache-notice, 204 #utw-expiration-save-btn-wrap th { 205 display: none; 206 } 207 208 #utw-expiration-save-btn-wrap td { 209 padding-left: 0; 210 } -
under-the-weather/trunk/css/admin-styles.min.css
r3365104 r3371900 1 .nav-tab-wrapper{margin-bottom:20px}.under-the-weather-visual-reference-container{display:flex;gap:50px;align-items:flex-start;background:#fff;border:1px solid #999;border-radius:5px;padding:10px 24px 15px 37px;width:288px}.under-the-weather-visual-reference-item{text-align:center}.under-the-weather-visual-reference-item img{max-width:100px}.under-the-weather-visual-reference-default-image{filter:drop-shadow(1px 2px 3px #555)}.under-the-weather-usage-report{display:flex;flex-direction:column;max-width:800px}.under-the-weather-chart-container{display:flex;justify-content:space-around;align-items:flex-end;height:250px;border:1px solid #c3c4c7;padding:10px 0;gap:10px;background:#f9f9f9 ;}.under-the-weather-chart-day{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;text-align:center;position:relative;height:100%}.under-the-weather-chart-bars{display:flex;justify-content:center;align-items:flex-end;height:100%;gap:5px}.under-the-weather-chart-bar{width:30px;background-color:#9ec2e6;border-radius:3px 3px 0 0;transition:height 0.3s ease-in-out;position:relative}.under-the-weather-chart-bar.api,.under-the-weather-legend-color.api{background-color:#f0ad4e}.under-the-weather-chart-bar.cache,.under-the-weather-legend-color.cache{background-color:#5cb85c}.under-the-weather-chart-bar.value{position:absolute;top:-22px;left:50%;transform:translateX(-50%);font-size:12px;font-weight:bold;color:#333}.under-the-weather-chart-day-label{margin-top:12px;font-size:13px;color:#555}.under-the-weather-chart-legend{display:flex;justify-content:center;gap:20px;background:#f9f9f9;border:1px solid #c3c4c7;padding:8px;}.under-the-weather-legend-item{display:flex;align-items:center;gap:8px;font-size:13px}.under-the-weather-legend-color{width:15px;height:15px;border-radius:3px}.under-the-weather-data-table{width:100%;}.under-the-weather-status-box{background:#f9f9f9;padding:15px;border-left:4px solid #0073aa;border-top:1px solid #c3c4c7;border-right:1px solid #c3c4c7;border-bottom:1px solid #c3c4c7;margin:15px 0;}.weather-widget-coordinate-finder-pre{background:#f1f1f1;border-radius:4px;padding:10px;white-space:pre-wrap;}.coordinate-finder-red-message{color:red;}1 .nav-tab-wrapper{margin-bottom:20px}.under-the-weather-visual-reference-container{display:flex;gap:50px;align-items:flex-start;background:#fff;border:1px solid #999;border-radius:5px;padding:10px 24px 15px 37px;width:288px}.under-the-weather-visual-reference-item{text-align:center}.under-the-weather-visual-reference-item img{max-width:100px}.under-the-weather-visual-reference-default-image{filter:drop-shadow(1px 2px 3px #555)}.under-the-weather-usage-report{display:flex;flex-direction:column;max-width:800px}.under-the-weather-chart-container{display:flex;justify-content:space-around;align-items:flex-end;height:250px;border:1px solid #c3c4c7;padding:10px 0;gap:10px;background:#f9f9f9}.under-the-weather-chart-day{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;text-align:center;position:relative;height:100%}.under-the-weather-chart-bars{display:flex;justify-content:center;align-items:flex-end;height:100%;gap:5px}.under-the-weather-chart-bar{width:30px;background-color:#9ec2e6;border-radius:3px 3px 0 0;transition:height .3s ease-in-out;position:relative}.under-the-weather-chart-bar.api,.under-the-weather-legend-color.api{background-color:#f0ad4e}.under-the-weather-chart-bar.cache,.under-the-weather-legend-color.cache{background-color:#5cb85c}.under-the-weather-chart-bar .value{position:absolute;top:-22px;left:50%;transform:translateX(-50%);font-size:12px;font-weight:700;color:#333}.under-the-weather-chart-day-label{margin-top:12px;font-size:13px;color:#555}.under-the-weather-chart-legend{display:flex;justify-content:center;gap:20px;background:#f9f9f9;border:1px solid #c3c4c7;padding:8px}.under-the-weather-legend-item{display:flex;align-items:center;gap:8px;font-size:13px}.under-the-weather-legend-color{width:15px;height:15px;border-radius:3px}.under-the-weather-data-table{width:100%}.under-the-weather-status-box{background:#f9f9f9;padding:15px;border:1px solid #c3c4c7;border-left:4px solid #0073aa;margin:15px 0}.weather-widget-coordinate-finder-pre{background:#f1f1f1;border-radius:4px;padding:10px;white-space:pre-wrap}.coordinate-finder-red-message{color:red}.utw-result-wrapper{margin-top:15px}.utw-slider-container{display:flex;align-items:center;margin-bottom:10px}.utw-slider-wrapper{width:350px}#utw-expiration-slider{width:100%;margin:0}.utw-slider-wrapper datalist{display:flex;justify-content:space-between;width:100%;padding:0;margin-top:5px;color:#50575e}.utw-slider-value svg{margin-bottom:-6px}.utw-section-save-wrapper{padding:10px 10px 10px 0;width:200px;line-height:1.3;font-weight:600;display:inline-block;vertical-align:middle}.utw-unsaved-message{font-weight:600;display:none;color:#d63638}.utw-expiration-save-wrapper{display:flex;align-items:center}#utw-expiration-save-btn-wrap th,#utw-min-cache-notice{display:none}#utw-expiration-save-btn-wrap td{padding-left:0} -
under-the-weather/trunk/css/under-the-weather.css
r3342551 r3371900 1 /* Weather Widget Styling*/1 /* --- Weather Widget Styling --- */ 2 2 .weather-widget { 3 3 min-height: 285px; 4 /*this setting can be removed. However, etting a minimum height helps to reduce browser repaints */4 /*this setting can be removed. However, setting a minimum height helps to reduce browser repaints */ 5 5 } 6 6 7 7 .weather-location-name { 8 8 border-bottom: 2px solid #eee; 9 font-size: 18px; 9 font-size: 1.375em; 10 font-weight: bold; 10 11 margin-bottom: 15px; 11 12 padding-bottom: 7px; … … 54 55 .forecast-day-name { 55 56 color: #444; 56 font-size: 14px;57 font-size: 0.65em; 57 58 margin-bottom: 8px; 58 59 } … … 64 65 } 65 66 66 .forecast-temps {67 color: #fff;68 font-size: 6px;67 .forecast-temps .slash { 68 color: transparent; 69 font-size: 0.375em; 69 70 /*Change the color and size to see the / between high and low */ 70 71 } … … 72 73 .forecast-temps .high { 73 74 color: #000; 74 font-size: 1 7px;75 font-size: 1.06em; /* Converted from 17px */ 75 76 font-weight: 700; 76 77 } 77 78 78 79 .forecast-temps .low { 79 color: # 666;80 font-size: 14px;80 color: #555; 81 font-size: 0.875em; /* Converted from 14px */ 81 82 } 82 83 … … 89 90 90 91 .today-forecast-label { 91 font-size: 1 8px;92 font-size: 1.125em; /* Converted from 18px */ 92 93 margin: 6px 0 11px; 93 94 } 94 95 95 .today-forecast-temps .temps-wrapper {96 color: #fff;97 font-size: 9px;96 .today-forecast-temps .temps-wrapper .slash{ 97 color: transparent; 98 font-size: 0.55em; 98 99 /*Change the color and size to see the / between high and low */ 99 100 } … … 101 102 .today-forecast-temps .high { 102 103 color: #000; 103 font-size: 36px;104 font-size: 2.25em; /* Converted from 36px */ 104 105 font-weight: 700; 105 106 } 106 107 107 108 .today-forecast-temps .low { 109 color: #555; 110 font-size: 1.625em; /* Converted from 26px */ 111 font-weight: 400; 112 } 113 114 .today-forecast-temps .temp-unit { 108 115 color: #666; 109 font-size: 26px; 110 font-weight: 400; 111 } 112 113 .today-forecast-temps .temp-unit { 114 color: #777; 115 font-size: 15px; 116 font-size: 0.94em; /* Converted from 15px */ 116 117 font-weight: 400; 117 118 position: relative; … … 120 121 121 122 .weather-widget .wi { 122 color: # 666;123 color: #555; 123 124 font-size: 45px; 124 125 line-height: 1; … … 131 132 132 133 .weather-extra-details { 133 border-top: 1px solid #eee;134 134 border-bottom: 1px solid #eee; 135 135 color: #555; … … 155 155 color: #999; 156 156 font-size: .8em; 157 margin-top: 1 5px;157 margin-top: 10px; 158 158 text-align: center; 159 } 159 border-top: 1px solid #eee; 160 padding-top: 5px; 161 } 162 163 /* --- Weather Alert Styling --- */ 164 165 .weather-alert { 166 display: flex; 167 align-items: center; /* Vertically aligns the icon with the text block */ 168 background-color: #f0f8ff; 169 border: 1px solid #add8e6; 170 border-radius: 4px; 171 padding: 10px 12px; 172 margin-bottom: 15px; 173 color: #333; 174 } 175 176 /* Left column for the icon */ 177 .weather-alert-icon-left { 178 margin-right: 12px; 179 } 180 181 /* Right column for the text messages */ 182 .weather-alert-message { 183 flex-grow: 1; /* Allows the message area to take up remaining space */ 184 } 185 186 .weather-alert-event { 187 font-weight: bold; 188 margin-bottom: 4px; 189 } 190 191 .weather-alert-sender { 192 font-size: 0.9em; 193 opacity: 0.8; 194 } 195 196 /* Styles for the PNG icon */ 197 .weather-alert-icon-left .weather-alert-icon { 198 width: 32px; /* Slightly larger for better visibility */ 199 height: 32px; 200 vertical-align: middle; 201 } 202 203 /* Styles for the Font Icon */ 204 .weather-alert-icon-left .wi { 205 font-size: 2em; /* Make the icon a bit bigger */ 206 line-height: 1; 207 } 208 209 /* --- Sunrise & Sunset Styling --- */ 210 211 .sunrise-sunset-container { 212 display: flex; 213 justify-content: space-around; 214 text-align: center; 215 padding: 10px 5px; 216 border-bottom: 1px solid #eee; 217 margin-bottom: 10px; 218 color: #555; 219 } 220 221 .sunrise-time, .sunset-time { 222 flex-basis: 50%; 223 } 224 225 .sunrise-sunset-label { 226 font-size: 0.8em; 227 margin: 0 0 0 10px; 228 display: inline; 229 } 230 231 .sunrise-sunset-value { 232 font-weight: bold; 233 font-size: 1.25em; 234 } 235 236 /* Icon sizing */ 237 .sunrise-sunset-container .wi { 238 font-size: 30px; /* Smaller icon size */ 239 line-height: 1; 240 color: #f28c1f; /* A nice sun color. Delete this to match forecast icons */ 241 display: inline; 242 } 243 244 .sunrise-sunset-container .sunrise-sunset-icon { 245 width: 25px; /* Visually Match the forecast day icon size */ 246 height: 25px; 247 filter: drop-shadow(1px 2px 2px #888); 248 display: inline; 249 } -
under-the-weather/trunk/css/under-the-weather.min.css
r3342551 r3371900 1 .weather-widget{min-height:285px}.weather-location-name{border-bottom:2px solid #eee;font-size:1 8px;margin-bottom:15px;padding-bottom:7px;text-align:center}.current-weather{align-items:center;border-bottom:2px solid #eee;display:flex;justify-content:space-around;margin-bottom:8px;padding:10px}.current-temp{font-size:2.5em;font-weight:700}.current-conditions{align-items:center;display:flex;flex-direction:column;font-size:17px;text-transform:capitalize}.current-conditions img{filter: drop-shadow(1px 2px 3px #555);height:50px;width:50px}.forecast-container{display:flex;justify-content:space-between;text-align:center}.forecast-day{flex:1;padding:5px}.forecast-day-name{color:#444;font-size:14px;margin-bottom:8px}.forecast-day img{filter: drop-shadow(1px 2px 3px #666);height:40px;width:40px}.forecast-temps{color:#fff;font-size:6px}.forecast-temps .high{color:#000;font-size:17px;font-weight:700}.forecast-temps .low{color:#666;font-size:14px}.today-forecast-temps{align-items:center;display:flex;flex-direction:column;justify-content:center}.today-forecast-label{font-size:18px;margin:6px 0 11px}.today-forecast-temps .temps-wrapper{color:#fff;font-size:9px}.today-forecast-temps .high{color:#000;font-size:36px;font-weight:700}.today-forecast-temps .low{color:#666;font-size:26px;font-weight:400}.today-forecast-temps .temp-unit{color:#777;font-size:15px;font-weight:400;position:relative;top:-8px}.weather-widget .wi{color:#666;font-size:45px;line-height:1;margin-bottom:5px}.forecast-day .wi{font-size:40px}.weather-extra-details{border-top:1px solid #eee;border-bottom:1px solid #eee;color:#555;display:flex;font-size:.9em;justify-content:space-between;margin-bottom:10px;padding:10px 5px}.wind-details{align-items:center;display:flex}.wind-details .wi{font-size:1.5em;line-height:1;margin-right:5px}.last-updated{color:#999;font-size:.8em;margin-top:15px;text-align:center}1 .weather-widget{min-height:285px}.weather-location-name{border-bottom:2px solid #eee;font-size:1.375em;font-weight:700;margin-bottom:15px;padding-bottom:7px;text-align:center}.current-weather{align-items:center;border-bottom:2px solid #eee;display:flex;justify-content:space-around;margin-bottom:8px;padding:10px}.current-temp{font-size:2.5em;font-weight:700}.current-conditions{align-items:center;display:flex;flex-direction:column;font-size:17px;text-transform:capitalize}.current-conditions img{filter:drop-shadow(1px 2px 3px #555);height:50px;width:50px}.forecast-container{display:flex;justify-content:space-between;text-align:center}.forecast-day{flex:1;padding:5px}.forecast-day-name{color:#444;font-size:.65em;margin-bottom:8px}.forecast-day img{filter:drop-shadow(1px 2px 3px #666);height:40px;width:40px}.forecast-temps .slash{color:transparent;font-size:.375em}.forecast-temps .high{color:#000;font-size:1.06em;font-weight:700}.forecast-temps .low{color:#555;font-size:.875em}.today-forecast-temps{align-items:center;display:flex;flex-direction:column;justify-content:center}.today-forecast-label{font-size:1.125em;margin:6px 0 11px}.today-forecast-temps .temps-wrapper .slash{color:transparent;font-size:.55em}.today-forecast-temps .high{color:#000;font-size:2.25em;font-weight:700}.today-forecast-temps .low{color:#555;font-size:1.625em;font-weight:400}.today-forecast-temps .temp-unit{color:#666;font-size:.94em;font-weight:400;position:relative;top:-8px}.weather-widget .wi{color:#555;font-size:45px;line-height:1;margin-bottom:5px}.forecast-day .wi{font-size:40px}.weather-extra-details{border-bottom:1px solid #eee;color:#555;display:flex;font-size:.9em;justify-content:space-between;margin-bottom:10px;padding:10px 5px}.wind-details{align-items:center;display:flex}.wind-details .wi{font-size:1.5em;line-height:1;margin-right:5px}.last-updated{color:#999;font-size:.8em;margin-top:10px;text-align:center;border-top:1px solid #eee;padding-top:5px}.weather-alert{display:flex;align-items:center;background-color:#f0f8ff;border:1px solid #add8e6;border-radius:4px;padding:10px 12px;margin-bottom:15px;color:#333}.weather-alert-icon-left{margin-right:12px}.weather-alert-message{flex-grow:1}.weather-alert-event{font-weight:700;margin-bottom:4px}.weather-alert-sender{font-size:.9em;opacity:.8}.weather-alert-icon-left .weather-alert-icon{width:32px;height:32px;vertical-align:middle}.weather-alert-icon-left .wi{font-size:2em;line-height:1}.sunrise-sunset-container{display:flex;justify-content:space-around;text-align:center;padding:10px 5px;border-bottom:1px solid #eee;margin-bottom:10px;color:#555}.sunrise-time,.sunset-time{flex-basis:50%}.sunrise-sunset-label{font-size:.8em;margin:0 0 0 10px;display:inline}.sunrise-sunset-value{font-weight:700;font-size:1.25em}.sunrise-sunset-container .wi{font-size:30px;line-height:1;color:#f28c1f;display:inline}.sunrise-sunset-container .sunrise-sunset-icon{width:25px;height:25px;filter:drop-shadow(1px 2px 2px #888);display:inline} -
under-the-weather/trunk/js/admin-geocoder.js
r3365104 r3371900 1 1 // js/admin-geocoder.js 2 2 document.addEventListener('DOMContentLoaded', function() { 3 4 5 // =================================================================== 6 // EXPIRATION SLIDER - Runs on all admin pages 7 // =================================================================== 8 9 // --- Logic to prepare the top save button for styling --- 10 const topSaveBtn = document.getElementById('utw-expiration-save-btn'); 11 if (topSaveBtn) { 12 // Find the parent table cell (td) and table row (tr) 13 const parentTd = topSaveBtn.closest('td'); 14 const parentTr = topSaveBtn.closest('tr'); 15 16 if (parentTd && parentTr) { 17 // 1. Add an ID to the table row for easy CSS targeting 18 parentTr.id = 'utw-expiration-save-btn-wrap'; 19 20 // 2. Find and remove the empty label cell (th) 21 const thLabel = parentTr.querySelector('th'); 22 if (thLabel) { 23 thLabel.remove(); 24 } 25 26 // 3. Add the colspan="2" attribute to the button's cell 27 parentTd.setAttribute('colspan', '2'); 28 } 29 } 30 31 32 const expirationSlider = document.getElementById('utw-expiration-slider'); 33 const expirationValueDisplay = document.getElementById('utw-expiration-value'); 34 const unsavedChangesMessage = document.getElementById('utw-unsaved-changes'); 35 const expirationSaveBtn = document.getElementById('utw-expiration-save-btn'); 36 const minCacheNotice = document.getElementById('utw-min-cache-notice'); 37 38 if (expirationSlider && expirationValueDisplay) { 39 const originalValue = expirationSlider.getAttribute('data-original-value'); 40 41 // Update display value in real-time as slider moves 42 expirationSlider.addEventListener('input', function() { 43 44 // Show the special notice if the user drags the slider to 0 45 if (parseFloat(this.value) === 0) { 46 minCacheNotice.style.display = 'block'; 47 } else { 48 minCacheNotice.style.display = 'none'; 49 } 50 51 // Enforce minimum of 0.5 52 let value = parseFloat(this.value); 53 if (value < 0.5) { 54 value = 0.5; 55 this.value = 0.5; 56 } 57 58 expirationValueDisplay.textContent = value; 59 60 // Show/hide unsaved changes message based on whether value changed 61 if (unsavedChangesMessage) { 62 if (String(value) !== originalValue) { 63 unsavedChangesMessage.style.display = 'inline'; 64 } else { 65 unsavedChangesMessage.style.display = 'none'; 66 } 67 } 68 }); 69 70 // Additional validation on change (when user releases the slider) 71 expirationSlider.addEventListener('change', function() { 72 let value = parseFloat(this.value); 73 if (value < 0.5) { 74 this.value = 0.5; 75 expirationValueDisplay.textContent = '0.5'; 76 } 77 }); 78 79 // Hide unsaved changes message when the upper save button is clicked 80 if (expirationSaveBtn) { 81 expirationSaveBtn.addEventListener('click', function() { 82 if (unsavedChangesMessage) { 83 unsavedChangesMessage.style.display = 'none'; 84 } 85 }); 86 } 87 88 // Also hide message if the main (bottom) form submit button is clicked 89 const mainSubmitBtn = document.querySelector('input[type="submit"][name="submit"]'); 90 if (mainSubmitBtn && mainSubmitBtn.id !== 'utw-expiration-save-btn') { 91 mainSubmitBtn.addEventListener('click', function() { 92 if (unsavedChangesMessage) { 93 unsavedChangesMessage.style.display = 'none'; 94 } 95 }); 96 } 97 } 98 99 // =================================================================== 100 // GEOCODER TOOL - Only runs when the tool exists on the page 101 // =================================================================== 102 3 103 const geocoderTool = document.getElementById('utw-geocoder-tool'); 4 104 if (!geocoderTool) { 5 return; 6 } 105 return; // Exit only AFTER checking for the expiration slider 106 } 107 7 108 const locationInput = document.getElementById('utw-location-input'); 8 109 const findButton = document.getElementById('utw-find-coords'); … … 12 113 13 114 // --- HISTORY FUNCTIONS --- 14 let searchHistory = []; // Local cache of the history115 let searchHistory = []; 15 116 const MAX_HISTORY = 5; 16 117 … … 18 119 function formatWidgetHtml(html) { 19 120 return html 20 //.replace('<div class="weather-widget"', '<div class="weather-widget"\n ')21 121 .replace(' data-lat=', '\n data-lat=') 22 122 .replace(' data-lon=', '\n data-lon=') … … 51 151 52 152 function saveToHistoryAndServer(newItem) { 53 54 // Add to local history first55 153 searchHistory.unshift(newItem); 56 154 searchHistory = searchHistory.slice(0, MAX_HISTORY); 57 155 58 // Update the UI immediately with local data 59 console.log('UTW: Updated history:', searchHistory); 156 console.log('UTW: Updated history:', searchHistory); 60 157 renderHistory(); 61 158 62 // Save to server63 159 fetch(ajaxurl, { 64 160 method: 'POST', … … 135 231 const locationName = result.display_name; 136 232 137 // Generate the HTML div for the user138 233 const widgetHtml = `<div class="weather-widget" data-lat="${lat}" data-lon="${lon}" data-location-name="${locationQuery}"></div>`; 139 234 … … 145 240 `; 146 241 147 // Add to history and save to server148 242 saveToHistoryAndServer({ locationName: locationQuery, widgetHtml }); 149 243 150 // Add click listener to the new copy button151 244 document.getElementById('utw-copy-button').addEventListener('click', function() { 152 245 copyToClipboard(formatWidgetHtml(widgetHtml), this); … … 165 258 166 259 if (historyList) { 167 // Use event delegation for copy buttons in the history list168 260 historyList.addEventListener('click', function(event) { 169 261 if (event.target.classList.contains('copy-history-btn')) { … … 173 265 }); 174 266 175 // Load history from server on page load176 267 loadHistoryFromServer(); 177 268 } 178 269 179 // Copy searches to clipboard180 270 function copyToClipboard(text, button) { 181 271 if (navigator.clipboard && window.isSecureContext) { … … 219 309 } 220 310 221 // Helper function to escape HTML for display in a <pre> tag222 311 function escapeHtml(unsafe) { 223 312 return unsafe -
under-the-weather/trunk/js/under-the-weather.js
r3369072 r3371900 1 // Validate Coordinates 2 function validateCoordinates(lat, lon) { 3 const latitude = parseFloat(lat); 4 const longitude = parseFloat(lon); 5 return (latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180); 6 } 7 8 // Validate data 9 function validateWeatherData(data) { 10 return data && 11 data.current && 12 data.daily && 13 Array.isArray(data.daily) && 14 data.daily.length > 0; 15 } 16 17 // Convert a DMS (Degrees, Minutes, Seconds) coordinate, or a DDM string to the desired Decimal Degrees (DD) format. 18 // This frontend parsing serves as a helpful handler to catch malformed coordinates when possible. 19 // Decimal Degrees coordinates should be used at all time (e.g., 34.1195, -118.3005). 20 // The quotation marks included in DMS coordinates will break the HTML structure of the weather widget. 21 // DMS coordinates (e.g., 34°07'10.2"N 118°18'01.8"W) should therefore be avoided. 22 function parseCoordinate(coordString) { 23 if (!coordString || typeof coordString !== 'string') { 24 return null; 25 } 26 27 // 1. Clean the input 28 let str = coordString.trim(); 29 if (str.endsWith(',')) { 30 str = str.slice(0, -1).trim(); 31 } 32 33 // 2. Try to parse as DDM (Degrees Decimal Minutes) 34 const ddmRegex = /([0-9]{1,3})[°\s]+([0-9]+(?:\.[0-9]+)?)['\s]+([NSEW])/i; 35 let parts = str.match(ddmRegex); 36 if (parts) { 37 const degrees = parseFloat(parts[1]); 38 const minutes = parseFloat(parts[2]); 39 const hemisphere = parts[3].toUpperCase(); 40 if (isNaN(degrees) || isNaN(minutes)) return null; 41 42 let decimal = degrees + (minutes / 60); 43 if (hemisphere === 'S' || hemisphere === 'W') decimal = -decimal; 44 return parseFloat(decimal.toFixed(4)); 45 } 46 47 // 3. Try to parse as DMS (Degrees, Minutes, Seconds) 48 const dmsRegex = /([0-9]{1,3})[°\s]+([0-9]{1,2})['\s]+([0-9]{1,2}(?:\.[0-9]+)?)["\s]+([NSEW])/i; 49 parts = str.match(dmsRegex); 50 if (parts) { 51 const degrees = parseFloat(parts[1]); 52 const minutes = parseFloat(parts[2]); 53 const seconds = parseFloat(parts[3]); 54 const hemisphere = parts[4].toUpperCase(); 55 if (isNaN(degrees) || isNaN(minutes) || isNaN(seconds)) return null; 56 57 let decimal = degrees + (minutes / 60) + (seconds / 3600); 58 if (hemisphere === 'S' || hemisphere === 'W') decimal = -decimal; 59 return parseFloat(decimal.toFixed(4)); 60 } 61 62 // 4. Try to parse as simple Decimal Degrees 63 const dd = parseFloat(str); 64 if (!isNaN(dd)) { 65 return dd; 66 } 67 68 // 5. If all formats fail 69 return null; 70 } 71 1 // Check webpage for Weather Widgets and load widget data. 72 2 document.addEventListener('DOMContentLoaded', function() { 73 const widget = document.querySelector('.weather-widget'); 74 if (!widget) { 75 return; 76 } 77 3 // 1. Find ALL weather widgets on the page 4 const weatherWidgets = document.querySelectorAll('.weather-widget'); 5 6 // 2. If no widgets are found, do nothing. 7 if (weatherWidgets.length === 0) { 8 return; 9 } 10 11 // 3. Loop through each widget and load its data 12 weatherWidgets.forEach(widget => { 13 loadWeatherData(widget); 14 }); 15 }); 16 17 /** 18 * Fetches and displays weather data for a single widget element. 19 * @param {HTMLElement} widget The widget's div element. 20 */ 21 function loadWeatherData(widget) { 78 22 const locationName = widget.dataset.locationName; 79 const unit = widget.dataset.unit ? widget.dataset.unit.toLowerCase() : 'imperial'; 80 const controller = new AbortController(); 81 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout 82 83 // Get the lat/lon from the data attributes 23 // Get the lat/lon from the data attributes 84 24 let lat = widget.dataset.lat; 85 25 let lon = widget.dataset.lon; 86 26 87 27 // Attempt to parse/convert them (this handles DD, DDM, and DMS formats) 88 28 const parsedLat = parseCoordinate(lat); … … 109 49 110 50 widget.innerHTML = '<div class="weather-loading">Loading weather data...</div>'; 111 51 52 const unit = widget.dataset.unit ? widget.dataset.unit.toLowerCase() : 'imperial'; 53 const controller = new AbortController(); 54 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout 112 55 const apiUrl = `/wp-json/under-the-weather/v1/forecast?lat=${lat}&lon=${lon}&location_name=${encodeURIComponent(locationName)}&unit=${unit}`; 113 56 … … 139 82 widget.innerHTML = `<p>Unable to load weather data. Please try again later.</p>`; 140 83 }); 141 }); 84 } 85 86 // Validate Coordinates 87 function validateCoordinates(lat, lon) { 88 const latitude = parseFloat(lat); 89 const longitude = parseFloat(lon); 90 return (latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180); 91 } 92 93 // Validate Weather Data 94 function validateWeatherData(data) { 95 return data && 96 data.current && 97 data.daily && 98 Array.isArray(data.daily) && 99 data.daily.length > 0; 100 } 101 102 // Convert a DMS (Degrees, Minutes, Seconds) coordinate, or a DDM string to the desired Decimal Degrees (DD) format. 103 // This frontend parsing serves as a helpful handler to catch malformed coordinates when possible. 104 // Decimal Degrees coordinates should be used at all time (e.g., 34.1195, -118.3005). 105 // The quotation marks included in DMS coordinates will break the HTML structure of the weather widget. 106 // DMS coordinates (e.g., 34°07'10.2"N 118°18'01.8"W) should therefore be avoided. 107 function parseCoordinate(coordString) { 108 if (!coordString || typeof coordString !== 'string') { 109 return null; 110 } 111 112 // 1. Clean the input 113 let str = coordString.trim(); 114 if (str.endsWith(',')) { 115 str = str.slice(0, -1).trim(); 116 } 117 118 // 2. Try to parse as DDM (Degrees Decimal Minutes) 119 const ddmRegex = /([0-9]{1,3})[°\s]+([0-9]+(?:\.[0-9]+)?)['\s]+([NSEW])/i; 120 let parts = str.match(ddmRegex); 121 if (parts) { 122 const degrees = parseFloat(parts[1]); 123 const minutes = parseFloat(parts[2]); 124 const hemisphere = parts[3].toUpperCase(); 125 if (isNaN(degrees) || isNaN(minutes)) return null; 126 127 let decimal = degrees + (minutes / 60); 128 if (hemisphere === 'S' || hemisphere === 'W') decimal = -decimal; 129 return parseFloat(decimal.toFixed(4)); 130 } 131 132 // 3. Try to parse as DMS (Degrees, Minutes, Seconds) 133 const dmsRegex = /([0-9]{1,3})[°\s]+([0-9]{1,2})['\s]+([0-9]{1,2}(?:\.[0-9]+)?)["\s]+([NSEW])/i; 134 parts = str.match(dmsRegex); 135 if (parts) { 136 const degrees = parseFloat(parts[1]); 137 const minutes = parseFloat(parts[2]); 138 const seconds = parseFloat(parts[3]); 139 const hemisphere = parts[4].toUpperCase(); 140 if (isNaN(degrees) || isNaN(minutes) || isNaN(seconds)) return null; 141 142 let decimal = degrees + (minutes / 60) + (seconds / 3600); 143 if (hemisphere === 'S' || hemisphere === 'W') decimal = -decimal; 144 return parseFloat(decimal.toFixed(4)); 145 } 146 147 // 4. Try to parse as simple Decimal Degrees 148 const dd = parseFloat(str); 149 if (!isNaN(dd)) { 150 return dd; 151 } 152 153 // 5. If all formats fail 154 return null; 155 } 156 157 /** 158 * Selects a weather icon based on the alert event text. 159 * @param {string} eventText The text of the weather alert (e.g., "Tornado Warning"). 160 * @returns {string} The corresponding Weather Icons class name. 161 */ 162 function getAlertIconClass(eventText) { 163 const text = eventText.toLowerCase(); 164 165 // Catastrophic Events 166 if (text.includes('tornado')) return 'wi-tornado'; 167 if (text.includes('hurricane')) return 'wi-hurricane-warning'; 168 if (text.includes('tsunami')) return 'wi-tsunami'; 169 if (text.includes('earthquake')) return 'wi-earthquake'; 170 171 // Storms & Precipitation 172 if (text.includes('thunderstorm') || text.includes('lightning')) return 'wi-thunderstorm'; 173 if (text.includes('gale')) return 'wi-gale-warning'; 174 if (text.includes('hail')) return 'wi-hail'; 175 if (text.includes('rain') || text.includes('showers') || text.includes('drizzle')) return 'wi-rain'; 176 if (text.includes('flood')) return 'wi-flood'; 177 178 // Winter Weather 179 if (text.includes('winter') || text.includes('snow') || text.includes('blizzard')) return 'wi-snow'; 180 if (text.includes('ice') || text.includes('frost') || text.includes('freeze') || text.includes('cold') || text.includes('chill')) return 'wi-snowflake-cold'; 181 182 // Temperature & Wind 183 if (text.includes('heat') || text.includes('hot')) return 'wi-hot'; 184 if (text.includes('wind')) return 'wi-strong-wind'; 185 186 // Atmospheric & Air Quality 187 if (text.includes('fog')) return 'wi-fog'; 188 if (text.includes('fire')) return 'wi-fire'; 189 if (text.includes('smoke')) return 'wi-smoke'; 190 if (text.includes('smog') || text.includes('air quality')) return 'wi-smog'; 191 if (text.includes('dust')) return 'wi-dust'; 192 if (text.includes('sandstorm') || text.includes('sand')) return 'wi-sandstorm'; 193 194 // A good fallback for any other severe weather 195 return 'wi-storm-warning'; 196 } 142 197 143 198 function displayWeather(data, widget) { 144 const { style_set, display_mode, forecast_days, show_details, show_ timestamp, show_unit } = under_the_weather_settings;199 const { style_set, display_mode, forecast_days, show_details, show_unit, show_alerts, show_timestamp, sunrise_sunset_format } = under_the_weather_settings; 145 200 const locationName = widget.dataset.locationName || ''; 146 201 … … 185 240 return "a minute ago"; 186 241 } 242 243 // START: New Sunrise/Sunset Logic 244 let sunriseSunsetHtml = ''; 245 // Check if the setting is enabled and the data exists 246 if (sunrise_sunset_format !== 'off' && data.current.sunrise && data.current.sunset) { 247 248 // Define formatting options based on the setting 249 const timeOptions = { 250 timeZone: data.timezone, 251 hour: 'numeric', 252 minute: '2-digit', 253 hour12: sunrise_sunset_format === '12' // Use 12-hour format if setting is '12' 254 }; 255 256 // Convert timestamps to readable times 257 const sunriseTime = new Date(data.current.sunrise * 1000).toLocaleTimeString('en-US', timeOptions); 258 const sunsetTime = new Date(data.current.sunset * 1000).toLocaleTimeString('en-US', timeOptions); 259 260 // Get icons based on the style set 261 let sunriseIcon = ''; 262 let sunsetIcon = ''; 263 if (style_set === 'weather_icons_font') { 264 sunriseIcon = '<i class="wi wi-sunrise"></i>'; 265 sunsetIcon = '<i class="wi wi-sunset"></i>'; 266 } else { 267 // Using generic day/night icons as a fallback for the default image set 268 const sunriseImgUrl = `${under_the_weather_plugin_url.url}images/seths--weather-images-sunrise.png`; 269 const sunsetImgUrl = `${under_the_weather_plugin_url.url}images/seths--weather-images-sunset.png`; 270 sunriseIcon = `<img src="${sunriseImgUrl}" class="sunrise-sunset-icon" alt="Sunrise Time">`; 271 sunsetIcon = `<img src="${sunsetImgUrl}" class="sunrise-sunset-icon" alt="Sunset Time">`; 272 } 273 274 sunriseSunsetHtml = ` 275 <div class="sunrise-sunset-container"> 276 <div class="sunrise-time"> 277 ${sunriseIcon} 278 <div class="sunrise-sunset-label">Sunrise</div> 279 <div class="sunrise-sunset-value">${sunriseTime}</div> 280 </div> 281 <div class="sunset-time"> 282 ${sunsetIcon} 283 <div class="sunrise-sunset-label">Sunset</div> 284 <div class="sunrise-sunset-value">${sunsetTime}</div> 285 </div> 286 </div> 287 `; 288 } 289 // END: New Sunrise/Sunset Logic 187 290 188 291 let primaryDisplayHtml = ''; … … 197 300 <div class="today-forecast-label">Today</div> 198 301 <div class="temps-wrapper"> 199 <span class="high">${highTemp}${tempSymbol}</span> /<span class="low">${lowTemp}${tempSymbol}</span>${displayUnitString}302 <span class="high">${highTemp}${tempSymbol}</span><span class="slash"> / </span><span class="low">${lowTemp}${tempSymbol}</span>${displayUnitString} 200 303 </div> 201 304 </div> … … 237 340 `; 238 341 } 342 343 let alertHtml = ''; 344 if (show_alerts && data.alerts && data.alerts.length > 0) { 345 data.alerts.forEach(alert => { 346 347 // Handle custom alert icons 348 let iconHtml = '';// This will hold the icon's HTML 349 if (style_set === 'weather_icons_font') { 350 // Use the dynamic font icon logic 351 const iconClass = getAlertIconClass(alert.event); 352 iconHtml = `<i class="wi ${iconClass}"></i>`; 353 } else { 354 // Use the new PNG fallback icon 355 const imageUrl = `${under_the_weather_plugin_url.url}images/seths--weather-images-warning.png`; 356 iconHtml = `<img src="${imageUrl}" class="weather-alert-icon" alt="Weather Alert">`; 357 } 358 359 alertHtml += ` 360 <div class="weather-alert"> 361 <div class="weather-alert-icon-left"> 362 ${iconHtml} 363 </div> 364 <div class="weather-alert-message"> 365 <div class="weather-alert-event">${alert.event}</div> 366 <div class="weather-alert-sender">Issued by: ${alert.sender_name}</div> 367 </div> 368 </div> 369 `; 370 }); 371 } 239 372 240 373 const forecastDaysToShow = parseInt(forecast_days, 10) || 5; … … 252 385 ${getIconHtml(day.weather[0])} 253 386 <div class="forecast-temps"> 254 <span class="high">${highTemp}${tempSymbol}</span> /<span class="low">${lowTemp}${tempSymbol}</span>387 <span class="high">${highTemp}${tempSymbol}</span><span class="slash"> / </span><span class="low">${lowTemp}${tempSymbol}</span> 255 388 </div> 256 389 </div> … … 265 398 const finalHtml = ` 266 399 <div class="weather-location-name">${locationName}</div> 267 ${primaryDisplayHtml} 400 ${alertHtml} 401 ${primaryDisplayHtml} 268 402 ${extraDetailsHtml} 403 ${sunriseSunsetHtml} 269 404 <div class="forecast-container"> 270 405 ${forecastHtml} -
under-the-weather/trunk/js/under-the-weather.min.js
r3369072 r3371900 1 function validateCoordinates(e,t){const a=parseFloat(e),n=parseFloat(t);return a>=-90&&a<=90&&n>=-180&&n<=180}function validateWeatherData(e){return e&&e.current&&e.daily&&Array.isArray(e.daily)&&e.daily.length>0}function parseCoordinate(e){if(!e||"string"!=typeof e)return null;let t=e.trim();t.endsWith(",")&&(t=t.slice(0,-1).trim());let a=t.match(/([0-9]{1,3})[°\s]+([0-9]+(?:\.[0-9]+)?)['\s]+([NSEW])/i);if(a){const e=parseFloat(a[1]),t=parseFloat(a[2]),n=a[3].toUpperCase();if(isNaN(e)||isNaN(t))return null;let r=e+t/60;return"S"!==n&&"W"!==n||(r=-r),parseFloat(r.toFixed(4))}if(a=t.match(/([0-9]{1,3})[°\s]+([0-9]{1,2})['\s]+([0-9]{1,2}(?:\.[0-9]+)?)["\s]+([NSEW])/i),a){const e=parseFloat(a[1]),t=parseFloat(a[2]),n=parseFloat(a[3]),r=a[4].toUpperCase();if(isNaN(e)||isNaN(t)||isNaN(n))return null;let s=e+t/60+n/3600;return"S"!==r&&"W"!==r||(s=-s),parseFloat(s.toFixed(4))}const n=parseFloat(t);return isNaN(n)?null:n}function displayWeather(e,t){const{style_set:a,display_mode:n,forecast_days:r,show_details:s,show_timestamp:o,show_unit:i}=under_the_weather_settings,d=t.dataset.locationName||"",l="°",c="metric"===e.units?"C":"F",u="metric"===e.units?"kph":"mph",h=i?`<span class="temp-unit">${c}</span>`:"";function p(e){return"weather_icons_font"===a?`<i class="wi ${e.icon_class}"></i>`:`<img src="${under_the_weather_plugin_url.url}images/default-weather-images-${e.icon}.png" alt="${e.description}">`}let w="";if("today_forecast"===n){const t=e.daily[0],a=Math.round(t.temp.max),n=Math.round(t.temp.min);w=`<div class="current-weather"><div class="today-forecast-temps"><div class="today-forecast-label">Today</div><div class="temps-wrapper"><span class="high">${a}${l}</span> / <span class="low">${n}${l}</span>${h}</div></div><div class="current-conditions">${p(t.weather[0])}<span>${t.weather[0].description}</span></div></div>`}else{const t=e.current;w=`<div class="current-weather"><div class="current-temp">${Math.round(t.temp)}${l}${h}</div><div class="current-conditions">${p(t.weather[0])}<span>${t.weather[0].description}</span></div></div>`}let f="";if(s){const t=Math.round(e.current.feels_like),a=Math.round(e.current.wind_speed),n=(v=e.current.wind_deg,["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"][Math.round(v/22.5)%16]),r=function(e){return`wi wi-wind from-${Math.round(e)}-deg`}(e.current.wind_deg);f=`<div class="weather-extra-details"><span>Feels like: ${t}${l}${h}</span><span class="wind-details"><i class="${r}"></i> ${n} ${a} ${u}</span></div>`}var v;const m=parseInt(r,10)||5,$=e.daily.slice(1,1+m);let g="";$.forEach((e=>{const t=new Date(1e3*e.dt).toLocaleDateString("en-US",{weekday:"short"}),a=Math.round(e.temp.max),n=Math.round(e.temp.min);g+=`<div class="forecast-day"><div class="forecast-day-name">${t}</div>${p(e.weather[0])}<div class="forecast-temps"><span class="high">${a}${l}</span> / <span class="low">${n}${l}</span></div></div>`}));let N="";o&&e.fetched_at&&(N=`<div class="last-updated">Updated ${function(e){const t=(new Date).getTime()/1e3,a=Math.floor(t-e);if(a<60)return"just now";let n=a/31536e3;return n>1?Math.floor(n)+" years ago":(n=a/2592e3,n>1?Math.floor(n)+" months ago":(n=a/86400,n>1?Math.floor(n)+" days ago":(n=a/3600,n>1?Math.floor(n)+" hours ago":(n=a/60,n>1?Math.floor(n)+" minutes ago":"a minute ago"))))}(e.fetched_at)}</div>`);const _=`<div class="weather-location-name">${d}</div>${w}${f}<div class="forecast-container">${g}</div>${N}`;t.innerHTML=_}document.addEventListener("DOMContentLoaded",(function(){const e=document.querySelector(".weather-widget");if(!e)return;const t=e.dataset.locationName,a=e.dataset.unit?e.dataset.unit.toLowerCase():"imperial",n=new AbortController,r=setTimeout((()=>n.abort()),1e4);let s=e.dataset.lat,o=e.dataset.lon;const i=parseCoordinate(s),d=parseCoordinate(o);if(null!==i&&(s=i),null!==d&&(o=d),!s||!o||!t)return void(e.innerHTML="Location data is missing.");if(!validateCoordinates(s,o))return void(e.innerHTML="Invalid location coordinates.");e.innerHTML='<div class="weather-loading">Loading weather data...</div>';const l=`/wp-json/under-the-weather/v1/forecast?lat=${s}&lon=${o}&location_name=${encodeURIComponent(t)}&unit=${a}`;fetch(l,{signal:n.signal,headers:{"X-WP-Nonce":under_the_weather_settings.nonce}}).then((t=>{if(clearTimeout(r),!t.ok)throw t.text().then((t=>{console.error("Error fetching weather data:",t),e.innerHTML="<p>Could not retrieve forecast. Server error.</p>"})),new Error("Network response was not ok");return t.json()})).then((t=>{if(!validateWeatherData(t))throw new Error("The weather data structure is invalid");displayWeather(t,e)})).catch((t=>{console.error("Network Error:",t),e.innerHTML="<p>Unable to load weather data. Please try again later.</p>"}))}));1 function loadWeatherData(e){const t=e.dataset.locationName;let n=e.dataset.lat,s=e.dataset.lon;const a=parseCoordinate(n),i=parseCoordinate(s);if(null!==a&&(n=a),null!==i&&(s=i),!n||!s||!t)return void(e.innerHTML="Location data is missing.");if(!validateCoordinates(n,s))return void(e.innerHTML="Invalid location coordinates.");e.innerHTML='<div class="weather-loading">Loading weather data...</div>';const r=e.dataset.unit?e.dataset.unit.toLowerCase():"imperial",l=new AbortController,o=setTimeout((()=>l.abort()),1e4),d=`/wp-json/under-the-weather/v1/forecast?lat=${n}&lon=${s}&location_name=${encodeURIComponent(t)}&unit=${r}`;fetch(d,{signal:l.signal,headers:{"X-WP-Nonce":under_the_weather_settings.nonce}}).then((t=>{if(clearTimeout(o),!t.ok)throw t.text().then((t=>{console.error("Error fetching weather data:",t),e.innerHTML="<p>Could not retrieve forecast. Server error.</p>"})),new Error("Network response was not ok");return t.json()})).then((t=>{if(!validateWeatherData(t))throw new Error("The weather data structure is invalid");displayWeather(t,e)})).catch((t=>{console.error("Network Error:",t),e.innerHTML="<p>Unable to load weather data. Please try again later.</p>"}))}function validateCoordinates(e,t){const n=parseFloat(e),s=parseFloat(t);return n>=-90&&n<=90&&s>=-180&&s<=180}function validateWeatherData(e){return e&&e.current&&e.daily&&Array.isArray(e.daily)&&e.daily.length>0}function parseCoordinate(e){if(!e||"string"!=typeof e)return null;let t=e.trim();t.endsWith(",")&&(t=t.slice(0,-1).trim());let n=t.match(/([0-9]{1,3})[°\s]+([0-9]+(?:\.[0-9]+)?)['\s]+([NSEW])/i);if(n){const e=parseFloat(n[1]),t=parseFloat(n[2]),s=n[3].toUpperCase();if(isNaN(e)||isNaN(t))return null;let a=e+t/60;return"S"!==s&&"W"!==s||(a=-a),parseFloat(a.toFixed(4))}if(n=t.match(/([0-9]{1,3})[°\s]+([0-9]{1,2})['\s]+([0-9]{1,2}(?:\.[0-9]+)?)["\s]+([NSEW])/i),n){const e=parseFloat(n[1]),t=parseFloat(n[2]),s=parseFloat(n[3]),a=n[4].toUpperCase();if(isNaN(e)||isNaN(t)||isNaN(s))return null;let i=e+t/60+s/3600;return"S"!==a&&"W"!==a||(i=-i),parseFloat(i.toFixed(4))}const s=parseFloat(t);return isNaN(s)?null:s}function getAlertIconClass(e){const t=e.toLowerCase();return t.includes("tornado")?"wi-tornado":t.includes("hurricane")?"wi-hurricane-warning":t.includes("tsunami")?"wi-tsunami":t.includes("earthquake")?"wi-earthquake":t.includes("thunderstorm")||t.includes("lightning")?"wi-thunderstorm":t.includes("gale")?"wi-gale-warning":t.includes("hail")?"wi-hail":t.includes("rain")||t.includes("showers")||t.includes("drizzle")?"wi-rain":t.includes("flood")?"wi-flood":t.includes("winter")||t.includes("snow")||t.includes("blizzard")?"wi-snow":t.includes("ice")||t.includes("frost")||t.includes("freeze")||t.includes("cold")||t.includes("chill")?"wi-snowflake-cold":t.includes("heat")||t.includes("hot")?"wi-hot":t.includes("wind")?"wi-strong-wind":t.includes("fog")?"wi-fog":t.includes("fire")?"wi-fire":t.includes("smoke")?"wi-smoke":t.includes("smog")||t.includes("air quality")?"wi-smog":t.includes("dust")?"wi-dust":t.includes("sandstorm")||t.includes("sand")?"wi-sandstorm":"wi-storm-warning"}function displayWeather(e,t){const{style_set:n,display_mode:s,forecast_days:a,show_details:i,show_unit:r,show_alerts:l,show_timestamp:o,sunrise_sunset_format:d}=under_the_weather_settings,c=t.dataset.locationName||"",u="°",h="metric"===e.units?"C":"F",w="metric"===e.units?"kph":"mph",p=r?`<span class="temp-unit">${h}</span>`:"";function m(e){return"weather_icons_font"===n?`<i class="wi ${e.icon_class}"></i>`:`<img src="${under_the_weather_plugin_url.url}images/default-weather-images-${e.icon}.png" alt="${e.description}">`}let v="";if("off"!==d&&e.current.sunrise&&e.current.sunset){const t={timeZone:e.timezone,hour:"numeric",minute:"2-digit",hour12:"12"===d},s=new Date(1e3*e.current.sunrise).toLocaleTimeString("en-US",t),a=new Date(1e3*e.current.sunset).toLocaleTimeString("en-US",t);let i="",r="";if("weather_icons_font"===n)i='<i class="wi wi-sunrise"></i>',r='<i class="wi wi-sunset"></i>';else{i=`<img src="${`${under_the_weather_plugin_url.url}images/seths--weather-images-sunrise.png`}" class="sunrise-sunset-icon" alt="Sunrise Time">`,r=`<img src="${`${under_the_weather_plugin_url.url}images/seths--weather-images-sunset.png`}" class="sunrise-sunset-icon" alt="Sunset Time">`}v=`<div class="sunrise-sunset-container"><div class="sunrise-time">${i}<div class="sunrise-sunset-label">Sunrise</div><div class="sunrise-sunset-value">${s}</div></div><div class="sunset-time">${r}<div class="sunrise-sunset-label">Sunset</div><div class="sunrise-sunset-value">${a}</div></div></div>`}let f="";if("today_forecast"===s){const t=e.daily[0],n=Math.round(t.temp.max),s=Math.round(t.temp.min);f=`<div class="current-weather"><div class="today-forecast-temps"><div class="today-forecast-label">Today</div><div class="temps-wrapper"> <span class="high">${n}${u}</span><span class="slash"> / </span><span class="low">${s}${u}</span>${p}</div></div><div class="current-conditions">${m(t.weather[0])}<span>${t.weather[0].description}</span></div></div>`}else{const t=e.current;f=`<div class="current-weather"><div class="current-temp">${Math.round(t.temp)}${u}${p}</div><div class="current-conditions">${m(t.weather[0])}<span>${t.weather[0].description}</span></div></div>`}let g="";if(i){const t=Math.round(e.current.feels_like),n=Math.round(e.current.wind_speed),s=($=e.current.wind_deg,["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"][Math.round($/22.5)%16]),a=function(e){return`wi wi-wind from-${Math.round(e)}-deg`}(e.current.wind_deg);g=`<div class="weather-extra-details"><span>Feels like: ${t}${u}${p}</span><span class="wind-details"><i class="${a}"></i> ${s} ${n} ${w}</span></div>`}var $;let _="";l&&e.alerts&&e.alerts.length>0&&e.alerts.forEach((e=>{let t="";if("weather_icons_font"===n){t=`<i class="wi ${getAlertIconClass(e.event)}"></i>`}else{t=`<img src="${`${under_the_weather_plugin_url.url}images/seths--weather-images-warning.png`}" class="weather-alert-icon" alt="Weather Alert">`}_+=`\n\t\t\t\t<div class="weather-alert">\n\t\t\t\t\t<div class="weather-alert-icon-left">\n\t\t\t\t\t\t${t}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class="weather-alert-message">\n\t\t\t\t\t\t<div class="weather-alert-event">${e.event}</div>\n\t\t\t\t\t\t<div class="weather-alert-sender">Issued by: ${e.sender_name}</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>`}));const N=parseInt(a,10)||5,y=e.daily.slice(1,1+N);let S="";y.forEach((e=>{const t=new Date(1e3*e.dt).toLocaleDateString("en-US",{weekday:"short"}),n=Math.round(e.temp.max),s=Math.round(e.temp.min);S+=` <div class="forecast-day"><div class="forecast-day-name">${t}</div>${m(e.weather[0])}<div class="forecast-temps"> <span class="high">${n}${u}</span><span class="slash"> / </span><span class="low">${s}${u}</span></div> </div>`}));let M="";o&&e.fetched_at&&(M=`<div class="last-updated">Updated ${function(e){const t=(new Date).getTime()/1e3,n=Math.floor(t-e);if(n<60)return"just now";let s=n/31536e3;return s>1?Math.floor(s)+" years ago":(s=n/2592e3,s>1?Math.floor(s)+" months ago":(s=n/86400,s>1?Math.floor(s)+" days ago":(s=n/3600,s>1?Math.floor(s)+" hours ago":(s=n/60,s>1?Math.floor(s)+" minutes ago":"a minute ago"))))}(e.fetched_at)}</div>`);const W=`<div class="weather-location-name">${c}</div>${_}\n\t\t${f}${g}\n\t\t${v}<div class="forecast-container">${S}</div>${M}\n`;t.innerHTML=W}document.addEventListener("DOMContentLoaded",(function(){const e=document.querySelectorAll(".weather-widget");0!==e.length&&e.forEach((e=>{loadWeatherData(e)}))})); -
under-the-weather/trunk/readme.txt
r3369072 r3371900 4 4 Requires at least: 5.0 5 5 Tested up to: 6.8 6 Stable tag: 2. 16 Stable tag: 2.2 7 7 Requires PHP: 7.2 8 8 License: GPLv2 or later … … 27 27 * **Easy to Use:** Add weather widgets using the WordPress block editor or by placing a simple `<div>` with data attributes anywhere on your site. 28 28 * **Server-Side Caching:** All API calls are cached on your server, dramatically reducing calls to the OpenWeather API and speeding up page loads for all users. 29 * **Smart Caching:** In addition to a configurable cache duration, the plugin automatically resets the forecast after midnight in the location's timezone, ensuring your visitors always see the current day's weather. 29 30 * **Visual Performance Report:** Monitor your site's API usage with a bar chart that displays a 7-day history of cached requests versus new calls to the OpenWeather API - a clear look at how the caching system is working to keep your site fast and your API calls low. 30 * **Highly Customizable:** Use the detailed settings page to control everything from cache duration to the number of forecast days. 31 * **Flexible Display:** Show either the current live weather or the high/low forecast for the current day. 31 * **Customizable Display:** Use the main display to show either the current live weather or the high/low forecast for the current day, and set the number of days to include in the forecast ahead. 32 32 * **Imperial & Metric Units:** Display weather in Fahrenheit/mph or Celsius/kph on a per-widget basis. 33 33 * **Extra Details:** Optionally display "Feels Like" temperature and detailed wind information. 34 * **Weather Alerts:** Display official severe weather alerts directly in the widget to keep visitors informed. 35 * **Sunrise & Sunset Times:** Optionally show daily sunrise and sunset times, with 12-hour and 24-hour format options. 34 36 * **Lightweight:** Enqueues assets only when needed and does not rely on heavy JavaScript libraries. 35 37 * **Settings Page Coordinate Finder:** An easy-to-use tool on the settings page retrieves coordinates by location name and generates ready-to-use widget `<div>` code. … … 85 87 The plugin's JavaScript will automatically find this element and populate it with the forecast. 86 88 89 **Using the Shortcode (Classic Editor & Widgets)** 90 91 You can also display the weather by using the `[under_the_weather]` shortcode. This is ideal for the Classic Editor, text widgets, or other page builders. 92 93 **Available attributes:** 94 * `lat`: (Required) The latitude for the forecast. 95 * `lon`: (Required) The longitude for the forecast. 96 * `location_name`: (Required) The name to display for the location. 97 * `unit`: (Optional) The unit system. Accepts `metric` or `imperial`. Defaults to `imperial`. 98 99 **Example:** 100 [under_the_weather lat="48.8566" lon="2.3522" location_name="Paris, France" unit="metric"] 101 87 102 == Configuration == 88 103 … … 92 107 93 108 **Cache Expiration Time:** 94 This setting controls how long the weather data is stored on your server before fetching a new forecast. 109 Use the slider to set the maximum time weather data is stored before fetching a new forecast, from 30 minutes to 8 hours. 110 111 The plugin also features a **smart caching** system that automatically ensures the cache expires after midnight in the location's local timezone. This prevents showing a stale forecast from the previous day, regardless of your slider setting. 112 95 113 For displaying live conditions (using the **Primary Display** or **Extra Details** options), a shorter cache time of 1 or 2 hours is recommended. 96 For displaying only the daily high/low, a longer cache time of 3 or 6hours is effective at reducing API calls.114 For displaying only the daily high/low, a longer cache time of 3 or 8 hours is effective at reducing API calls. 97 115 98 116 **Widget Display & Style** … … 108 126 109 127 **Extra Details:** 110 Selecting this option will **display 'Feels Like' and wind** (direction and speed) information beneath the primary display. This setting adds nuance to the current weather conditions display. 128 Selecting this option will **display 'Feels Like' and wind** (direction and speed) information beneath the primary display. This setting adds nuance to the current weather conditions display. 129 130 **Sunrise & Sunset:** This setting allows you to display the local sunrise and sunset times for the location, which is useful for planning outdoor activities. Choose to show the times in a 12-hour (e.g., 6:30 AM) or 24-hour (e.g., 18:30) format. 131 132 **Weather Alerts:** When enabled, the widget will display any active severe weather alerts (e.g., thunderstorm warnings, flood advisories) issued by official authorities for the specified location. This provides critical, at-a-glance information for your visitors. 111 133 112 134 **Display Timestamp:** … … 181 203 No. To retrieve fresh weather data every time a widget page loads, you can uncheck "Enable Cache" under the plugin's advanced settings. The caching system provides a great benefit for reducing API hits, but turning off this function during your initial widget setup may be useful. 182 204 205 = Will my website ever show yesterday's weather If I set a long cache time? = 206 207 Cinderella's magic disappears at midnight and weather caches expire at midnight too. Visitors should never see a cache of the previous day's forecast. 208 209 For example, if you set the cache expiration time to 8 hours and a weather cache is created at 10 p.m. on a Friday (using the weather location's time), that cache will expire at midnight, and someone visiting the site the next day at 5 a.m. will not see the previous day's cache even though fewer than 8 hours have passed. 210 211 The plugin uses whichever expiration time is **shorter** to provide the most effective caching. You control the maximum cache duration with the "Cache Expiration Time" slider. However, to ensure your visitors never see yesterday's weather, the plugin also calculates the time until midnight in the widget's local timezone. If the time until midnight is shorter than your slider setting, the cache will expire at midnight. 212 183 213 = The weather isn't updating. Why? = 184 214 … … 207 237 = Can I still use the manual div method if I prefer it? = 208 238 209 Absolutely! The traditional method of adding `<div class="weather-widget">` with data attributes still works perfectly. The new block is simply an additional, more user-friendly option for those using the WordPress block editor. 210 211 The `<div>` method is particularly useful for theme developers and sites that dynamically populate widget attributes from post meta or custom fields. 239 Absolutely! While the **block** is the recommended, user-friendly method for the modern WordPress editor, the plugin fully supports traditional methods for maximum flexibility. 240 241 You can use the `[under_the_weather]` shortcode to easily place the widget in the Classic Editor, text widgets, or with various page builders. 242 243 Additionally, the manual `<div>` method still works perfectly. It is particularly useful for theme developers who need to integrate the widget directly into template files or dynamically populate its data from custom fields. 244 245 The traditional method of adding `<div class="weather-widget">` with data attributes still works perfectly and is particularly useful for theme developers and sites that dynamically populate widget attributes from post meta or custom fields. 212 246 213 247 = What coordinate format should I use? = … … 220 254 221 255 If you're unsure what coordinates to use, the **Coordinate Finder** tool is the best way to retrieve accurate coordinates in the correct format. 256 257 = Where do the weather alerts come from? = 258 259 The alerts are provided directly by the OpenWeather API, which sources them from official meteorological agencies in each country. This ensures the information is timely and authoritative. 222 260 223 261 = What does the "Enable Rate Limiting" setting do? = … … 263 301 6. The Coordinate Finder tool, which generates widget code from a location name. 264 302 7. The "Under The Weather Forecast" block in the WordPress editor. 303 8. The weather widget with Weather Alerts shown 304 9. The weather widget with Sunrise and Sunset times shown 265 305 266 306 == Credits == … … 288 328 289 329 == Changelog == 330 331 = 2.2 = 332 * NEW: Introduced a `[under_the_weather]` shortcode to allow for easy placement of the weather widget in the Classic Editor, text widgets, and other page builders. 333 * NEW: Added a display option to show the day's sunrise time and sunset time, helpful in scheduling outdoor activities. 334 * NEW: Added an option to display severe weather alerts from official authorities directly within the widget. This feature can be enabled on the plugin's settings page. 335 * IMPROVEMENT: Incorporated clear warning icons for severe weather alerts. 336 * IMPROVEMENT: The plugin can now handle multiple weather widgets on a single page 337 * IMPROVEMENT: The front-end widget now loads its data asynchronously (AJAX). This improves perceived page load performance and allows multiple widgets on the same page to load their data independently. 338 * IMPROVEMENT: The settings page now features a Cache Expiration Time slider that allows greater flexibility and provides users with a visual way to select how long cached weather should be saved. 339 * NEW: Midnight expiration is now built into the Cache Expiration logic, so you never have to worry about displaying a cached copy of yesterday's forecast. 290 340 291 341 = 2.1 = … … 387 437 = 2.0 = 388 438 This version includes a "Under The Weather Forecast" block for the WordPress block editor. 439 440 = 2.2 = 441 This version introduces a new `[under_the_weather]` shortcode for easy widget placement and adds options to display severe weather alerts and daily sunrise/sunset times. -
under-the-weather/trunk/under-the-weather.php
r3369072 r3371900 4 4 * Plugin URI: https://www.sethcreates.com/plugins-for-wordpress/under-the-weather/ 5 5 * Description: A lightweight weather widget that caches OpenWeather API data and offers multiple style options. 6 * Version: 2. 16 * Version: 2.2 7 7 * Author: Seth Smigelski 8 8 * Author URI: https://www.sethcreates.com/plugins-for-wordpress/ … … 15 15 16 16 // Define a constant for the plugin version for easy maintenance. 17 define( 'UNDER_THE_WEATHER_VERSION', '2.1.0' ); 18 17 define( 'UNDER_THE_WEATHER_VERSION', '2.2.0' ); 18 19 // Add the Under The Weather Forecast block. 19 20 add_action('init', 'under_the_weather_register_widget_block'); 20 21 function under_the_weather_register_widget_block() { 21 22 register_block_type( __DIR__ . '/build' ); 23 } 24 25 // Add shortcode support. 26 add_action( 'init', 'under_the_weather_register_shortcode' ); 27 function under_the_weather_register_shortcode() { 28 add_shortcode( 'under_the_weather', 'under_the_weather_shortcode_callback' ); 22 29 } 23 30 … … 51 58 add_settings_section('under_the_weather_settings_section', __('API & Cache Settings', 'under-the-weather'), 'under_the_weather_settings_section_callback', $page_slug); 52 59 add_settings_field('under_the_weather_api_key', __('OpenWeather API Key', 'under-the-weather'), 'under_the_weather_api_key_field_html', $page_slug, 'under_the_weather_settings_section'); 53 add_settings_field('under_the_weather_expiration', __('Cache Expiration Time', 'under-the-weather'), 'under_the_weather_expiration_field_html', $page_slug, 'under_the_weather_settings_section'); 60 add_settings_field('under_the_weather_expiration', __('Cache Expiration Time (Hours)', 'under-the-weather'), 'under_the_weather_expiration_field_html', $page_slug, 'under_the_weather_settings_section'); 61 62 // Extra save button field (with empty label) 63 add_settings_field('under_the_weather_section_save', '', 'under_the_weather_save_button_callback', $page_slug, 'under_the_weather_settings_section'); 54 64 55 65 // Section for controlling widget display and style … … 59 69 add_settings_field('under_the_weather_display_mode', __('Primary Display', 'under-the-weather'), 'under_the_weather_display_mode_field_html', $page_slug, 'under_the_weather_display_section'); 60 70 add_settings_field('under_the_weather_forecast_days', __('Number of Forecast Days', 'under-the-weather'), 'under_the_weather_forecast_days_field_html', $page_slug, 'under_the_weather_display_section'); 71 add_settings_field('under_the_weather_show_unit', __('Unit Symbol', 'under-the-weather'), 'under_the_weather_show_unit_field_html', $page_slug, 'under_the_weather_display_section'); 61 72 add_settings_field('under_the_weather_show_details', __('Extra Details', 'under-the-weather'), 'under_the_weather_show_details_field_html', $page_slug, 'under_the_weather_display_section'); 62 add_settings_field('under_the_weather_show_unit', __('Display Unit Symbol', 'under-the-weather'), 'under_the_weather_show_unit_field_html', $page_slug, 'under_the_weather_display_section'); 63 add_settings_field('under_the_weather_show_timestamp', __('Display Timestamp', 'under-the-weather'), 'under_the_weather_show_timestamp_field_html', $page_slug, 'under_the_weather_display_section'); 73 add_settings_field('under_the_weather_sunrise_sunset', __('Sunrise & Sunset', 'under-the-weather'), 'under_the_weather_sunrise_sunset_field_html', $page_slug, 'under_the_weather_display_section'); 74 add_settings_field('under_the_weather_show_alerts', __('Weather Alerts', 'under-the-weather'), 'under_the_weather_show_alerts_field_html', $page_slug, 'under_the_weather_display_section'); 75 add_settings_field('under_the_weather_show_timestamp', __('Timestamps', 'under-the-weather'), 'under_the_weather_show_timestamp_field_html', $page_slug, 'under_the_weather_display_section'); 64 76 65 77 // Section for "Advanced Settings" … … 92 104 * Sanitize and validate all settings before saving to the database. 93 105 */ 106 107 // Sanitize the API key 94 108 function under_the_weather_sanitize_settings($input) { 95 109 $new_input = []; 110 111 // Sanitize the API key 96 112 if (isset($input['api_key'])) { 97 113 $api_key = sanitize_text_field($input['api_key']); 98 if (empty($api_key) || under_the_weather_validate_api_key($api_key)) { 114 if (empty($api_key)) { 115 $new_input['api_key'] = ''; // Allow clearing 116 } elseif (under_the_weather_validate_api_key($api_key)) { 99 117 $new_input['api_key'] = $api_key; 100 118 } else { 101 119 add_settings_error('under_the_weather_settings', 'invalid_api_key', 102 120 __('Invalid API key format. Please check your OpenWeather API key.', 'under-the-weather')); 121 // Keep the old value if new one is invalid 122 $old_options = get_option('under_the_weather_settings'); 123 $new_input['api_key'] = isset($old_options['api_key']) ? $old_options['api_key'] : ''; 103 124 } 104 125 } 105 if (isset($input['expiration']) && in_array($input['expiration'], ['1','2','3','6'])) { $new_input['expiration'] = $input['expiration']; } 106 if (isset($input['style_set']) && in_array($input['style_set'], ['default_images', 'weather_icons_font'])) { $new_input['style_set'] = $input['style_set']; } 107 if (isset($input['display_mode']) && in_array($input['display_mode'], ['current', 'today_forecast'])) { $new_input['display_mode'] = $input['display_mode']; } 108 if (isset($input['forecast_days']) && in_array($input['forecast_days'], ['2','3','4','5','6'])) { $new_input['forecast_days'] = $input['forecast_days']; } 109 110 // sanitization for Rate Limit options 126 127 // Sanitize the expiration time from the range slider 128 if (isset($input['expiration']) && is_numeric($input['expiration'])) { 129 $expiration = floatval($input['expiration']); 130 // Ensure the value is within the allowed range (0.5 to 8) 131 if ($expiration >= 0.5 && $expiration <= 8) { 132 $new_input['expiration'] = $expiration; 133 } else { 134 // If out of range, enforce minimum of 0.5 135 $new_input['expiration'] = max(0.5, min(8, $expiration)); 136 add_settings_error('under_the_weather_settings', 'expiration_adjusted', 137 __('Cache expiration time adjusted to minimum of 0.5 hours (30 minutes).', 'under-the-weather'), 'updated'); 138 } 139 } else { 140 $new_input['expiration'] = 4; // Default to 4 if not numeric 141 } 142 143 // Sanitize the Rate Limit options 111 144 $new_input['enable_rate_limit'] = isset($input['enable_rate_limit']) ? '1' : '0'; 112 145 if (isset($input['rate_limit_count']) && is_numeric($input['rate_limit_count'])) { … … 117 150 } 118 151 152 // Sanitize the Sunrise and Sunset preference with 12 hour time format and 24 hour time format 153 if (isset($input['sunrise_sunset']) && in_array($input['sunrise_sunset'], ['off', '12', '24'])) { 154 $new_input['sunrise_sunset'] = $input['sunrise_sunset']; 155 } else { 156 $new_input['sunrise_sunset'] = 'off'; 157 } 158 159 // Sanitize the major display options 160 if (isset($input['style_set']) && in_array($input['style_set'], ['default_images', 'weather_icons_font'])) { $new_input['style_set'] = $input['style_set']; } 161 if (isset($input['display_mode']) && in_array($input['display_mode'], ['current', 'today_forecast'])) { $new_input['display_mode'] = $input['display_mode']; } 162 if (isset($input['forecast_days']) && in_array($input['forecast_days'], ['2','3','4','5','6'])) { $new_input['forecast_days'] = $input['forecast_days']; } 163 119 164 $new_input['show_details'] = isset($input['show_details']) ? '1' : '0'; 120 165 $new_input['show_unit'] = isset($input['show_unit']) ? '1' : '0'; 166 $new_input['show_alerts'] = isset($input['show_alerts']) ? '1' : '0'; 121 167 $new_input['show_timestamp'] = isset($input['show_timestamp']) ? '1' : '0'; 122 168 $new_input['enable_cache'] = isset($input['enable_cache']) ? '1' : '0'; … … 148 194 <p><em><?php esc_html_e('Default Images', 'under-the-weather'); ?></em></p> 149 195 </div> 150 <div style="text-align: center;">196 <div class="under-the-weather-visual-reference-item"> 151 197 <img src="<?php echo esc_url($plugin_assets_url . 'font-style-example.svg'); ?>" alt="Weather Icons Font Style Example"> 152 198 <p><em><?php esc_html_e('Weather Icons Font', 'under-the-weather'); ?></em></p> … … 159 205 $options = get_option('under_the_weather_settings'); $value = isset($options['api_key']) ? $options['api_key'] : ''; echo '<input type="text" name="under_the_weather_settings[api_key]" value="' . esc_attr($value) . '" class="regular-text" placeholder="' . esc_attr__('Enter your API key', 'under-the-weather') . '">'; 160 206 } 161 function under_the_weather_expiration_field_html() { 162 $options = get_option('under_the_weather_settings'); $value = isset($options['expiration']) ? $options['expiration'] : '2'; echo '<select name="under_the_weather_settings[expiration]"><option value="1" '.selected($value, '1', false).'>' . esc_html__('1 Hour', 'under-the-weather') . '</option><option value="2" '.selected($value, '2', false).'>' . esc_html__('2 Hours', 'under-the-weather') . '</option><option value="3" '.selected($value, '3', false).'>' . esc_html__('3 Hours', 'under-the-weather') . '</option><option value="6" '.selected($value, '6', false).'>' . esc_html__('6 Hours', 'under-the-weather') . '</option></select>'; 207 208 // Use a slider to set the cache expiration time. Default: 4 hours. 209 function under_the_weather_expiration_field_html() { 210 $options = get_option('under_the_weather_settings'); 211 $value = isset($options['expiration']) ? $options['expiration'] : '4'; 212 ?> 213 <div class="utw-slider-container"> 214 <div class="utw-slider-wrapper"> 215 <input 216 type="range" 217 id="utw-expiration-slider" 218 name="under_the_weather_settings[expiration]" 219 value="<?php echo esc_attr($value); ?>" 220 min="0" 221 max="8" 222 step="0.5" 223 list="expiration-markers" 224 data-original-value="<?php echo esc_attr($value); ?>" 225 > 226 <datalist id="expiration-markers"> 227 <option value="0" label="0"></option> 228 <option value="1"></option> <option value="2" label="2"></option> 229 <option value="3"></option> <option value="4" label="4"></option> 230 <option value="5"></option> <option value="6" label="6"></option> 231 <option value="7"></option> <option value="8" label="8"></option> 232 </datalist> 233 </div> 234 </div> 235 <div class="utw-expiration-display"> 236 <span class="utw-slider-value"> 237 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 256 256"><path fill="#48484A" d="M128 44a96 96 0 1 0 96 96a96.11 96.11 0 0 0-96-96m0 168a72 72 0 1 1 72-72a72.08 72.08 0 0 1-72 72m36.49-112.49a12 12 0 0 1 0 17l-28 28a12 12 0 0 1-17-17l28-28a12 12 0 0 1 17 0M92 16a12 12 0 0 1 12-12h48a12 12 0 0 1 0 24h-48a12 12 0 0 1-12-12"/></svg> Cached weather will expire after <strong id="utw-expiration-value"><?php echo esc_html($value); ?></strong> hours. 238 </span> 239 </div> 240 <p id="utw-min-cache-notice" class="description"> 241 <?php esc_html_e('Minimum cache time is 30 minutes. To disable caching, use the advanced settings below.', 'under-the-weather'); ?> 242 </p> 243 <?php 244 } 245 // Add an extra save settigns button, just below the expiration slider. 246 function under_the_weather_save_button_callback() { 247 ?> 248 <div class="utw-section-save-wrapper"> 249 <?php submit_button( 250 __('Save Settings', 'under-the-weather'), 251 'primary', 252 'submit', 253 false, 254 ['id' => 'utw-expiration-save-btn'] 255 ); ?> 256 </div> 257 <div id="utw-unsaved-changes" class="utw-unsaved-message"> 258 <?php esc_html_e('You have unsaved changes', 'under-the-weather'); ?> 259 260 </div> 261 <?php 163 262 } 164 263 function under_the_weather_style_set_field_html() { … … 175 274 } 176 275 function under_the_weather_show_unit_field_html() { 177 $options = get_option('under_the_weather_settings'); $value = isset($options['show_unit']) ? $options['show_unit'] : '0'; echo "<input type='checkbox' name='under_the_weather_settings[show_unit]' value='1' " . checked($value, '1', false) . "> " . esc_html__('Show the temperature unit symbol (F or C) in the primary display.', 'under-the-weather'); 276 $options = get_option('under_the_weather_settings'); $value = isset($options['show_unit']) ? $options['show_unit'] : '0'; echo "<input type='checkbox' name='under_the_weather_settings[show_unit]' value='1' " . checked($value, '1', false) . "> " . esc_html__('Show the temperature unit symbol (F or C) in the primary display.', 'under-the-weather'); 277 } 278 function under_the_weather_show_alerts_field_html() { 279 $options = get_option('under_the_weather_settings'); 280 // Default to '1' (checked) to make the new feature visible 281 $value = isset($options['show_alerts']) ? $options['show_alerts'] : '1'; 282 echo "<input type='checkbox' name='under_the_weather_settings[show_alerts]' value='1' " . checked($value, '1', false) . "> " . esc_html__('Show active weather alerts from reporting authorities.', 'under-the-weather'); 283 } 284 function under_the_weather_sunrise_sunset_field_html() { 285 $options = get_option('under_the_weather_settings'); 286 $value = isset($options['sunrise_sunset']) ? $options['sunrise_sunset'] : 'off'; // Default to 'off' 287 ?> 288 <fieldset> 289 <label><input type="radio" name="under_the_weather_settings[sunrise_sunset]" value="off" <?php checked($value, 'off'); ?>> <?php esc_html_e('Off', 'under-the-weather'); ?></label><br> 290 <label><input type="radio" name="under_the_weather_settings[sunrise_sunset]" value="12" <?php checked($value, '12'); ?>> <?php esc_html_e('Show in 12-hour format (e.g., 6:30 PM)', 'under-the-weather'); ?></label><br> 291 <label><input type="radio" name="under_the_weather_settings[sunrise_sunset]" value="24" <?php checked($value, '24'); ?>> <?php esc_html_e('Show in 24-hour format (e.g., 18:30)', 'under-the-weather'); ?></label> 292 </fieldset> 293 <?php 178 294 } 179 295 function under_the_weather_show_timestamp_field_html() { … … 211 327 <?php esc_html_e('Find Coordinates', 'under-the-weather'); ?> 212 328 </button> 213 <div id="utw-result-wrapper" style="margin-top: 15px;">329 <div id="utw-result-wrapper"> 214 330 </div> 215 331 </div> … … 361 477 function under_the_weather_enqueue_assets() { 362 478 $options = get_option('under_the_weather_settings'); 363 if (empty($options)) return; 364 479 if (empty($options)) return; 480 481 // Register the main style so WordPress knows about it. 482 wp_register_style('under-the-weather-styles', plugins_url('css/under-the-weather.min.css', __FILE__), [], UNDER_THE_WEATHER_VERSION); 483 484 // Register dependent icon styles. 485 if (isset($options['style_set']) && $options['style_set'] === 'weather_icons_font') { 486 wp_register_style('under-the-weather-icons', plugins_url('css/weather-icons.min.css', __FILE__), [], '2.0'); 487 if (!empty($options['show_details'])) { 488 wp_register_style('under-the-weather-wind-icons', plugins_url('css/weather-icons-wind.min.css', __FILE__), [], '2.0'); 489 } 490 } 491 492 // Conditionally ENQUEUE based on the global setting. 365 493 if (!empty($options['enqueue_style'])) { 366 wp_enqueue_style('under-the-weather-styles', plugins_url('css/under-the-weather.min.css', __FILE__), [], UNDER_THE_WEATHER_VERSION);494 wp_enqueue_style('under-the-weather-styles'); 367 495 if (isset($options['style_set']) && $options['style_set'] === 'weather_icons_font') { 368 wp_enqueue_style('under-the-weather-icons' , plugins_url('css/weather-icons.min.css', __FILE__), [], '2.0');496 wp_enqueue_style('under-the-weather-icons'); 369 497 if (!empty($options['show_details'])) { 370 wp_enqueue_style('under-the-weather-wind-icons' , plugins_url('css/weather-icons-wind.min.css', __FILE__), [], '2.0');498 wp_enqueue_style('under-the-weather-wind-icons'); 371 499 } 372 500 } … … 467 595 'forecast_days' => isset($options['forecast_days']) ? intval($options['forecast_days']) : 5, 468 596 'show_details' => !empty($options['show_details']), 597 'show_alerts' => !empty($options['show_alerts']), 598 'sunrise_sunset_format' => isset($options['sunrise_sunset']) ? $options['sunrise_sunset'] : 'off', 469 599 'show_timestamp' => !empty($options['show_timestamp']), 470 600 'show_unit' => !empty($options['show_unit']), … … 477 607 wp_localize_script('under-the-weather-script', 'under_the_weather_plugin_url', $plugin_url_data); 478 608 } 479 480 609 481 610 // ============================================================================= … … 787 916 $api_url = "https://api.openweathermap.org/data/3.0/onecall?lat={$lat}&lon={$lon}&appid={$api_key}&units={$unit}"; 788 917 789 // Check API Response918 // Check API Response 790 919 $response_body = under_the_weather_safe_api_call($api_url); 791 920 if ($response_body === false) { … … 793 922 } 794 923 795 $weather_data = json_decode($response_body); 924 $weather_data = json_decode($response_body); 925 796 926 under_the_weather_update_usage_stats('api'); 797 927 … … 811 941 $weather_data->daily[array_search($day, $weather_data->daily)]->weather[0]->icon_class = under_the_weather_get_icon_class($day->weather[0]->icon); 812 942 } 813 } 814 815 if ($caching_enabled) { 816 set_transient($transient_key, $weather_data, $expiration_hours * HOUR_IN_SECONDS); 817 } 943 } 944 945 // Establish midnight cache expiration logic, so the previous day's weather is not shown from the cache 946 // Incorporate midnight cache expiration with timed cache expiration preference 947 // Add a 10-minute buffer to avoid caching the previous day's forecast at midnight due to service clock differences. 948 // This is like treating 12:10 a.m. as midnight as a precaution 949 950 if ($caching_enabled) { 951 // Enforce minimum cache time of 30 minutes 952 $expiration_hours = isset($options['expiration']) ? floatval($options['expiration']) : 4; 953 $expiration_hours = max(0.5, $expiration_hours); 954 955 $midnight_expiration_seconds = 0; 956 957 if (isset($weather_data->timezone) && is_string($weather_data->timezone)) { 958 try { 959 $timezone_obj = new DateTimeZone($weather_data->timezone); 960 $now = new DateTime('now', $timezone_obj); 961 $midnight = new DateTime('tomorrow midnight', $timezone_obj); 962 $seconds_until_midnight = $midnight->getTimestamp() - $now->getTimestamp(); 963 $midnight_expiration_seconds = $seconds_until_midnight + (10 * 60); 964 } catch (Exception $e) { 965 under_the_weather_log('Invalid timezone from API: ' . $weather_data->timezone); 966 $midnight_expiration_seconds = 0; 967 } 968 } 969 970 // Calculate fixed duration 971 $fixed_duration_seconds = $expiration_hours * HOUR_IN_SECONDS; 972 973 // Use the shorter of: fixed duration or midnight expiration 974 if ($midnight_expiration_seconds > 0) { 975 $final_expiration_seconds = min($fixed_duration_seconds, $midnight_expiration_seconds); 976 } else { 977 $final_expiration_seconds = $fixed_duration_seconds; 978 } 979 980 // Final safety check: ensure at least 30 minutes 981 $final_expiration_seconds = max(1800, $final_expiration_seconds); 982 983 set_transient($transient_key, $weather_data, $final_expiration_seconds); 984 } 985 818 986 819 987 return new WP_REST_Response($weather_data, 200); 988 } 989 990 /** 991 * 992 * Callback function for the [under_the_weather] shortcode. 993 * @param array $atts Shortcode attributes. 994 * @return string HTML output for the weather widget. 995 * 996 */ 997 function under_the_weather_shortcode_callback( $atts ) { 998 // 1. Define attributes and set default values. 999 $atts = shortcode_atts( 1000 array( 1001 'lat' => '', 1002 'lon' => '', 1003 'location_name' => '', 1004 'unit' => 'imperial', // Default to imperial 1005 ), 1006 $atts, 1007 'under_the_weather' 1008 ); 1009 1010 // 2. Validate that essential attributes are present. 1011 if ( empty( $atts['lat'] ) || empty( $atts['lon'] ) || empty( $atts['location_name'] ) ) { 1012 // Return an error message or an empty string if essential data is missing. 1013 return ''; 1014 } 1015 1016 // 3. Enqueue the necessary scripts and styles. 1017 // This ensures they are only loaded on pages where the shortcode is used. 1018 wp_enqueue_style( 'under-the-weather-styles' ); 1019 // We also need to enqueue the icon styles if they are selected in the settings. 1020 $options = get_option('under_the_weather_settings'); 1021 if (isset($options['style_set']) && $options['style_set'] === 'weather_icons_font') { 1022 wp_enqueue_style('under-the-weather-icons'); 1023 if (!empty($options['show_details'])) { 1024 wp_enqueue_style('under-the-weather-wind-icons'); 1025 } 1026 } 1027 under_the_weather_load_scripts_manually(); 1028 1029 // 4. Build and return the HTML div. 1030 return sprintf( 1031 '<div class="weather-widget" data-lat="%s" data-lon="%s" data-location-name="%s" data-unit="%s"></div>', 1032 esc_attr( $atts['lat'] ), 1033 esc_attr( $atts['lon'] ), 1034 esc_attr( $atts['location_name'] ), 1035 esc_attr( $atts['unit'] ) 1036 ); 820 1037 } 821 1038
Note: See TracChangeset
for help on using the changeset viewer.