diff --git a/assets/print-calendar.css b/assets/print-calendar.css index a0146ad..a56321d 100644 --- a/assets/print-calendar.css +++ b/assets/print-calendar.css @@ -27,10 +27,7 @@ body { .print-shell { margin: 0; - width: calc(100% / var(--sheet-scale)); background: #fff; - transform-origin: top left; - transform: scale(var(--sheet-scale)); } .print-page { @@ -48,7 +45,10 @@ body { } body.print-preview .print-shell { + flex: 0 0 auto; box-shadow: 0 10px 30px rgba(17, 24, 39, 0.2); + transform-origin: top left; + transform: scale(var(--sheet-scale)); } body.print-preview .print-shell.letter { @@ -60,6 +60,104 @@ body { width: 11in; min-height: 17in; } + + body.print-preview.month-pages { + display: block; + overflow-x: auto; + } + + body.print-preview.month-pages .print-shell, + body.print-preview.month-pages .print-shell.letter, + body.print-preview.month-pages .print-shell.ledger { + width: auto; + min-height: auto; + background: transparent; + box-shadow: none; + transform: none; + } + + body.print-preview.month-pages .print-page { + padding: 0; + } + + body.print-preview.month-pages .month-page { + box-sizing: border-box; + width: 8.5in; + min-height: 11in; + margin-right: auto; + margin-left: auto; + padding: var(--pc-page-padding); + background: #fff; + box-shadow: 0 10px 30px rgba(17, 24, 39, 0.2); + } + + body.print-preview.month-pages.ledger .month-page { + width: 11in; + min-height: 17in; + } + + body.print-preview.month-pages .month-page + .month-page { + margin-top: 24px; + } + + body.print-preview.month-pages .month-page .header { + margin-bottom: 24px; + } + + body.print-preview.month-pages .month-page .footer-meta { + margin-top: 24px; + } + + body.print-preview.month-pages .sheet-grid { + display: block; + } + +body.print-preview.month-pages .month { + width: 100%; +} + +body.month-pages .month-title { + font-size: calc(26px * var(--month-font-scale)); + padding: 4px; +} + +body.month-pages .dow span { + font-size: calc(12px * var(--month-font-scale)); + padding: 4px 2px; +} + +body.month-pages .day-num { + font-size: calc(14px * var(--month-font-scale)); +} + +body.month-pages .event-name { + font-size: calc(13px * var(--month-font-scale)); + line-height: 0.98; +} + +body.month-pages .matchup-name { + font-size: calc(13px * var(--month-font-scale)); + line-height: 0.92; +} + +body.month-pages .event-time { + font-size: calc(14px * var(--month-font-scale)); +} + +body.month-pages .event.matchup .event-time { + font-size: calc(11px * var(--month-font-scale)); +} + +body.month-pages .event.matchup .event-venue { + font-size: calc(10px * var(--month-font-scale)); +} +} + +@media screen and (max-width: 900px) { + body.print-preview { + justify-content: flex-start; + overflow-x: auto; + } } .header { @@ -235,7 +333,7 @@ body { 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; @@ -252,12 +350,46 @@ body { color: var(--day-num-color, #fff); } +.day.has-matchups { + background: #fff; + 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; + width: var(--corner-badge-size); + height: var(--corner-badge-size); + border-radius: 0; + background: #fff; + color: var(--team-ink, #111); + text-shadow: none; +} + .events-stack { + position: relative; + z-index: 1; height: 100%; display: grid; grid-template-rows: repeat(var(--event-count), minmax(0, 1fr)); } +.day.has-matchups .events-stack { + box-sizing: border-box; + grid-template-rows: repeat(var(--event-count), minmax(0, 1fr)); + gap: 2px; + padding: 2px; +} + .event { --event-top-band: calc(var(--corner-badge-size, 11px) + var(--corner-badge-offset, 2px)); --event-bottom-band: 26px; @@ -275,6 +407,17 @@ body { --event-bg: var(--team-link-color, var(--team-secondary, #8b3f1f)); } +.event.matchup { + display: grid; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr) auto; + gap: 1px; + min-height: 0; + padding: 2px 3px; + border-radius: 2px; + overflow: hidden; +} + .event-center { position: absolute; top: var(--event-top-band); @@ -293,6 +436,17 @@ body { text-align: center; } +.event.matchup .event-center { + position: relative; + top: auto; + left: auto; + right: auto; + bottom: auto; + min-height: 0; + align-items: center; + justify-content: center; +} + .event-center img { width: auto; height: var(--event-logo-height); @@ -315,6 +469,8 @@ body { hyphens: none; line-height: 1.05; font-weight: 700; + font-stretch: condensed; + font-variation-settings: "wdth" 42, "wght" 700; opacity: 0.85; } @@ -322,10 +478,38 @@ body { font-family: var(--pc-font-display); font-weight: 700; font-variation-settings: "wdth" 30, "wght" 700; - letter-spacing: -0.01em; text-transform: uppercase; } +.matchup-name { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0; + padding: 0; + font-size: calc(8.5px * var(--month-font-scale)); + line-height: 0.98; + font-variation-settings: "wdth" 36, "wght" 800; + opacity: 1; +} + +.matchup-vs { + font-size: 0.66em; + font-weight: 900; + opacity: 0.78; +} + +.matchup-team { + display: block; + width: 100%; + overflow: hidden; + text-align: center; + text-overflow: clip; + white-space: nowrap; + font-stretch: condensed; +} + .ha-flag { position: absolute; top: var(--corner-badge-offset, 2px); @@ -341,7 +525,7 @@ body { font-weight: 900; font-variation-settings: "wdth" 84, "wght" 900; line-height: 1; - letter-spacing: -0.01em; + letter-spacing: 0; background: #111; color: #fff; } @@ -366,11 +550,26 @@ body { overflow: hidden; } +.event.matchup .event-meta { + position: relative; + left: auto; + right: auto; + bottom: auto; + display: grid; + grid-template-columns: minmax(0, auto) minmax(0, 1fr); + align-items: center; + justify-content: center; + column-gap: 3px; + padding: 0; +} + .event-time { order: 1; max-width: 100%; font-size: calc(12px * var(--month-font-scale)); font-weight: 800; + font-stretch: condensed; + font-variation-settings: "wdth" 56, "wght" 800; line-height: 1; text-transform: uppercase; text-align: center; @@ -381,11 +580,19 @@ body { opacity: 0.95; } +.event.matchup .event-time { + min-width: 0; + font-size: calc(7.5px * var(--month-font-scale)); + font-weight: 900; +} + .event-venue { order: 2; max-width: 100%; font-size: calc(8px * var(--month-font-scale)); font-weight: 700; + font-stretch: condensed; + font-variation-settings: "wdth" 50, "wght" 700; line-height: 1; text-align: center; text-transform: uppercase; @@ -395,6 +602,12 @@ body { opacity: 0.88; } +.event.matchup .event-venue { + min-width: 0; + font-size: calc(7px * var(--month-font-scale)); + text-align: left; +} + .empty { padding: 16px; border: 2px dashed #c8d2de; @@ -472,6 +685,100 @@ body { background: #fff; } +body.black-white .header, +body.black-white .month-title, +body.black-white .dow span, +body.black-white .day, +body.black-white .event, +body.black-white .empty, +body.black-white .footer-meta, +body.black-white .footer-qr-image { + border-color: #2f3337 !important; +} + +body.black-white .month-title, +body.black-white .ha-flag { + background: #1f2328 !important; + color: #fff !important; + text-shadow: none !important; +} + +body.black-white .dow span, +body.black-white .day, +body.black-white .day.muted, +body.black-white .day.no-events, +body.black-white .day.has-matchups, +body.black-white .empty, +body.black-white .legend-item, +body.black-white .event.a .ha-flag { + background: #f7f7f7 !important; + color: #111 !important; + text-shadow: none !important; +} + +body.black-white .dow span { + background: #e4e7eb !important; +} + +body.black-white .day, +body.black-white .day.has-matchups, +body.black-white .legend-item { + background: #fff !important; +} + +body.black-white .event, +body.black-white .event.h, +body.black-white .event.a, +body.black-white .event.matchup { + background: #e4e7eb !important; + color: #111 !important; + text-shadow: none !important; +} + +body.black-white .day.muted, +body.black-white .day.no-events { + background: #f2f3f5 !important; + color: #4b5563 !important; +} + +body.black-white .month-title, +body.black-white .dow span, +body.black-white .day, +body.black-white .event { + border: 1px solid #2f3337; +} + +body.black-white .day-num, +body.black-white .day.has-events .day-num, +body.black-white .day.has-matchups .day-num, +body.black-white.month-pages .day .day-num { + background: #fff !important; + color: #111 !important; + box-shadow: 0 0 0 1px #2f3337 !important; + text-shadow: none !important; +} + +body.black-white .ha-flag, +body.black-white .event.a .ha-flag { + box-shadow: 0 0 0 1px #2f3337; +} + +body.black-white .event-name, +body.black-white .event-time, +body.black-white .event-venue, +body.black-white .matchup-vs, +body.black-white .footer-qr-label, +body.black-white .footer-qr-link, +body.black-white .meta { + color: #111 !important; + opacity: 1 !important; +} + +body.black-white .meta, +body.black-white .footer-qr-label { + color: #374151 !important; +} + @media print { body, body.print-preview { @@ -487,6 +794,7 @@ body { width: auto; min-height: auto; box-shadow: none; + transform: none; } .print-page { @@ -500,4 +808,27 @@ body { .title { font-size: calc(26px * var(--month-font-scale)); } + + body.month-pages .sheet-grid { + display: block; + } + + body.month-pages .month-page { + break-after: page; + page-break-after: always; + } + + body.month-pages .month-page:last-child { + break-after: auto; + page-break-after: auto; + } + + body.month-pages .month { + width: 100%; + } + + body.month-pages .grid { + break-inside: avoid; + page-break-inside: avoid; + } } diff --git a/includes/sp-event-export.php b/includes/sp-event-export.php index 3e82472..851a683 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,33 @@ 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']; } +/** + * Get supported event title formats. + * + * @return string[] + */ +function tse_sp_event_export_get_title_formats() { + return array( 'selected_first', '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, tse_sp_event_export_get_title_formats(), true ) ? $format : 'selected_first'; +} + /** * Sanitize an export format. * @@ -173,24 +196,26 @@ function tse_sp_event_export_parse_id_list( $value ) { * @return array */ function tse_sp_event_export_normalize_request_args( $source = null ) { - $source = is_array( $source ) ? $source : $_GET; - $format = isset( $source['format'] ) ? tse_sp_event_export_sanitize_format( wp_unslash( $source['format'] ) ) : 'matchup'; - $team_ids = isset( $source['team_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['team_id'] ) ) : array(); - $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(); + $source = is_array( $source ) ? $source : $_GET; + $format = isset( $source['format'] ) ? tse_sp_event_export_sanitize_format( wp_unslash( $source['format'] ) ) : 'matchup'; + $team_ids = isset( $source['team_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['team_id'] ) ) : array(); + $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, - 'team_ids' => $team_ids, - 'season_id' => isset( $season_ids[0] ) ? $season_ids[0] : 0, - 'season_ids'=> $season_ids, - 'league_id' => isset( $league_ids[0] ) ? $league_ids[0] : 0, - 'league_ids'=> $league_ids, - 'field_id' => isset( $field_ids[0] ) ? $field_ids[0] : 0, - 'field_ids' => $field_ids, - 'format' => $format, - 'columns' => isset( $source['columns'] ) ? tse_sp_event_export_sanitize_columns( $format, wp_unslash( $source['columns'] ) ) : tse_sp_event_export_get_default_columns( $format ), + 'team_id' => isset( $team_ids[0] ) ? $team_ids[0] : 0, + 'team_ids' => $team_ids, + 'season_id' => isset( $season_ids[0] ) ? $season_ids[0] : 0, + 'season_ids' => $season_ids, + 'league_id' => isset( $league_ids[0] ) ? $league_ids[0] : 0, + 'league_ids' => $league_ids, + '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 +237,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 +316,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(); - $query_posts = tse_sp_event_export_query_posts( $filters ); - $events = array(); - $team_name = $team_id > 0 ? get_the_title( $team_id ) : ''; + $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(); foreach ( $query_posts as $event ) { $event_id = $event instanceof WP_Post ? (int) $event->ID : 0; @@ -314,21 +335,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 +374,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. * @@ -476,25 +592,22 @@ function tse_sp_event_export_fold_ical_line( $line ) { * @return string */ 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(); - $team_id = isset( $team_ids[0] ) ? $team_ids[0] : 0; + $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 ); } @@ -915,13 +1028,14 @@ function tse_sp_event_export_get_feed_url( $args = array(), $feed_type = 'csv' ) $filters = tse_sp_event_export_normalize_request_args( $args ); $feed = 'ics' === sanitize_key( $feed_type ) ? 'sp-ics' : 'sp-csv'; $query = array( - 'feed' => $feed, - 'format' => $filters['format'], - 'team_id' => ! empty( $filters['team_ids'] ) ? implode( ',', $filters['team_ids'] ) : '', - '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'] ) : '', - 'columns' => implode( ',', $filters['columns'] ), + 'feed' => $feed, + 'format' => $filters['format'], + 'team_id' => ! empty( $filters['team_ids'] ) ? implode( ',', $filters['team_ids'] ) : '', + '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'] ), ); return add_query_arg( array_filter( $query, 'strlen' ), home_url( '/' ) ); diff --git a/includes/sp-printable-calendars.php b/includes/sp-printable-calendars.php index a0dc180..73b3a82 100644 --- a/includes/sp-printable-calendars.php +++ b/includes/sp-printable-calendars.php @@ -146,8 +146,14 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { $vars[] = 'sp_team'; $vars[] = 'sp_season'; $vars[] = 'sp_league'; + $vars[] = 'sp_field'; + $vars[] = 'team_label'; + $vars[] = 'field_label'; + $vars[] = 'title_format'; $vars[] = 'paper'; $vars[] = 'autoprint'; + $vars[] = 'month_pages'; + $vars[] = 'black_white'; return $vars; } @@ -221,16 +227,24 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { $this->default_settings() ); - $current = wp_parse_args( - is_array( $input ) ? $input : array(), - $existing - ); + $current = is_array( $input ) ? $input : array(); + $active_season_id = isset( $current['active_season_id'] ) ? absint( (string) $current['active_season_id'] ) : 0; return array( 'calendar_feed_url' => isset( $existing['calendar_feed_url'] ) && is_string( $existing['calendar_feed_url'] ) ? $existing['calendar_feed_url'] : '', 'sync_interval_minutes' => isset( $existing['sync_interval_minutes'] ) ? absint( (string) $existing['sync_interval_minutes'] ) : 60, - 'venue_color_overrides' => $this->sanitize_venue_color_overrides( isset( $current['venue_color_overrides'] ) ? $current['venue_color_overrides'] : array() ), - 'venue_use_team_primary' => $this->sanitize_venue_primary_flags( isset( $current['venue_use_team_primary'] ) ? $current['venue_use_team_primary'] : array() ), + 'venue_color_overrides' => $this->merge_sanitized_season_settings( + isset( $existing['venue_color_overrides'] ) && is_array( $existing['venue_color_overrides'] ) ? $existing['venue_color_overrides'] : array(), + $this->sanitize_venue_color_overrides( isset( $current['venue_color_overrides'] ) ? $current['venue_color_overrides'] : array() ), + $active_season_id, + true + ), + 'venue_use_team_primary' => $this->merge_sanitized_season_settings( + isset( $existing['venue_use_team_primary'] ) && is_array( $existing['venue_use_team_primary'] ) ? $existing['venue_use_team_primary'] : array(), + $this->sanitize_venue_primary_flags( isset( $current['venue_use_team_primary'] ) ? $current['venue_use_team_primary'] : array() ), + $active_season_id, + false + ), 'printable_venue_label_mode' => $this->sanitize_venue_label_mode( isset( $current['printable_venue_label_mode'] ) ? $current['printable_venue_label_mode'] : '' ), ); } @@ -322,6 +336,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo '
'; settings_fields( self::OPTION_GROUP ); + echo ''; echo '

' . esc_html__( 'Field Colors By Season', 'tonys-sportspress-enhancements' ) . '

'; echo '

' . esc_html__( 'Pick venue colors per season. Colors are darkened automatically when needed so white text still reads clearly.', 'tonys-sportspress-enhancements' ) . '

'; @@ -359,11 +374,11 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo ''; echo ''; echo ''; - echo '

' . esc_html__( 'Choose whether the printable schedule shows the venue full name, abbreviation, or short name below each game time.', 'tonys-sportspress-enhancements' ) . '

'; + echo '

' . esc_html__( 'Default field label mode for printable URLs that do not include a field_label parameter.', 'tonys-sportspress-enhancements' ) . '

'; echo ''; echo ''; echo ''; @@ -426,17 +441,17 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { private function render_printable_url_builder( $season_id ) { $leagues = function_exists( 'tse_sp_schedule_exporter_get_leagues' ) ? tse_sp_schedule_exporter_get_leagues() : array(); $teams = function_exists( 'tse_sp_schedule_exporter_get_teams' ) ? tse_sp_schedule_exporter_get_teams() : array(); + $fields = function_exists( 'tse_sp_schedule_exporter_get_fields' ) ? tse_sp_schedule_exporter_get_fields() : array(); $paper = '11x17'; 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 ''; + echo ''; + + echo ''; + + echo ''; + echo ''; echo ''; @@ -495,7 +557,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo ''; echo '
'; echo '

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

'; - echo '

' . esc_html__( 'The printable route requires a single team selection.', 'tonys-sportspress-enhancements' ) . '

'; + echo '

' . esc_html__( 'The printable route requires at least one selected team.', 'tonys-sportspress-enhancements' ) . '

'; echo ''; $this->render_printable_url_builder_script(); @@ -522,18 +584,37 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { var team = root.querySelector('#tse-printable-builder-team'); var season = root.querySelector('#tse-printable-builder-season'); var league = root.querySelector('#tse-printable-builder-league'); + var field = root.querySelector('#tse-printable-builder-field'); var paper = root.querySelector('#tse-printable-builder-paper'); + var teamLabel = root.querySelector('#tse-printable-builder-team-label'); + var fieldLabel = root.querySelector('#tse-printable-builder-field-label'); + var titleFormat = root.querySelector('#tse-printable-builder-title-format'); var autoprint = root.querySelector('#tse-printable-builder-autoprint'); + var monthPages = root.querySelector('#tse-printable-builder-month-pages'); + var blackWhite = root.querySelector('#tse-printable-builder-black-white'); var output = root.querySelector('#tse-printable-builder-output'); var copyButton = root.querySelector('#tse-printable-builder-copy'); var openLink = root.querySelector('#tse-printable-builder-open'); + function selectedValues(select) { + if (!select) { + return []; + } + + return Array.prototype.slice.call(select.selectedOptions || []).map(function(option){ + return option.value; + }).filter(function(value){ + return value && value !== '0'; + }); + } + function buildUrl() { var url = new URL(baseUrl, window.location.origin); + var teamValues = selectedValues(team); url.searchParams.set(queryFlag, '1'); - if (team.value && team.value !== '0') { - url.searchParams.set('sp_team', team.value); + if (teamValues.length) { + url.searchParams.set('sp_team', teamValues.join(',')); } else { url.searchParams.delete('sp_team'); } @@ -550,22 +631,52 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { url.searchParams.delete('sp_league'); } + if (field.value && field.value !== '0') { + url.searchParams.set('sp_field', field.value); + } else { + url.searchParams.delete('sp_field'); + } + if (paper.value) { url.searchParams.set('paper', paper.value); } + if (teamLabel.value) { + url.searchParams.set('team_label', teamLabel.value); + } + + if (fieldLabel.value) { + url.searchParams.set('field_label', fieldLabel.value); + } + + if (titleFormat.value) { + url.searchParams.set('title_format', titleFormat.value); + } + if (autoprint.checked) { url.searchParams.set('autoprint', '1'); } else { url.searchParams.delete('autoprint'); } + if (monthPages.checked) { + url.searchParams.set('month_pages', '1'); + } else { + url.searchParams.delete('month_pages'); + } + + if (blackWhite.checked) { + url.searchParams.set('black_white', '1'); + } else { + url.searchParams.delete('black_white'); + } + output.value = url.toString(); openLink.href = url.toString(); - openLink.toggleAttribute('disabled', !(team.value && team.value !== '0')); + openLink.toggleAttribute('disabled', !teamValues.length); } - [team, season, league, paper, autoprint].forEach(function(input){ + [team, season, league, field, paper, teamLabel, fieldLabel, titleFormat, autoprint, monthPages, blackWhite].forEach(function(input){ input.addEventListener('change', buildUrl); }); @@ -618,8 +729,8 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { return; } - $team_id = absint( (string) get_query_var( 'sp_team' ) ); - if ( $team_id <= 0 || 'sp_team' !== get_post_type( $team_id ) ) { + $team_ids = $this->parse_team_ids( get_query_var( 'sp_team' ) ); + if ( empty( $team_ids ) ) { status_header( 400 ); nocache_headers(); echo esc_html__( 'Missing or invalid team id.', 'tonys-sportspress-enhancements' ); @@ -631,23 +742,33 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { $season_id = absint( (string) get_option( 'sportspress_season', '0' ) ); } $league_id = absint( (string) get_query_var( 'sp_league' ) ); + $field_ids = $this->parse_term_ids( get_query_var( 'sp_field' ), 'sp_venue' ); + $team_label_mode = $this->sanitize_label_mode( get_query_var( 'team_label' ), 'abbreviation' ); + $field_label_mode = $this->sanitize_label_mode( get_query_var( 'field_label' ), $this->get_venue_label_mode() ); + $title_format = $this->sanitize_title_format( get_query_var( 'title_format' ) ); $paper = $this->normalize_paper_size( (string) get_query_var( 'paper' ) ); $autoprint = '1' === (string) get_query_var( 'autoprint' ); + $month_pages = '1' === (string) get_query_var( 'month_pages' ); + $black_white = '1' === (string) get_query_var( 'black_white' ); - $team_name = get_the_title( $team_id ); - $team_logo = get_the_post_thumbnail( $team_id, array( 72, 72 ), array( 'class' => 'team-logo-img' ) ); + $is_multi_team = count( $team_ids ) > 1; + $primary_team_id = isset( $team_ids[0] ) ? (int) $team_ids[0] : 0; + $team_name = $this->get_printable_title( $team_ids, $team_label_mode ); + $team_logo = $is_multi_team ? '' : get_the_post_thumbnail( $primary_team_id, array( 72, 72 ), array( 'class' => 'team-logo-img' ) ); $brand_logo = $this->get_header_brand_logo(); $site_url = home_url( '/' ); $qr_url = 'https://api.qrserver.com/v1/create-qr-code/?size=144x144&data=' . rawurlencode( $site_url ); $season_name = ''; - $entries = $this->get_schedule_entries( $team_id, $season_id, $league_id ); - $team_palette = $this->get_team_color_palette( $team_id ); - $team_primary_for_fields = $this->get_strict_team_primary_color( $team_id ); + $field_name = count( $field_ids ) === 1 ? $this->get_term_label( (int) $field_ids[0], 'sp_venue', $field_label_mode ) : ''; + $entries = $this->get_schedule_entries( $team_ids, $season_id, $league_id, $field_ids, $team_label_mode, $field_label_mode, $title_format ); + $team_palette = $is_multi_team ? $this->get_default_printable_palette() : $this->get_team_color_palette( $primary_team_id ); + $team_primary_for_fields = $is_multi_team ? '' : $this->get_strict_team_primary_color( $primary_team_id ); $entries_by_day = array(); $venue_colors = array(); $month_keys = array(); $layout = array(); + $suppress_event_venue = 1 === count( $field_ids ); if ( $season_id > 0 ) { $season = get_term( $season_id, 'sp_season' ); @@ -661,14 +782,14 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { if ( '' !== $entry['venue_name'] && is_string( $entry['venue_key'] ) && ! isset( $venue_colors[ $entry['venue_key'] ] ) ) { $venue_colors[ $entry['venue_key'] ] = array( - 'name' => (string) $entry['venue_name'], - 'color' => $this->get_venue_color( (string) $entry['venue_name'], $season_id, (int) $entry['venue_id'], $team_primary_for_fields ), + 'name' => ! empty( $entry['venue_label'] ) ? (string) $entry['venue_label'] : (string) $entry['venue_name'], + 'color' => $this->get_venue_color( (string) $entry['venue_name'], $season_id, (int) $entry['venue_id'], $team_primary_for_fields, $is_multi_team ), ); } } $month_keys = $this->get_month_keys( $entries ); - $layout = $this->get_sheet_layout( count( $month_keys ), $paper ); + $layout = $this->get_sheet_layout( $month_pages ? 1 : count( $month_keys ), $paper ); status_header( 200 ); nocache_headers(); @@ -688,7 +809,14 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo ''; } echo ''; - echo ''; + $body_classes = array( 'print-preview', $paper ); + if ( $month_pages ) { + $body_classes[] = 'month-pages'; + } + if ( $black_white ) { + $body_classes[] = 'black-white'; + } + echo ''; $root_vars = array( '--sheet-scale:' . $layout['sheet_scale'], @@ -710,7 +838,55 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { ); echo '
'; - echo '
'; + echo ''; + exit; + } + + /** + * Render the printable page header. + * + * @param string $team_name Header title. + * @param array $meta_parts Header metadata strings. + * @param string $team_logo Team logo markup. + * @param string $brand_logo Brand logo markup. + * @return void + */ + private function render_printable_header( $team_name, $meta_parts, $team_logo, $brand_logo ) { echo '
'; echo '
'; if ( '' !== $team_logo ) { @@ -718,7 +894,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { } echo '
'; echo '

' . esc_html( $team_name ) . '

'; - echo '

' . esc_html( $season_name ? $season_name : __( 'Current', 'tonys-sportspress-enhancements' ) ) . '

'; + echo '

' . esc_html( implode( ' | ', array_filter( array_map( 'strval', $meta_parts ) ) ) ) . '

'; echo '
'; echo '
'; @@ -726,19 +902,17 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo ''; } echo '
'; + } - if ( empty( $entries ) ) { - echo '
'; - echo '

' . esc_html__( 'No SportsPress events were found for this team and season.', 'tonys-sportspress-enhancements' ) . '

'; - echo '
'; - } else { - echo '
'; - foreach ( $month_keys as $month_key ) { - $this->render_month_grid( $month_key, $entries_by_day, $venue_colors, $team_palette ); - } - echo '
'; - } - + /** + * Render the printable page footer. + * + * @param array $venue_colors Venue legend data. + * @param string $site_url Site URL. + * @param string $qr_url QR code image URL. + * @return void + */ + private function render_printable_footer( $venue_colors, $site_url, $qr_url ) { echo ''; - echo ''; - echo ''; - echo ''; - exit; } /** @@ -776,8 +946,9 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @param string $paper Paper size. * @return string */ - private function build_url( $team_id, $season_id, $paper, $league_id = 0 ) { + private function build_url( $team_id, $season_id, $paper, $league_id = 0, $field_id = 0, $month_pages = false, $team_label_mode = 'name', $field_label_mode = '', $title_format = 'selected_first', $black_white = false ) { $paper = $this->normalize_paper_size( $paper ); + $field_label_mode = '' === $field_label_mode ? $this->get_venue_label_mode() : $field_label_mode; return add_query_arg( array( @@ -785,12 +956,132 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { 'sp_team' => (string) absint( $team_id ), 'sp_season' => $season_id > 0 ? (string) absint( $season_id ) : '', 'sp_league' => $league_id > 0 ? (string) absint( $league_id ) : '', + 'sp_field' => $field_id > 0 ? (string) absint( $field_id ) : '', + 'team_label' => $this->sanitize_label_mode( $team_label_mode, 'name' ), + 'field_label' => $this->sanitize_label_mode( $field_label_mode, $this->get_venue_label_mode() ), + 'title_format' => $this->sanitize_title_format( $title_format ), 'paper' => (string) $paper, + 'month_pages' => $month_pages ? '1' : '', + 'black_white' => $black_white ? '1' : '', ), home_url( '/' ) ); } + /** + * Parse and validate one or more team IDs. + * + * @param mixed $value Raw query value. + * @return int[] + */ + private function parse_team_ids( $value ) { + $ids = $this->parse_id_list( $value ); + $valid = array(); + + foreach ( $ids as $id ) { + if ( 'sp_team' === get_post_type( $id ) ) { + $valid[] = $id; + } + } + + return array_values( array_unique( $valid ) ); + } + + /** + * Parse and validate one or more term IDs for a taxonomy. + * + * @param mixed $value Raw query value. + * @param string $taxonomy Taxonomy name. + * @return int[] + */ + private function parse_term_ids( $value, $taxonomy ) { + $ids = $this->parse_id_list( $value ); + $valid = array(); + + foreach ( $ids as $id ) { + $term = get_term( $id, $taxonomy ); + if ( $term && ! is_wp_error( $term ) ) { + $valid[] = $id; + } + } + + return array_values( array_unique( $valid ) ); + } + + /** + * Parse scalar, array, or comma-delimited ID values. + * + * @param mixed $value Raw value. + * @return int[] + */ + private function parse_id_list( $value ) { + $values = is_array( $value ) ? $value : explode( ',', (string) $value ); + $ids = array(); + + foreach ( $values as $raw_value ) { + $id = absint( trim( (string) $raw_value ) ); + if ( $id > 0 ) { + $ids[] = $id; + } + } + + return array_values( array_unique( $ids ) ); + } + + /** + * Build the printable title for selected teams. + * + * @param int[] $team_ids Team IDs. + * @return string + */ + private function get_printable_title( $team_ids, $mode = 'name' ) { + $names = array(); + + foreach ( $team_ids as $team_id ) { + $title = $this->get_team_label( $team_id, $mode ); + if ( is_string( $title ) && '' !== trim( $title ) ) { + $names[] = trim( $title ); + } + } + + if ( empty( $names ) ) { + return __( 'Printable Schedule', 'tonys-sportspress-enhancements' ); + } + + return implode( ', ', $names ); + } + + /** + * Get a term display name. + * + * @param int $term_id Term ID. + * @param string $taxonomy Taxonomy name. + * @return string + */ + private function get_term_name( $term_id, $taxonomy ) { + $term = get_term( $term_id, $taxonomy ); + + return $term && ! is_wp_error( $term ) && isset( $term->name ) ? (string) $term->name : ''; + } + + /** + * Get a term display label. + * + * @param int $term_id Term ID. + * @param string $taxonomy Taxonomy name. + * @param string $mode Label mode. + * @return string + */ + private function get_term_label( $term_id, $taxonomy, $mode = 'name' ) { + $term = get_term( $term_id, $taxonomy ); + + if ( ! $term || is_wp_error( $term ) || ! isset( $term->name ) ) { + return ''; + } + + return $this->get_configured_venue_label( $term, $mode ); + } + /** * Default option payload. * @@ -802,7 +1093,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { 'sync_interval_minutes' => 60, 'venue_color_overrides' => array(), 'venue_use_team_primary'=> array(), - 'printable_venue_label_mode' => 'full_name', + 'printable_venue_label_mode' => 'name', ); } @@ -836,18 +1127,44 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { } /** - * Get supported venue label modes. + * Get supported label modes. * * @return array */ - private function get_venue_label_mode_options() { + private function get_label_mode_options() { return array( - 'full_name' => __( 'Full Name', 'tonys-sportspress-enhancements' ), + 'name' => __( 'Name', 'tonys-sportspress-enhancements' ), + 'shortname' => __( 'Short Name', 'tonys-sportspress-enhancements' ), 'abbreviation' => __( 'Abbreviation', 'tonys-sportspress-enhancements' ), - 'short_name' => __( 'Short Name', 'tonys-sportspress-enhancements' ), ); } + /** + * Sanitize label mode. + * + * @param mixed $value Raw value. + * @param string $fallback Fallback mode. + * @return string + */ + private function sanitize_label_mode( $value, $fallback = 'name' ) { + $mode = is_string( $value ) ? sanitize_key( $value ) : ''; + $aliases = array( + 'full_name' => 'name', + 'fullname' => 'name', + 'short_name' => 'shortname', + 'short' => 'shortname', + 'abbr' => 'abbreviation', + ); + if ( isset( $aliases[ $mode ] ) ) { + $mode = $aliases[ $mode ]; + } + + $allowed = array_keys( $this->get_label_mode_options() ); + $fallback = isset( $aliases[ $fallback ] ) ? $aliases[ $fallback ] : $fallback; + + return in_array( $mode, $allowed, true ) ? $mode : ( in_array( $fallback, $allowed, true ) ? $fallback : 'name' ); + } + /** * Sanitize venue label mode. * @@ -855,10 +1172,32 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @return string */ private function sanitize_venue_label_mode( $value ) { - $mode = is_string( $value ) ? sanitize_key( $value ) : ''; - $allowed = array_keys( $this->get_venue_label_mode_options() ); + return $this->sanitize_label_mode( $value, 'name' ); + } - return in_array( $mode, $allowed, true ) ? $mode : 'full_name'; + /** + * Get supported event title formats. + * + * @return array + */ + private function get_title_format_options() { + return array( + 'selected_first' => __( 'Selected Teams First', 'tonys-sportspress-enhancements' ), + 'matchup' => __( 'Matchup', 'tonys-sportspress-enhancements' ), + ); + } + + /** + * Sanitize event title format. + * + * @param mixed $value Raw value. + * @return string + */ + private function sanitize_title_format( $value ) { + $format = is_string( $value ) ? sanitize_key( $value ) : ''; + $options = array_keys( $this->get_title_format_options() ); + + return in_array( $format, $options, true ) ? $format : 'selected_first'; } /** @@ -943,6 +1282,36 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { return $sanitized; } + /** + * Merge submitted season-scoped settings without preserving unchecked boxes. + * + * @param array $existing Existing settings. + * @param array $submitted Sanitized submitted settings. + * @param int $active_season_id Active season ID. + * @param bool $preserve_missing Whether to preserve season if missing. + * @return array + */ + private function merge_sanitized_season_settings( $existing, $submitted, $active_season_id, $preserve_missing ) { + $merged = is_array( $existing ) ? $existing : array(); + + if ( $active_season_id <= 0 ) { + return ! empty( $submitted ) ? $submitted : $merged; + } + + $season_key = (string) $active_season_id; + if ( isset( $submitted[ $season_key ] ) && is_array( $submitted[ $season_key ] ) ) { + $merged[ $season_key ] = $submitted[ $season_key ]; + } elseif ( $preserve_missing ) { + if ( isset( $existing[ $season_key ] ) && is_array( $existing[ $season_key ] ) ) { + $merged[ $season_key ] = $existing[ $season_key ]; + } + } else { + unset( $merged[ $season_key ] ); + } + + return $merged; + } + /** * Get seasons list. * @@ -1085,12 +1454,25 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { /** * Collect event entries for the calendar. * - * @param int $team_id Team ID. - * @param int $season_id Season ID. - * @param int $league_id League ID. + * @param int|int[] $team_ids Team IDs. + * @param int $season_id Season ID. + * @param int $league_id League ID. + * @param int[] $field_ids Field IDs. * @return array */ - private function get_schedule_entries( $team_id, $season_id, $league_id = 0 ) { + private function get_schedule_entries( $team_ids, $season_id, $league_id = 0, $field_ids = array(), $team_label_mode = 'abbreviation', $field_label_mode = '', $title_format = 'selected_first' ) { + $team_ids = is_array( $team_ids ) ? array_values( array_filter( array_map( 'absint', $team_ids ) ) ) : array( absint( $team_ids ) ); + $primary_team = isset( $team_ids[0] ) ? (int) $team_ids[0] : 0; + $is_multi_team = count( $team_ids ) > 1; + $field_ids = is_array( $field_ids ) ? array_values( array_filter( array_map( 'absint', $field_ids ) ) ) : array(); + $team_label_mode = $this->sanitize_label_mode( $team_label_mode, 'abbreviation' ); + $field_label_mode = '' === $field_label_mode ? $this->get_venue_label_mode() : $this->sanitize_label_mode( $field_label_mode, $this->get_venue_label_mode() ); + $title_format = $this->sanitize_title_format( $title_format ); + + if ( empty( $team_ids ) ) { + return array(); + } + $args = array( 'post_type' => 'sp_event', 'post_status' => array( 'publish', 'future' ), @@ -1101,7 +1483,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { 'meta_query' => array( array( 'key' => 'sp_team', - 'value' => array( (string) $team_id ), + 'value' => array_map( 'strval', $team_ids ), 'compare' => 'IN', ), ), @@ -1125,6 +1507,14 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { ); } + if ( ! empty( $field_ids ) ) { + $tax_query[] = array( + 'taxonomy' => 'sp_venue', + 'field' => 'term_id', + 'terms' => $field_ids, + ); + } + if ( ! empty( $tax_query ) ) { if ( count( $tax_query ) > 1 ) { $tax_query['relation'] = 'AND'; @@ -1143,7 +1533,8 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { } $teams = array_values( array_unique( array_map( 'intval', get_post_meta( $event_id, 'sp_team', false ) ) ) ); - if ( ! in_array( $team_id, $teams, true ) ) { + $matching_team_ids = array_values( array_intersect( $team_ids, $teams ) ); + if ( empty( $matching_team_ids ) ) { continue; } @@ -1152,13 +1543,19 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { continue; } + $home_id = isset( $teams[0] ) ? (int) $teams[0] : 0; + $away_id = isset( $teams[1] ) ? (int) $teams[1] : 0; + $context_id = $is_multi_team ? 0 : $primary_team; $opponent_id = 0; - foreach ( $teams as $team_option_id ) { - if ( $team_option_id !== $team_id ) { - $opponent_id = $team_option_id; - break; + if ( ! $is_multi_team ) { + foreach ( $teams as $team_option_id ) { + if ( $team_option_id !== $context_id ) { + $opponent_id = $team_option_id; + break; + } } } + $title_data = $this->get_event_title_data( $home_id, $away_id, $team_ids, $team_label_mode, $title_format ); $venue = $this->get_event_venue_term( $event_id ); $venue_name = ''; @@ -1167,22 +1564,30 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { if ( $venue ) { $venue_name = (string) $venue->name; $venue_id = (int) $venue->term_id; - $venue_label = $this->get_configured_venue_label( $venue ); + $venue_label = $this->get_configured_venue_label( $venue, $field_label_mode ); } $entries[] = array( - 'event_id' => $event_id, - 'day_key' => wp_date( 'Y-m-d', $timestamp ), - 'month_key' => wp_date( 'Y-m', $timestamp ), - 'timestamp' => $timestamp, - 'is_home' => isset( $teams[0] ) && $teams[0] === $team_id, - 'opponent_id' => $opponent_id, - 'opponent_name' => $opponent_id > 0 ? $this->get_team_label( $opponent_id ) : __( 'TBD', 'tonys-sportspress-enhancements' ), - 'event_time' => function_exists( 'sp_get_time' ) ? sp_get_time( $event_id ) : get_post_time( get_option( 'time_format' ), false, $event_id, true ), - 'venue_name' => $venue_name, - 'venue_label' => $venue_label, - 'venue_id' => $venue_id, - 'venue_key' => $venue_id > 0 ? 'v:' . $venue_id : 'n:' . strtolower( $venue_name ), + 'event_id' => $event_id, + 'day_key' => wp_date( 'Y-m-d', $timestamp ), + 'month_key' => wp_date( 'Y-m', $timestamp ), + 'timestamp' => $timestamp, + 'is_home' => ! $is_multi_team && $home_id === $context_id, + 'is_matchup' => 'matchup' === $title_format, + 'opponent_id' => $opponent_id, + 'opponent_name' => $opponent_id > 0 ? $this->get_team_label( $opponent_id, $team_label_mode ) : __( 'TBD', 'tonys-sportspress-enhancements' ), + 'title_team_name' => $title_data['team_name'], + 'title_separator' => $title_data['separator'], + 'title_opponent_name' => $title_data['opponent_name'], + 'home_team_id' => $home_id, + 'away_team_id' => $away_id, + 'home_team_name' => $home_id > 0 ? $this->get_team_label( $home_id, $team_label_mode ) : __( 'TBD', 'tonys-sportspress-enhancements' ), + 'away_team_name' => $away_id > 0 ? $this->get_team_label( $away_id, $team_label_mode ) : __( 'TBD', 'tonys-sportspress-enhancements' ), + 'event_time' => function_exists( 'sp_get_time' ) ? sp_get_time( $event_id ) : get_post_time( get_option( 'time_format' ), false, $event_id, true ), + 'venue_name' => $venue_name, + 'venue_label' => $venue_label, + 'venue_id' => $venue_id, + 'venue_key' => $venue_id > 0 ? 'v:' . $venue_id : 'n:' . strtolower( $venue_name ), ); } @@ -1223,6 +1628,91 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { return $months; } + /** + * Get event title parts for the selected title format. + * + * @param int $home_id Home team ID. + * @param int $away_id Away team ID. + * @param int[] $selected_ids Selected team IDs. + * @param string $team_label_mode Team label mode. + * @param string $title_format Title format. + * @return array + */ + private function get_event_title_data( $home_id, $away_id, $selected_ids, $team_label_mode, $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 = $this->sanitize_title_format( $title_format ); + + if ( 'matchup' === $title_format ) { + return array( + 'team_name' => $away_id > 0 ? $this->get_team_label( $away_id, $team_label_mode ) : __( 'TBD', 'tonys-sportspress-enhancements' ), + 'separator' => 'at', + 'opponent_name' => $home_id > 0 ? $this->get_team_label( $home_id, $team_label_mode ) : __( 'TBD', 'tonys-sportspress-enhancements' ), + ); + } + + $context = $this->get_selected_team_context( $home_id, $away_id, $selected_ids ); + if ( empty( $context['team_id'] ) ) { + return array( + 'team_name' => '', + 'separator' => '', + 'opponent_name' => '', + ); + } + + $opponent_id = isset( $context['opponent_id'] ) ? (int) $context['opponent_id'] : 0; + if ( 1 === count( $selected_ids ) ) { + return array( + 'team_name' => '', + 'separator' => '', + 'opponent_name' => $opponent_id > 0 ? $this->get_team_label( $opponent_id, $team_label_mode ) : __( 'TBD', 'tonys-sportspress-enhancements' ), + ); + } + + return array( + 'team_name' => $this->get_team_label( (int) $context['team_id'], $team_label_mode ), + 'separator' => isset( $context['separator'] ) ? (string) $context['separator'] : '', + 'opponent_name' => $opponent_id > 0 ? $this->get_team_label( $opponent_id, $team_label_mode ) : __( 'TBD', 'tonys-sportspress-enhancements' ), + ); + } + + /** + * Get selected-team perspective for an event. + * + * @param int $home_id Home team ID. + * @param int $away_id Away team ID. + * @param int[] $selected_ids Selected team IDs. + * @return array + */ + private function get_selected_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', + ); + } + + if ( $home_id > 0 && in_array( $home_id, $selected_ids, true ) ) { + return array( + 'team_id' => $home_id, + 'opponent_id' => $away_id, + 'separator' => 'vs', + ); + } + + return array( + 'team_id' => 0, + 'opponent_id' => 0, + 'separator' => '', + ); + } + /** * Render a single month grid. * @@ -1230,8 +1720,10 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @param array $entries_by_day Entries keyed by day. * @param array $venue_colors Venue colors. * @param array $team_palette Team palette. + * @param bool $is_multi_team Whether this is a combined team schedule. + * @param bool $suppress_venue Whether to hide per-event venue labels. */ - private function render_month_grid( $month_key, $entries_by_day, $venue_colors, $team_palette ) { + private function render_month_grid( $month_key, $entries_by_day, $venue_colors, $team_palette, $is_multi_team = false, $suppress_venue = false ) { $month = DateTimeImmutable::createFromFormat( 'Y-m-d', $month_key . '-01' ); if ( ! $month instanceof DateTimeImmutable ) { return; @@ -1257,15 +1749,26 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { for ( $day = 1; $day <= $days_in_month; $day++ ) { $day_key = sprintf( '%s-%02d', $month_key, $day ); $day_entries = isset( $entries_by_day[ $day_key ] ) ? $entries_by_day[ $day_key ] : array(); - $day_class = ! empty( $day_entries ) ? 'day has-events' : 'day no-events'; + $has_split_title = false; + foreach ( $day_entries as $day_entry ) { + if ( ! empty( $day_entry['is_matchup'] ) || ! empty( $day_entry['title_team_name'] ) ) { + $has_split_title = true; + break; + } + } + $day_class = ! empty( $day_entries ) ? 'day has-events' : 'day no-events'; + if ( $has_split_title ) { + $day_class .= ' has-matchups'; + } $day_style = ''; if ( ! empty( $day_entries ) ) { $first_entry = $day_entries[0]; $first_is_home = ! empty( $first_entry['is_home'] ); + $first_split_title = ! empty( $first_entry['is_matchup'] ) || ! empty( $first_entry['title_team_name'] ); $first_venue_key = isset( $first_entry['venue_key'] ) && is_string( $first_entry['venue_key'] ) ? $first_entry['venue_key'] : ''; $first_venue_color = ( '' !== $first_venue_key && isset( $venue_colors[ $first_venue_key ]['color'] ) ) ? (string) $venue_colors[ $first_venue_key ]['color'] : ''; - $first_background = '' !== $first_venue_color ? $first_venue_color : ( $first_is_home ? $team_palette['primary'] : $team_palette['secondary'] ); + $first_background = '' !== $first_venue_color ? $first_venue_color : ( $first_is_home || $first_split_title ? $team_palette['primary'] : $team_palette['secondary'] ); $first_foreground = $this->get_readable_text_color( $first_background ); $first_background = $this->ensure_minimum_contrast( $first_background, $first_foreground, self::MIN_WHITE_CONTRAST ); $first_foreground = $this->get_readable_text_color( $first_background ); @@ -1280,13 +1783,15 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo '
'; foreach ( $day_entries as $entry ) { $is_home = ! empty( $entry['is_home'] ); - $event_class = $is_home ? 'h' : 'a'; + $is_matchup = ! empty( $entry['is_matchup'] ); + $uses_split_title = $is_matchup || ! empty( $entry['title_team_name'] ); + $event_class = $uses_split_title ? 'matchup' : ( $is_home ? 'h' : 'a' ); $venue_key = isset( $entry['venue_key'] ) && is_string( $entry['venue_key'] ) ? $entry['venue_key'] : ''; $venue_color = ( '' !== $venue_key && isset( $venue_colors[ $venue_key ]['color'] ) ) ? (string) $venue_colors[ $venue_key ]['color'] : ''; $opponent_id = isset( $entry['opponent_id'] ) ? (int) $entry['opponent_id'] : 0; - $logo = $opponent_id > 0 ? get_the_post_thumbnail( $opponent_id, 'medium', array( 'class' => 'event-logo-img', 'loading' => 'eager', 'decoding' => 'async' ) ) : ''; + $logo = ( ! $uses_split_title && $opponent_id > 0 ) ? get_the_post_thumbnail( $opponent_id, 'medium', array( 'class' => 'event-logo-img', 'loading' => 'eager', 'decoding' => 'async' ) ) : ''; $has_logo = '' !== $logo; - $event_background = '' !== $venue_color ? $venue_color : ( $is_home ? $team_palette['primary'] : $team_palette['secondary'] ); + $event_background = '' !== $venue_color ? $venue_color : ( $is_home || $uses_split_title ? $team_palette['primary'] : $team_palette['secondary'] ); $event_foreground = $this->get_readable_text_color( $event_background ); $event_background = $this->ensure_minimum_contrast( $event_background, $event_foreground, self::MIN_WHITE_CONTRAST ); $event_foreground = $this->get_readable_text_color( $event_background ); @@ -1297,13 +1802,22 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { if ( $has_logo ) { echo wp_kses_post( $logo ); } else { - echo '' . esc_html( isset( $entry['opponent_name'] ) ? (string) $entry['opponent_name'] : '' ) . ''; + if ( $uses_split_title ) { + $team_name = isset( $entry['title_team_name'] ) ? (string) $entry['title_team_name'] : __( 'TBD', 'tonys-sportspress-enhancements' ); + $separator = isset( $entry['title_separator'] ) ? (string) $entry['title_separator'] : 'at'; + $opponent_name = isset( $entry['title_opponent_name'] ) ? (string) $entry['title_opponent_name'] : __( 'TBD', 'tonys-sportspress-enhancements' ); + echo '' . esc_html( $team_name ) . '' . esc_html( $separator ) . '' . esc_html( $opponent_name ) . ''; + } else { + echo '' . esc_html( isset( $entry['title_opponent_name'] ) ? (string) $entry['title_opponent_name'] : '' ) . ''; + } } echo '
'; - echo '' . esc_html( $is_home ? 'H' : 'A' ) . ''; + if ( ! $uses_split_title ) { + echo '' . esc_html( $is_home ? 'H' : 'A' ) . ''; + } echo '
'; echo '
' . esc_html( isset( $entry['event_time'] ) ? (string) $entry['event_time'] : '' ) . '
'; - if ( ! empty( $entry['venue_label'] ) ) { + if ( ! $suppress_venue && ! empty( $entry['venue_label'] ) ) { echo '
' . esc_html( (string) $entry['venue_label'] ) . '
'; } echo '
'; @@ -1333,9 +1847,32 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @param int $team_id Team ID. * @return string */ - private function get_team_label( $team_id ) { - if ( function_exists( 'sp_team_abbreviation' ) ) { - $label = (string) sp_team_abbreviation( $team_id ); + private function get_team_label( $team_id, $mode = 'abbreviation' ) { + $mode = $this->sanitize_label_mode( $mode, 'abbreviation' ); + + if ( 'shortname' === $mode ) { + if ( function_exists( 'sp_team_short_name' ) ) { + $label = trim( (string) sp_team_short_name( $team_id ) ); + if ( '' !== $label ) { + return $label; + } + } + + $label = trim( (string) get_post_meta( $team_id, 'sp_short_name', true ) ); + if ( '' !== $label ) { + return $label; + } + } + + if ( 'abbreviation' === $mode ) { + if ( function_exists( 'sp_team_abbreviation' ) ) { + $label = trim( (string) sp_team_abbreviation( $team_id ) ); + if ( '' !== $label ) { + return $label; + } + } + + $label = trim( (string) get_post_meta( $team_id, 'sp_abbreviation', true ) ); if ( '' !== $label ) { return $label; } @@ -1369,21 +1906,21 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @param WP_Term $venue Venue term. * @return string */ - private function get_configured_venue_label( $venue ) { + private function get_configured_venue_label( $venue, $mode = '' ) { $full_name = isset( $venue->name ) ? trim( (string) $venue->name ) : ''; if ( '' === $full_name || ! isset( $venue->term_id ) ) { return ''; } $term_id = (int) $venue->term_id; - $mode = $this->get_venue_label_mode(); + $mode = '' === $mode ? $this->get_venue_label_mode() : $this->sanitize_label_mode( $mode, $this->get_venue_label_mode() ); if ( 'abbreviation' === $mode ) { $abbreviation = trim( (string) get_term_meta( $term_id, 'tse_abbreviation', true ) ); return '' !== $abbreviation ? $abbreviation : $full_name; } - if ( 'short_name' === $mode ) { + if ( 'shortname' === $mode ) { $short_name = trim( (string) get_term_meta( $term_id, 'tse_short_name', true ) ); return '' !== $short_name ? $short_name : $full_name; } @@ -1397,15 +1934,27 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @param string $venue_name Venue name. * @param int $season_id Season ID. * @param int $venue_id Venue ID. - * @param string $team_primary Team primary color. + * @param string $team_primary Team primary color. + * @param bool $ignore_team_primary Whether to ignore venue primary flags. * @return string */ - private function get_venue_color( $venue_name, $season_id, $venue_id, $team_primary ) { - if ( $this->should_use_team_primary_for_venue( $season_id, $venue_id ) && '' !== $this->sanitize_color( $team_primary ) ) { + private function get_venue_color( $venue_name, $season_id, $venue_id, $team_primary, $ignore_team_primary = false ) { + $custom = $this->get_custom_venue_color( $season_id, $venue_id ); + if ( $ignore_team_primary && '' !== $custom ) { + return $custom; + } + + if ( $ignore_team_primary ) { + $suggested = $this->get_suggested_venue_color( $season_id, $venue_id ); + if ( '' !== $suggested ) { + return $suggested; + } + } + + if ( ! $ignore_team_primary && $this->should_use_team_primary_for_venue( $season_id, $venue_id ) && '' !== $this->sanitize_color( $team_primary ) ) { return $this->sanitize_color( $team_primary ); } - $custom = $this->get_custom_venue_color( $season_id, $venue_id ); if ( '' !== $custom ) { return $custom; } @@ -1416,6 +1965,36 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { return $palette[ $index ]; } + /** + * Get the settings-screen suggested venue color for a season. + * + * @param int $season_id Season ID. + * @param int $venue_id Venue ID. + * @return string + */ + private function get_suggested_venue_color( $season_id, $venue_id ) { + if ( $season_id <= 0 || $venue_id <= 0 ) { + return ''; + } + + $venues = $this->get_venues_for_season( $season_id ); + if ( empty( $venues ) ) { + return ''; + } + + $palette_count = count( $this->suggested_palette ); + foreach ( $venues as $index => $venue ) { + if ( ! is_array( $venue ) || ! isset( $venue['id'] ) || (int) $venue['id'] !== (int) $venue_id ) { + continue; + } + + $suggested = isset( $this->suggested_palette[ $index % max( 1, $palette_count ) ] ) ? $this->suggested_palette[ $index % max( 1, $palette_count ) ] : ''; + return '' !== $suggested ? $this->adjust_for_white_text( $suggested, self::MIN_WHITE_CONTRAST ) : ''; + } + + return ''; + } + /** * Resolve primary team color without site fallbacks. * @@ -1615,6 +2194,21 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { ); } + /** + * Get neutral printable colors for combined-team schedules. + * + * @return array + */ + private function get_default_printable_palette() { + return array( + 'primary' => '#334155', + 'secondary' => '#64748B', + 'accent' => '#475569', + 'ink' => '#111827', + 'muted_ink' => '#334155', + ); + } + /** * Sanitize a six-digit hex color. * @@ -1841,7 +2435,9 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { $rows = (int) ceil( $count / $columns ); $font_scale = 1.0; - if ( $count >= 3 && $count <= 4 ) { + if ( $count == 1 ) { + $font_scale = 1.1; + } elseif ( $count >= 3 && $count <= 4 ) { $font_scale = 0.9; } elseif ( $count >= 5 && $count <= 6 ) { $font_scale = 0.78; diff --git a/includes/sp-schedule-exporter.php b/includes/sp-schedule-exporter.php index 5f6b160..66f6330 100644 --- a/includes/sp-schedule-exporter.php +++ b/includes/sp-schedule-exporter.php @@ -86,11 +86,13 @@ function tse_sp_schedule_exporter_render_shortcode() { $seasons = tse_sp_schedule_exporter_get_seasons(); $season_id = tse_sp_schedule_exporter_resolve_season_id( $seasons ); $teams = tse_sp_schedule_exporter_get_teams( $league_id, $season_id ); - $team_id = tse_sp_schedule_exporter_resolve_team_id( $teams ); - $fields = tse_sp_schedule_exporter_get_fields(); - $field_id = tse_sp_schedule_exporter_resolve_field_id( $fields ); - $export_type = tse_sp_schedule_exporter_resolve_export_type(); - $subformat = tse_sp_schedule_exporter_resolve_subformat(); + $team_ids = tse_sp_schedule_exporter_resolve_team_ids( $teams ); + $fields = tse_sp_schedule_exporter_get_fields(); + $field_id = tse_sp_schedule_exporter_resolve_field_id( $fields ); + $export_type = tse_sp_schedule_exporter_resolve_export_type(); + $subformat = tse_sp_schedule_exporter_resolve_subformat(); + $title_format = tse_sp_schedule_exporter_resolve_title_format(); + $black_white = tse_sp_schedule_exporter_resolve_black_white(); if ( empty( $teams ) ) { return '

' . esc_html__( 'No SportsPress teams match the selected league and season.', 'tonys-sportspress-enhancements' ) . '

'; @@ -115,6 +117,15 @@ function tse_sp_schedule_exporter_render_shortcode() {

+
+
+ +

+
+

-

+

@@ -152,14 +163,14 @@ function tse_sp_schedule_exporter_render_shortcode() {

- - +

@@ -174,15 +185,36 @@ function tse_sp_schedule_exporter_render_shortcode() {
+
+ +
+ +
+ +
+ $team_id, 'season_id' => $season_id, 'league_id' => $league_id, 'field_id' => $field_id, 'format' => $subformat ), 'csv' ); - $ics_url = tse_sp_event_export_get_feed_url( array( 'team_id' => $team_id, 'season_id' => $season_id, 'league_id' => $league_id, 'field_id' => $field_id ), 'ics' ); - $print_url = tse_sp_schedule_exporter_get_printable_url( $team_id, $season_id, 'letter', $league_id ); + $feed_args = array( + 'team_id' => $team_ids, + 'season_id' => $season_id, + 'league_id' => $league_id, + 'field_id' => $field_id, + 'title_format' => $title_format, + ); + $csv_url = tse_sp_event_export_get_feed_url( array_merge( $feed_args, array( 'format' => $subformat ) ), 'csv' ); + $ics_url = tse_sp_event_export_get_feed_url( $feed_args, 'ics' ); + $print_url = tse_sp_schedule_exporter_get_printable_url( $team_ids, $season_id, 'letter', $league_id, false, $field_id, false, 'name', 'name', $title_format, $black_white ); $current_url = tse_sp_schedule_exporter_get_output_url( $export_type, $csv_url, $ics_url, $print_url ); ?>
@@ -331,26 +363,34 @@ function tse_sp_schedule_exporter_get_teams( $league_id = 0, $season_id = 0 ) { } /** - * Resolve selected team ID. + * Resolve selected team IDs. * * @param WP_Post[] $teams Team posts. - * @return int + * @return int[] */ -function tse_sp_schedule_exporter_resolve_team_id( $teams ) { - $requested = isset( $_GET['team_id'] ) ? absint( wp_unslash( $_GET['team_id'] ) ) : 0; - if ( $requested > 0 && 'sp_team' === get_post_type( $requested ) ) { - foreach ( $teams as $team ) { - if ( $team instanceof WP_Post && (int) $team->ID === $requested ) { - return $requested; - } +function tse_sp_schedule_exporter_resolve_team_ids( $teams ) { + $available = array(); + foreach ( $teams as $team ) { + if ( $team instanceof WP_Post ) { + $available[] = (int) $team->ID; } } - if ( isset( $teams[0] ) && $teams[0] instanceof WP_Post ) { - return (int) $teams[0]->ID; + $requested = array(); + if ( isset( $_GET['team_id'] ) ) { + $requested = tse_sp_event_export_parse_id_list( wp_unslash( $_GET['team_id'] ) ); } - return 0; + $selected = array_values( array_intersect( $requested, $available ) ); + if ( ! empty( $selected ) ) { + return $selected; + } + + if ( isset( $available[0] ) ) { + return array( (int) $available[0] ); + } + + return array(); } /** @@ -543,6 +583,38 @@ function tse_sp_schedule_exporter_resolve_subformat() { return tse_sp_event_export_sanitize_format( $requested ); } +/** + * Resolve selected title format. + * + * @return string + */ +function tse_sp_schedule_exporter_resolve_title_format() { + $requested = isset( $_GET['title_format'] ) ? sanitize_key( wp_unslash( $_GET['title_format'] ) ) : 'selected_first'; + + return tse_sp_schedule_exporter_resolve_title_format_value( $requested ); +} + +/** + * Normalize a title format value. + * + * @param string $value Raw value. + * @return string + */ +function tse_sp_schedule_exporter_resolve_title_format_value( $value ) { + $value = sanitize_key( (string) $value ); + + return in_array( $value, tse_sp_event_export_get_title_formats(), true ) ? $value : 'selected_first'; +} + +/** + * Resolve whether printable URLs should use black-and-white styling. + * + * @return bool + */ +function tse_sp_schedule_exporter_resolve_black_white() { + return isset( $_GET['black_white'] ) && '1' === (string) wp_unslash( $_GET['black_white'] ); +} + /** * Get current output URL for the selected export type. * @@ -700,17 +772,29 @@ function tse_sp_schedule_exporter_get_primary_venue( $event_id ) { * @param int $season_id Season ID. * @param string $paper Paper size. * @param int $league_id League ID. + * @param string $team_label_mode Team label mode. + * @param string $field_label_mode Field label mode. + * @param string $title_format Title format. + * @param bool $black_white Whether to use black-and-white printable styling. * @return string */ -function tse_sp_schedule_exporter_get_printable_url( $team_id, $season_id, $paper, $league_id = 0, $autoprint = false ) { +function tse_sp_schedule_exporter_get_printable_url( $team_id, $season_id, $paper, $league_id = 0, $autoprint = false, $field_id = 0, $month_pages = false, $team_label_mode = 'name', $field_label_mode = 'name', $title_format = 'selected_first', $black_white = false ) { + $team_ids = is_array( $team_id ) ? array_values( array_filter( array_map( 'absint', $team_id ) ) ) : array( absint( $team_id ) ); + return add_query_arg( array( Tony_Sportspress_Printable_Calendars::QUERY_FLAG => '1', - 'sp_team' => (string) absint( $team_id ), + 'sp_team' => implode( ',', $team_ids ), 'sp_season' => $season_id > 0 ? (string) absint( $season_id ) : '', 'sp_league' => $league_id > 0 ? (string) absint( $league_id ) : '', + 'sp_field' => $field_id > 0 ? (string) absint( $field_id ) : '', + 'team_label' => sanitize_key( (string) $team_label_mode ), + 'field_label' => sanitize_key( (string) $field_label_mode ), + 'title_format' => tse_sp_schedule_exporter_resolve_title_format_value( $title_format ), 'paper' => $paper, 'autoprint' => $autoprint ? '1' : '', + 'month_pages' => $month_pages ? '1' : '', + 'black_white' => $black_white ? '1' : '', ), home_url( '/' ) ); @@ -758,17 +842,28 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) { var league = form.querySelector('[name="league_id"]'); var season = form.querySelector('[name="season_id"]'); - var team = form.querySelector('[name="team_id"]'); + var team = form.querySelector('[name="team_id[]"], [name="team_id"]'); var exportType = form.querySelector('[name="export_type"]'); var subformat = form.querySelector('[name="subformat"]'); + var titleFormat = form.querySelector('[name="title_format"]'); var field = form.querySelector('[name="field_id"]'); + var monthPages = form.querySelector('[name="month_pages"]'); + var blackWhite = form.querySelector('[name="black_white"]'); var outputUrl = scope.querySelector('.tse-output-url'); var openButton = scope.querySelector('.tse-open-link'); var iosButton = scope.querySelector('.tse-ics-ios-link'); var androidButton = scope.querySelector('.tse-ics-android-link'); var outputNote = scope.querySelector('.tse-output-note'); var copyButton = scope.querySelector('.tse-copy-link'); - var teamValue = team ? (team.value || '0') : '0'; + var teamValues = team ? Array.prototype.slice.call(team.selectedOptions || []).map(function(option){ + return option.value; + }).filter(function(value){ + return value && value !== '0'; + }) : []; + if (!teamValues.length && team && team.value && team.value !== '0') { + teamValues = [team.value]; + } + var teamValue = teamValues.length ? teamValues.join(',') : '0'; var activeSubformat = subformat ? (subformat.value || 'matchup') : 'matchup'; var selectedExportType = exportType ? (exportType.value || 'csv') : 'csv'; @@ -792,6 +887,7 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) { if (season) csvUrl.searchParams.set('season_id', season.value || '0'); if (team) csvUrl.searchParams.set('team_id', teamValue); if (field) csvUrl.searchParams.set('field_id', field.value || '0'); + if (titleFormat) csvUrl.searchParams.set('title_format', titleFormat.value || 'selected_first'); csvUrl.searchParams.set('format', activeSubformat); var columns = Array.prototype.slice.call(scope.querySelectorAll('[data-columns-format="' + activeSubformat + '"]:checked')).map(function(input){ return input.value; @@ -808,6 +904,7 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) { if (season) icsUrl.searchParams.set('season_id', season.value || '0'); if (team) icsUrl.searchParams.set('team_id', teamValue); if (field) icsUrl.searchParams.set('field_id', field.value || '0'); + if (titleFormat) icsUrl.searchParams.set('title_format', titleFormat.value || 'selected_first'); icsUrl.searchParams.delete('format'); icsUrl.searchParams.delete('columns'); } @@ -816,6 +913,18 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) { if (league) printUrl.searchParams.set('sp_league', league.value || '0'); if (season) printUrl.searchParams.set('sp_season', season.value || '0'); if (team) printUrl.searchParams.set('sp_team', teamValue); + if (field) printUrl.searchParams.set('sp_field', field.value || '0'); + if (titleFormat) printUrl.searchParams.set('title_format', titleFormat.value || 'selected_first'); + if (monthPages && monthPages.checked) { + printUrl.searchParams.set('month_pages', '1'); + } else { + printUrl.searchParams.delete('month_pages'); + } + if (blackWhite && blackWhite.checked) { + printUrl.searchParams.set('black_white', '1'); + } else { + printUrl.searchParams.delete('black_white'); + } printUrl.searchParams.set('paper', 'letter'); } @@ -833,11 +942,11 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) { resolvedUrl = printUrl.toString(); if (teamValue === '0') { disabled = true; - note = 'Printable requires a specific team. All teams is not supported.'; + note = 'Printable requires at least one selected team.'; } } else if (selectedExportType === 'csv' && activeSubformat === 'team' && teamValue === '0') { disabled = true; - note = 'CSV team layout requires a specific team. All teams is not supported.'; + note = 'CSV team layout requires at least one selected team.'; } if (outputUrl) { @@ -885,10 +994,10 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) { syncLinks(scope); - scope.querySelectorAll('.tse-schedule-exporter-form select').forEach(function(select){ - select.addEventListener('change', function(){ - if (select.dataset.autoSubmit === '1') { - select.form.submit(); + scope.querySelectorAll('.tse-schedule-exporter-form select, .tse-schedule-exporter-form input[type="checkbox"]').forEach(function(input){ + input.addEventListener('change', function(){ + if (input.dataset.autoSubmit === '1') { + input.form.submit(); return; } diff --git a/tests/test-sp-schedule-exporter.php b/tests/test-sp-schedule-exporter.php new file mode 100644 index 0000000..c6630d7 --- /dev/null +++ b/tests/test-sp-schedule-exporter.php @@ -0,0 +1,341 @@ +original_get = $_GET; + + foreach ( array( 'sp_venue', 'sp_league', 'sp_season' ) as $taxonomy ) { + if ( ! taxonomy_exists( $taxonomy ) ) { + register_taxonomy( $taxonomy, 'sp_event' ); + } + } + } + + /** + * Restore request globals. + */ + public function tear_down() { + $_GET = $this->original_get; + + parent::tear_down(); + } + + /** + * Create a SportsPress team post. + * + * @param string $name Team name. + * @return int + */ + private function create_team( $name ) { + return self::factory()->post->create( + array( + 'post_type' => 'sp_team', + 'post_status' => 'publish', + 'post_title' => $name, + ) + ); + } + + /** + * Create a SportsPress event with ordered teams. + * + * @param int $home_id Home team ID. + * @param int $away_id Away team ID. + * @param int[] $venue_ids Venue IDs. + * @return int + */ + private function create_event( $home_id, $away_id, $venue_ids = array() ) { + $event_id = self::factory()->post->create( + array( + 'post_type' => 'sp_event', + 'post_status' => 'publish', + 'post_title' => 'Game', + 'post_date' => '2026-05-20 18:00:00', + 'post_date_gmt' => '2026-05-20 23:00:00', + ) + ); + + add_post_meta( $event_id, 'sp_team', (string) $home_id ); + add_post_meta( $event_id, 'sp_team', (string) $away_id ); + + if ( ! empty( $venue_ids ) ) { + wp_set_object_terms( $event_id, $venue_ids, 'sp_venue' ); + } + + return $event_id; + } + + /** + * Multiple selected team IDs should be retained in request order. + */ + public function test_resolve_team_ids_accepts_multiple_request_values() { + $team_one = $this->create_team( 'Blue' ); + $team_two = $this->create_team( 'Red' ); + $teams = array( get_post( $team_one ), get_post( $team_two ) ); + + $_GET['team_id'] = array( (string) $team_one, (string) $team_two ); + + $this->assertSame( array( $team_one, $team_two ), tse_sp_schedule_exporter_resolve_team_ids( $teams ) ); + } + + /** + * Printable URLs should carry multiple team IDs and the selected field. + */ + public function test_printable_url_accepts_multiple_teams_and_field() { + $url = tse_sp_schedule_exporter_get_printable_url( array( 12, 34 ), 56, 'letter', 78, false, 90, true ); + $query = array(); + + wp_parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query ); + + $this->assertSame( '12,34', $query['sp_team'] ); + $this->assertSame( '90', $query['sp_field'] ); + $this->assertSame( 'name', $query['team_label'] ); + $this->assertSame( 'name', $query['field_label'] ); + $this->assertSame( 'selected_first', $query['title_format'] ); + $this->assertSame( '1', $query['month_pages'] ); + } + + /** + * Printable URLs should carry selected team and field label modes. + */ + public function test_printable_url_accepts_label_modes() { + $url = tse_sp_schedule_exporter_get_printable_url( 12, 56, 'letter', 78, false, 90, false, 'shortname', 'abbreviation' ); + $query = array(); + + wp_parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query ); + + $this->assertSame( 'shortname', $query['team_label'] ); + $this->assertSame( 'abbreviation', $query['field_label'] ); + } + + /** + * Printable URLs should carry black-and-white mode when requested. + */ + public function test_printable_url_accepts_black_white_mode() { + $url = tse_sp_schedule_exporter_get_printable_url( 12, 56, 'letter', 78, false, 90, false, 'name', 'name', 'selected_first', true ); + $query = array(); + + wp_parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query ); + + $this->assertSame( '1', $query['black_white'] ); + } + + /** + * Single-team printable entries should keep the existing opponent perspective. + */ + public function test_printable_single_team_entries_keep_opponent_perspective() { + $home_id = $this->create_team( 'Home Team' ); + $away_id = $this->create_team( 'Away Team' ); + $venue_id = self::factory()->term->create( array( 'taxonomy' => 'sp_venue', 'name' => 'North Field' ) ); + $this->create_event( $home_id, $away_id, array( $venue_id ) ); + + $printable = Tony_Sportspress_Printable_Calendars::instance(); + $method = new ReflectionMethod( $printable, 'get_schedule_entries' ); + $method->setAccessible( true ); + + $entries = $method->invoke( $printable, $home_id, 0, 0, array() ); + + $this->assertCount( 1, $entries ); + $this->assertFalse( $entries[0]['is_matchup'] ); + $this->assertTrue( $entries[0]['is_home'] ); + $this->assertSame( 'Away Team', $entries[0]['opponent_name'] ); + } + + /** + * Printable entries should honor requested team and field labels. + */ + public function test_printable_entries_honor_label_modes() { + $home_id = $this->create_team( 'Home Team' ); + $away_id = $this->create_team( 'Away Team' ); + $venue_id = self::factory()->term->create( array( 'taxonomy' => 'sp_venue', 'name' => 'North Field' ) ); + update_post_meta( $away_id, 'sp_abbreviation', 'AWY' ); + update_term_meta( $venue_id, 'tse_abbreviation', 'NF' ); + $this->create_event( $home_id, $away_id, array( $venue_id ) ); + + $printable = Tony_Sportspress_Printable_Calendars::instance(); + $method = new ReflectionMethod( $printable, 'get_schedule_entries' ); + $method->setAccessible( true ); + + $entries = $method->invoke( $printable, $home_id, 0, 0, array(), 'abbreviation', 'abbreviation' ); + + $this->assertCount( 1, $entries ); + $this->assertSame( 'AWY', $entries[0]['opponent_name'] ); + $this->assertSame( 'NF', $entries[0]['venue_label'] ); + } + + /** + * Team CSV layout should support multiple selected teams with away-first context. + */ + public function test_team_export_multiple_selected_teams_uses_away_team_first() { + $home_id = $this->create_team( 'Home Team' ); + $away_id = $this->create_team( 'Away Team' ); + $this->create_event( $home_id, $away_id ); + + $events = tse_sp_event_export_get_events( + array( + 'team_ids' => array( $home_id, $away_id ), + ) + ); + + $this->assertCount( 1, $events ); + $this->assertSame( 'Away Team', $events[0]['team_name'] ); + $this->assertSame( 'Away Team at Home Team', $events[0]['title'] ); + $this->assertSame( 'at', $events[0]['separator'] ); + $this->assertSame( 'Home Team', $events[0]['opponent_name'] ); + $this->assertSame( 'Away', $events[0]['location_flag'] ); + } + + /** + * Team CSV layout should use vs when the selected team is home. + */ + public function test_team_export_home_selected_team_uses_vs_separator() { + $home_id = $this->create_team( 'Home Team' ); + $away_id = $this->create_team( 'Away Team' ); + $this->create_event( $home_id, $away_id ); + + $events = tse_sp_event_export_get_events( + array( + 'team_ids' => array( $home_id ), + ) + ); + + $this->assertCount( 1, $events ); + $this->assertSame( 'Home Team', $events[0]['team_name'] ); + $this->assertSame( 'Away Team', $events[0]['title'] ); + $this->assertSame( 'vs', $events[0]['separator'] ); + $this->assertSame( 'Away Team', $events[0]['opponent_name'] ); + $this->assertSame( 'Home', $events[0]['location_flag'] ); + } + + /** + * Team CSV layout should use at when the selected team is away. + */ + public function test_team_export_away_selected_team_uses_at_separator() { + $home_id = $this->create_team( 'Home Team' ); + $away_id = $this->create_team( 'Away Team' ); + $this->create_event( $home_id, $away_id ); + + $events = tse_sp_event_export_get_events( + array( + 'team_ids' => array( $away_id ), + ) + ); + + $this->assertCount( 1, $events ); + $this->assertSame( 'Away Team', $events[0]['team_name'] ); + $this->assertSame( 'Home Team', $events[0]['title'] ); + $this->assertSame( 'at', $events[0]['separator'] ); + $this->assertSame( 'Home Team', $events[0]['opponent_name'] ); + $this->assertSame( 'Away', $events[0]['location_flag'] ); + } + + /** + * Matchup title format should ignore selected-team perspective. + */ + public function test_team_export_matchup_title_format_uses_away_at_home() { + $home_id = $this->create_team( 'Home Team' ); + $away_id = $this->create_team( 'Away Team' ); + $this->create_event( $home_id, $away_id ); + + $events = tse_sp_event_export_get_events( + array( + 'team_ids' => array( $home_id ), + 'title_format' => 'matchup', + ) + ); + + $this->assertCount( 1, $events ); + $this->assertSame( 'Away Team at Home Team', $events[0]['title'] ); + } + + /** + * Multi-team printable entries should use selected-team-first titles by default. + */ + public function test_printable_multi_team_entries_use_selected_first_titles() { + $home_id = $this->create_team( 'Home Team' ); + $away_id = $this->create_team( 'Away Team' ); + $venue_id = self::factory()->term->create( array( 'taxonomy' => 'sp_venue', 'name' => 'North Field' ) ); + $this->create_event( $home_id, $away_id, array( $venue_id ) ); + + $printable = Tony_Sportspress_Printable_Calendars::instance(); + $method = new ReflectionMethod( $printable, 'get_schedule_entries' ); + $method->setAccessible( true ); + + $entries = $method->invoke( $printable, array( $home_id, $away_id ), 0, 0, array() ); + + $this->assertCount( 1, $entries ); + $this->assertFalse( $entries[0]['is_matchup'] ); + $this->assertSame( 'Away Team', $entries[0]['title_team_name'] ); + $this->assertSame( 'at', $entries[0]['title_separator'] ); + $this->assertSame( 'Home Team', $entries[0]['title_opponent_name'] ); + } + + /** + * Exactly one selected field should suppress per-event venue labels. + */ + public function test_render_month_grid_can_suppress_event_venue_label() { + $printable = Tony_Sportspress_Printable_Calendars::instance(); + $method = new ReflectionMethod( $printable, 'render_month_grid' ); + $method->setAccessible( true ); + + $entries = array( + '2026-05-20' => array( + array( + 'day_key' => '2026-05-20', + 'month_key' => '2026-05', + 'timestamp' => strtotime( '2026-05-20 18:00:00' ), + 'is_matchup' => true, + 'away_team_name' => 'Away', + 'home_team_name' => 'Home', + 'event_time' => '6:00 PM', + 'venue_label' => 'North', + 'venue_name' => 'North Field', + 'venue_key' => 'v:1', + ), + ), + ); + + ob_start(); + $method->invoke( + $printable, + '2026-05', + $entries, + array( 'v:1' => array( 'name' => 'North Field', 'color' => '#1D4ED8' ) ), + array( + 'primary' => '#1D4ED8', + 'secondary' => '#DC2626', + 'accent' => '#1D4ED8', + 'ink' => '#111827', + 'muted_ink' => '#334155', + ), + true, + true + ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'Away', $output ); + $this->assertStringContainsString( 'Home', $output ); + $this->assertStringNotContainsString( 'class="event-venue"', $output ); + } +}