Changeset 3476743
- Timestamp:
- 03/06/2026 10:30:48 PM (4 weeks ago)
- Location:
- statusdot/trunk
- Files:
-
- 6 edited
-
assets/css/statusdot-admin.css (modified) (2 diffs)
-
assets/css/statusdot-status.css (modified) (1 diff)
-
assets/js/statusdot-frontend.js (modified) (4 diffs)
-
includes/class-statusdot-opening-hours.php (modified) (22 diffs)
-
readme.txt (modified) (3 diffs)
-
statusdot.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
statusdot/trunk/assets/css/statusdot-admin.css
r3474528 r3476743 247 247 248 248 /* Pro schedule sections (Busy windows / Holidays / Exceptions / Rules) */ 249 .statusdot-card--section .statusdot-card__head{ 250 display:flex; 251 align-items:center; 252 justify-content:space-between; 253 gap:12px; 254 padding-bottom:14px; 255 border-bottom:1px solid #eef0f2; 256 margin-bottom:14px; 257 } 249 258 .statusdot-card--section .statusdot-card__head h3{ 250 259 margin:0 0 6px; 251 font-size:1 4px;260 font-size:16px; 252 261 line-height:1.3; 253 262 } … … 293 302 font-size:13px; 294 303 } 304 305 306 /* Idle pill */ 307 .statusdot-pill{display:inline-block;padding:4px 10px;border-radius:999px;background:#f0f0f1;color:#2c3338;font-size:12px;font-weight:600;} 308 .statusdot-pill--active{background:#fff3cd;color:#6b4f00;border:1px solid rgba(0,0,0,.08);} 309 310 /* Keep the live preview visible while scrolling */ 311 /* Live preview card */ 312 .statusdot-live-preview{ 313 position:sticky; 314 /* Keep the preview aligned with the top of the main card columns (avoid floating above cards). */ 315 top:96px; 316 align-self:flex-start; 317 background:#fff; 318 border:1px solid #dcdcde; 319 border-radius:12px; 320 padding:20px; 321 box-shadow:0 1px 2px rgba(0,0,0,.04); 322 width:520px; 323 max-width:100%; 324 } 325 326 327 328 329 .statusdot-live-preview strong{ 330 font-size:20px; 331 } 332 333 334 /* Pro schedule: Status Mode title should match Free styling */ 335 .statusdot-statusmode-title{margin:0 0 8px;font-size:16px;line-height:1.3;} -
statusdot/trunk/assets/css/statusdot-status.css
r3474528 r3476743 308 308 } 309 309 310 .statusdot-sep{margin:0 6px;opacity:.8;}310 .statusdot-sep{margin:0;opacity:.8;} 311 311 312 312 /* Fallback color before AJAX resolves status. -
statusdot/trunk/assets/js/statusdot-frontend.js
r3474528 r3476743 54 54 } 55 55 } 56 cdNode.textContent = data.countdown || cdNode.textContent || ''; 56 if (typeof data.countdown !== 'undefined') { 57 cdNode.textContent = data.countdown || ''; 58 } else { 59 cdNode.textContent = cdNode.textContent || ''; 60 } 61 62 // Hide countdown when empty 63 if (!cdNode.textContent || !cdNode.textContent.trim()) { 64 cdNode.style.display = 'none'; 65 wrap.removeAttribute('data-statusdot-countdown-end'); 66 } else { 67 cdNode.style.display = ''; 68 } 69 70 // Separator visibility (Free/Pro) 71 // NOTE: 72 // The separator BETWEEN status text and the countdown time is always a plain space 73 // rendered by the server as " " inside .statusdot-sep. 74 // The configurable separator ( - / — / | / • ) is part of the status text itself. 75 var sepNode = wrap.querySelector('.statusdot-sep'); 76 if (sepNode) { 77 var tnode = wrap.querySelector('.statusdot-status-text'); 78 var hasText = tnode && tnode.textContent && tnode.textContent.trim(); 79 var hasCd = cdNode && cdNode.textContent && cdNode.textContent.trim(); 80 sepNode.style.display = (hasText && hasCd) ? '' : 'none'; 81 } 57 82 58 83 // Fallback: if server only returned a formatted countdown (HH:MM:SS / MM:SS), … … 75 100 } 76 101 77 // separator visibility 78 var sep = wrap.querySelector('.statusdot-sep'); 79 if (sep) { 80 var showSep = false; 81 if (textNode && cdNode) { 82 showSep = (textNode.textContent || '').trim().length > 0 && (cdNode.textContent || '').trim().length > 0; 83 } 84 sep.style.display = showSep ? '' : 'none'; 85 } 102 // separator visibility handled above 86 103 } 87 104 … … 136 153 if (tz) qs += '&tz=' + encodeURIComponent(tz); 137 154 if (window.StatusDotData && StatusDotData.nonce) qs += '&nonce=' + encodeURIComponent(StatusDotData.nonce); 155 if (window.StatusDotData && StatusDotData.rev) qs += '&rev=' + encodeURIComponent(StatusDotData.rev); 138 156 139 157 return fetch(url + qs, { credentials: 'same-origin' }) … … 213 231 var diff = Math.round((endMs - Date.now()) / 1000); 214 232 if (diff < 0) diff = 0; 215 cdNode.textContent = fmtHMS(diff);216 // separator233 cdNode.textContent = diff > 0 ? fmtHMS(diff) : ''; 234 // Show/hide the spacer between text and countdown 217 235 var sep = w.querySelector('.statusdot-sep'); 218 var textNode = w.querySelector('.statusdot-status-text'); 219 if (sep && textNode) { 220 var showSep = (textNode.textContent || '').trim().length > 0 && (cdNode.textContent || '').trim().length > 0; 221 sep.style.display = showSep ? '' : 'none'; 236 if (sep) { 237 var tnode = w.querySelector('.statusdot-status-text'); 238 var hasText = tnode && tnode.textContent && tnode.textContent.trim(); 239 var hasCd = cdNode && cdNode.textContent && cdNode.textContent.trim(); 240 sep.style.display = (hasText && hasCd) ? '' : 'none'; 222 241 } 223 242 }); -
statusdot/trunk/includes/class-statusdot-opening-hours.php
r3474528 r3476743 9 9 const OPTION_OPEN_PREFIX = 'statusdot_open_'; // monday..sunday 10 10 const OPTION_CLOSE_PREFIX = 'statusdot_close_'; // monday..sunday 11 12 // Display controls (Free) 13 const OPTION_SHOW_TEXT_OPEN = 'statusdot_show_text_open'; 14 const OPTION_SHOW_TEXT_BUSY = 'statusdot_show_text_busy'; 15 const OPTION_SHOW_TEXT_CLOSED = 'statusdot_show_text_closed'; 16 const OPTION_SHOW_LABEL_OPENS = 'statusdot_show_label_opens_in'; 17 const OPTION_SHOW_LABEL_CLOSES= 'statusdot_show_label_closes_in'; 18 const OPTION_SHOW_TIME_OPEN = 'statusdot_show_time_open'; 19 const OPTION_SHOW_TIME_BUSY = 'statusdot_show_time_busy'; 20 const OPTION_SHOW_TIME_CLOSED = 'statusdot_show_time_closed'; 21 const OPTION_SEPARATOR_MODE = 'statusdot_separator_mode'; 22 23 // Idle override (Back in...) (Free/Pro) 24 const OPTION_IDLE_UNTIL = 'statusdot_idle_until'; 25 const OPTION_IDLE_MINUTES = 'statusdot_idle_minutes'; 26 const OPTION_IDLE_AFTER = 'statusdot_idle_after'; // schedule|open_247|closed|busy 27 11 28 12 29 public static function init() { … … 49 66 } 50 67 68 /** 69 * Sanitize a time field that may come in as: 70 * - "HH:MM" (preferred) 71 * - "H" / "HH" (legacy hour-only) 72 * - int hour (legacy) 73 * 74 * Always returns a zero-padded "HH:MM" string. 75 */ 76 private static function sanitize_time_field( $value, string $default = '09:00' ): string { 77 $default = (string) $default; 78 $default = preg_match( '/^([01]?\d|2[0-3]):([0-5]\d)$/' , $default ) ? $default : '09:00'; 79 80 if ( $value === null ) { 81 return $default; 82 } 83 84 // Handle numeric (legacy hour-only). 85 if ( is_int( $value ) || ( is_string( $value ) && preg_match( '/^\d{1,2}$/' , $value ) ) ) { 86 $h = (int) $value; 87 if ( $h >= 0 && $h <= 23 ) { 88 return sprintf( '%02d:00', $h ); 89 } 90 return $default; 91 } 92 93 $raw = trim( (string) $value ); 94 if ( $raw === '' ) { 95 return $default; 96 } 97 98 // Preferred: HH:MM. 99 if ( preg_match( '/^([01]?\d|2[0-3]):([0-5]\d)$/' , $raw, $m ) ) { 100 $h = (int) $m[1]; 101 $min = (int) $m[2]; 102 return sprintf( '%02d:%02d', $h, $min ); 103 } 104 105 return $default; 106 } 107 108 109 /** 110 * Sanitize a clock time string (HH:MM). Alias used by admin UI helpers. 111 */ 112 private static function sanitize_clock_time( $value, string $default = '09:00' ): string { 113 return self::sanitize_time_field( $value, $default ); 114 } 115 116 /** 117 * Fetch a time option with a fallback to the legacy MU option key. 118 * Always returns a sanitized "HH:MM" string. 119 */ 120 private static function get_time_opt_with_fallback( string $new_key, ?string $old_key = null, string $default = '09:00' ): string { 121 $val = get_option( $new_key, null ); 122 if ( $val === null && $old_key ) { 123 $val = get_option( $old_key, null ); 124 } 125 if ( $val === null ) { 126 $val = $default; 127 } 128 return self::sanitize_time_field( $val, $default ); 129 } 130 51 131 public static function maybe_migrate_old_options() { 52 132 if (!current_user_can('manage_options')) return; … … 74 154 75 155 if ($closed_val !== null) update_option(self::OPTION_DAY_CLOSED_PREFIX . $day, (int)$closed_val); 76 if ($open_val !== null) update_option( self::OPTION_OPEN_PREFIX . $day, (int)$open_val);77 if ($close_val !== null) update_option( self::OPTION_CLOSE_PREFIX . $day, (int)$close_val);156 if ($open_val !== null) update_option( self::OPTION_OPEN_PREFIX . $day, self::sanitize_time_field( $open_val, '09:00' ) ); 157 if ($close_val !== null) update_option( self::OPTION_CLOSE_PREFIX . $day, self::sanitize_time_field( $close_val, '17:00' ) ); 78 158 } 79 159 } … … 97 177 if (!current_user_can('manage_options')) return; 98 178 99 if (isset($_POST['statusdot_save_hours'])) { 100 check_admin_referer('statusdot_save_hours_action', 'statusdot_nonce'); 179 if ( isset($_POST['statusdot_save_hours']) || isset($_POST['statusdot_idle_start']) || isset($_POST['statusdot_idle_stop']) ) { 180 // If the POST came from the Pro settings form, let the Pro module handle it. 181 // IMPORTANT: Do NOT return early here, otherwise WordPress will render a blank page after saving. 182 if ( isset( $_POST['statusdot_pro_nonce'] ) ) { 183 $ok = (bool) wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['statusdot_pro_nonce'] ) ), 'statusdot_pro_save_action' ); 184 if ( ! $ok ) { 185 wp_nonce_ays( 'statusdot_pro_save_action' ); 186 } 187 // Pro handler runs on admin_init. 188 } else { 189 check_admin_referer( 'statusdot_save_hours_action', 'statusdot_nonce' ); 190 191 // Idle override controls (Back in...). 192 if ( isset( $_POST['statusdot_idle_stop'] ) ) { 193 update_option( self::OPTION_IDLE_UNTIL, 0 ); 194 } 195 196 if ( isset( $_POST['statusdot_idle_start'] ) ) { 197 $mins = isset( $_POST['idle_minutes'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['idle_minutes'] ) ) : 30; 198 $mins = max( 1, min( 1440, $mins ) ); // 1..1440 199 update_option( self::OPTION_IDLE_MINUTES, $mins ); 200 update_option( self::OPTION_IDLE_UNTIL, time() + ( $mins * 60 ) ); 201 } 202 101 203 102 204 $days = self::days(); 205 $posted_day_closed = (array) filter_input( INPUT_POST, 'day_closed', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); 206 $posted_open_hours = (array) filter_input( INPUT_POST, 'open_hour', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); 207 $posted_close_hours = (array) filter_input( INPUT_POST, 'close_hour', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); 208 $posted_day_closed = is_array( $posted_day_closed ) ? wp_unslash( $posted_day_closed ) : []; 209 $posted_open_hours = is_array( $posted_open_hours ) ? wp_unslash( $posted_open_hours ) : []; 210 $posted_close_hours = is_array( $posted_close_hours ) ? wp_unslash( $posted_close_hours ) : []; 103 211 104 212 foreach ($days as $day) { 105 $is_closed = isset( $_POST['day_closed'][$day]) ? 1 : 0;213 $is_closed = isset( $posted_day_closed[ $day ] ) ? 1 : 0; 106 214 update_option(self::OPTION_DAY_CLOSED_PREFIX . $day, $is_closed); 107 215 108 update_option(self::OPTION_OPEN_PREFIX . $day, isset($_POST['open_hour'][$day]) ? intval($_POST['open_hour'][$day]) : 9); 109 update_option(self::OPTION_CLOSE_PREFIX . $day, isset($_POST['close_hour'][$day]) ? intval($_POST['close_hour'][$day]) : 17); 216 // Sanitize each element explicitly so Plugin Check recognises the input is sanitized. 217 $open_hour_raw = isset( $posted_open_hours[ $day ] ) ? sanitize_text_field( (string) $posted_open_hours[ $day ] ) : '09:00'; 218 $close_hour_raw = isset( $posted_close_hours[ $day ] ) ? sanitize_text_field( (string) $posted_close_hours[ $day ] ) : '17:00'; 219 update_option( self::OPTION_OPEN_PREFIX . $day, self::sanitize_time_field( $open_hour_raw, '09:00' ) ); 220 update_option( self::OPTION_CLOSE_PREFIX . $day, self::sanitize_time_field( $close_hour_raw, '17:00' ) ); 110 221 } 111 222 … … 114 225 update_option(self::OPTION_MODE, $status_mode); 115 226 116 echo '<div class="updated"><p>' . esc_html__('Settings updated.', 'statusdot') . '</p></div>'; 227 228 229 // Display controls (Free). Default is enabled for all. 230 update_option(self::OPTION_SHOW_TEXT_OPEN, isset($_POST['show_text_open']) ? 1 : 0); 231 update_option(self::OPTION_SHOW_TEXT_BUSY, isset($_POST['show_text_busy']) ? 1 : 0); 232 update_option(self::OPTION_SHOW_TEXT_CLOSED, isset($_POST['show_text_closed']) ? 1 : 0); 233 234 $show_time_open = isset($_POST['show_time_open']) ? 1 : 0; 235 $show_time_busy = isset($_POST['show_time_busy']) ? 1 : 0; 236 $show_time_closed = isset($_POST['show_time_closed']) ? 1 : 0; 237 238 // Keep countdown labels linked to the related time visibility. 239 update_option(self::OPTION_SHOW_LABEL_OPENS, $show_time_closed); 240 update_option(self::OPTION_SHOW_LABEL_CLOSES, $show_time_open); 241 242 update_option(self::OPTION_SHOW_TIME_OPEN, $show_time_open); 243 update_option(self::OPTION_SHOW_TIME_BUSY, $show_time_busy); 244 update_option(self::OPTION_SHOW_TIME_CLOSED, $show_time_closed); 245 246 $separator_mode = isset( $_POST['separator_mode'] ) ? sanitize_text_field( wp_unslash( $_POST['separator_mode'] ) ) : '-'; 247 // No "None" option in Free/Pro UI; default is '-'. 248 // If older installs have 'none' saved, map it to '-'. 249 if ( $separator_mode === 'none' ) { 250 $separator_mode = '-'; 251 } 252 $allowed_separator_modes = array( '-', '—', '|', '•' ); 253 if ( ! in_array( $separator_mode, $allowed_separator_modes, true ) ) { 254 $separator_mode = '-'; 255 } 256 update_option( self::OPTION_SEPARATOR_MODE, $separator_mode ); 257 258 // Bump settings revision so frontend AJAX requests bypass any caches immediately. 259 update_option( 'statusdot_settings_rev', time() ); 260 echo '<div class="updated"><p>' . esc_html__('Settings updated.', 'statusdot') . '</p></div>'; 261 } 117 262 } 118 263 … … 120 265 $busy_mode = (int) get_option(self::OPTION_BUSY, 0); 121 266 $mode = get_option(self::OPTION_MODE, 'normal'); 267 268 // Determine the effective status right now (used for admin labels). 269 $idle_until = (int) get_option( self::OPTION_IDLE_UNTIL, 0 ); 270 $idle_active = ( $idle_until > time() ); 271 272 $tz = function_exists( 'wp_timezone' ) ? wp_timezone() : new DateTimeZone( wp_timezone_string() ); 273 $now_dt = new DateTime( 'now', $tz ); 274 $today_key = strtolower( $now_dt->format( 'l' ) ); 275 276 $is_closed_today = (int) get_option( self::OPTION_DAY_CLOSED_PREFIX . $today_key, 0 ); 277 $open_time_today = self::sanitize_clock_time( (string) get_option( self::OPTION_OPEN_PREFIX . $today_key, '09:00' ), '09:00' ); 278 $close_time_today = self::sanitize_clock_time( (string) get_option( self::OPTION_CLOSE_PREFIX . $today_key, '17:00' ), '17:00' ); 279 280 $is_open_now = false; 281 if ( $mode === 'open_247' ) { 282 $is_open_now = true; 283 } elseif ( $mode !== 'normal' ) { 284 $is_open_now = false; 285 } elseif ( $is_closed_today ) { 286 $is_open_now = false; 287 } else { 288 try { 289 list( $oh, $om ) = array_map( 'intval', explode( ':', $open_time_today ) ); 290 list( $ch, $cm ) = array_map( 'intval', explode( ':', $close_time_today ) ); 291 $open_dt = ( clone $now_dt )->setTime( $oh, $om, 0 ); 292 $close_dt = ( clone $now_dt )->setTime( $ch, $cm, 0 ); 293 $is_open_now = ( $now_dt >= $open_dt && $now_dt < $close_dt ); 294 } catch ( Exception $e ) { 295 $is_open_now = false; 296 } 297 } 298 299 $effective = 'closed'; 300 if ( $mode === 'closed' ) { 301 $effective = 'closed'; 302 } elseif ( $mode === 'open_247' ) { 303 if ( $idle_active ) { 304 $effective = 'idle'; 305 } elseif ( $busy_mode ) { 306 $effective = 'busy'; 307 } else { 308 $effective = 'open'; 309 } 310 } else { // normal (weekly schedule) 311 if ( ! $is_open_now ) { 312 $effective = 'closed'; 313 } elseif ( $idle_active ) { 314 $effective = 'idle'; 315 } elseif ( $busy_mode ) { 316 $effective = 'busy'; 317 } else { 318 $effective = 'open'; 319 } 320 } 321 322 $status_state_html = ''; 323 if ( $effective === 'open' ) { 324 $status_state_html = '<span style="color:#00a32a;font-weight:600;">(' . esc_html__( 'Open', 'statusdot' ) . ')</span>'; 325 } elseif ( $effective === 'busy' ) { 326 $status_state_html = '<span style="color:#dba617;font-weight:600;">(' . esc_html__( 'Busy', 'statusdot' ) . ')</span>'; 327 } elseif ( $effective === 'idle' ) { 328 $status_state_html = '<span style="color:#dba617;font-weight:600;">(' . esc_html__( 'Idle', 'statusdot' ) . ')</span>'; 329 } else { 330 $status_state_html = '<span style="color:#d63638;font-weight:600;">(' . esc_html__( 'Closed', 'statusdot' ) . ')</span>'; 331 } 122 332 123 333 // Pro build can enhance the settings UI (tabs, extra sections). … … 160 370 <?php wp_nonce_field('statusdot_save_hours_action', 'statusdot_nonce'); ?> 161 371 372 <p style="margin: 10px 0 16px;"> 373 <button type="submit" name="statusdot_save_hours" class="button button-primary"><?php echo esc_html__( 'Save Changes', 'statusdot' ); ?></button> 374 </p> 375 162 376 <div class="notice notice-success" style="margin: 10px 0;"> 163 377 <p style="margin: 8px 0;"> … … 168 382 169 383 170 171 <?php 172 $is_pro_active = (bool) apply_filters( 'statusdot_is_pro_active', false ); 173 $free_state = $is_pro_active 174 ? '<span style="color:#d63638;font-weight:600;">(' . esc_html__( 'Inactive', 'statusdot' ) . ')</span>' 175 : '<span style="color:#00a32a;font-weight:600;">(' . esc_html__( 'Active', 'statusdot' ) . ')</span>' . '</span>'; 176 ?> 177 <h2><?php echo wp_kses_post(esc_html__('Status Mode', 'statusdot') . ' ' . $free_state); ?></h2> 384 <div class="statusdot-card statusdot-display-card" style="margin-top:14px;"> 385 <div class="statusdot-card__head"> 386 <h2><?php echo esc_html__( 'Display options', 'statusdot' ); ?></h2> 387 <p><?php echo esc_html__( 'Choose what text and countdown to show next to the status dot.', 'statusdot' ); ?></p> 388 </div> 389 <div class="statusdot-card__body"> 390 <?php 391 $opt_text_open = (int) get_option( self::OPTION_SHOW_TEXT_OPEN, 1 ); 392 $opt_text_busy = (int) get_option( self::OPTION_SHOW_TEXT_BUSY, 1 ); 393 $opt_text_closed = (int) get_option( self::OPTION_SHOW_TEXT_CLOSED, 1 ); 394 395 $opt_time_open = (int) get_option( self::OPTION_SHOW_TIME_OPEN, 1 ); 396 $opt_time_busy = (int) get_option( self::OPTION_SHOW_TIME_BUSY, 1 ); 397 $opt_time_closed = (int) get_option( self::OPTION_SHOW_TIME_CLOSED, 1 ); 398 $opt_separator = (string) get_option( self::OPTION_SEPARATOR_MODE, '-' ); 399 if ( $opt_separator === 'none' ) { 400 $opt_separator = '-'; 401 } 402 ?> 403 <table class="form-table" role="presentation"> 404 <tbody> 405 <tr> 406 <th scope="row"><?php echo esc_html__( 'Enable status text', 'statusdot' ); ?></th> 407 <td> 408 <label><input type="checkbox" name="show_text_open" <?php checked( $opt_text_open, 1 ); ?> /> <?php echo esc_html__( 'Enable open text', 'statusdot' ); ?></label><br> 409 <label><input type="checkbox" name="show_text_busy" <?php checked( $opt_text_busy, 1 ); ?> /> <?php echo esc_html__( 'Enable busy/idle text', 'statusdot' ); ?></label><br> 410 <label><input type="checkbox" name="show_text_closed" <?php checked( $opt_text_closed, 1 ); ?> /> <?php echo esc_html__( 'Enable closed text', 'statusdot' ); ?></label> 411 </td> 412 </tr> 413 <tr> 414 <th scope="row"><?php echo esc_html__( 'Show countdown label + time', 'statusdot' ); ?></th> 415 <td> 416 <label><input type="checkbox" name="show_time_open" <?php checked( $opt_time_open, 1 ); ?> /> <?php echo esc_html__( 'Show “Closes in” + time when open', 'statusdot' ); ?></label><br> 417 <label><input type="checkbox" name="show_time_busy" <?php checked( $opt_time_busy, 1 ); ?> /> <?php echo esc_html__( 'Show “Back in” + time when idle', 'statusdot' ); ?></label><br> 418 <label><input type="checkbox" name="show_time_closed" <?php checked( $opt_time_closed, 1 ); ?> /> <?php echo esc_html__( 'Show “Opens in” + time when closed', 'statusdot' ); ?></label> 419 <p class="description" style="margin:6px 0 0;"><?php echo esc_html__( 'These options control both the countdown text label and the live countdown time.', 'statusdot' ); ?></p> 420 </td> 421 </tr> 422 <tr> 423 <th scope="row"><?php echo esc_html__( 'Separator', 'statusdot' ); ?></th> 424 <td> 425 <select id="statusdot_separator_mode" name="separator_mode"> 426 <option value="-" <?php selected( $opt_separator, '-' ); ?>>-</option> 427 <option value="—" <?php selected( $opt_separator, '—' ); ?>>—</option> 428 <option value="|" <?php selected( $opt_separator, '|' ); ?>>|</option> 429 <option value="•" <?php selected( $opt_separator, '•' ); ?>>•</option> 430 </select> 431 <p class="description"><?php echo esc_html__( 'Shown between the status text and the countdown label when both are enabled.', 'statusdot' ); ?></p> 432 </td> 433 </tr> 434 </tbody> 435 </table> 436 </div> 437 </div> 438 439 440 441 <h2><?php echo wp_kses_post( esc_html__( 'Status Mode', 'statusdot' ) . ' ' . $status_state_html ); ?></h2> 178 442 <label> 179 443 <input type="radio" name="status_mode" value="normal" <?php checked($mode, 'normal'); ?>> 180 <?php echo esc_html__('Use Opening Hours', 'statusdot'); ?>444 <?php echo esc_html__( 'Use Opening Hours (Weekly Schedule)', 'statusdot' ); ?> 181 445 </label><br> 182 446 <label> … … 189 453 </label> 190 454 191 <p style="margin-top:14px;"> 192 <input type="submit" name="statusdot_save_hours" class="button button-primary" value="<?php echo esc_attr__('Save Changes', 'statusdot'); ?>"> 455 <p style="margin:10px 0 0;"> 456 <label> 457 <input type="checkbox" name="busy_mode" value="1" <?php checked($busy_mode, 1); ?>> 458 <?php echo esc_html__('Enable Busy Mode (Orange)', 'statusdot'); ?> 459 </label> 193 460 </p> 194 461 462 <p style="margin:12px 0 0;"> 463 <button type="submit" name="statusdot_save_hours" class="button button-primary"><?php echo esc_html__( 'Save Changes', 'statusdot' ); ?></button> 464 </p> 465 466 <?php 467 $idle_until = (int) get_option( self::OPTION_IDLE_UNTIL, 0 ); 468 $idle_mins = (int) get_option( self::OPTION_IDLE_MINUTES, 30 ); 469 $idle_active = ( $idle_until > time() ); 470 $idle_state = $idle_active 471 ? '<span style="color:#00a32a;font-weight:600;">(' . esc_html__( 'Active', 'statusdot' ) . ')</span>' 472 : '<span style="color:#d63638;font-weight:600;">(' . esc_html__( 'Inactive', 'statusdot' ) . ')</span>'; 473 $weekly_active = ( $mode === 'normal' ); 474 $weekly_state = ( $weekly_active && $idle_active ) 475 ? '<span style="color:#dba617;font-weight:600;">(' . esc_html__( 'Idle override is active', 'statusdot' ) . ')</span>' 476 : ( ( $weekly_active && $busy_mode && $is_open_now ) 477 ? '<span style="color:#dba617;font-weight:600;">(' . esc_html__( 'Active - Busy Mode Enabled', 'statusdot' ) . ')</span>' 478 : ( $weekly_active 479 ? '<span style="color:#00a32a;font-weight:600;">(' . esc_html__( 'Active', 'statusdot' ) . ')</span>' 480 : '<span style="color:#d63638;font-weight:600;">(' . esc_html__( 'Inactive', 'statusdot' ) . ')</span>' 481 ) 482 ); 483 ?> 484 485 <div class="statusdot-card statusdot-idle-card" style="margin-top:14px;"> 486 <div class="statusdot-card-head"> 487 <h2><?php echo wp_kses_post( esc_html__( 'Idle override (Back in...)', 'statusdot' ) . ' ' . $idle_state ); ?></h2> 488 </div> 489 <div class="statusdot-card-body"> 490 <p class="description"><?php echo esc_html__( 'Temporarily show an idle status (orange) with a "Back in" countdown.', 'statusdot' ); ?></p> 491 492 <table class="form-table" role="presentation"> 493 <tbody> 494 <tr> 495 <th scope="row"><?php echo esc_html__( 'Back in (minutes)', 'statusdot' ); ?></th> 496 <td> 497 <input type="number" name="idle_minutes" min="1" max="1440" value="<?php echo esc_attr( $idle_mins ); ?>" class="small-text" /> 498 <p class="description"><?php echo esc_html__( 'Sets how long the Idle status stays active.', 'statusdot' ); ?></p> 499 </td> 500 </tr> 501 502 <tr> 503 <th scope="row"><?php echo esc_html__( 'Status', 'statusdot' ); ?></th> 504 <td> 505 <?php if ( $idle_active ) : ?> 506 <span class="statusdot-pill statusdot-pill--active"><?php echo esc_html__( 'Idle is active', 'statusdot' ); ?></span> 507 <?php else : ?> 508 <span class="statusdot-pill"><?php echo esc_html__( 'Idle is not active', 'statusdot' ); ?></span> 509 <?php endif; ?> 510 </td> 511 </tr> 512 </tbody> 513 </table> 514 515 <p style="margin-top:10px;"> 516 <button type="submit" name="statusdot_idle_start" class="button button-primary"><?php echo esc_html__( 'Start Idle', 'statusdot' ); ?></button> 517 <button type="submit" name="statusdot_idle_stop" class="button"><?php echo esc_html__( 'Stop Idle', 'statusdot' ); ?></button> 518 </p> 519 </div> 520 </div> 195 521 <div class="statusdot-card statusdot-card--weekly"> 196 522 <div class="statusdot-card-head"> 197 <h2><?php echo wp_kses_post( esc_html__('Weekly Schedule', 'statusdot') . ' ' . $free_state ); ?></h2>523 <h2><?php echo wp_kses_post( esc_html__('Weekly Schedule', 'statusdot') . ' ' . $weekly_state ); ?></h2> 198 524 </div> 199 525 <div class="statusdot-card-body"> … … 213 539 <td><?php echo esc_html(ucfirst($day)); ?></td> 214 540 <td> 215 <input type=" number" min="0" max="23"541 <input type="time" step="60" class="statusdot-time" 216 542 name="open_hour[<?php echo esc_attr($day); ?>]" 217 value="<?php echo esc_attr( (int) get_option(self::OPTION_OPEN_PREFIX . $day, 9)); ?>">543 value="<?php echo esc_attr( self::sanitize_time_field( get_option( self::OPTION_OPEN_PREFIX . $day, '09:00' ), '09:00' ) ); ?>"> 218 544 </td> 219 545 <td> 220 <input type=" number" min="0" max="23"546 <input type="time" step="60" class="statusdot-time" 221 547 name="close_hour[<?php echo esc_attr($day); ?>]" 222 value="<?php echo esc_attr( (int) get_option(self::OPTION_CLOSE_PREFIX . $day, 17)); ?>">548 value="<?php echo esc_attr( self::sanitize_time_field( get_option( self::OPTION_CLOSE_PREFIX . $day, '17:00' ), '17:00' ) ); ?>"> 223 549 </td> 224 550 <td> … … 235 561 </div> 236 562 </div> 237 238 <p style="margin-top:20px;">239 <label>240 <input type="checkbox" name="busy_mode" value="1" <?php checked($busy_mode, 1); ?>>241 <?php echo esc_html__('Enable Busy Mode (Orange)', 'statusdot'); ?>242 </label>243 </p>244 245 563 <p> 246 564 <input type="submit" name="statusdot_save_hours" class="button button-primary" value="<?php echo esc_attr__('Save Changes', 'statusdot'); ?>"> … … 254 572 <div class="statusdot-compare-headcopy"> 255 573 <h2><?php echo esc_html__( 'Upgrade to Pro (optional)', 'statusdot' ); ?></h2> 256 <p><?php echo esc_html__( 'Pro adds multiple schedules and locations, holidays, exceptions, busy windows, customizable status text and countdown, advancedstyling controls, and more.', 'statusdot' ); ?></p>574 <p><?php echo esc_html__( 'Pro adds multiple schedules and locations, overrides, holidays, exceptions, busy windows, customizable status text and countdown, styling controls, and more.', 'statusdot' ); ?></p> 257 575 </div> 258 576 <div class="statusdot-compare-cta"> … … 287 605 [ 'label' => __( 'Exceptions and one off overrides', 'statusdot' ), 'free' => false, 'pro' => true ], 288 606 [ 'label' => __( 'Busy windows time ranges', 'statusdot' ), 'free' => false, 'pro' => true ], 607 [ 'label' => __( 'Status text and countdown', 'statusdot' ), 'free' => true, 'pro' => true ], 608 [ 'label' => __( 'Idle override (Back in...)', 'statusdot' ), 'free' => true, 'pro' => true ], 289 609 [ 'label' => __( 'Customizable status text and countdown', 'statusdot' ), 'free' => false, 'pro' => true ], 290 610 [ 'label' => __( 'Dot size and gap control', 'statusdot' ), 'free' => false, 'pro' => true ], 291 611 [ 'label' => __( 'Custom colors and gradients', 'statusdot' ), 'free' => false, 'pro' => true ], 292 [ 'label' => __( 'Border and separator control', 'statusdot' ), 'free' => false, 'pro' => true ], 612 [ 'label' => __( 'Separator choice', 'statusdot' ), 'free' => true, 'pro' => true ], 613 [ 'label' => __( 'Border control', 'statusdot' ), 'free' => false, 'pro' => true ], 293 614 [ 'label' => __( 'Pulse animation styles', 'statusdot' ), 'free' => false, 'pro' => true ], 294 615 [ 'label' => __( 'Pulse style override per status', 'statusdot' ), 'free' => false, 'pro' => true ], … … 332 653 $now = new DateTimeImmutable('now', $tz); 333 654 $current_hour = (int) $now->format('G'); 655 $current_min = (int) $now->format('i'); 334 656 $today = strtolower($now->format('l')); // monday... 335 657 … … 343 665 ); 344 666 345 $open_ hour = (int) self::get_opt_with_fallback(667 $open_time = self::get_time_opt_with_fallback( 346 668 self::OPTION_OPEN_PREFIX . $today, 347 669 'opening_hours_open_' . $today . '_' . $site_id, 348 9670 '09:00' 349 671 ); 350 351 $close_hour = (int) self::get_opt_with_fallback( 672 $close_time = self::get_time_opt_with_fallback( 352 673 self::OPTION_CLOSE_PREFIX . $today, 353 674 'opening_hours_close_' . $today . '_' . $site_id, 354 17675 '17:00' 355 676 ); 677 678 list( $open_hour, $open_min ) = array_map( 'intval', explode( ':', $open_time ) ); 679 list( $close_hour, $close_min ) = array_map( 'intval', explode( ':', $close_time ) ); 356 680 357 681 $busy_mode = (int) self::get_opt_with_fallback( … … 361 685 ); 362 686 363 $mode = (string) self::get_opt_with_fallback(687 $mode_option = (string) self::get_opt_with_fallback( 364 688 self::OPTION_MODE, 365 689 'opening_hours_mode_' . $site_id, … … 367 691 ); 368 692 693 $idle_until = (int) get_option( self::OPTION_IDLE_UNTIL, 0 ); 694 $idle_active = ( $idle_until > time() ); 695 369 696 // 1) Force closed always wins 370 if ($mode === 'closed') {697 if ($mode_option === 'closed') { 371 698 $icon = 'red-dot.svg'; 372 699 $color = 'red-dot'; … … 374 701 375 702 // Determine if we're open right now 376 if ($mode === 'open_247') {703 if ($mode_option === 'open_247') { 377 704 $is_open_now = true; 378 705 } elseif ($is_closed_today) { 379 706 $is_open_now = false; 380 707 } else { 381 $is_open_now = ($current_hour >= $open_hour && $current_hour < $close_hour); 708 try { 709 $open_dt = $now->setTime( $open_hour, $open_min, 0 ); 710 $close_dt = $now->setTime( $close_hour, $close_min, 0 ); 711 } catch ( Exception $e ) { 712 $open_dt = null; 713 $close_dt = null; 714 } 715 if ( $open_dt && $close_dt ) { 716 $is_open_now = ( $now >= $open_dt && $now < $close_dt ); 717 } else { 718 $is_open_now = false; 719 } 382 720 } 383 721 … … 385 723 // - open_247 => busy can override anytime 386 724 // - normal => busy only when currently open 387 if ($busy_mode && ($mode === 'open_247' || $is_open_now)) {725 if ($busy_mode && ($mode_option === 'open_247' || $is_open_now)) { 388 726 $icon = 'orange-dot.svg'; 389 727 $color = 'orange-dot'; 390 728 } else { 391 729 // 3) Base status rules 392 if ($mode === 'open_247') {730 if ($mode_option === 'open_247') { 393 731 $icon = 'green-dot.svg'; 394 732 $color = 'green-dot'; … … 405 743 $icon_url = STATUSDOT_PLUGIN_URL . 'assets/icons/' . $icon; 406 744 745 // Semantic status used by the frontend (open, busy, closed) 746 if ( $mode_option === 'closed' ) { 747 $mode = 'closed'; 748 } elseif ( ! empty( $idle_active ) ) { 749 $icon = 'orange-dot.svg'; 750 $color = 'orange-dot'; 751 $mode = 'idle'; 752 } elseif ( $busy_mode && ( $mode_option === 'open_247' || ! empty( $is_open_now ) ) ) { 753 $mode = 'busy'; 754 } elseif ( $mode_option === 'open_247' || ! empty( $is_open_now ) ) { 755 $mode = 'open'; 756 } else { 757 $mode = 'closed'; 758 } 759 407 760 $data = [ 408 761 'icon' => add_query_arg('v', time(), $icon_url), 409 762 'color' => $color, 410 'status' => $mode, // open|busy|closed 763 'status' => ( $mode === 'idle' ? 'busy' : $mode ), // open|busy|closed 764 // Semantic mode (open|busy|closed|idle) used by the frontend for small display tweaks. 765 'status_semantic' => $mode, 411 766 'label' => ucfirst( $mode ), 412 ]; 413 414 // Pro integration: allow Pro (or others) to extend/override the payload 767 768 'is_idle' => ( $mode === 'idle' ), 769 'idle_until' => (int) ( $mode === 'idle' ? get_option( self::OPTION_IDLE_UNTIL, 0 ) : 0 ), 770 ]; 771 772 // Basic status text + countdown (Free). Pro can override via the filter below. 773 $show_text_open = (int) get_option( self::OPTION_SHOW_TEXT_OPEN, 1 ); 774 $show_text_busy = (int) get_option( self::OPTION_SHOW_TEXT_BUSY, 1 ); 775 $show_text_closed = (int) get_option( self::OPTION_SHOW_TEXT_CLOSED, 1 ); 776 777 $show_lbl_opens = (int) get_option( self::OPTION_SHOW_LABEL_OPENS, 1 ); 778 $show_lbl_closes = (int) get_option( self::OPTION_SHOW_LABEL_CLOSES, 1 ); 779 780 $show_time_open = (int) get_option( self::OPTION_SHOW_TIME_OPEN, 1 ); 781 $show_time_idle = (int) get_option( self::OPTION_SHOW_TIME_BUSY, 1 ); // stored as busy, used for idle timer 782 $show_time_closed = (int) get_option( self::OPTION_SHOW_TIME_CLOSED, 1 ); 783 784 $idle_until = (int) get_option( self::OPTION_IDLE_UNTIL, 0 ); 785 786 $raw_countdown = self::get_basic_countdown_seconds( $mode, $now, $today, $open_hour, $open_min, $close_hour, $close_min, (int) $is_closed_today, $idle_until ); 787 788 // Text: base status + optional label (even when time is hidden). 789 $data['text'] = self::get_default_status_text_with_label( 790 $mode, 791 $raw_countdown, 792 (bool) $show_text_open, 793 (bool) $show_text_busy, 794 (bool) $show_text_closed, 795 (bool) $show_lbl_opens, 796 (bool) $show_lbl_closes, 797 (bool) $show_time_open, 798 (bool) $show_time_idle, 799 (bool) $show_time_closed 800 ); 801 802 // Countdown time shown only when enabled for the current status. 803 $show_time = false; 804 if ( is_int( $raw_countdown ) && $raw_countdown > 0 ) { 805 if ( $mode === 'open' ) { 806 $show_time = (bool) $show_time_open; 807 } elseif ( $mode === 'closed' ) { 808 $show_time = (bool) $show_time_closed; 809 } elseif ( $mode === 'idle' ) { 810 $show_time = (bool) $show_time_idle; 811 } 812 } 813 814 $data['countdown_seconds'] = ( $show_time && is_int( $raw_countdown ) ) ? $raw_countdown : null; 815 $data['countdown'] = ( $show_time && is_int( $raw_countdown ) ) ? self::format_countdown_mmss( $raw_countdown ) : ''; 816 // Pro integration: allow Pro (or others) to extend/override the payload 415 817 $data = apply_filters('statusdot_status_data', $data, [ 416 818 'site_id' => $site_id, 417 'mode' => $mode ,819 'mode' => $mode_option, 418 820 ]); 419 821 420 822 return $data; 823 } 824 825 826 private static function get_separator_value() : string { 827 $mode = (string) get_option( self::OPTION_SEPARATOR_MODE, '-' ); 828 $allowed = array( 'none', '-', '—', '|', '•', 'custom' ); 829 if ( ! in_array( $mode, $allowed, true ) ) { 830 $mode = '-'; 831 } 832 if ( $mode === 'none' ) { 833 return ''; 834 } 835 if ( $mode === 'custom' ) { 836 $custom = (string) get_option( 'statusdot_separator_custom', '' ); 837 return $custom; 838 } 839 return $mode; 421 840 } 422 841 … … 455 874 $refresh = max( 1, absint( $atts['refresh'] ) ); 456 875 457 // Free build: dot -only output (WP.org safe).876 // Free build: dot + basic status text + countdown (not customizable). 458 877 $out = '<span class="statusdot-wrap" data-statusdot-id="' . esc_attr( $id ) . '" data-statusdot-refresh="' . esc_attr( $refresh ) . '">'; 459 878 $out .= '<span class="statusdot-status-icon statusdot-status-icon--unknown" data-statusdot-id="' . esc_attr( $id ) . '" data-statusdot-refresh="' . esc_attr( $refresh ) . '" aria-hidden="true"></span>'; 460 879 $out .= '<span class="screen-reader-text">' . esc_html__( 'Status indicator', 'statusdot' ) . '</span>'; 880 $out .= '<span class="statusdot-meta">'; 881 $out .= '<span class="statusdot-status-text"></span>'; 882 // Spacer between the status text (which already contains the configured separator) 883 // and the live countdown time. Keep this as a plain space. 884 $out .= '<span class="statusdot-sep" aria-hidden="true"> </span>'; 885 $out .= '<span class="statusdot-status-countdown"></span>'; 886 $out .= '</span>'; 461 887 $out .= '</span>'; 462 888 … … 467 893 public static function shortcode_legacy_onder() { return self::shortcode(['id' => 'onder']); } 468 894 public static function shortcode_legacy_network(){ return self::shortcode(['id' => 'network']); } 895 896 /** 897 * Default status text (Free): "Open now — Closes in", "Closed now — Opens in", "Busy now". 898 */ 899 private static function get_default_status_text_with_label( 900 string $mode, 901 ?int $raw_countdown, 902 bool $show_text_open, 903 bool $show_text_busy, 904 bool $show_text_closed, 905 bool $show_lbl_opens, 906 bool $show_lbl_closes, 907 bool $show_time_open, 908 bool $show_time_idle, 909 bool $show_time_closed 910 ): string { 911 $parts = array(); 912 $sep_val = self::get_separator_value(); 913 $joiner = ( $sep_val === '' ) ? ' ' : ( ' ' . $sep_val . ' ' ); 914 915 if ( $mode === 'idle' ) { 916 if ( $show_text_busy ) { 917 $parts[] = esc_html__( 'Idle', 'statusdot' ); 918 } 919 if ( is_int( $raw_countdown ) && $raw_countdown > 0 && $show_time_idle ) { 920 $parts[] = esc_html__( 'Back in', 'statusdot' ); 921 } 922 return implode( $joiner, array_filter( $parts ) ); 923 } 924 925 if ( $mode === 'busy' ) { 926 return $show_text_busy ? esc_html__( 'Busy now', 'statusdot' ) : ''; 927 } 928 929 if ( $mode === 'open' ) { 930 if ( $show_text_open ) { 931 $parts[] = esc_html__( 'Open now', 'statusdot' ); 932 } 933 if ( is_int( $raw_countdown ) && $raw_countdown > 0 && $show_lbl_closes && $show_time_open ) { 934 $parts[] = esc_html__( 'Closes in', 'statusdot' ); 935 } 936 return implode( $joiner, array_filter( $parts ) ); 937 } 938 939 if ( $show_text_closed ) { 940 $parts[] = esc_html__( 'Closed now', 'statusdot' ); 941 } 942 if ( is_int( $raw_countdown ) && $raw_countdown > 0 && $show_lbl_opens && $show_time_closed ) { 943 $parts[] = esc_html__( 'Opens in', 'statusdot' ); 944 } 945 return implode( $joiner, array_filter( $parts ) ); 946 } 947 948 /** 949 * Basic countdown seconds (Free): only for open->close and closed->open. 950 * Returns null when not applicable. 951 */ 952 private static function get_basic_countdown_seconds( 953 string $mode, 954 DateTimeImmutable $now, 955 string $today, 956 int $open_hour, 957 int $open_min, 958 int $close_hour, 959 int $close_min, 960 int $is_closed_today, 961 int $idle_until 962 ): ?int { 963 964 // Idle: countdown to idle_until 965 if ( $mode === 'idle' ) { 966 $diff = $idle_until - time(); 967 return ( $diff > 0 ) ? $diff : null; 968 } 969 970 // Busy (manual) never shows countdown in Free. 971 if ( $mode === 'busy' ) { 972 return null; 973 } 974 975 // If schedule closed today, no open countdown. 976 if ( $is_closed_today ) { 977 if ( $mode === 'open' ) { 978 return null; 979 } 980 // closed: find next open day 981 return self::seconds_until_next_open_day( $now, $today ); 982 } 983 984 try { 985 $open_dt = $now->setTime( $open_hour, $open_min, 0 ); 986 $close_dt = $now->setTime( $close_hour, $close_min, 0 ); 987 } catch ( Exception $e ) { 988 return null; 989 } 990 991 if ( $mode === 'open' ) { 992 $diff = $close_dt->getTimestamp() - $now->getTimestamp(); 993 return ( $diff > 0 ) ? $diff : null; 994 } 995 996 // closed: countdown to open 997 $diff = $open_dt->getTimestamp() - $now->getTimestamp(); 998 if ( $diff > 0 ) { 999 return $diff; 1000 } 1001 1002 // If we're already past today's opening time, go to next open day. 1003 return self::seconds_until_next_open_day( $now, $today ); 1004 } 1005 1006 private static function seconds_until_next_open_day( DateTimeImmutable $now, ?string $today = null ): ?int { 1007 $site_id = get_current_blog_id(); 1008 $tz = $now->getTimezone(); 1009 1010 for ( $i = 1; $i <= 7; $i++ ) { 1011 $cand = $now->modify( '+' . $i . ' day' ); 1012 if ( ! $cand instanceof DateTimeImmutable ) { 1013 continue; 1014 } 1015 $day = strtolower( $cand->format( 'l' ) ); 1016 1017 $is_closed = (int) self::get_opt_with_fallback( 1018 self::OPTION_DAY_CLOSED_PREFIX . $day, 1019 'opening_hours_day_closed_' . $day . '_' . $site_id, 1020 0 1021 ); 1022 1023 if ( $is_closed ) { 1024 continue; 1025 } 1026 1027 $open_time = self::get_time_opt_with_fallback( 1028 self::OPTION_OPEN_PREFIX . $day, 1029 'opening_hours_open_' . $day . '_' . $site_id, 1030 '09:00' 1031 ); 1032 list( $open_hour, $open_min ) = array_map( 'intval', explode( ':', $open_time ) ); 1033 1034 try { 1035 $open_dt = ( new DateTimeImmutable( $cand->format('Y-m-d') . ' 00:00:00', $tz ) )->setTime( $open_hour, $open_min, 0 ); 1036 } catch ( Exception $e ) { 1037 continue; 1038 } 1039 1040 $secs = (int) ( $open_dt->getTimestamp() - $now->getTimestamp() ); 1041 return $secs > 0 ? $secs : null; 1042 } 1043 1044 return null; 1045 } 1046 1047 /** 1048 * Format seconds as H:MM (>=1h) or M:SS (<1h). For Free display. 1049 */ 1050 private static function format_countdown_mmss( int $seconds ): string { 1051 $seconds = max( 0, (int) $seconds ); 1052 $h = (int) floor( $seconds / 3600 ); 1053 $m = (int) floor( ( $seconds % 3600 ) / 60 ); 1054 $s = (int) ( $seconds % 60 ); 1055 1056 if ( $h > 0 ) { 1057 return sprintf( '%d:%02d', $h, $m ); 1058 } 1059 return sprintf( '%d:%02d', ( $m ), $s ); 1060 } 1061 1062 469 1063 } -
statusdot/trunk/readme.txt
r3474528 r3476743 2 2 Contributors: designplug, freemius 3 3 Donate link: https://www.paypal.com/paypalme/DesignPlugNL 4 Tags: opening-hours, business-hours, status-indicator, open-closed, countdown4 Tags: opening-hours, business-hours, open-closed, countdown, status-indicator 5 5 Requires at least: 5.8 6 6 Tested up to: 6.9 7 Stable tag: 2. 0.07 Stable tag: 2.1.0 8 8 Requires PHP: 7.4 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html 11 12 Smart real-time opening hours with visual open, busy, and closed status indicators. 11 Real-time opening hours with a clean status dot, optional text, and countdown timers. 13 12 14 13 == Description == 14 StatusDot helps you show whether you're **Open**, **Busy**, **Closed**, or temporarily **Idle** — using a simple dot indicator that updates automatically. 15 15 16 StatusDot lets you manage and display business opening hours with a clear, real-time visual status indicator.16 Configure a weekly schedule (supports **HH:MM**), optionally enable **Busy mode**, or override everything with **Force Closed** or **Open 24/7**. You can also start an **Idle override** ("Back in...") timer when you're away. 17 17 18 Define weekly opening hours, enable a temporary “Busy” mode, or override everything with Force Closed or Open 24/7 modes. 19 The plugin automatically calculates the current status and displays it as a small animated dot (green, orange, or red). 18 StatusDot can show status text + a live countdown, for example: 19 **Open now — Closes in 04:52:14** 20 20 21 The status can be displayed anywhere using a shortcode and updates automatically via AJAX — no page reload required. 21 Updates are handled via lightweight AJAX polling, so visitors see changes without a full page refresh. 22 23 Place it anywhere using the shortcode. Multiple instances per page are supported. 22 24 23 25 == Features == 24 25 * Weekly opening hours 26 * Visual status indicator: 27 * Green = Open 28 * Orange = Busy 29 * Red = Closed 30 * Force Closed mode 31 * Open 24/7 mode 32 * AJAX-based live updates 26 * Weekly opening hours (HH:MM, including minutes) 27 * Status modes: 28 * Use Opening Hours (Weekly Schedule) 29 * Force Closed 30 * Open 24/7 31 * Manual Busy mode (orange status) 32 * Idle override ("Back in...") with start/stop and countdown 33 * Display options: 34 * Toggle status text (Open/Busy/Closed) 35 * Toggle countdown label + time per state (Closes in / Opens in / Back in) 36 * Separator selection (-, —, |, •) 37 * Live countdown to the next opening/closing moment 38 * AJAX-based live updates (configurable refresh interval) 33 39 * Unlimited shortcodes per page 34 * Works with major page builders 40 * Works with major page builders (Gutenberg, Elementor, etc.) 35 41 * Lightweight and dependency-free 36 42 37 43 == Shortcode == 38 39 44 Basic usage: 40 41 45 [statusdot] 42 46 43 47 Optional attributes: 44 45 48 [statusdot id="header" refresh="30"] 46 49 47 * `id` – Optional unique identifier ( auto-generated if omitted)50 * `id` – Optional unique identifier (useful for targeting with custom CSS). Default: header 48 51 * `refresh` – Refresh interval in seconds (default: 30) 49 52 50 53 == Installation == 51 52 54 1. Upload the `statusdot` folder to `/wp-content/plugins/` 53 55 2. Activate the plugin via **Plugins → Installed Plugins** 54 56 3. Go to **Settings → StatusDot** 55 4. Configure your opening hours 57 4. Configure your opening hours and display options 56 58 5. Add the shortcode anywhere on your site 57 59 58 60 == Frequently Asked Questions == 59 60 61 = Can I use the shortcode multiple times on one page? = 61 62 Yes. You can use the shortcode unlimited times. Each instance updates independently. 62 63 63 64 = Can I override the schedule? = 64 Yes. You can temporarily override the weekly schedule using Force Closed or Open 24/7 modes.65 Yes. Use **Force Closed**, **Open 24/7**, **Busy mode**, or the **Idle override** timer. 65 66 66 67 = Does it work with page builders? = … … 68 69 69 70 = Does it affect performance? = 70 No. The plugin is lightweight and only makes a small AJAX request at configurable intervals.71 StatusDot is lightweight and only makes a small AJAX request at the interval you set. 71 72 72 73 = Are additional features available? = 73 An optional extended version of the plugin includes advanced scheduling features.74 An optional extended version of the plugin includes advanced scheduling and customization features. 74 75 75 76 == Screenshots == 76 77 1. Frontend status indicator (open, busy, closed) 78 2. Opening hours settings page 77 1. Frontend open / busy / closed status with countdown 78 2. Settings page (schedule + display options) 79 79 80 80 == Changelog == 81 = 2.1.0 = 82 * New: Display options (status text + countdown label/time toggles) 83 * New: Separator selection (-, —, |, •) 84 * New: Idle override (Back in...) with start/stop 85 * Improved: Weekly schedule supports HH:MM (minutes) 86 * Improved: Instant refresh after saving settings 87 * UI polish and WordPress.org compliance fixes 88 89 = 2.0.1 = 90 * Add basic status text and countdown to the free version 91 * Improve shortcode copy/paste formatting 92 * Minor readme improvements 81 93 82 94 = 2.0.0 = 83 * Freemius integration95 * Licensing integration (optional upgrade) 84 96 * Code quality improvements 85 97 * WordPress.org compatibility fixes … … 90 102 91 103 == Upgrade Notice == 104 = 2.1.0 = 105 Adds Display Options, Separator selection, and an Idle override — plus HH:MM schedule support and instant refresh after saving settings. 92 106 93 = 2.0. 0=94 Improved compatibility and internal enhancements.107 = 2.0.1 = 108 Adds basic status text and countdown display to the free version. -
statusdot/trunk/statusdot.php
r3474528 r3476743 4 4 * Plugin Name: StatusDot 5 5 * Description: Minimal opening hours status dot (open/busy/closed). 6 * Version: 2. 0.06 * Version: 2.1.0 7 7 * Author: Design Plug 8 8 * Author URI: https://profiles.wordpress.org/designplug/ … … 60 60 // Plugin constants 61 61 // ---------------------------- 62 define( 'STATUSDOT_VERSION', '2. 0.0' );62 define( 'STATUSDOT_VERSION', '2.1.0' ); 63 63 define( 'STATUSDOT_PLUGIN_FILE', __FILE__ ); 64 64 define( 'STATUSDOT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); … … 114 114 'pro_nonce' => $pro_nonce, 115 115 'is_pro' => (bool) $is_pro, 116 'rev' => (int) get_option( 'statusdot_settings_rev', 0 ), 116 117 ] ); 117 118 wp_enqueue_script( 'statusdot-frontend' );
Note: See TracChangeset
for help on using the changeset viewer.