Plugin Directory

Changeset 3371900


Ignore:
Timestamp:
10/02/2025 03:55:46 PM (5 months ago)
Author:
sethsm
Message:

Update to version 2.2 from GitHub

Location:
under-the-weather
Files:
12 added
25 edited
1 copied

Legend:

Unmodified
Added
Removed
  • under-the-weather/tags/2.2/README.md

    r3369072 r3371900  
    88* **Requires at least:** 5.0
    99* **Tested up to:** 6.8
    10 * **Stable tag:** 2.1
     10* **Stable tag:** 2.2
    1111* **Requires PHP:** 7.2
    1212* **License:** GPLv2 or later
     
    3131* **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.
    3232* **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.
    3534* **Imperial & Metric Units:** Display weather in Fahrenheit/mph or Celsius/kph on a per-widget basis.
    3635* **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.
    3738* **Lightweight:** Enqueues assets only when needed and does not rely on heavy JavaScript libraries.
    3839* **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.
     
    112113The plugin's JavaScript will automatically find this element and populate it with the forecast.
    113114
     115### Using the Shortcode (Classic Editor & Widgets)
     116
     117You 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
    114128---
    115129
     
    121135
    122136**Cache Expiration Time:**
    123 This setting controls how long the weather data is stored on your server before fetching a new forecast.
     137Use the slider to set the maximum time weather data is stored before fetching a new forecast, from 30 minutes to 8 hours.
     138
     139The 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
    124141For 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 6 hours is effective at reducing API calls.
     142For displaying only the daily high/low, a longer cache time of 3 or 8 hours is effective at reducing API calls.
    126143
    127144**Widget Display & Style**
     
    137154
    138155**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.
     156Selecting 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
    140162
    141163**Display Timestamp:**
     
    235257No. 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.
    236258
     259### Will my website ever show yesterday's weather If I set a long cache time?
     260
     261Cinderella'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
     263For 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
     265The 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
    237267### The weather isn't updating. Why?
    238268
     
    261291### Can I still use the manual div method if I prefer it?
    262292
    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.
     293Absolutely! While the **block** is the recommended, user-friendly method for the modern WordPress editor, the plugin fully supports traditional methods for maximum flexibility.
     294
     295You can use the `[under_the_weather]` shortcode to easily place the widget in the Classic Editor, text widgets, or with various page builders.
     296
     297Additionally, 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
     299The 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.
    266300
    267301### What coordinate format should I use?
     
    274308
    275309If 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
     313The 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.
    276314
    277315### What does the "Enable Rate Limiting" setting do?
     
    326364_The weather widget displaying current conditions with default icons (in Celsius) and extra details enabled._
    327365
     366![The weather widget with Weather Alerts shown](https://ps.w.org/under-the-weather/assets/screenshot-8.png)
     367
     368_The weather widget with Weather Alerts shown._
     369
     370![The weather widget with Sunrise and Sunset times shown](https://ps.w.org/under-the-weather/assets/screenshot-9.png)
     371
     372_The weather widget with Sunrise and Sunset times shown._
     373
    328374---
    329375
     
    357403## Changelog
    358404
     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 
    359415### 2.1
    360416* **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.
     
    457513### 2.0
    458514This version includes a "Under The Weather Forecast" block for the WordPress block editor.
     515
     516### 2.2
     517This 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  
    33  "apiVersion": 3,
    44  "name": "under-the-weather/widget",
    5   "version": "2.1.0",
     5  "version": "2.2.0",
    66  "title": "Under The Weather Forecast",
    77  "category": "widgets",
  • under-the-weather/tags/2.2/css/admin-styles.css

    r3365104 r3371900  
    2323     max-width: 100px;
    2424}
    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
    3031.under-the-weather-usage-report {
    3132    display: flex;
     
    133134    margin: 15px 0;
    134135}
     136
     137/* --- Coordinate Finder --- */
     138
    135139.weather-widget-coordinate-finder-pre{
    136140     background: #f1f1f1;
     
    142146    color: red;
    143147}
     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 --- */
    22.weather-widget {
    33    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 */
    55}
    66
    77.weather-location-name {
    88    border-bottom: 2px solid #eee;
    9     font-size: 18px;
     9    font-size: 1.375em;
     10    font-weight: bold;
    1011    margin-bottom: 15px;
    1112    padding-bottom: 7px;
     
    5455.forecast-day-name {
    5556    color: #444;
    56     font-size: 14px;
     57    font-size: 0.65em;
    5758    margin-bottom: 8px;
    5859}
     
    6465}
    6566
    66 .forecast-temps {
    67     color: #fff;
    68     font-size: 6px;
     67.forecast-temps .slash {
     68    color: transparent;
     69    font-size: 0.375em;
    6970    /*Change the color and size to see the / between high and low */
    7071}
     
    7273.forecast-temps .high {
    7374    color: #000;
    74     font-size: 17px;
     75    font-size: 1.06em; /* Converted from 17px */
    7576    font-weight: 700;
    7677}
    7778
    7879.forecast-temps .low {
    79     color: #666;
    80     font-size: 14px;
     80    color: #555;
     81    font-size: 0.875em; /* Converted from 14px */
    8182}
    8283
     
    8990
    9091.today-forecast-label {
    91     font-size: 18px;
     92    font-size: 1.125em; /* Converted from 18px */
    9293    margin: 6px 0 11px;
    9394}
    9495
    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;
    9899    /*Change the color and size to see the / between high and low */
    99100}
     
    101102.today-forecast-temps .high {
    102103    color: #000;
    103     font-size: 36px;
     104    font-size: 2.25em; /* Converted from 36px */
    104105    font-weight: 700;
    105106}
    106107
    107108.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 {
    108115    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 */
    116117    font-weight: 400;
    117118    position: relative;
     
    120121
    121122.weather-widget .wi {
    122     color: #666;
     123    color: #555;
    123124    font-size: 45px;
    124125    line-height: 1;
     
    131132
    132133.weather-extra-details {
    133     border-top: 1px solid #eee;
    134134    border-bottom: 1px solid #eee;
    135135    color: #555;
     
    155155    color: #999;
    156156    font-size: .8em;
    157     margin-top: 15px;
     157    margin-top: 10px;
    158158    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:18px;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  
    11// js/admin-geocoder.js
    22document.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   
    3103    const geocoderTool = document.getElementById('utw-geocoder-tool');
    4104    if (!geocoderTool) {
    5         return;
    6     }
     105        return; // Exit only AFTER checking for the expiration slider
     106    }
     107   
    7108    const locationInput = document.getElementById('utw-location-input');
    8109    const findButton = document.getElementById('utw-find-coords');
     
    12113
    13114    // --- HISTORY FUNCTIONS ---
    14     let searchHistory = []; // Local cache of the history
     115    let searchHistory = [];
    15116    const MAX_HISTORY = 5;
    16117   
     
    18119    function formatWidgetHtml(html) {
    19120        return html
    20             //.replace('<div class="weather-widget"', '<div class="weather-widget"\n    ')
    21121            .replace(' data-lat=', '\n     data-lat=')
    22122            .replace(' data-lon=', '\n     data-lon=') 
     
    51151
    52152    function saveToHistoryAndServer(newItem) {
    53            
    54         // Add to local history first
    55153        searchHistory.unshift(newItem);
    56154        searchHistory = searchHistory.slice(0, MAX_HISTORY);
    57155       
    58         // Update the UI immediately with local data
    59         console.log('UTW: Updated history:', searchHistory);
     156        console.log('UTW: Updated history:', searchHistory);
    60157        renderHistory();
    61158       
    62         // Save to server
    63159        fetch(ajaxurl, {
    64160            method: 'POST',
     
    135231                        const locationName = result.display_name;
    136232
    137                         // Generate the HTML div for the user
    138233                        const widgetHtml = `<div class="weather-widget" data-lat="${lat}" data-lon="${lon}" data-location-name="${locationQuery}"></div>`;
    139234
     
    145240                        `;
    146241                       
    147                         // Add to history and save to server
    148242                        saveToHistoryAndServer({ locationName: locationQuery, widgetHtml });
    149243
    150                         // Add click listener to the new copy button
    151244                        document.getElementById('utw-copy-button').addEventListener('click', function() {
    152245                            copyToClipboard(formatWidgetHtml(widgetHtml), this);
     
    165258   
    166259    if (historyList) {
    167         // Use event delegation for copy buttons in the history list
    168260        historyList.addEventListener('click', function(event) {
    169261            if (event.target.classList.contains('copy-history-btn')) {
     
    173265        });
    174266       
    175         // Load history from server on page load
    176267        loadHistoryFromServer();
    177268    }
    178269   
    179     // Copy searches to clipboard
    180270    function copyToClipboard(text, button) {
    181271        if (navigator.clipboard && window.isSecureContext) {
     
    219309    }
    220310
    221     // Helper function to escape HTML for display in a <pre> tag
    222311    function escapeHtml(unsafe) {
    223312        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.
    722document.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 */
     21function loadWeatherData(widget) {
    7822    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
    8424    let lat = widget.dataset.lat;
    8525    let lon = widget.dataset.lon;
    86    
     26
    8727    // Attempt to parse/convert them (this handles DD, DDM, and DMS formats)
    8828    const parsedLat = parseCoordinate(lat);
     
    10949
    11050    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
    11255    const apiUrl = `/wp-json/under-the-weather/v1/forecast?lat=${lat}&lon=${lon}&location_name=${encodeURIComponent(locationName)}&unit=${unit}`;
    11356
     
    13982      widget.innerHTML = `<p>Unable to load weather data. Please try again later.</p>`;
    14083    });
    141 });
     84}
     85
     86// Validate Coordinates
     87function 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
     94function 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.
     107function 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 */
     162function 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}
    142197
    143198function 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;
    145200    const locationName = widget.dataset.locationName || '';
    146201   
     
    185240        return "a minute ago";
    186241    }
     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
    187290
    188291    let primaryDisplayHtml = '';
     
    197300                    <div class="today-forecast-label">Today</div>
    198301                    <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}
    200303                    </div>
    201304                </div>
     
    237340        `;
    238341    }
     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    }
    239372
    240373    const forecastDaysToShow = parseInt(forecast_days, 10) || 5;
     
    252385            ${getIconHtml(day.weather[0])}
    253386            <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>
    255388            </div>
    256389          </div>
     
    265398    const finalHtml = `
    266399        <div class="weather-location-name">${locationName}</div>
    267         ${primaryDisplayHtml}
     400        ${alertHtml}
     401        ${primaryDisplayHtml}
    268402        ${extraDetailsHtml}
     403        ${sunriseSunsetHtml}
    269404        <div class="forecast-container">
    270405            ${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>"}))}));
     1function 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  
    44Requires at least: 5.0
    55Tested up to: 6.8
    6 Stable tag: 2.1
     6Stable tag: 2.2
    77Requires PHP: 7.2
    88License: GPLv2 or later
     
    2727* **Easy to Use:** Add weather widgets using the WordPress block editor or by placing a simple `<div>` with data attributes anywhere on your site.
    2828* **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.
    2930* **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.
    3232* **Imperial & Metric Units:** Display weather in Fahrenheit/mph or Celsius/kph on a per-widget basis.
    3333* **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.
    3436* **Lightweight:** Enqueues assets only when needed and does not rely on heavy JavaScript libraries.
    3537* **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.
     
    8587The plugin's JavaScript will automatically find this element and populate it with the forecast.
    8688
     89**Using the Shortcode (Classic Editor & Widgets)**
     90
     91You 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
    87102== Configuration ==
    88103
     
    92107
    93108**Cache Expiration Time:**
    94 This setting controls how long the weather data is stored on your server before fetching a new forecast.
     109Use the slider to set the maximum time weather data is stored before fetching a new forecast, from 30 minutes to 8 hours.
     110
     111The 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
    95113For 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 6 hours is effective at reducing API calls.
     114For displaying only the daily high/low, a longer cache time of 3 or 8 hours is effective at reducing API calls.
    97115
    98116**Widget Display & Style**
     
    108126
    109127**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.
     128Selecting 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.
    111133
    112134**Display Timestamp:**
     
    181203No. 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.
    182204
     205= Will my website ever show yesterday's weather If I set a long cache time? =
     206
     207Cinderella'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
     209For 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
     211The 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
    183213= The weather isn't updating. Why? =
    184214
     
    207237= Can I still use the manual div method if I prefer it? =
    208238
    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.
     239Absolutely! While the **block** is the recommended, user-friendly method for the modern WordPress editor, the plugin fully supports traditional methods for maximum flexibility.
     240
     241You can use the `[under_the_weather]` shortcode to easily place the widget in the Classic Editor, text widgets, or with various page builders.
     242
     243Additionally, 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
     245The 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.
    212246
    213247= What coordinate format should I use? =
     
    220254
    221255If 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
     259The 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.
    222260
    223261= What does the "Enable Rate Limiting" setting do? =
     
    2633016. The Coordinate Finder tool, which generates widget code from a location name.
    2643027. The "Under The Weather Forecast" block in the WordPress editor.
     3038. The weather widget with Weather Alerts shown
     3049. The weather widget with Sunrise and Sunset times shown
    265305
    266306== Credits ==
     
    288328
    289329== 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.
    290340
    291341= 2.1 =
     
    387437= 2.0 =
    388438This version includes a "Under The Weather Forecast" block for the WordPress block editor.
     439
     440= 2.2 =
     441This 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  
    44 * Plugin URI:        https://www.sethcreates.com/plugins-for-wordpress/under-the-weather/
    55 * Description:       A lightweight weather widget that caches OpenWeather API data and offers multiple style options.
    6  * Version:           2.1
     6 * Version:           2.2
    77 * Author:            Seth Smigelski
    88 * Author URI:        https://www.sethcreates.com/plugins-for-wordpress/
     
    1515
    1616// Define a constant for the plugin version for easy maintenance.
    17 define( 'UNDER_THE_WEATHER_VERSION', '2.1.0' );
    18 
     17define( 'UNDER_THE_WEATHER_VERSION', '2.2.0' );
     18
     19// Add the Under The Weather Forecast block.
    1920add_action('init', 'under_the_weather_register_widget_block');
    2021function under_the_weather_register_widget_block() {
    2122    register_block_type( __DIR__ . '/build' );
     23}
     24
     25// Add shortcode support.
     26add_action( 'init', 'under_the_weather_register_shortcode' );
     27function under_the_weather_register_shortcode() {
     28    add_shortcode( 'under_the_weather', 'under_the_weather_shortcode_callback' );
    2229}
    2330
     
    5158    add_settings_section('under_the_weather_settings_section', __('API & Cache Settings', 'under-the-weather'), 'under_the_weather_settings_section_callback', $page_slug);
    5259    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');   
    5464
    5565    // Section for controlling widget display and style
     
    5969    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');
    6070    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');   
    6172    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');
     73add_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');
    6476
    6577    // Section for "Advanced Settings"
     
    92104 * Sanitize and validate all settings before saving to the database.
    93105 */
     106 
     107// Sanitize the API key
    94108function under_the_weather_sanitize_settings($input) {
    95109    $new_input = [];
     110   
     111    // Sanitize the API key
    96112    if (isset($input['api_key'])) {
    97113        $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)) {
    99117            $new_input['api_key'] = $api_key;
    100118        } else {
    101119            add_settings_error('under_the_weather_settings', 'invalid_api_key',
    102120                __('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'] : '';
    103124        }
    104125    }
    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
    111144    $new_input['enable_rate_limit'] = isset($input['enable_rate_limit']) ? '1' : '0';
    112145    if (isset($input['rate_limit_count']) && is_numeric($input['rate_limit_count'])) {
     
    117150    }
    118151   
     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   
    119164    $new_input['show_details'] = isset($input['show_details']) ? '1' : '0';
    120165    $new_input['show_unit'] = isset($input['show_unit']) ? '1' : '0';
     166    $new_input['show_alerts'] = isset($input['show_alerts']) ? '1' : '0';
    121167    $new_input['show_timestamp'] = isset($input['show_timestamp']) ? '1' : '0';
    122168    $new_input['enable_cache'] = isset($input['enable_cache']) ? '1' : '0';
     
    148194            <p><em><?php esc_html_e('Default Images', 'under-the-weather'); ?></em></p>
    149195        </div>
    150         <div style="text-align: center;">
     196        <div class="under-the-weather-visual-reference-item">
    151197            <img src="<?php echo esc_url($plugin_assets_url . 'font-style-example.svg'); ?>" alt="Weather Icons Font Style Example">
    152198            <p><em><?php esc_html_e('Weather Icons Font', 'under-the-weather'); ?></em></p>
     
    159205    $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') . '">';
    160206    }
    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.
     209function 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.
     246function 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
    163262}
    164263function under_the_weather_style_set_field_html() {
     
    175274}
    176275function 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}
     278function 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}
     284function 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
    178294}
    179295function under_the_weather_show_timestamp_field_html() {
     
    211327            <?php esc_html_e('Find Coordinates', 'under-the-weather'); ?>
    212328        </button>
    213         <div id="utw-result-wrapper" style="margin-top: 15px;">
     329        <div id="utw-result-wrapper">
    214330            </div>
    215331    </div>
     
    361477function under_the_weather_enqueue_assets() {
    362478    $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.
    365493    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');
    367495        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');
    369497            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');
    371499            }
    372500        }
     
    467595        'forecast_days'  => isset($options['forecast_days']) ? intval($options['forecast_days']) : 5,
    468596        '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',
    469599        'show_timestamp' => !empty($options['show_timestamp']),
    470600        'show_unit'      => !empty($options['show_unit']),
     
    477607    wp_localize_script('under-the-weather-script', 'under_the_weather_plugin_url', $plugin_url_data);
    478608}
    479 
    480609
    481610// =============================================================================
     
    787916    $api_url = "https://api.openweathermap.org/data/3.0/onecall?lat={$lat}&lon={$lon}&appid={$api_key}&units={$unit}";
    788917   
    789     //Check API Response
     918    // Check API Response
    790919    $response_body = under_the_weather_safe_api_call($api_url);
    791920    if ($response_body === false) {
     
    793922    }
    794923   
    795     $weather_data = json_decode($response_body);
     924    $weather_data = json_decode($response_body);
     925   
    796926    under_the_weather_update_usage_stats('api');
    797927   
     
    811941            $weather_data->daily[array_search($day, $weather_data->daily)]->weather[0]->icon_class = under_the_weather_get_icon_class($day->weather[0]->icon);
    812942        }
    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
    818986   
    819987    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 */
     997function 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    );
    8201037}
    8211038
  • under-the-weather/trunk/README.md

    r3369072 r3371900  
    88* **Requires at least:** 5.0
    99* **Tested up to:** 6.8
    10 * **Stable tag:** 2.1
     10* **Stable tag:** 2.2
    1111* **Requires PHP:** 7.2
    1212* **License:** GPLv2 or later
     
    3131* **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.
    3232* **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.
    3534* **Imperial & Metric Units:** Display weather in Fahrenheit/mph or Celsius/kph on a per-widget basis.
    3635* **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.
    3738* **Lightweight:** Enqueues assets only when needed and does not rely on heavy JavaScript libraries.
    3839* **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.
     
    112113The plugin's JavaScript will automatically find this element and populate it with the forecast.
    113114
     115### Using the Shortcode (Classic Editor & Widgets)
     116
     117You 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
    114128---
    115129
     
    121135
    122136**Cache Expiration Time:**
    123 This setting controls how long the weather data is stored on your server before fetching a new forecast.
     137Use the slider to set the maximum time weather data is stored before fetching a new forecast, from 30 minutes to 8 hours.
     138
     139The 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
    124141For 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 6 hours is effective at reducing API calls.
     142For displaying only the daily high/low, a longer cache time of 3 or 8 hours is effective at reducing API calls.
    126143
    127144**Widget Display & Style**
     
    137154
    138155**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.
     156Selecting 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
    140162
    141163**Display Timestamp:**
     
    235257No. 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.
    236258
     259### Will my website ever show yesterday's weather If I set a long cache time?
     260
     261Cinderella'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
     263For 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
     265The 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
    237267### The weather isn't updating. Why?
    238268
     
    261291### Can I still use the manual div method if I prefer it?
    262292
    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.
     293Absolutely! While the **block** is the recommended, user-friendly method for the modern WordPress editor, the plugin fully supports traditional methods for maximum flexibility.
     294
     295You can use the `[under_the_weather]` shortcode to easily place the widget in the Classic Editor, text widgets, or with various page builders.
     296
     297Additionally, 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
     299The 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.
    266300
    267301### What coordinate format should I use?
     
    274308
    275309If 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
     313The 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.
    276314
    277315### What does the "Enable Rate Limiting" setting do?
     
    326364_The weather widget displaying current conditions with default icons (in Celsius) and extra details enabled._
    327365
     366![The weather widget with Weather Alerts shown](https://ps.w.org/under-the-weather/assets/screenshot-8.png)
     367
     368_The weather widget with Weather Alerts shown._
     369
     370![The weather widget with Sunrise and Sunset times shown](https://ps.w.org/under-the-weather/assets/screenshot-9.png)
     371
     372_The weather widget with Sunrise and Sunset times shown._
     373
    328374---
    329375
     
    357403## Changelog
    358404
     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 
    359415### 2.1
    360416* **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.
     
    457513### 2.0
    458514This version includes a "Under The Weather Forecast" block for the WordPress block editor.
     515
     516### 2.2
     517This 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  
    33  "apiVersion": 3,
    44  "name": "under-the-weather/widget",
    5   "version": "2.1.0",
     5  "version": "2.2.0",
    66  "title": "Under The Weather Forecast",
    77  "category": "widgets",
  • under-the-weather/trunk/css/admin-styles.css

    r3365104 r3371900  
    2323     max-width: 100px;
    2424}
    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
    3031.under-the-weather-usage-report {
    3132    display: flex;
     
    133134    margin: 15px 0;
    134135}
     136
     137/* --- Coordinate Finder --- */
     138
    135139.weather-widget-coordinate-finder-pre{
    136140     background: #f1f1f1;
     
    142146    color: red;
    143147}
     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 --- */
    22.weather-widget {
    33    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 */
    55}
    66
    77.weather-location-name {
    88    border-bottom: 2px solid #eee;
    9     font-size: 18px;
     9    font-size: 1.375em;
     10    font-weight: bold;
    1011    margin-bottom: 15px;
    1112    padding-bottom: 7px;
     
    5455.forecast-day-name {
    5556    color: #444;
    56     font-size: 14px;
     57    font-size: 0.65em;
    5758    margin-bottom: 8px;
    5859}
     
    6465}
    6566
    66 .forecast-temps {
    67     color: #fff;
    68     font-size: 6px;
     67.forecast-temps .slash {
     68    color: transparent;
     69    font-size: 0.375em;
    6970    /*Change the color and size to see the / between high and low */
    7071}
     
    7273.forecast-temps .high {
    7374    color: #000;
    74     font-size: 17px;
     75    font-size: 1.06em; /* Converted from 17px */
    7576    font-weight: 700;
    7677}
    7778
    7879.forecast-temps .low {
    79     color: #666;
    80     font-size: 14px;
     80    color: #555;
     81    font-size: 0.875em; /* Converted from 14px */
    8182}
    8283
     
    8990
    9091.today-forecast-label {
    91     font-size: 18px;
     92    font-size: 1.125em; /* Converted from 18px */
    9293    margin: 6px 0 11px;
    9394}
    9495
    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;
    9899    /*Change the color and size to see the / between high and low */
    99100}
     
    101102.today-forecast-temps .high {
    102103    color: #000;
    103     font-size: 36px;
     104    font-size: 2.25em; /* Converted from 36px */
    104105    font-weight: 700;
    105106}
    106107
    107108.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 {
    108115    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 */
    116117    font-weight: 400;
    117118    position: relative;
     
    120121
    121122.weather-widget .wi {
    122     color: #666;
     123    color: #555;
    123124    font-size: 45px;
    124125    line-height: 1;
     
    131132
    132133.weather-extra-details {
    133     border-top: 1px solid #eee;
    134134    border-bottom: 1px solid #eee;
    135135    color: #555;
     
    155155    color: #999;
    156156    font-size: .8em;
    157     margin-top: 15px;
     157    margin-top: 10px;
    158158    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:18px;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  
    11// js/admin-geocoder.js
    22document.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   
    3103    const geocoderTool = document.getElementById('utw-geocoder-tool');
    4104    if (!geocoderTool) {
    5         return;
    6     }
     105        return; // Exit only AFTER checking for the expiration slider
     106    }
     107   
    7108    const locationInput = document.getElementById('utw-location-input');
    8109    const findButton = document.getElementById('utw-find-coords');
     
    12113
    13114    // --- HISTORY FUNCTIONS ---
    14     let searchHistory = []; // Local cache of the history
     115    let searchHistory = [];
    15116    const MAX_HISTORY = 5;
    16117   
     
    18119    function formatWidgetHtml(html) {
    19120        return html
    20             //.replace('<div class="weather-widget"', '<div class="weather-widget"\n    ')
    21121            .replace(' data-lat=', '\n     data-lat=')
    22122            .replace(' data-lon=', '\n     data-lon=') 
     
    51151
    52152    function saveToHistoryAndServer(newItem) {
    53            
    54         // Add to local history first
    55153        searchHistory.unshift(newItem);
    56154        searchHistory = searchHistory.slice(0, MAX_HISTORY);
    57155       
    58         // Update the UI immediately with local data
    59         console.log('UTW: Updated history:', searchHistory);
     156        console.log('UTW: Updated history:', searchHistory);
    60157        renderHistory();
    61158       
    62         // Save to server
    63159        fetch(ajaxurl, {
    64160            method: 'POST',
     
    135231                        const locationName = result.display_name;
    136232
    137                         // Generate the HTML div for the user
    138233                        const widgetHtml = `<div class="weather-widget" data-lat="${lat}" data-lon="${lon}" data-location-name="${locationQuery}"></div>`;
    139234
     
    145240                        `;
    146241                       
    147                         // Add to history and save to server
    148242                        saveToHistoryAndServer({ locationName: locationQuery, widgetHtml });
    149243
    150                         // Add click listener to the new copy button
    151244                        document.getElementById('utw-copy-button').addEventListener('click', function() {
    152245                            copyToClipboard(formatWidgetHtml(widgetHtml), this);
     
    165258   
    166259    if (historyList) {
    167         // Use event delegation for copy buttons in the history list
    168260        historyList.addEventListener('click', function(event) {
    169261            if (event.target.classList.contains('copy-history-btn')) {
     
    173265        });
    174266       
    175         // Load history from server on page load
    176267        loadHistoryFromServer();
    177268    }
    178269   
    179     // Copy searches to clipboard
    180270    function copyToClipboard(text, button) {
    181271        if (navigator.clipboard && window.isSecureContext) {
     
    219309    }
    220310
    221     // Helper function to escape HTML for display in a <pre> tag
    222311    function escapeHtml(unsafe) {
    223312        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.
    722document.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 */
     21function loadWeatherData(widget) {
    7822    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
    8424    let lat = widget.dataset.lat;
    8525    let lon = widget.dataset.lon;
    86    
     26
    8727    // Attempt to parse/convert them (this handles DD, DDM, and DMS formats)
    8828    const parsedLat = parseCoordinate(lat);
     
    10949
    11050    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
    11255    const apiUrl = `/wp-json/under-the-weather/v1/forecast?lat=${lat}&lon=${lon}&location_name=${encodeURIComponent(locationName)}&unit=${unit}`;
    11356
     
    13982      widget.innerHTML = `<p>Unable to load weather data. Please try again later.</p>`;
    14083    });
    141 });
     84}
     85
     86// Validate Coordinates
     87function 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
     94function 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.
     107function 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 */
     162function 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}
    142197
    143198function 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;
    145200    const locationName = widget.dataset.locationName || '';
    146201   
     
    185240        return "a minute ago";
    186241    }
     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
    187290
    188291    let primaryDisplayHtml = '';
     
    197300                    <div class="today-forecast-label">Today</div>
    198301                    <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}
    200303                    </div>
    201304                </div>
     
    237340        `;
    238341    }
     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    }
    239372
    240373    const forecastDaysToShow = parseInt(forecast_days, 10) || 5;
     
    252385            ${getIconHtml(day.weather[0])}
    253386            <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>
    255388            </div>
    256389          </div>
     
    265398    const finalHtml = `
    266399        <div class="weather-location-name">${locationName}</div>
    267         ${primaryDisplayHtml}
     400        ${alertHtml}
     401        ${primaryDisplayHtml}
    268402        ${extraDetailsHtml}
     403        ${sunriseSunsetHtml}
    269404        <div class="forecast-container">
    270405            ${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>"}))}));
     1function 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  
    44Requires at least: 5.0
    55Tested up to: 6.8
    6 Stable tag: 2.1
     6Stable tag: 2.2
    77Requires PHP: 7.2
    88License: GPLv2 or later
     
    2727* **Easy to Use:** Add weather widgets using the WordPress block editor or by placing a simple `<div>` with data attributes anywhere on your site.
    2828* **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.
    2930* **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.
    3232* **Imperial & Metric Units:** Display weather in Fahrenheit/mph or Celsius/kph on a per-widget basis.
    3333* **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.
    3436* **Lightweight:** Enqueues assets only when needed and does not rely on heavy JavaScript libraries.
    3537* **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.
     
    8587The plugin's JavaScript will automatically find this element and populate it with the forecast.
    8688
     89**Using the Shortcode (Classic Editor & Widgets)**
     90
     91You 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
    87102== Configuration ==
    88103
     
    92107
    93108**Cache Expiration Time:**
    94 This setting controls how long the weather data is stored on your server before fetching a new forecast.
     109Use the slider to set the maximum time weather data is stored before fetching a new forecast, from 30 minutes to 8 hours.
     110
     111The 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
    95113For 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 6 hours is effective at reducing API calls.
     114For displaying only the daily high/low, a longer cache time of 3 or 8 hours is effective at reducing API calls.
    97115
    98116**Widget Display & Style**
     
    108126
    109127**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.
     128Selecting 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.
    111133
    112134**Display Timestamp:**
     
    181203No. 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.
    182204
     205= Will my website ever show yesterday's weather If I set a long cache time? =
     206
     207Cinderella'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
     209For 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
     211The 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
    183213= The weather isn't updating. Why? =
    184214
     
    207237= Can I still use the manual div method if I prefer it? =
    208238
    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.
     239Absolutely! While the **block** is the recommended, user-friendly method for the modern WordPress editor, the plugin fully supports traditional methods for maximum flexibility.
     240
     241You can use the `[under_the_weather]` shortcode to easily place the widget in the Classic Editor, text widgets, or with various page builders.
     242
     243Additionally, 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
     245The 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.
    212246
    213247= What coordinate format should I use? =
     
    220254
    221255If 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
     259The 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.
    222260
    223261= What does the "Enable Rate Limiting" setting do? =
     
    2633016. The Coordinate Finder tool, which generates widget code from a location name.
    2643027. The "Under The Weather Forecast" block in the WordPress editor.
     3038. The weather widget with Weather Alerts shown
     3049. The weather widget with Sunrise and Sunset times shown
    265305
    266306== Credits ==
     
    288328
    289329== 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.
    290340
    291341= 2.1 =
     
    387437= 2.0 =
    388438This version includes a "Under The Weather Forecast" block for the WordPress block editor.
     439
     440= 2.2 =
     441This 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  
    44 * Plugin URI:        https://www.sethcreates.com/plugins-for-wordpress/under-the-weather/
    55 * Description:       A lightweight weather widget that caches OpenWeather API data and offers multiple style options.
    6  * Version:           2.1
     6 * Version:           2.2
    77 * Author:            Seth Smigelski
    88 * Author URI:        https://www.sethcreates.com/plugins-for-wordpress/
     
    1515
    1616// Define a constant for the plugin version for easy maintenance.
    17 define( 'UNDER_THE_WEATHER_VERSION', '2.1.0' );
    18 
     17define( 'UNDER_THE_WEATHER_VERSION', '2.2.0' );
     18
     19// Add the Under The Weather Forecast block.
    1920add_action('init', 'under_the_weather_register_widget_block');
    2021function under_the_weather_register_widget_block() {
    2122    register_block_type( __DIR__ . '/build' );
     23}
     24
     25// Add shortcode support.
     26add_action( 'init', 'under_the_weather_register_shortcode' );
     27function under_the_weather_register_shortcode() {
     28    add_shortcode( 'under_the_weather', 'under_the_weather_shortcode_callback' );
    2229}
    2330
     
    5158    add_settings_section('under_the_weather_settings_section', __('API & Cache Settings', 'under-the-weather'), 'under_the_weather_settings_section_callback', $page_slug);
    5259    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');   
    5464
    5565    // Section for controlling widget display and style
     
    5969    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');
    6070    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');   
    6172    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');
     73add_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');
    6476
    6577    // Section for "Advanced Settings"
     
    92104 * Sanitize and validate all settings before saving to the database.
    93105 */
     106 
     107// Sanitize the API key
    94108function under_the_weather_sanitize_settings($input) {
    95109    $new_input = [];
     110   
     111    // Sanitize the API key
    96112    if (isset($input['api_key'])) {
    97113        $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)) {
    99117            $new_input['api_key'] = $api_key;
    100118        } else {
    101119            add_settings_error('under_the_weather_settings', 'invalid_api_key',
    102120                __('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'] : '';
    103124        }
    104125    }
    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
    111144    $new_input['enable_rate_limit'] = isset($input['enable_rate_limit']) ? '1' : '0';
    112145    if (isset($input['rate_limit_count']) && is_numeric($input['rate_limit_count'])) {
     
    117150    }
    118151   
     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   
    119164    $new_input['show_details'] = isset($input['show_details']) ? '1' : '0';
    120165    $new_input['show_unit'] = isset($input['show_unit']) ? '1' : '0';
     166    $new_input['show_alerts'] = isset($input['show_alerts']) ? '1' : '0';
    121167    $new_input['show_timestamp'] = isset($input['show_timestamp']) ? '1' : '0';
    122168    $new_input['enable_cache'] = isset($input['enable_cache']) ? '1' : '0';
     
    148194            <p><em><?php esc_html_e('Default Images', 'under-the-weather'); ?></em></p>
    149195        </div>
    150         <div style="text-align: center;">
     196        <div class="under-the-weather-visual-reference-item">
    151197            <img src="<?php echo esc_url($plugin_assets_url . 'font-style-example.svg'); ?>" alt="Weather Icons Font Style Example">
    152198            <p><em><?php esc_html_e('Weather Icons Font', 'under-the-weather'); ?></em></p>
     
    159205    $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') . '">';
    160206    }
    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.
     209function 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.
     246function 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
    163262}
    164263function under_the_weather_style_set_field_html() {
     
    175274}
    176275function 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}
     278function 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}
     284function 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
    178294}
    179295function under_the_weather_show_timestamp_field_html() {
     
    211327            <?php esc_html_e('Find Coordinates', 'under-the-weather'); ?>
    212328        </button>
    213         <div id="utw-result-wrapper" style="margin-top: 15px;">
     329        <div id="utw-result-wrapper">
    214330            </div>
    215331    </div>
     
    361477function under_the_weather_enqueue_assets() {
    362478    $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.
    365493    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');
    367495        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');
    369497            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');
    371499            }
    372500        }
     
    467595        'forecast_days'  => isset($options['forecast_days']) ? intval($options['forecast_days']) : 5,
    468596        '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',
    469599        'show_timestamp' => !empty($options['show_timestamp']),
    470600        'show_unit'      => !empty($options['show_unit']),
     
    477607    wp_localize_script('under-the-weather-script', 'under_the_weather_plugin_url', $plugin_url_data);
    478608}
    479 
    480609
    481610// =============================================================================
     
    787916    $api_url = "https://api.openweathermap.org/data/3.0/onecall?lat={$lat}&lon={$lon}&appid={$api_key}&units={$unit}";
    788917   
    789     //Check API Response
     918    // Check API Response
    790919    $response_body = under_the_weather_safe_api_call($api_url);
    791920    if ($response_body === false) {
     
    793922    }
    794923   
    795     $weather_data = json_decode($response_body);
     924    $weather_data = json_decode($response_body);
     925   
    796926    under_the_weather_update_usage_stats('api');
    797927   
     
    811941            $weather_data->daily[array_search($day, $weather_data->daily)]->weather[0]->icon_class = under_the_weather_get_icon_class($day->weather[0]->icon);
    812942        }
    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
    818986   
    819987    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 */
     997function 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    );
    8201037}
    8211038
Note: See TracChangeset for help on using the changeset viewer.