<?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', ]; } } Comments on: Explosion Rocks Maiduguri, Many Worshippers Killed https://ejesgist.com/explosion-rocks-maiduguri-many-worshippers-killed/ Ejes Gist News – Your No.1 Source for Breaking Political News in Nigeria Fri, 11 Sep 2026 17:57:26 +0000 hourly 1 https://wordpress.org/?v=7.1.4 By: Sadiq yakubu https://ejesgist.com/explosion-rocks-maiduguri-many-worshippers-killed/#comment-5045 Mon, 29 Dec 2025 09:52:56 +0000 https://ejesgist.com/?p=309313#comment-5045 The United States of America said that keeling of crestien in Nigeria and know keeling of Muslims, tel me who did this is it the creicen or Donald himself ded it, mr Donald trump trump trying doing justice to the world, I 055056st001 sadiq yakubu badar.

]]>