<?php /** * RankwellPress Pro  Sports Widget & Shortcode * * Server-renders the first paint (SEO + no layout shift); sports-widget.js takes over from there. * The markup produced here must stay in sync with fixtureCardElement() in sports-widget.js. * * v1.x.x: * - Replaced emoji glyphs (Ø=ÜÍ, &, &) with inline SVG icons for * consistent cross-platform rendering and better a11y. */ namespace RankwellPro\Sports; use RankwellPro\REST\SportsREST; use WP_Widget; if ( ! defined( 'ABSPATH' ) ) exit; class SportsWidget extends WP_Widget { /** Tabs the widget understands: the fetcher's filters + two client-side views. */ private const EXTRA_TABS = [ 'standings', 'favorites' ]; /** * Tabs that must never be served from a full-page cache. Only "live" qualifies: the other tabs * are refreshed by sports-widget.js on load, so their server HTML can safely come from a page cache. */ private const REALTIME_TABS = [ 'live' ]; /** Counts widgets rendered in this request (unique DOM ids; only #0 reads ?sports_* URL params). */ private static int $instances = 0; private static bool $i18n_printed = false; /** Per-request memo of SportsFetcher::selectable_leagues() (called several times per render). */ private static ?array $leagues = null; public function __construct() { parent::__construct( 'rankwellpro_sports_widget', 'RankwellPress: Sports Widget', [ 'classname' => 'rwp-sports-widget-container', 'description' => __( 'Displays fixtures, tables, and stats.', 'rankwellpress-pro' ), ] ); add_shortcode( 'rwp_sports', [ $this, 'render_shortcode' ] ); } /* %%% Config helpers %%% */ private static function leagues(): array { return self::$leagues ??= SportsFetcher::selectable_leagues(); } private static function filter_labels(): array { return [ 'all' => __( 'All', 'rankwellpress-pro' ), 'live' => __( 'Live', 'rankwellpress-pro' ), 'today' => __( 'Today', 'rankwellpress-pro' ), 'results' => __( 'Results', 'rankwellpress-pro' ), 'upcoming' => __( 'Upcoming', 'rankwellpress-pro' ), 'standings' => __( 'Table', 'rankwellpress-pro' ), ]; } /** * SportsFetcher::sanitize_filter() only knows fixture filters and silently turns * "standings"/"favorites" into "all"  this one knows every tab the widget has. */ private static function sanitize_tab( string $value ): string { $value = strtolower( trim( $value ) ); if ( 'yesterday' === $value ) $value = 'results'; $allowed = array_merge( SportsFetcher::FILTERS, self::EXTRA_TABS ); return in_array( $value, $allowed, true ) ? $value : SportsFetcher::DEFAULT_FILTER; } private function resolve_filter( array $instance, bool $allow_url ): string { if ( $allow_url && isset( $_GET['sports_filter'] ) ) { $url_filter = strtolower( sanitize_text_field( wp_unslash( $_GET['sports_filter'] ) ) ); $allowed = array_merge( SportsFetcher::FILTERS, self::EXTRA_TABS, [ 'favorites' ] ); if ( in_array( $url_filter, $allowed, true ) ) return $url_filter; } return self::sanitize_tab( (string) ( $instance['default_filter'] ?? SportsFetcher::DEFAULT_FILTER ) ); } private function resolve_league( array $instance, bool $allow_url ): string { $leagues = self::leagues(); if ( $allow_url && isset( $_GET['sports_league'] ) ) { $url_league = sanitize_key( wp_unslash( $_GET['sports_league'] ) ); if ( array_key_exists( $url_league, $leagues ) ) return $url_league; } $default = (string) ( $instance['default_league'] ?? 'all' ); return array_key_exists( $default, $leagues ) ? $default : 'all'; } /* %%% Shortcode %%% */ public function render_shortcode( $atts ) { $atts = shortcode_atts( [ 'filter' => 'all', 'league' => 'all', 'limit' => 10, 'title' => '', ], $atts, 'rwp_sports' ); $instance = [ 'default_filter' => sanitize_key( (string) $atts['filter'] ), 'default_league' => sanitize_key( (string) $atts['league'] ), 'limit' => (int) $atts['limit'], 'title' => sanitize_text_field( (string) $atts['title'] ), ]; $args = [ 'before_widget' => '<div class="rwp-shortcode-wrap">', 'after_widget' => '</div>', 'before_title' => '', 'after_title' => '', ]; ob_start(); $this->widget( $args, $instance ); return ob_get_clean(); } /* %%% Output %%% */ public function widget( $args, $instance ) { $index = self::$instances++; $limit = isset( $instance['limit'] ) ? max( 1, min( 50, (int) $instance['limit'] ) ) : 10; $filter = $this->resolve_filter( (array) $instance, 0 === $index ); $league = $this->resolve_league( (array) $instance, 0 === $index ); $default_title = ( 'all' === $league ) ? __( 'All Leagues', 'rankwellpress-pro' ) : SportsFetcher::LEAGUES[ $league ]['label']; $title = isset( $instance['title'] ) && '' !== trim( (string) $instance['title'] ) ? (string) $instance['title'] : $default_title; $this->maybe_send_nocache_headers( $filter ); // %% Gather the first-paint data (never let a provider problem break the page) %% $ssr = [ 'kind' => 'none', 'grouped' => null, 'matches' => [], 'has_more' => false, 'next' => 0 ]; if ( ! in_array( $filter, self::EXTRA_TABS, true ) ) { try { $ssr = $this->load_ssr_data( $filter, $limit, $league ); } catch ( \Throwable $e ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { error_log( '[RWP Sports widget] ' . $e->getMessage() ); } } } $now = time(); $has_live = false; foreach ( $ssr['matches'] as $m ) { if ( 'live' === SportsFetcher::display_state( $m, $now ) ) { $has_live = true; break; } } $widget_id = 'rwp-sports-' . $index; $main_id = $widget_id . '-main'; // Timezone handed to the JS: an IANA name when the site has one, otherwise the fixed // offset in minutes east of UTC (manual offsets like "+01:00" are not valid IANA names). $tz = wp_timezone_string(); $tz_name = ( false !== strpos( $tz, '/' ) || 'UTC' === $tz ) ? $tz : ''; $tz_offset = '' === $tz_name ? (string) intdiv( wp_timezone()->getOffset( new \DateTimeImmutable( 'now', new \DateTimeZone( 'UTC' ) ) ), 60 ) : ''; echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput $this->print_i18n_once(); ?> <div class="rwp-sports-widget" id="<?php echo esc_attr( $widget_id ); ?>" data-filter="<?php echo esc_attr( $filter ); ?>" data-league="<?php echo esc_attr( $league ); ?>" data-has-live="<?php echo $has_live ? '1' : '0'; ?>" data-rest-url="<?php echo esc_url( rest_url( 'rankwell/v1/live-scores' ) ); ?>" data-limit="<?php echo esc_attr( (string) $limit ); ?>" data-timezone="<?php echo esc_attr( $tz_name ); ?>" data-utc-offset="<?php echo esc_attr( $tz_offset ); ?>" data-render-time="<?php echo esc_attr( (string) $now ); ?>"> <div class="rwp-sports-header"> <div class="rwp-sports-header-top"> <span class="rwp-sports-title"><?php echo esc_html( $title ); ?></span> <span class="rwp-sports-live"><span class="rwp-live-dot"></span><?php esc_html_e( 'Live Scores', 'rankwellpress-pro' ); ?></span> </div> <div class="rwp-sports-tools"> <div class="rwp-sports-league-wrap"> <select class="rwp-sports-league-select" aria-label="<?php esc_attr_e( 'Select league', 'rankwellpress-pro' ); ?>"> <?php foreach ( self::leagues() as $slug => $data ) : ?> <option value="<?php echo esc_attr( $slug ); ?>" <?php selected( $league, $slug ); ?>><?php echo esc_html( $data['label'] ); ?></option> <?php endforeach; ?> </select> </div> <input type="search" class="rwp-sports-search" placeholder="<?php esc_attr_e( 'Search team...', 'rankwellpress-pro' ); ?>" aria-label="<?php esc_attr_e( 'Search team', 'rankwellpress-pro' ); ?>" autocomplete="off"> </div> </div> <div class="rwp-sports-tabs" role="tablist" aria-label="<?php esc_attr_e( 'Match views', 'rankwellpress-pro' ); ?>"> <?php foreach ( self::filter_labels() as $slug => $label ) : $active = ( $slug === $filter ); ?> <button type="button" role="tab" class="rwp-sports-tab <?php echo $active ? 'is-active' : ''; ?>" data-filter="<?php echo esc_attr( $slug ); ?>" aria-selected="<?php echo $active ? 'true' : 'false'; ?>" aria-controls="<?php echo esc_attr( $main_id ); ?>" tabindex="<?php echo $active ? '0' : '-1'; ?>"> <?php echo esc_html( $label ); ?> </button> <?php endforeach; ?> <?php $fav_active = ( 'favorites' === $filter ); ?> <button type="button" role="tab" class="rwp-sports-tab <?php echo $fav_active ? 'is-active' : ''; ?>" data-filter="favorites" aria-selected="<?php echo $fav_active ? 'true' : 'false'; ?>" aria-controls="<?php echo esc_attr( $main_id ); ?>" tabindex="<?php echo $fav_active ? '0' : '-1'; ?>"><span class="rwp-tab-star" style="color:#fbbf24;display:inline-flex;align-items:center;" aria-hidden="true"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span> <?php esc_html_e( 'Saved', 'rankwellpress-pro' ); ?></button> </div> <div class="rwp-main-content" id="<?php echo esc_attr( $main_id ); ?>" role="tabpanel" aria-live="polite" data-ssr-filter="<?php echo esc_attr( $filter ); ?>"> <?php $this->current_league = $league; $this->render_main( $filter, $ssr ); ?> </div> <div class="rwp-sports-footer"><span class="rwp-last-updated"></span></div> </div> <div class="rwp-modal-overlay" style="display:none;" aria-hidden="true"> <div class="rwp-modal-content" role="document"> <button type="button" class="rwp-modal-close" aria-label="<?php esc_attr_e( 'Close', 'rankwellpress-pro' ); ?>">&times;</button> <div class="rwp-modal-body"></div> </div> </div> <?php echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput } /** * Only the "live" tab is kept out of full-page caches by default. Everything else is served from the page * cache and silently refreshed by sports-widget.js on load (see data-render-time), which keeps these pages * fast. Filterable: return false to allow page caching for the live tab too, or true to restore the old * behaviour (no page caching for any real-time tab) via the REALTIME_TABS list. */ private function maybe_send_nocache_headers( string $filter ): void { if ( ! in_array( $filter, self::REALTIME_TABS, true ) ) return; if ( ! apply_filters( 'rwp_sports_send_nocache_headers', true, $filter ) ) return; if ( ! defined( 'DONOTCACHEPAGE' ) ) define( 'DONOTCACHEPAGE', true ); // WP Rocket, W3TC, LiteSpeed, WP Super Cache if ( ! headers_sent() ) { header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); header( 'X-Accel-Expires: 0' ); header( 'CDN-Cache-Control: no-store' ); header( 'Surrogate-Control: no-store' ); } } /** * First-paint data, memoised for the request and cached briefly across requests so page loads don't each * wait on the provider. TTL (seconds) is filterable; return 0 to disable the cross-request cache. * Realtime tabs (live/today/all) default to 0 because SportsFetcher::getMaster() already caches them * for 15 30s; a second layer here would only add staleness. * Failures throw and are never cached. * * @return array{kind:string,grouped:?array,matches:array,has_more:bool,next:int} */ private function load_ssr_data( string $filter, int $limit, string $league ): array { static $memo = []; $key = $filter . '|' . $limit . '|' . $league; if ( isset( $memo[ $key ] ) ) return $memo[ $key ]; $default_ttl = in_array( $filter, [ 'live', 'today', 'all' ], true ) ? 0 : 30; $ttl = (int) apply_filters( 'rwp_sports_ssr_cache_ttl', $default_ttl, $filter, $league ); $transient = 'rwp_sp_ssr_' . md5( $key ); if ( $ttl > 0 ) { $cached = get_transient( $transient ); if ( is_array( $cached ) && isset( $cached['kind'], $cached['matches'] ) ) { return $memo[ $key ] = $cached; } } $data = $this->fetch_ssr_data( $filter, $limit, $league ); if ( $ttl > 0 ) set_transient( $transient, $data, $ttl ); return $memo[ $key ] = $data; } /** @return array{kind:string,grouped:?array,matches:array,has_more:bool,next:int} */ private function fetch_ssr_data( string $filter, int $limit, string $league ): array { switch ( $filter ) { case 'results': $grouped = SportsFetcher::getResultsGrouped( $limit, 0, $league ); return [ 'kind' => 'grouped', 'grouped' => $grouped, 'matches' => $this->flatten_sections( $grouped['sections'] ?? [] ), 'has_more' => empty( $grouped['exhausted'] ), 'next' => (int) ( $grouped['cursor'] ?? 0 ) ]; case 'upcoming': $grouped = SportsFetcher::getUpcomingGrouped( $limit, 0, $league ); return [ 'kind' => 'grouped', 'grouped' => $grouped, 'matches' => $grouped['matches'] ?? [], 'has_more' => empty( $grouped['exhausted'] ), 'next' => (int) ( $grouped['cursor'] ?? 0 ) ]; case 'all': $grouped = SportsFetcher::getAllGrouped( $limit, 0, $league ); return [ 'kind' => 'grouped', 'grouped' => $grouped, 'matches' => $grouped['matches'] ?? [], 'has_more' => empty( $grouped['exhausted'] ), 'next' => (int) ( $grouped['cursor'] ?? 0 ) ]; default: // live, today $matches = SportsFetcher::getFixtures( $filter, $limit, 0, $league ); $has_more = count( $matches ) >= $limit && SportsFetcher::countAvailable( $filter, $league ) > $limit; return [ 'kind' => 'flat', 'grouped' => null, 'matches' => $matches, 'has_more' => $has_more, 'next' => count( $matches ) ]; } } private function flatten_sections( array $sections ): array { $out = []; foreach ( $sections as $sec ) { foreach ( (array) ( $sec['days'] ?? [] ) as $day ) { foreach ( (array) ( $day['matches'] ?? [] ) as $m ) $out[] = $m; } } return $out; } /** League the widget instance is showing; league group headings are only useful on "All Leagues". */ private string $current_league = 'all'; private function render_main( string $filter, array $ssr ): void { if ( 'standings' === $filter ) { echo '<div class="rwp-standings-wrap"><p class="rwp-fixtures-empty">' . esc_html__( 'Loading table...', 'rankwellpress-pro' ) . '</p></div>'; return; } if ( 'favorites' === $filter ) { echo '<p class="rwp-fixtures-empty">' . esc_html__( 'Loading saved matches...', 'rankwellpress-pro' ) . '</p>'; return; } if ( empty( $ssr['matches'] ) ) { echo '<p class="rwp-fixtures-empty">' . esc_html__( 'No matches found.', 'rankwellpress-pro' ) . '</p>'; return; } if ( 'grouped' === $ssr['kind'] ) { echo '<div class="rwp-results-sections">'; $row_offset = 0; foreach ( (array) ( $ssr['grouped']['sections'] ?? [] ) as $section ) { $row_offset = $this->render_results_section( $section, $row_offset ); } echo '</div>'; $this->render_load_more( $ssr['has_more'], $ssr['next'], true ); return; } $row_offset = 0; echo '<div class="rwp-league-groups">'; $this->render_league_groups( $ssr['matches'], $row_offset ); echo '</div>'; $this->render_load_more( $ssr['has_more'], $ssr['next'], false ); } private function render_load_more( bool $show, int $offset, bool $grouped ): void { if ( ! $show ) return; printf( '<div class="rwp-sports-load-more-wrap"><button type="button" class="rwp-sports-load-more%s" data-offset="%d">%s</button></div>', $grouped ? ' rwp-sports-load-more--results' : '', (int) $offset, esc_html__( 'Load More Matches', 'rankwellpress-pro' ) ); } /** Strings the JS shows; printed once per page so translations work without a separate localize call. */ private function print_i18n_once(): void { if ( self::$i18n_printed ) return; self::$i18n_printed = true; $strings = [ 'loading' => __( 'Loading...', 'rankwellpress-pro' ), 'noMatches' => __( 'No matches found.', 'rankwellpress-pro' ), 'unableToLoad' => __( 'Unable to load matches.', 'rankwellpress-pro' ), 'loadMore' => __( 'Load More Matches', 'rankwellpress-pro' ), 'tryAgain' => __( 'Error. Try again.', 'rankwellpress-pro' ), 'upcoming' => __( 'UPCOMING', 'rankwellpress-pro' ), 'live' => __( 'LIVE', 'rankwellpress-pro' ), 'ft' => __( 'FT', 'rankwellpress-pro' ), 'ht' => __( 'HT', 'rankwellpress-pro' ), 'updated' => __( 'Updated:', 'rankwellpress-pro' ), 'noFavs' => __( 'No saved matches yet. Click the star icon to save matches.', 'rankwellpress-pro' ), 'standingsNA' => __( 'Standings not available for this league right now.', 'rankwellpress-pro' ), 'standingsErr' => __( 'Error loading table.', 'rankwellpress-pro' ), 'detailsError' => __( 'Could not load match details. Please try again.', 'rankwellpress-pro' ), 'noEvents' => __( 'No match events yet.', 'rankwellpress-pro' ), 'noEventsDone' => __( 'Goals and match events are not available for this match.', 'rankwellpress-pro' ), 'timeline' => __( 'Match Timeline', 'rankwellpress-pro' ), 'statistics' => __( 'Statistics', 'rankwellpress-pro' ), 'referee' => __( 'Referee:', 'rankwellpress-pro' ), 'tabMatch' => __( 'Match', 'rankwellpress-pro' ), 'tabH2H' => __( 'Head to Head', 'rankwellpress-pro' ), 'h2hLoading' => __( 'Loading head-to-head...', 'rankwellpress-pro' ), 'h2hNone' => __( 'No previous meetings found for these teams.', 'rankwellpress-pro' ), 'h2hError' => __( 'Could not load head-to-head. Please try again.', 'rankwellpress-pro' ), 'h2hWins' => __( 'Wins', 'rankwellpress-pro' ), 'h2hDraws' => __( 'Draws', 'rankwellpress-pro' ), 'h2hGoals' => __( 'Goals', 'rankwellpress-pro' ), 'h2hLast' => __( 'Previous meetings', 'rankwellpress-pro' ), 'h2hForm' => __( 'Recent form', 'rankwellpress-pro' ), 'saveMatch' => __( 'Save match', 'rankwellpress-pro' ), 'unsaveMatch' => __( 'Remove saved match', 'rankwellpress-pro' ), ]; echo '<script>window.rwpSportsI18n=Object.assign(window.rwpSportsI18n||{},' . wp_json_encode( $strings, JSON_HEX_TAG | JSON_HEX_AMP ) . ');</script>'; } /* %%% Sections / rows %%% */ private function render_results_section( array $section, int $row_offset ): int { $type = (string) ( $section['type'] ?? 'past' ); $label = (string) ( $section['label'] ?? '' ); $days = (array) ( $section['days'] ?? [] ); $total = 0; foreach ( $days as $d ) $total += count( (array) ( $d['matches'] ?? [] ) ); if ( 0 === $total && 'today' !== $type ) return $row_offset; ?> <div class="rwp-results-section rwp-results-section--<?php echo esc_attr( $type ); ?>"> <?php if ( '' !== $label ) : ?><h3 class="rwp-results-section-title"><?php echo esc_html( $label ); ?></h3><?php endif; ?> <?php if ( 'today' === $type ) : ?> <?php if ( 0 === $total ) : ?> <p class="rwp-results-empty"><?php esc_html_e( 'No matches', 'rankwellpress-pro' ); ?></p> <?php else : ?> <?php $this->render_league_groups( $days[0]['matches'], $row_offset ); ?> <?php endif; ?> <?php else : ?> <?php foreach ( $days as $day ) : if ( empty( $day['matches'] ) ) continue; $day_label = (string) ( $day['label'] ?? '' ); ?> <div class="rwp-results-day"<?php if ( ! empty( $day['date'] ) ) : ?> data-date="<?php echo esc_attr( (string) $day['date'] ); ?>"<?php endif; ?>> <?php if ( '' !== $day_label ) : ?><h4 class="rwp-results-day-title"><?php echo esc_html( $day_label ); ?></h4><?php endif; ?> <?php $this->render_league_groups( $day['matches'], $row_offset ); ?> </div> <?php endforeach; ?> <?php endif; ?> </div> <?php return $row_offset; } /** * One block per league: <div.rwp-league-group> [<h5> league name] <ul.rwp-fixtures>. The heading is left out when * a single league is selected (every card would repeat it). Markup mirrors buildLeagueGroup() in sports-widget.js. */ private function render_league_groups( array $matches, int &$row_offset ): void { $show_title = ( 'all' === $this->current_league ); foreach ( SportsFormatter::group_by_league( $matches ) as $group ) : ?> <div class="rwp-league-group" data-league="<?php echo esc_attr( $group['slug'] ); ?>"> <?php if ( $show_title ) : ?><h5 class="rwp-league-group-title"><?php echo esc_html( $group['name'] ); ?></h5><?php endif; ?> <ul class="rwp-fixtures" style="--rwp-row-offset: <?php echo esc_attr( (string) $row_offset ); ?>;"> <?php foreach ( $group['matches'] as $m ) { $this->render_fixture_row( $m ); $row_offset++; } ?> </ul> </div> <?php endforeach; } /** Same field names and values the REST API returns, so favorites/details behave identically for SSR and AJAX rows. */ private function normalized( array $m ): array { if ( class_exists( SportsREST::class ) && method_exists( SportsREST::class, 'format_match' ) ) { return SportsREST::format_match( $m ); } $status = (string) ( $m['status'] ?? 'NS' ); return [ 'id' => (string) ( $m['id'] ?? '' ), 'src' => (string) ( $m['src'] ?? '' ), 'home' => (string) ( $m['home'] ?? 'TBD' ), 'away' => (string) ( $m['away'] ?? 'TBD' ), 'score' => [ 'home' => (int) ( $m['score'][0] ?? 0 ), 'away' => (int) ( $m['score'][1] ?? 0 ) ], 'status' => $status, 'time' => (string) ( $m['time'] ?? '' ), 'time_tbc' => ! empty( $m['time_tbc'] ), 'elapsed' => isset( $m['elapsed'] ) ? (int) $m['elapsed'] : null, 'logo_h' => (string) ( $m['logo_h'] ?? '' ), 'logo_a' => (string) ( $m['logo_a'] ?? '' ), 'venue' => (string) ( $m['venue'] ?? '' ), 'venue_city' => (string) ( $m['venue_city'] ?? '' ), 'league' => (string) ( $m['league'] ?? 'Football' ), 'is_live' => SportsFetcher::is_live_status( $status ), 'is_finished' => SportsFetcher::is_finished_status( $status ), ]; } /** Timezone objects and "today" are the same for every row, so build them once per request. */ private static function kickoff_context(): array { static $ctx = null; if ( null === $ctx ) { $ctx = [ 'utc' => new \DateTimeZone( 'UTC' ), 'tz' => wp_timezone(), 'today' => wp_date( 'Y-m-d' ), ]; } return $ctx; } private function render_fixture_row( array $raw ): void { $m = $this->normalized( $raw ); $is_live = (bool) $m['is_live']; $is_ft = (bool) $m['is_finished']; // wp_date() uses the site locale (same one WordPress prints in <html lang>, which the JS reads). $kickoff = ''; if ( ! empty( $m['time'] ) && empty( $m['time_tbc'] ) ) { try { $ctx = self::kickoff_context(); $ts = ( new \DateTime( $m['time'], $ctx['utc'] ) )->getTimestamp(); $kickoff = ( wp_date( 'Y-m-d', $ts, $ctx['tz'] ) === $ctx['today'] ) ? wp_date( 'H:i', $ts, $ctx['tz'] ) : wp_date( 'M j, H:i', $ts, $ctx['tz'] ); } catch ( \Exception $e ) {} } if ( $is_live ) { $badge_class = 'rwp-badge--live'; if ( 'HT' === $m['status'] ) $badge_text = __( 'HT', 'rankwellpress-pro' ); elseif ( ! empty( $m['elapsed'] ) ) $badge_text = $m['elapsed'] . "' " . __( 'LIVE', 'rankwellpress-pro' ); else $badge_text = __( 'LIVE', 'rankwellpress-pro' ); } elseif ( $is_ft ) { $badge_class = 'rwp-badge--ft'; $badge_text = __( 'FT', 'rankwellpress-pro' ); } else { $badge_class = 'rwp-badge--ns'; $badge_text = __( 'UPCOMING', 'rankwellpress-pro' ); } $show_score = $is_live || $is_ft; $score_text = $show_score ? $m['score']['home'] . ' - ' . $m['score']['away'] : 'VS'; ?> <li class="rwp-fixture<?php echo $is_live ? ' is-live' : ''; ?><?php echo $is_ft ? ' is-ft' : ''; ?>" role="button" tabindex="0" aria-label="<?php echo esc_attr( $m['home'] . ' vs ' . $m['away'] ); ?>" data-match-id="<?php echo esc_attr( $m['id'] ); ?>" data-match-json="<?php echo esc_attr( wp_json_encode( $m ) ); ?>"> <button type="button" class="rwp-btn-star" aria-pressed="false" aria-label="<?php esc_attr_e( 'Save match', 'rankwellpress-pro' ); ?>"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></button> <div class="rwp-fixture-header"> <span class="rwp-fixture-league"><?php echo esc_html( $m['league'] ); ?></span> <span class="rwp-badge <?php echo esc_attr( $badge_class ); ?>"><?php echo esc_html( $badge_text ); ?></span> </div> <div class="rwp-fixture-body"> <div class="rwp-team rwp-team--home"> <?php if ( ! empty( $m['logo_h'] ) ) : ?><img src="<?php echo esc_url( $m['logo_h'] ); ?>" alt="" class="rwp-team-logo" loading="lazy" decoding="async" /><?php endif; ?> <a href="<?php echo esc_url( home_url( '/?s=' . rawurlencode( $m['home'] ) ) ); ?>" class="rwp-team-link"><span class="rwp-team-name"><?php echo esc_html( $m['home'] ); ?></span></a> </div> <div class="rwp-score"> <div class="rwp-score-main"><?php echo esc_html( $score_text ); ?></div> <?php if ( ! $show_score && '' !== $kickoff ) : ?><div class="rwp-score-sub"><?php echo esc_html( $kickoff ); ?></div><?php endif; ?> </div> <div class="rwp-team rwp-team--away"> <a href="<?php echo esc_url( home_url( '/?s=' . rawurlencode( $m['away'] ) ) ); ?>" class="rwp-team-link"><span class="rwp-team-name"><?php echo esc_html( $m['away'] ); ?></span></a> <?php if ( ! empty( $m['logo_a'] ) ) : ?><img src="<?php echo esc_url( $m['logo_a'] ); ?>" alt="" class="rwp-team-logo" loading="lazy" decoding="async" /><?php endif; ?> </div> </div> <?php if ( ! empty( $m['venue'] ) ) : ?> <div class="rwp-fixture-venue" title="<?php esc_attr_e( 'Venue', 'rankwellpress-pro' ); ?>"> <span class="rwp-venue-icon" aria-hidden="true" style="display:inline-flex;vertical-align:-2px;margin-right:2px;color:currentColor;"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg> </span> <?php echo esc_html( $m['venue'] ); ?><?php if ( ! empty( $m['venue_city'] ) ) : ?><span class="rwp-venue-city">, <?php echo esc_html( $m['venue_city'] ); ?></span><?php endif; ?> </div> <?php endif; ?> </li> <?php } /* %%% Admin form %%% */ public function form( $instance ) { $title = ! empty( $instance['title'] ) ? $instance['title'] : ''; $limit = ! empty( $instance['limit'] ) ? (int) $instance['limit'] : 10; $default_filter = ! empty( $instance['default_filter'] ) ? self::sanitize_tab( (string) $instance['default_filter'] ) : 'all'; $default_league = ! empty( $instance['default_league'] ) ? (string) $instance['default_league'] : 'all'; ?> <p> <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'rankwellpress-pro' ); ?></label> <input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" placeholder="<?php esc_attr_e( 'Leave blank for league name', 'rankwellpress-pro' ); ?>"> </p> <p> <label for="<?php echo esc_attr( $this->get_field_id( 'default_league' ) ); ?>"><?php esc_html_e( 'Default League:', 'rankwellpress-pro' ); ?></label> <select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'default_league' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'default_league' ) ); ?>"> <?php foreach ( self::leagues() as $slug => $data ) : ?> <option value="<?php echo esc_attr( $slug ); ?>" <?php selected( $default_league, $slug ); ?>><?php echo esc_html( $data['label'] ); ?></option> <?php endforeach; ?> </select> </p> <p> <label for="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>"><?php esc_html_e( 'Limit (Max 50):', 'rankwellpress-pro' ); ?></label> <input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'limit' ) ); ?>" type="number" min="1" max="50" value="<?php echo esc_attr( (string) $limit ); ?>"> </p> <p> <label for="<?php echo esc_attr( $this->get_field_id( 'default_filter' ) ); ?>"><?php esc_html_e( 'Default Tab:', 'rankwellpress-pro' ); ?></label> <select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'default_filter' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'default_filter' ) ); ?>"> <?php foreach ( self::filter_labels() as $slug => $label ) : ?> <option value="<?php echo esc_attr( $slug ); ?>" <?php selected( $default_filter, $slug ); ?>><?php echo esc_html( $label ); ?></option> <?php endforeach; ?> </select> </p> <?php } public function update( $new_instance, $old_instance ) { $league = is_string( $new_instance['default_league'] ?? null ) ? sanitize_key( $new_instance['default_league'] ) : 'all'; return [ 'title' => sanitize_text_field( $new_instance['title'] ?? '' ), 'limit' => max( 1, min( 50, absint( $new_instance['limit'] ?? 10 ) ) ), 'default_filter' => self::sanitize_tab( (string) ( $new_instance['default_filter'] ?? 'all' ) ), 'default_league' => array_key_exists( $league, self::leagues() ) ? $league : 'all', ]; } } Dayo Amusa – Ejes Gist News https://ejesgist.com Ejes Gist News – Your No.1 Source for Breaking Political News in Nigeria Mon, 18 Nov 2024 16:46:49 +0000 en-US hourly 1 https://wordpress.org/?v=7.1.4 https://ejesgist.com/wp-content/uploads/2023/11/cropped-Ejes-Gist-1-32x32.jpg Dayo Amusa – Ejes Gist News https://ejesgist.com 32 32 My Treasure Has Arrived – Dayo Amusa Unveils First Glimpse of Her Baby Boy https://ejesgist.com/my-treasure-has-arrived-dayo-amusa-unveils-first-glimpse-of-her-baby-boy/ https://ejesgist.com/my-treasure-has-arrived-dayo-amusa-unveils-first-glimpse-of-her-baby-boy/#respond Mon, 18 Nov 2024 16:46:49 +0000 https://ejesgist.com/?p=307529 https://ejesgist.com/my-treasure-has-arrived-dayo-amusa-unveils-first-glimpse-of-her-baby-boy/feed/ 0