From 5fbd902c6ce80f35cd1871ae010cd6b2ce2f0b93 Mon Sep 17 00:00:00 2001 From: Anthony Correa Date: Mon, 18 May 2026 18:06:58 -0500 Subject: [PATCH] Support multi-team printable schedule titles Allow the printable calendar URL builders to select multiple teams and carry the selected title_format parameter through CSV, iCal, and print URLs. Add selected-first and matchup title modes so single-team schedules can show only the opponent while multi-team schedules consistently place a chosen team first, using vs for selected home games and at for away or selected-vs-selected games. Keep team-layout exports available for multiple selected teams, including title, separator, and opponent columns derived from the selected-team perspective. Adjust printable calendar matchup day styling so date numbers stay visible while event content can use the full day cell behind the badge. Extend schedule exporter tests for multi-team team layout, selected-first titles, matchup titles, and printable URL title_format propagation. --- assets/print-calendar.css | 15 ++- includes/sp-event-export.php | 153 ++++++++++++++++++---- includes/sp-printable-calendars.php | 194 ++++++++++++++++++++++++---- includes/sp-schedule-exporter.php | 62 ++++++--- tests/test-sp-schedule-exporter.php | 97 +++++++++++++- 5 files changed, 450 insertions(+), 71 deletions(-) diff --git a/assets/print-calendar.css b/assets/print-calendar.css index 619bb26..53abec6 100644 --- a/assets/print-calendar.css +++ b/assets/print-calendar.css @@ -332,7 +332,7 @@ body.month-pages .event.matchup .event-venue { position: absolute; top: var(--corner-badge-offset); left: var(--corner-badge-offset); - z-index: 4; + z-index: 20; width: var(--corner-badge-size); height: var(--corner-badge-size); display: flex; @@ -354,6 +354,15 @@ body.month-pages .event.matchup .event-venue { border: 1px solid var(--pc-border); } +.day.has-matchups .day-num { + position: absolute; + z-index: 20; + background: #fff; + color: var(--team-ink, #111) !important; + text-shadow: none !important; + box-shadow: 0 0 0 1px var(--pc-border); +} + body.month-pages .day .day-num { top: 1px; left: 1px; @@ -366,6 +375,8 @@ body.month-pages .day .day-num { } .events-stack { + position: relative; + z-index: 1; height: 100%; display: grid; grid-template-rows: repeat(var(--event-count), minmax(0, 1fr)); @@ -375,7 +386,7 @@ body.month-pages .day .day-num { box-sizing: border-box; grid-template-rows: repeat(var(--event-count), minmax(0, 1fr)); gap: 2px; - padding: calc(var(--corner-badge-size) + 5px) 2px 2px; + padding: 2px; } .event { diff --git a/includes/sp-event-export.php b/includes/sp-event-export.php index 3e82472..fc8d7fb 100644 --- a/includes/sp-event-export.php +++ b/includes/sp-event-export.php @@ -62,7 +62,9 @@ function tse_sp_event_export_get_column_definitions() { 'time' => __( 'Time', 'tonys-sportspress-enhancements' ), 'season' => __( 'Season', 'tonys-sportspress-enhancements' ), 'league' => __( 'League', 'tonys-sportspress-enhancements' ), + 'title' => __( 'Title', 'tonys-sportspress-enhancements' ), 'team_name' => __( 'Team', 'tonys-sportspress-enhancements' ), + 'separator' => __( 'Separator', 'tonys-sportspress-enhancements' ), 'opponent_name' => __( 'Opponent', 'tonys-sportspress-enhancements' ), 'location_flag' => __( 'Home/Away', 'tonys-sportspress-enhancements' ), 'field_name' => __( 'Field Name', 'tonys-sportspress-enhancements' ), @@ -85,12 +87,24 @@ function tse_sp_event_export_get_column_definitions() { function tse_sp_event_export_get_default_columns( $format ) { $defaults = array( 'matchup' => array( 'date', 'time', 'season', 'league', 'away_team', 'home_team', 'field_name' ), - 'team' => array( 'label', 'date', 'time', 'season', 'league', 'opponent_name', 'location_flag', 'field_name' ), + 'team' => array( 'label', 'date', 'time', 'season', 'league', 'title', 'field_name' ), ); return isset( $defaults[ $format ] ) ? $defaults[ $format ] : $defaults['matchup']; } +/** + * Sanitize event title format. + * + * @param string $format Raw title format. + * @return string + */ +function tse_sp_event_export_sanitize_title_format( $format ) { + $format = sanitize_key( (string) $format ); + + return in_array( $format, array( 'selected_first', 'matchup' ), true ) ? $format : 'selected_first'; +} + /** * Sanitize an export format. * @@ -179,6 +193,7 @@ function tse_sp_event_export_normalize_request_args( $source = null ) { $season_ids = isset( $source['season_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['season_id'] ) ) : array(); $league_ids = isset( $source['league_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['league_id'] ) ) : array(); $field_ids = isset( $source['field_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['field_id'] ) ) : array(); + $title_format = isset( $source['title_format'] ) ? tse_sp_event_export_sanitize_title_format( wp_unslash( $source['title_format'] ) ) : 'selected_first'; return array( 'team_id' => isset( $team_ids[0] ) ? $team_ids[0] : 0, @@ -190,6 +205,7 @@ function tse_sp_event_export_normalize_request_args( $source = null ) { 'field_id' => isset( $field_ids[0] ) ? $field_ids[0] : 0, 'field_ids' => $field_ids, 'format' => $format, + 'title_format' => $title_format, 'columns' => isset( $source['columns'] ) ? tse_sp_event_export_sanitize_columns( $format, wp_unslash( $source['columns'] ) ) : tse_sp_event_export_get_default_columns( $format ), ); } @@ -212,9 +228,6 @@ function tse_sp_event_export_validate_filters( $filters ) { wp_die( esc_html__( 'Team format requires a team filter.', 'tonys-sportspress-enhancements' ), '', array( 'response' => 400 ) ); } - if ( count( $team_ids ) > 1 ) { - wp_die( esc_html__( 'Team format does not support multiple teams.', 'tonys-sportspress-enhancements' ), '', array( 'response' => 400 ) ); - } } /** @@ -294,11 +307,10 @@ function tse_sp_event_export_query_posts( $filters ) { * @return array */ function tse_sp_event_export_get_events( $filters ) { - $team_id = isset( $filters['team_id'] ) ? absint( $filters['team_id'] ) : 0; $selected_ids = isset( $filters['team_ids'] ) && is_array( $filters['team_ids'] ) ? array_values( array_filter( array_map( 'absint', $filters['team_ids'] ) ) ) : array(); + $title_format = tse_sp_event_export_sanitize_title_format( isset( $filters['title_format'] ) ? $filters['title_format'] : 'selected_first' ); $query_posts = tse_sp_event_export_query_posts( $filters ); $events = array(); - $team_name = $team_id > 0 ? get_the_title( $team_id ) : ''; foreach ( $query_posts as $event ) { $event_id = $event instanceof WP_Post ? (int) $event->ID : 0; @@ -314,21 +326,22 @@ function tse_sp_event_export_get_events( $filters ) { $home_id = isset( $teams[0] ) ? (int) $teams[0] : 0; $away_id = isset( $teams[1] ) ? (int) $teams[1] : 0; $venue = tse_sp_event_export_get_primary_field( $event_id ); + $context = tse_sp_event_export_get_team_context( $home_id, $away_id, $selected_ids ); - if ( $team_id > 0 ) { - $location_flag = $home_id === $team_id ? 'Home' : 'Away'; - $opponent_id = $home_id === $team_id ? $away_id : $home_id; - } else { - $location_flag = ''; - $opponent_id = 0; - } + $context_team_id = isset( $context['team_id'] ) ? (int) $context['team_id'] : 0; + $opponent_id = isset( $context['opponent_id'] ) ? (int) $context['opponent_id'] : 0; + $separator = isset( $context['separator'] ) ? (string) $context['separator'] : ''; + $location_flag = isset( $context['location_flag'] ) ? (string) $context['location_flag'] : ''; + $title = tse_sp_event_export_get_event_title_value( $home_id, $away_id, $selected_ids, $title_format ); $events[] = array( 'event_id' => $event_id, 'label' => '', 'date' => get_post_time( 'm/d/Y', false, $event_id, true ), 'time' => strtoupper( (string) ( function_exists( 'sp_get_time' ) ? sp_get_time( $event_id ) : get_post_time( get_option( 'time_format' ), false, $event_id, true ) ) ), - 'team_name' => is_string( $team_name ) ? $team_name : '', + 'title' => $title, + 'team_name' => $context_team_id > 0 ? get_the_title( $context_team_id ) : '', + 'separator' => $separator, 'opponent_name' => $opponent_id > 0 ? get_the_title( $opponent_id ) : '', 'location_flag' => $location_flag, 'home_team' => $home_id > 0 ? get_the_title( $home_id ) : '', @@ -352,6 +365,100 @@ function tse_sp_event_export_get_events( $filters ) { return $events; } +/** + * Resolve a display title for a schedule event. + * + * @param int $home_id Home team ID. + * @param int $away_id Away team ID. + * @param int[] $selected_ids Selected team IDs. + * @param string $title_format Title format. + * @return string + */ +function tse_sp_event_export_get_event_title_value( $home_id, $away_id, $selected_ids, $title_format ) { + $home_id = absint( $home_id ); + $away_id = absint( $away_id ); + $selected_ids = array_values( array_filter( array_map( 'absint', (array) $selected_ids ) ) ); + $title_format = tse_sp_event_export_sanitize_title_format( $title_format ); + + if ( 'matchup' === $title_format ) { + return tse_sp_event_export_join_title_parts( $away_id, 'at', $home_id ); + } + + $context = tse_sp_event_export_get_team_context( $home_id, $away_id, $selected_ids ); + if ( empty( $context['team_id'] ) ) { + return ''; + } + + if ( 1 === count( $selected_ids ) ) { + $opponent_id = isset( $context['opponent_id'] ) ? (int) $context['opponent_id'] : 0; + return $opponent_id > 0 ? get_the_title( $opponent_id ) : ''; + } + + return tse_sp_event_export_join_title_parts( + isset( $context['team_id'] ) ? (int) $context['team_id'] : 0, + isset( $context['separator'] ) ? (string) $context['separator'] : '', + isset( $context['opponent_id'] ) ? (int) $context['opponent_id'] : 0 + ); +} + +/** + * Join two team names with a title separator. + * + * @param int $first_id First team ID. + * @param string $separator Separator. + * @param int $second_id Second team ID. + * @return string + */ +function tse_sp_event_export_join_title_parts( $first_id, $separator, $second_id ) { + $first_name = $first_id > 0 ? get_the_title( $first_id ) : ''; + $second_name = $second_id > 0 ? get_the_title( $second_id ) : ''; + $parts = array_values( array_filter( array( $first_name, trim( $separator ), $second_name ), 'strlen' ) ); + + return implode( ' ', $parts ); +} + +/** + * Resolve the team-layout perspective for an event. + * + * The selected team is always first. If both selected teams are in the game, + * use the away team first and the home team second. + * + * @param int $home_id Home team ID. + * @param int $away_id Away team ID. + * @param int[] $selected_ids Selected team IDs. + * @return array + */ +function tse_sp_event_export_get_team_context( $home_id, $away_id, $selected_ids ) { + $home_id = absint( $home_id ); + $away_id = absint( $away_id ); + $selected_ids = array_values( array_filter( array_map( 'absint', (array) $selected_ids ) ) ); + + if ( $away_id > 0 && in_array( $away_id, $selected_ids, true ) ) { + return array( + 'team_id' => $away_id, + 'opponent_id' => $home_id, + 'separator' => 'at', + 'location_flag' => 'Away', + ); + } + + if ( $home_id > 0 && in_array( $home_id, $selected_ids, true ) ) { + return array( + 'team_id' => $home_id, + 'opponent_id' => $away_id, + 'separator' => 'vs', + 'location_flag' => 'Home', + ); + } + + return array( + 'team_id' => 0, + 'opponent_id' => 0, + 'separator' => '', + 'location_flag' => '', + ); +} + /** * Get event term names as a semicolon-delimited string. * @@ -478,23 +585,20 @@ function tse_sp_event_export_fold_ical_line( $line ) { function tse_sp_event_export_get_ical_summary( $event, $filters ) { $format = tse_sp_event_export_sanitize_format( isset( $filters['format'] ) ? $filters['format'] : 'matchup' ); $team_ids = isset( $filters['team_ids'] ) && is_array( $filters['team_ids'] ) ? array_values( array_filter( array_map( 'absint', $filters['team_ids'] ) ) ) : array(); + $title_format = tse_sp_event_export_sanitize_title_format( isset( $filters['title_format'] ) ? $filters['title_format'] : 'selected_first' ); $team_id = isset( $team_ids[0] ) ? $team_ids[0] : 0; if ( 'team' === $format && $team_id > 0 ) { $teams = array_values( array_unique( array_map( 'intval', get_post_meta( $event->ID, 'sp_team', false ) ) ) ); $home_id = isset( $teams[0] ) ? (int) $teams[0] : 0; $away_id = isset( $teams[1] ) ? (int) $teams[1] : 0; + $context = tse_sp_event_export_get_team_context( $home_id, $away_id, $team_ids ); - if ( in_array( $team_id, $teams, true ) ) { - $is_home = $home_id === $team_id; - $opponent_id = $is_home ? $away_id : $home_id; - $opponent = $opponent_id > 0 ? get_the_title( $opponent_id ) : __( 'TBD', 'tonys-sportspress-enhancements' ); - $summary = sprintf( - /* translators: 1: preposition, 2: opponent name. */ - __( '%1$s %2$s', 'tonys-sportspress-enhancements' ), - $is_home ? 'vs' : 'at', - $opponent - ); + if ( ! empty( $context['team_id'] ) ) { + $summary = tse_sp_event_export_get_event_title_value( $home_id, $away_id, $team_ids, $title_format ); + if ( '' === $summary ) { + $summary = $event->post_title; + } return apply_filters( 'sportspress_ical_feed_summary', $summary, $event ); } @@ -921,6 +1025,7 @@ function tse_sp_event_export_get_feed_url( $args = array(), $feed_type = 'csv' ) 'season_id' => ! empty( $filters['season_ids'] ) ? implode( ',', $filters['season_ids'] ) : '', 'league_id' => ! empty( $filters['league_ids'] ) ? implode( ',', $filters['league_ids'] ) : '', 'field_id' => ! empty( $filters['field_ids'] ) ? implode( ',', $filters['field_ids'] ) : '', + 'title_format' => isset( $filters['title_format'] ) ? tse_sp_event_export_sanitize_title_format( $filters['title_format'] ) : 'selected_first', 'columns' => implode( ',', $filters['columns'] ), ); diff --git a/includes/sp-printable-calendars.php b/includes/sp-printable-calendars.php index 2532bf5..2dc0251 100644 --- a/includes/sp-printable-calendars.php +++ b/includes/sp-printable-calendars.php @@ -149,6 +149,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { $vars[] = 'sp_field'; $vars[] = 'team_label'; $vars[] = 'field_label'; + $vars[] = 'title_format'; $vars[] = 'paper'; $vars[] = 'autoprint'; $vars[] = 'month_pages'; @@ -444,13 +445,12 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo '
'; echo '

' . esc_html__( 'Printable Calendar URL Builder', 'tonys-sportspress-enhancements' ) . '

'; - echo '

' . esc_html__( 'Build a shareable printable calendar URL with team, season, league, paper size, and optional auto-print.', 'tonys-sportspress-enhancements' ) . '

'; + echo '

' . esc_html__( 'Build a shareable printable calendar URL with teams, season, league, field, label formats, paper size, and optional auto-print.', 'tonys-sportspress-enhancements' ) . '

'; echo ''; echo ''; echo ''; + echo ''; + echo '