Plugin Directory

Changeset 3476743


Ignore:
Timestamp:
03/06/2026 10:30:48 PM (4 weeks ago)
Author:
designplug
Message:

Release 2.1.0 to trunk

Location:
statusdot/trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • statusdot/trunk/assets/css/statusdot-admin.css

    r3474528 r3476743  
    247247
    248248/* 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}
    249258.statusdot-card--section .statusdot-card__head h3{
    250259  margin:0 0 6px;
    251   font-size:14px;
     260  font-size:16px;
    252261  line-height:1.3;
    253262}
     
    293302  font-size:13px;
    294303}
     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  
    308308}
    309309
    310 .statusdot-sep{margin:0 6px;opacity:.8;}
     310.statusdot-sep{margin:0;opacity:.8;}
    311311
    312312/* Fallback color before AJAX resolves status.
  • statusdot/trunk/assets/js/statusdot-frontend.js

    r3474528 r3476743  
    5454        }
    5555      }
    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      }
    5782
    5883      // Fallback: if server only returned a formatted countdown (HH:MM:SS / MM:SS),
     
    75100    }
    76101
    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
    86103  }
    87104
     
    136153    if (tz) qs += '&tz=' + encodeURIComponent(tz);
    137154    if (window.StatusDotData && StatusDotData.nonce) qs += '&nonce=' + encodeURIComponent(StatusDotData.nonce);
     155    if (window.StatusDotData && StatusDotData.rev) qs += '&rev=' + encodeURIComponent(StatusDotData.rev);
    138156
    139157    return fetch(url + qs, { credentials: 'same-origin' })
     
    213231      var diff = Math.round((endMs - Date.now()) / 1000);
    214232      if (diff < 0) diff = 0;
    215       cdNode.textContent = fmtHMS(diff);
    216       // separator
     233      cdNode.textContent = diff > 0 ? fmtHMS(diff) : '';
     234      // Show/hide the spacer between text and countdown
    217235      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';
    222241      }
    223242    });
  • statusdot/trunk/includes/class-statusdot-opening-hours.php

    r3474528 r3476743  
    99    const OPTION_OPEN_PREFIX = 'statusdot_open_'; // monday..sunday
    1010    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
    1128
    1229    public static function init() {
     
    4966    }
    5067
     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
    51131    public static function maybe_migrate_old_options() {
    52132        if (!current_user_can('manage_options')) return;
     
    74154
    75155                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' ) );
    78158            }
    79159        }
     
    97177        if (!current_user_can('manage_options')) return;
    98178
    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
    101203
    102204            $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 ) : [];
    103211
    104212            foreach ($days as $day) {
    105                 $is_closed = isset($_POST['day_closed'][$day]) ? 1 : 0;
     213                $is_closed = isset( $posted_day_closed[ $day ] ) ? 1 : 0;
    106214                update_option(self::OPTION_DAY_CLOSED_PREFIX . $day, $is_closed);
    107215
    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' ) );
    110221            }
    111222
     
    114225            update_option(self::OPTION_MODE, $status_mode);
    115226
    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            }
    117262        }
    118263
     
    120265        $busy_mode = (int) get_option(self::OPTION_BUSY, 0);
    121266        $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        }
    122332
    123333        // Pro build can enhance the settings UI (tabs, extra sections).
     
    160370                <?php wp_nonce_field('statusdot_save_hours_action', 'statusdot_nonce'); ?>
    161371
     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
    162376                        <div class="notice notice-success" style="margin: 10px 0;">
    163377                            <p style="margin: 8px 0;">
     
    168382
    169383
    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>
    178442                <label>
    179443                    <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' ); ?>
    181445                </label><br>
    182446                <label>
     
    189453                </label>
    190454
    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>
    193460                </p>
    194461
     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>
    195521<div class="statusdot-card statusdot-card--weekly">
    196522  <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>
    198524  </div>
    199525  <div class="statusdot-card-body">
     
    213539                                <td><?php echo esc_html(ucfirst($day)); ?></td>
    214540                                <td>
    215                                     <input type="number" min="0" max="23"
     541                                    <input type="time" step="60" class="statusdot-time"
    216542                                        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' ) ); ?>">
    218544                                </td>
    219545                                <td>
    220                                     <input type="number" min="0" max="23"
     546                                    <input type="time" step="60" class="statusdot-time"
    221547                                        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' ) ); ?>">
    223549                                </td>
    224550                                <td>
     
    235561  </div>
    236562</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 
    245563                <p>
    246564                    <input type="submit" name="statusdot_save_hours" class="button button-primary" value="<?php echo esc_attr__('Save Changes', 'statusdot'); ?>">
     
    254572                            <div class="statusdot-compare-headcopy">
    255573                                <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, advanced styling 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>
    257575                            </div>
    258576                            <div class="statusdot-compare-cta">
     
    287605                                        [ 'label' => __( 'Exceptions and one off overrides', 'statusdot' ), 'free' => false, 'pro' => true ],
    288606                                        [ '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 ],
    289609                                        [ 'label' => __( 'Customizable status text and countdown', 'statusdot' ), 'free' => false, 'pro' => true ],
    290610                                        [ 'label' => __( 'Dot size and gap control', 'statusdot' ), 'free' => false, 'pro' => true ],
    291611                                        [ '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 ],
    293614                                        [ 'label' => __( 'Pulse animation styles', 'statusdot' ), 'free' => false, 'pro' => true ],
    294615                                        [ 'label' => __( 'Pulse style override per status', 'statusdot' ), 'free' => false, 'pro' => true ],
     
    332653        $now = new DateTimeImmutable('now', $tz);
    333654        $current_hour = (int) $now->format('G');
     655        $current_min  = (int) $now->format('i');
    334656        $today = strtolower($now->format('l')); // monday...
    335657
     
    343665        );
    344666
    345         $open_hour = (int) self::get_opt_with_fallback(
     667        $open_time = self::get_time_opt_with_fallback(
    346668            self::OPTION_OPEN_PREFIX . $today,
    347669            'opening_hours_open_' . $today . '_' . $site_id,
    348             9
     670            '09:00'
    349671        );
    350 
    351         $close_hour = (int) self::get_opt_with_fallback(
     672        $close_time = self::get_time_opt_with_fallback(
    352673            self::OPTION_CLOSE_PREFIX . $today,
    353674            'opening_hours_close_' . $today . '_' . $site_id,
    354             17
     675            '17:00'
    355676        );
     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 ) );
    356680
    357681        $busy_mode = (int) self::get_opt_with_fallback(
     
    361685        );
    362686
    363         $mode = (string) self::get_opt_with_fallback(
     687        $mode_option = (string) self::get_opt_with_fallback(
    364688            self::OPTION_MODE,
    365689            'opening_hours_mode_' . $site_id,
     
    367691        );
    368692
     693        $idle_until = (int) get_option( self::OPTION_IDLE_UNTIL, 0 );
     694        $idle_active = ( $idle_until > time() );
     695
    369696        // 1) Force closed always wins
    370         if ($mode === 'closed') {
     697        if ($mode_option === 'closed') {
    371698            $icon = 'red-dot.svg';
    372699            $color = 'red-dot';
     
    374701
    375702            // Determine if we're open right now
    376             if ($mode === 'open_247') {
     703            if ($mode_option === 'open_247') {
    377704                $is_open_now = true;
    378705            } elseif ($is_closed_today) {
    379706                $is_open_now = false;
    380707            } 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                }
    382720            }
    383721
     
    385723            // - open_247 => busy can override anytime
    386724            // - 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)) {
    388726                $icon = 'orange-dot.svg';
    389727                $color = 'orange-dot';
    390728            } else {
    391729                // 3) Base status rules
    392                 if ($mode === 'open_247') {
     730                if ($mode_option === 'open_247') {
    393731                    $icon = 'green-dot.svg';
    394732                    $color = 'green-dot';
     
    405743        $icon_url = STATUSDOT_PLUGIN_URL . 'assets/icons/' . $icon;
    406744
     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
    407760        $data = [
    408761            'icon'   => add_query_arg('v', time(), $icon_url),
    409762            '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,
    411766            '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
    415817        $data = apply_filters('statusdot_status_data', $data, [
    416818            'site_id' => $site_id,
    417             'mode'    => $mode,
     819            'mode'    => $mode_option,
    418820        ]);
    419821
    420822        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;
    421840    }
    422841
     
    455874        $refresh = max( 1, absint( $atts['refresh'] ) );
    456875
    457         // Free build: dot-only output (WP.org safe).
     876        // Free build: dot + basic status text + countdown (not customizable).
    458877        $out  = '<span class="statusdot-wrap" data-statusdot-id="' . esc_attr( $id ) . '" data-statusdot-refresh="' . esc_attr( $refresh ) . '">';
    459878        $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>';
    460879        $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">&nbsp;</span>';
     885        $out .= '<span class="statusdot-status-countdown"></span>';
     886        $out .= '</span>';
    461887        $out .= '</span>';
    462888
     
    467893    public static function shortcode_legacy_onder()  { return self::shortcode(['id' => 'onder']); }
    468894    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
    4691063}
  • statusdot/trunk/readme.txt

    r3474528 r3476743  
    22Contributors: designplug, freemius
    33Donate link: https://www.paypal.com/paypalme/DesignPlugNL
    4 Tags: opening-hours, business-hours, status-indicator, open-closed, countdown
     4Tags: opening-hours, business-hours, open-closed, countdown, status-indicator
    55Requires at least: 5.8
    66Tested up to: 6.9
    7 Stable tag: 2.0.0
     7Stable tag: 2.1.0
    88Requires PHP: 7.4
    99License: GPLv2 or later
    1010License 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.
     11Real-time opening hours with a clean status dot, optional text, and countdown timers.
    1312
    1413== Description ==
     14StatusDot helps you show whether you're **Open**, **Busy**, **Closed**, or temporarily **Idle** — using a simple dot indicator that updates automatically.
    1515
    16 StatusDot lets you manage and display business opening hours with a clear, real-time visual status indicator.
     16Configure 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.
    1717
    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).
     18StatusDot can show status text + a live countdown, for example:
     19**Open now — Closes in 04:52:14**
    2020
    21 The status can be displayed anywhere using a shortcode and updates automatically via AJAX — no page reload required.
     21Updates are handled via lightweight AJAX polling, so visitors see changes without a full page refresh.
     22
     23Place it anywhere using the shortcode. Multiple instances per page are supported.
    2224
    2325== 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)
    3339* Unlimited shortcodes per page
    34 * Works with major page builders
     40* Works with major page builders (Gutenberg, Elementor, etc.)
    3541* Lightweight and dependency-free
    3642
    3743== Shortcode ==
    38 
    3944Basic usage:
    40 
    4145[statusdot]
    4246
    4347Optional attributes:
    44 
    4548[statusdot id="header" refresh="30"]
    4649
    47 * `id` – Optional unique identifier (auto-generated if omitted)
     50* `id` – Optional unique identifier (useful for targeting with custom CSS). Default: header
    4851* `refresh` – Refresh interval in seconds (default: 30)
    4952
    5053== Installation ==
    51 
    52541. Upload the `statusdot` folder to `/wp-content/plugins/`
    53552. Activate the plugin via **Plugins → Installed Plugins**
    54563. Go to **Settings → StatusDot**
    55 4. Configure your opening hours
     574. Configure your opening hours and display options
    56585. Add the shortcode anywhere on your site
    5759
    5860== Frequently Asked Questions ==
    59 
    6061= Can I use the shortcode multiple times on one page? =
    6162Yes. You can use the shortcode unlimited times. Each instance updates independently.
    6263
    6364= Can I override the schedule? =
    64 Yes. You can temporarily override the weekly schedule using Force Closed or Open 24/7 modes.
     65Yes. Use **Force Closed**, **Open 24/7**, **Busy mode**, or the **Idle override** timer.
    6566
    6667= Does it work with page builders? =
     
    6869
    6970= Does it affect performance? =
    70 No. The plugin is lightweight and only makes a small AJAX request at configurable intervals.
     71StatusDot is lightweight and only makes a small AJAX request at the interval you set.
    7172
    7273= Are additional features available? =
    73 An optional extended version of the plugin includes advanced scheduling features.
     74An optional extended version of the plugin includes advanced scheduling and customization features.
    7475
    7576== Screenshots ==
    76 
    77 1. Frontend status indicator (open, busy, closed)
    78 2. Opening hours settings page
     771. Frontend open / busy / closed status with countdown
     782. Settings page (schedule + display options)
    7979
    8080== 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
    8193
    8294= 2.0.0 =
    83 * Freemius integration
     95* Licensing integration (optional upgrade)
    8496* Code quality improvements
    8597* WordPress.org compatibility fixes
     
    90102
    91103== Upgrade Notice ==
     104= 2.1.0 =
     105Adds Display Options, Separator selection, and an Idle override — plus HH:MM schedule support and instant refresh after saving settings.
    92106
    93 = 2.0.0 =
    94 Improved compatibility and internal enhancements.
     107= 2.0.1 =
     108Adds basic status text and countdown display to the free version.
  • statusdot/trunk/statusdot.php

    r3474528 r3476743  
    44 * Plugin Name: StatusDot
    55 * Description: Minimal opening hours status dot (open/busy/closed).
    6  * Version:     2.0.0
     6 * Version:     2.1.0
    77 * Author:      Design Plug
    88 * Author URI:  https://profiles.wordpress.org/designplug/
     
    6060// Plugin constants
    6161// ----------------------------
    62 define( 'STATUSDOT_VERSION', '2.0.0' );
     62define( 'STATUSDOT_VERSION', '2.1.0' );
    6363define( 'STATUSDOT_PLUGIN_FILE', __FILE__ );
    6464define( 'STATUSDOT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
     
    114114                'pro_nonce' => $pro_nonce,
    115115                'is_pro'    => (bool) $is_pro,
     116                'rev'       => (int) get_option( 'statusdot_settings_rev', 0 ),
    116117            ] );
    117118            wp_enqueue_script( 'statusdot-frontend' );
Note: See TracChangeset for help on using the changeset viewer.