From b7fd2d7a06be0f20137e60937c8af53bbaa5ca5f Mon Sep 17 00:00:00 2001 From: alex-magana Date: Thu, 9 Apr 2026 22:03:53 +0300 Subject: [PATCH 01/16] Add sport data fetching --- src/app/lib/logger.const.js | 1 + .../utilities/pageRequests/getPageData.ts | 30 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/app/lib/logger.const.js b/src/app/lib/logger.const.js index 9df32070cc8..8aa6fd901fa 100644 --- a/src/app/lib/logger.const.js +++ b/src/app/lib/logger.const.js @@ -15,6 +15,7 @@ const logCodes = { DATA_REQUEST_RECEIVED: 'data_request_received', DATA_RESPONSE_FROM_CACHE: 'data_response_from_cache', BFF_FETCH_ERROR: 'bff_fetch_error', + SPORT_DATA_FETCH_ERROR: 'sport_data_fetch_error', IDCTA_FETCH_ERROR: 'idcta_fetch_error', // Files diff --git a/ws-nextjs-app/utilities/pageRequests/getPageData.ts b/ws-nextjs-app/utilities/pageRequests/getPageData.ts index 7959b4c69c6..76ea0b3b348 100644 --- a/ws-nextjs-app/utilities/pageRequests/getPageData.ts +++ b/ws-nextjs-app/utilities/pageRequests/getPageData.ts @@ -4,6 +4,7 @@ import sendCustomMetric from '#server/utilities/customMetrics'; import { NON_200_RESPONSE } from '#server/utilities/customMetrics/metrics.const'; import getAgent from '#server/utilities/getAgent'; import fetchDataFromBFF from '#app/routes/utils/fetchDataFromBFF'; +import fetchDataFromSportData from '#app/routes/utils/fetchDataFromSportData'; import nodeLogger from '#lib/logger.node'; import { PageTypes, Services, Variants } from '#app/models/types/global'; @@ -67,8 +68,35 @@ const getPageData = async ({ }); } + // Evaluate the presence of sportDataEvent + // Fetch Sport Data + const sportDataEvent = json?.data?.sportDataEvent || null; + + let sportEventDetails; + + if (sportDataEvent) { + const { id: sportEventId } = sportDataEvent; + const { status: sportDataStatus, json: sportDataJson } = + (await fetchDataFromSportData({ + type: 'event', + urn: sportEventId, + getAgent, + })) || null; + + sportEventDetails = { + status: sportDataStatus, + ...sportDataJson.data, + }; + } + const data = json - ? { pageData: json.data, status } + ? { + pageData: { + ...json.data, + sportEventDetails, + }, + status, + } : { error: message, status }; return { data }; From f1985cec19f5484169c2184f5801f6344a40310d Mon Sep 17 00:00:00 2001 From: alex-magana Date: Thu, 9 Apr 2026 22:06:08 +0300 Subject: [PATCH 02/16] Add sport data fetcher module --- .../utils/fetchDataFromSportData/index.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/app/routes/utils/fetchDataFromSportData/index.ts diff --git a/src/app/routes/utils/fetchDataFromSportData/index.ts b/src/app/routes/utils/fetchDataFromSportData/index.ts new file mode 100644 index 00000000000..5b0e7bb9970 --- /dev/null +++ b/src/app/routes/utils/fetchDataFromSportData/index.ts @@ -0,0 +1,58 @@ +import Url from 'url-parse'; +import fetchPageData from '#app/routes/utils/fetchPageData'; +import getErrorStatusCode from '#app/routes/utils/fetchPageData/utils/getErrorStatusCode'; +import { SPORT_DATA_FETCH_ERROR } from '#lib/logger.const'; +import { FetchError, GetAgent } from '#models/types/fetch'; +import nodeLogger from '#lib/logger.node'; + +const logger = nodeLogger(__filename); + +interface fetchDataFromSportData { + type: string; + urn: string; + getAgent?: GetAgent; +} + +export default async ({ type, urn, getAgent }: fetchDataFromSportData) => { + const queryParameters = { + type, + urn, + }; + + const fetchUrl = Url('https://fabl.api.bbci.co.uk/module/sport-data').set( + 'query', + queryParameters, + ); + + const useCerts = true; + + const agent = useCerts && getAgent ? await getAgent() : undefined; + const timeout = useCerts ? undefined : 60000; + + try { + const fetchPageDataArgs = { + path: fetchUrl.toString(), + ...(agent && { agent }), + ...(timeout && { timeout }), + }; + + // @ts-expect-error - Ignore fetchPageData argument types + const { status, json } = await fetchPageData(fetchPageDataArgs); + + return { + status, + json, + }; + } catch (error: unknown) { + const { message, status = getErrorStatusCode() } = error as FetchError; + + logger.error(SPORT_DATA_FETCH_ERROR, { + type, + urn, + status, + message, + }); + + throw error; + } +}; From 418821d4212b0cd3346e7038867a91b091c790b8 Mon Sep 17 00:00:00 2001 From: alex-magana Date: Tue, 14 Apr 2026 13:18:03 +0300 Subject: [PATCH 03/16] Add Head2HeadV2 component to Simorgh --- .../head-to-head-v2/assets/thumbnail.svg | 57 ++ .../components/action-grid.jsx | 21 + .../head-to-head-v2/components/action.jsx | 47 ++ .../components/actions-time.jsx | 40 + .../head-to-head-v2/components/actions.jsx | 41 + .../head-to-head-v2/components/card.jsx | 36 + .../head-to-head-v2/components/centre.jsx | 57 ++ .../conditional-onward-journey-link.jsx | 54 ++ .../components/fixture-time.jsx | 60 ++ .../head-to-head-v2/components/footer.jsx | 90 +++ .../components/grouped-events.jsx | 108 +++ .../components/head-to-head-banner.jsx | 119 +++ .../components/head-to-head-header.jsx | 107 +++ .../head-to-head-v2/components/key-events.jsx | 51 ++ .../components/match-progress.jsx | 87 ++ .../components/penalty-scores.jsx | 69 ++ .../head-to-head-v2/components/period.jsx | 30 + .../components/score-details.jsx | 72 ++ .../head-to-head-v2/components/score.jsx | 115 +++ .../head-to-head-v2/components/team-name.jsx | 62 ++ .../head-to-head-v2/components/team.jsx | 102 +++ .../head-to-head-v2-mid-events.stories.jsx | 399 ++++++++++ .../head-to-head-v2-post-events.stories.jsx | 265 ++++++ .../head-to-head-v2-pre-events.stories.jsx | 120 +++ .../head-to-head-v2-rugby-events.stories.jsx | 23 + .../head-to-head-v2/head-to-head-v2.d.ts | 162 ++++ .../head-to-head-v2/head-to-head-v2.jsx | 88 ++ .../head-to-head-v2/head-to-head-v2.mdx | 106 +++ .../head-to-head-v2.stories.jsx | 175 ++++ .../helpers/badges/parse-urn.js | 45 ++ .../helpers/badges/should-show-team-badges.js | 22 + .../helpers/badges/team-badge-config.js | 39 + .../head-to-head-v2/helpers/colour-styles.js | 58 ++ .../helpers/concise-styles.jsx | 9 + .../SportDataHeader/head-to-head-v2/index.js | 1 + .../head-to-head-v2/metadata.json | 24 + .../static-data/premier-league-venues.json | 24 + .../transformed/rugby-event/index.js | 5 + .../rugby-union-cancelled-event.js | 41 + .../rugby-union-mid-event-half-time.js | 47 ++ .../rugby-event/rugby-union-mid-event.js | 47 ++ .../rugby-event/rugby-union-post-event.js | 120 +++ .../rugby-event/rugby-union-pre-event.js | 42 + .../storybook/helpers/base-component.jsx | 115 +++ .../storybook/helpers/short-name-map.js | 29 + .../tests/head-to-head-v2.client.test.jsx | 753 ++++++++++++++++++ .../tests/rugby-screenshots.stories.jsx | 179 +++++ .../tests/screenshots.stories.jsx | 226 ++++++ 48 files changed, 4589 insertions(+) create mode 100755 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/assets/thumbnail.svg create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/action-grid.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/action.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/actions-time.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/actions.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/card.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/centre.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/conditional-onward-journey-link.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/fixture-time.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/footer.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/grouped-events.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/head-to-head-banner.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/head-to-head-header.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/key-events.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/match-progress.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/penalty-scores.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/period.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/score-details.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/score.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/team-name.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/team.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-mid-events.stories.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-post-events.stories.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-pre-events.stories.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-rugby-events.stories.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2.d.ts create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2.mdx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2.stories.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/helpers/badges/parse-urn.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/helpers/badges/should-show-team-badges.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/helpers/badges/team-badge-config.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/helpers/colour-styles.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/helpers/concise-styles.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/index.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/metadata.json create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/static-data/premier-league-venues.json create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/static-data/transformed/rugby-event/index.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/static-data/transformed/rugby-event/rugby-union-cancelled-event.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/static-data/transformed/rugby-event/rugby-union-mid-event-half-time.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/static-data/transformed/rugby-event/rugby-union-mid-event.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/static-data/transformed/rugby-event/rugby-union-post-event.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/static-data/transformed/rugby-event/rugby-union-pre-event.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/storybook/helpers/base-component.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/storybook/helpers/short-name-map.js create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/tests/head-to-head-v2.client.test.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/tests/rugby-screenshots.stories.jsx create mode 100644 ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/tests/screenshots.stories.jsx diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/assets/thumbnail.svg b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/assets/thumbnail.svg new file mode 100755 index 00000000000..95b93f6c272 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/assets/thumbnail.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/action-grid.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/action-grid.jsx new file mode 100644 index 00000000000..63215380206 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/action-grid.jsx @@ -0,0 +1,21 @@ +import styled from '@bbc/web-styled'; +import { GROUP_3, createSize } from '@bbc/web-gel-foundations'; + +export const GRID_AREAS = { + homeText: 'home_text', + awayText: 'away_text', + centreText: 'centre_text' +}; + +export const ActionGrid = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-areas: + '${GRID_AREAS.centreText} ${GRID_AREAS.centreText}' + '${GRID_AREAS.homeText} ${GRID_AREAS.awayText}'; + + @media (min-width: ${GROUP_3}) { + grid-template-columns: 1fr ${createSize(150)} 1fr; + grid-template-areas: '${GRID_AREAS.homeText} ${GRID_AREAS.centreText} ${GRID_AREAS.awayText}'; + } +`; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/action.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/action.jsx new file mode 100644 index 00000000000..b6a92361883 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/action.jsx @@ -0,0 +1,47 @@ +/* eslint-disable jsx-a11y/aria-role */ +import React from 'react'; +import styled from '@bbc/web-styled'; +import { + fontEmphasised, + fontScaleBody, + fontScaleDescription, + GROUP_3, + SPACING_1, + SPACING_2, + SPACING_3 +} from '@bbc/web-gel-foundations'; +import ActionsTime from './actions-time.jsx'; + +const StyledAction = styled.li` + ${fontEmphasised} + + ${fontScaleDescription} + padding-bottom: ${SPACING_2}; + @media (min-width: ${GROUP_3}) { + ${fontScaleBody} + padding-bottom: ${SPACING_2}; + ${({ alignment }) => `padding-${alignment === 'home' ? 'left' : 'right'}: ${SPACING_3}`}; + } +`; + +const StyledUl = styled.ul` + @media (min-width: ${GROUP_3}) { + display: flex; + flex-wrap: wrap; + padding-top: ${SPACING_1}; + justify-content: ${({ alignment }) => (alignment === 'home' ? `flex-end` : `flex-start`)}; + } +`; + +const Action = ({ contestantActions, alignment }) => ( + + {contestantActions.map((player, index) => ( + + {player.playerName} + + + ))} + +); + +export default Action; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/actions-time.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/actions-time.jsx new file mode 100644 index 00000000000..af14fad2d1e --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/actions-time.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import styled from '@bbc/web-styled'; +import VisuallyHidden from '@bbc/web-components/visually-hidden/index.js'; +import Card from './card.jsx'; + +const goalTypesHandled = { + Penalty: 'pen', + 'Own Goal': 'og' +}; + +const TextBlock = styled.span` + white-space: nowrap; +`; + +const displayGoalType = goalType => (goalTypesHandled[goalType] ? ` ${goalTypesHandled[goalType]}` : ''); + +const ActionsTime = ({ player }) => { + const times = player.actions.map(action => `${action.timeLabel.value}${displayGoalType(action.type)}`); + const timesAccessible = player.actions + .map(action => `${action.typeLabel.accessible} ${action.timeLabel.accessible}`) + .join(', '); + return ( + <> + {times.map((time, index) => ( + + + {index === 0 && '('} + {player.actionType === 'card' && } + {time} + {index === times.length - 1 && ')'} + + {index !== times.length - 1 && ', '} + + ))} + {timesAccessible} + + ); +}; + +export default ActionsTime; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/actions.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/actions.jsx new file mode 100644 index 00000000000..fdea4c9580d --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/actions.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { GroupedEvents } from './grouped-events.jsx'; +import { ActionGrid } from './action-grid.jsx'; +import ScoreDetails from './score-details.jsx'; +import { KeyEvents } from './key-events.jsx'; + +export const Actions = ({ data }) => { + const homeKeyEvents = data.home?.actions || []; + const awayKeyEvents = data.away?.actions || []; + + const hasGroupedEvents = data.groupedActions && data.groupedActions.length > 0; + const hasKeyEvents = homeKeyEvents.length > 0 || awayKeyEvents.length > 0; + + return ( + <> + + + {hasKeyEvents && ( + + )} + + {hasGroupedEvents && ( + + )} + + ); +}; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/card.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/card.jsx new file mode 100644 index 00000000000..fb8fc0d3a1a --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/card.jsx @@ -0,0 +1,36 @@ +/* eslint-disable jsx-a11y/aria-role */ +import React from 'react'; +import styled from '@bbc/web-styled'; +import { createSize } from '@bbc/web-gel-foundations'; +import redcard from '@bbc/web-assets/static/sport/football/red-card.svg'; +import secondyellowcard from '@bbc/web-assets/static/sport/football/second-yellow-card.svg'; + +const CardImage = styled.img` + padding: 0 ${createSize(3.2)} 0 ${createSize(3.2)}; +`; + +const StyledRedCard = styled(CardImage)` + width: ${createSize(11.2)}; + margin-bottom: ${createSize(-3.2)}; +`; + +const StyledYellowCard = styled(CardImage)` + margin-bottom: ${createSize(-6.4)}; + width: ${createSize(16)}; +`; + +const CardContainer = styled.div` + display: inline-block; +`; + +const Card = ({ player }) => ( + + {player.actions[0].type === 'Red Card' ? ( + + ) : ( + + )} + +); + +export default Card; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/centre.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/centre.jsx new file mode 100644 index 00000000000..970cf8976b5 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/centre.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import styled, { css } from '@bbc/web-styled'; +import { createSize, GROUP_3 } from '@bbc/web-gel-foundations'; +import { isCalledOffStatus, isInProgressStatus, isResultStatus } from '@bbc/web-sport-utils'; +import Time from './fixture-time.jsx'; +import Score from './score.jsx'; + +// ensures team names / badges line up down the page across "HH:mm", "TBC", single-digit and double-digit scores +// it is acceptable for the badges/team names to be spaced more widely for triple-digit+ scores, as these are very rare +const getCentreMinWidthPx = maxScoreLength => + maxScoreLength && maxScoreLength > 1 ? { desktop: 106, mobile: 90 } : { desktop: 85, mobile: 77 }; + +const StyledCentre = styled.div` + display: flex; + flex-direction: column; + align-self: space-evenly; + + ${({ maxScoreLength }) => css` + min-width: ${createSize(getCentreMinWidthPx(maxScoreLength).mobile)}; + @media (min-width: ${GROUP_3}) { + min-width: ${createSize(getCentreMinWidthPx(maxScoreLength).desktop)}; + } + `} +`; + +export const shouldShowScores = statusGroup => + isInProgressStatus(statusGroup) || + isResultStatus(statusGroup) || + isCalledOffStatus(statusGroup) || + statusGroup === 'Postponed'; + +const Played = ({ data, isConciseView }) => ( + +); + +const Centre = ({ data, isConciseView, maxScoreLength }) => { + const { status } = data; + + return ( + + {shouldShowScores(status) ? ( + + ) : ( + + ); +}; + +export default Centre; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/conditional-onward-journey-link.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/conditional-onward-journey-link.jsx new file mode 100644 index 00000000000..73a5f164de4 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/conditional-onward-journey-link.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import styled from '@bbc/web-styled'; +import { Track } from '@bbc/web-click-view-tracker'; +import { GridContainer, TeamHome, TeamAway } from './head-to-head-banner.jsx'; +import { Name as ParticipantsName } from '../../head-to-head/components/participant.jsx'; + +const OnwardJourneyLink = styled.a` + cursor: pointer; + + &:link { + text-decoration: none; + color: ${({ theme }) => theme.colourPalette.primary}; + } + + &:visited { + color: ${({ theme }) => theme.colourPalette.primary}; + } + + &:hover, + &:focus > ${GridContainer} { + ${TeamHome}, ${TeamAway}, ${ParticipantsName} { + color: ${({ theme }) => theme.colourPalette.hyperlink}; + text-decoration-line: underline; + } + } +`; + +export const ConditionalOnwardJourneyLink = ({ + isConciseView, + onwardJourneyLink, + children, + tipoTopicId, + trackingEvent +}) => { + if (isConciseView && onwardJourneyLink) { + return trackingEvent ? ( + + {({ trackRef }) => ( +
+ + {children} + +
+ )} + + ) : ( + + {children} + + ); + } + + return children; +}; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/fixture-time.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/fixture-time.jsx new file mode 100644 index 00000000000..2dd6fe11c49 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/fixture-time.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import styled, { css } from '@bbc/web-styled'; +import { + GROUP_3, + SPACING_1, + SPACING_3, + SPACING_4, + SPACING_7, + createSize, + fontScaleBody, + fontScaleIndexHeadlineMedium, + fontScaleSectionHeading, + fontStandard, + // eslint-disable-next-line no-restricted-imports + fontWeights +} from '@bbc/web-gel-foundations'; +import VisuallyHidden from '@bbc/web-components/visually-hidden/index.js'; +import { fixedHeightConciseView } from '../helpers/concise-styles.jsx'; + +const StyledTime = styled.time` + display: flex; + align-items: center; + justify-content: center; + ${fontScaleBody} + font-size: ${createSize(40)}; + line-height: 1.125; + font-weight: ${fontWeights.medium}; + padding: 0 ${SPACING_1}; + + @media (min-width: ${GROUP_3}) { + font-size: ${createSize(50)}; + line-height: 1.08; + padding: 0 ${SPACING_7}; + } + + ${({ theme, isConciseView }) => + isConciseView && + css` + ${fontStandard({ theme })} + ${fixedHeightConciseView} + ${fontScaleIndexHeadlineMedium} + padding: 0 ${SPACING_3}; + + @media (min-width: ${GROUP_3}) { + padding: 0 ${SPACING_4}; + ${fontScaleSectionHeading} + } + `} +`; + +const Time = ({ time, isConciseView }) => ( + <> + + {time.accessibleTime} + +); + +export default Time; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/footer.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/footer.jsx new file mode 100644 index 00000000000..d64efcbd713 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/footer.jsx @@ -0,0 +1,90 @@ +// import React from 'react'; +// import styled from '@bbc/web-styled'; +import styled from '@emotion/styled'; +import { + SPACING_1, + SPACING_2, + GROUP_3, + SPACING_4, + fontScaleDescription, + fontScaleBody, + createSize, +} from '@bbc/web-gel-foundations'; +import { getStyledLineColour } from '../helpers/colour-styles.js'; + +const StyledFooter = styled.div` + ${fontScaleDescription} + padding-bottom: ${SPACING_4}; + text-align: center; + + @media (min-width: ${GROUP_3}) { + ${fontScaleBody} + padding-bottom: ${SPACING_2}; + } +`; + +const FooterTextWrapper = styled.div` + display: inline-block; + font-size: ${createSize(13)}; + + &:not(:first-child) { + margin-left: ${SPACING_2}; + } +`; + +const Venue = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + padding-bottom: ${SPACING_1}; +`; + +const AttendanceValue = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; +`; + +const VenueLabel = styled.span` + color: ${({ theme }) => theme.colourPalette.secondary}; + padding-right: ${SPACING_1}; +`; + +const AttendanceLabel = styled.span` + color: ${({ theme }) => theme.colourPalette.secondary}; + padding-right: ${SPACING_1}; +`; + +const HorizontalRule = styled.hr` + width: ${createSize(12)}; + border: none; + border-top: 1px solid ${({ theme, status, isConciseView }) => getStyledLineColour({ theme, status, isConciseView })}; + padding-bottom: ${SPACING_1}; +`; + +const Footer = ({ venue, status, attendanceValue, attendanceInfo }) => { + const formattedAttendanceValue = attendanceValue?.toLocaleString(); + + return ( + + + + + Venue: + {attendanceInfo ? `${venue} (${attendanceInfo})` : venue} + + + + {attendanceValue && ( + + Attendance: + {formattedAttendanceValue} + + )} + + + ); +}; + +export default Footer; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/grouped-events.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/grouped-events.jsx new file mode 100644 index 00000000000..0e28c658bdf --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/grouped-events.jsx @@ -0,0 +1,108 @@ +import React from 'react'; +import styled, { css } from '@bbc/web-styled'; +import { + GROUP_3, + SPACING_2, + SPACING_5, + SPACING_6, + fontEmphasised, + SPACING_1, + SPACING_4, + SPACING_3, + fontScaleBody, + fontScaleDescription, + createSize +} from '@bbc/web-gel-foundations'; +import VisuallyHidden from '@bbc/web-components/visually-hidden/index.js'; +import { ActionGrid, GRID_AREAS } from './action-grid.jsx'; + +const GroupedEventsWrapper = styled.div` + margin: ${SPACING_2} ${SPACING_6} 0; +`; + +const ActionWrapper = styled.div` + ${({ theme }) => css` + border-top: ${createSize(1)} solid ${theme.colourPalette.border.decorativeSubtle}; + `}; +`; + +const GroupLabel = styled.div` + grid-area: ${GRID_AREAS.centreText}; + ${fontEmphasised} + text-align: center; + ${fontScaleBody} + padding: ${SPACING_2} 0 ${SPACING_1}; + + @media (min-width: ${GROUP_3}) { + padding: ${SPACING_2} 0 ${SPACING_5}; + } +`; + +const GroupedHomeEvent = styled.div` + grid-area: ${GRID_AREAS.homeText}; + text-align: right; + + ${fontScaleDescription} + padding: 0 ${SPACING_4} ${SPACING_3} 0; + + @media (min-width: ${GROUP_3}) { + ${fontScaleBody} + padding: ${SPACING_2} 0 ${SPACING_5}; + } +`; + +const GroupedAwayEvent = styled.div` + grid-area: ${GRID_AREAS.awayText}; + text-align: left; + + ${fontScaleDescription} + padding: 0 0 ${SPACING_3} ${SPACING_4}; + + @media (min-width: ${GROUP_3}) { + ${fontScaleBody} + padding: ${SPACING_2} 0 ${SPACING_5}; + } +`; + +const Actions = ({ teamActions, teamAccessibleActions }) => { + if (teamAccessibleActions?.length) { + return ( + <> + {teamActions.join(', ')} + {teamAccessibleActions.join(', ')} + + ); + } + + return teamActions.join(', '); +}; + +export const GroupedEvents = ({ groupedEvents, homeName, awayName }) => ( + + {groupedEvents.map( + ({ groupName, homeTeamActions, homeTeamAccessibleActions, awayTeamActions, awayTeamAccessibleActions }) => ( + + + {groupName.fullName} + + {homeTeamActions.length > 0 && ( + <> + {`${homeName},`} + + + )} + + + {awayTeamActions.length > 0 && ( + <> + {`${awayName},`} + + + )} + + + + ) + )} + +); diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/head-to-head-banner.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/head-to-head-banner.jsx new file mode 100644 index 00000000000..946515684aa --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/head-to-head-banner.jsx @@ -0,0 +1,119 @@ +import React from 'react'; +import styled, { css } from '@bbc/web-styled'; +import { GROUP_3, createSize } from '@bbc/web-gel-foundations'; +import VisuallyHidden from '@bbc/web-components/visually-hidden/index.js'; +import Team from './team.jsx'; +import Centre from './centre.jsx'; +import MatchProgress from './match-progress.jsx'; +import PenaltyScores from './penalty-scores.jsx'; + +export const GridContainer = styled.div` + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: ${({ isConciseView }) => (isConciseView ? `center` : `none`)}; + grid-template-areas: + 'home_team scores away_team' + 'progress progress progress'; + ${({ isConciseView, shouldHideBadges }) => + !isConciseView && + !shouldHideBadges && + css` + @media (max-width: calc(${GROUP_3} - ${createSize(1)})) { + grid-template-columns: 1fr auto auto 1fr; + grid-template-areas: + 'home_team scores scores away_team' + 'home_team progress progress away_team'; + } + `} +`; + +const WithInlineFallback = styled.div` + @supports not (display: grid) { + display: inline-block; + width: 33%; + } +`; + +export const TeamHome = styled(WithInlineFallback)` + grid-area: home_team; + display: flex; + align-items: stretch; +`; + +export const TeamAway = styled(WithInlineFallback)` + grid-area: away_team; + display: flex; + align-items: stretch; +`; + +const Scores = styled(WithInlineFallback)` + grid-area: scores; + margin: auto; +`; + +const MatchProgressContainer = styled.div` + grid-area: progress; +`; + +const ItemWrapper = ({ data, isConciseView, shouldHideBadges, maxScoreLength, teamBadgePlaceholderFallbackType }) => { + const shouldDisplayPenScores = data.home.runningScores?.penaltyShootout && data.away.runningScores?.penaltyShootout; + return ( + <> + + + + + + + {data.status === 'PreEvent' && plays} + + + + + + + + + {shouldDisplayPenScores && } + + ); +}; + +export const HeadToHeadBanner = ({ + data, + isConciseView, + eventSummary, + shouldHideBadges, + maxScoreLength, + teamBadgePlaceholderFallbackType +}) => ( + <> + {eventSummary} + + +); + +export default HeadToHeadBanner; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/head-to-head-header.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/head-to-head-header.jsx new file mode 100644 index 00000000000..28890cfa1a0 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/head-to-head-header.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import styled, { css } from '@bbc/web-styled'; +import { + GROUP_3, + SPACING_6, + SPACING_4, + SPACING_2, + SPACING_1, + fontScaleBody, + fontScaleDescription +} from '@bbc/web-gel-foundations'; +import { isLiveStatus } from '@bbc/web-sport-utils'; + +const HeaderWrapper = styled.div` + display: flex; + justify-content: center; + flex-direction: column; + ${fontScaleDescription} + padding-bottom: ${SPACING_4}; + + ${({ status }) => css` + padding-top: ${isLiveStatus(status) ? 0 : SPACING_4}; + `} + @media (min-width: ${GROUP_3}) { + flex-direction: row; + ${fontScaleBody} + ${({ status }) => css` + padding-top: ${isLiveStatus(status) ? SPACING_2 : SPACING_6}; + `} + } +`; + +const DateWrapper = styled.div` + display: flex; + justify-content: flex-end; + + flex-direction: column; + @media (min-width: ${GROUP_3}) { + flex-direction: row; + } +`; + +const DateHeader = styled.div` + display: flex; + justify-content: center; + + padding-bottom: ${SPACING_1}; + @media (min-width: ${GROUP_3}) { + padding-bottom: 0; + } +`; + +const Interpunct = styled.div` + color: ${({ theme }) => theme.colourPalette.secondary}; + + display: none; + @media (min-width: ${GROUP_3}) { + display: inline; + padding: 0 ${SPACING_2}; + } +`; + +const TournamentHeader = styled.div` + display: flex; + justify-content: center; + flex-wrap: wrap; +`; + +const Date = styled.time` + color: ${({ theme }) => theme.colourPalette.secondary}; + flex-shrink: 0; +`; + +const CompetitionFormatter = styled.div` + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + padding: 0px; + flex-shrink: 1; +`; + +const formatTournamentDescriptionLabel = tournamentDescriptionLabel => { + const tournamentGroupsArray = tournamentDescriptionLabel.split(' - '); + + return tournamentGroupsArray.map((element, i) => { + if (tournamentGroupsArray.length === i + 1) { + return {element}; + } + return {element} - ; + }); +}; + +const HeadToHeadHeader = ({ date, tournamentDescriptionLabel, status }) => ( + + {!isLiveStatus(status) && ( + + + {date} + + + + )} + {formatTournamentDescriptionLabel(tournamentDescriptionLabel)} + +); + +export default HeadToHeadHeader; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/key-events.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/key-events.jsx new file mode 100644 index 00000000000..2a92f7a826d --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/key-events.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import styled, { css } from '@bbc/web-styled'; +import { GROUP_3, SPACING_4 } from '@bbc/web-gel-foundations'; +import Heading from '@bbc/web-components/heading/index.js'; +import Action from './action.jsx'; +import { GRID_AREAS } from './action-grid.jsx'; + +const KeyEventsStyles = css` + padding: 0 ${SPACING_4}; + @media (min-width: ${GROUP_3}) { + padding: 0; + } + + @supports not (display: grid) { + display: inline-flex; + width: 50%; + padding: 0 ${SPACING_4}; + box-sizing: border-box; + } +`; + +const KeyEventsHome = styled.div` + ${KeyEventsStyles} + text-align: right; + grid-area: ${GRID_AREAS.homeText}; +`; + +const KeyEventsAway = styled.div` + ${KeyEventsStyles} + grid-area: ${GRID_AREAS.awayText}; +`; + +export const KeyEvents = ({ homeKeyEvents, awayKeyEvents, homeName, awayName }) => ( + <> + + Key Events + + + + {homeName} + + + + + + {awayName} + + + + +); diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/match-progress.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/match-progress.jsx new file mode 100644 index 00000000000..f131d123317 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/match-progress.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import styled, { css } from '@bbc/web-styled'; +import VisuallyHidden from '@bbc/web-components/visually-hidden/index.js'; +import { fontScaleDescription, SPACING_1, SPACING_2 } from '@bbc/web-gel-foundations'; +import { getFallbackFootballPeriodLabel, isInProgressStatus } from '@bbc/web-sport-utils'; +import { shouldShowScores } from './centre.jsx'; +import Period from './period.jsx'; + +const MatchProgressWrapper = styled.div` + display: flex; + flex-direction: column; + + ${({ isConciseView }) => + !isConciseView && + css` + padding: ${SPACING_2} 0 ${SPACING_1}; + gap: ${SPACING_2}; + `} +`; + +const AggregateScore = styled.div` + ${fontScaleDescription} + text-align: center; + ${({ theme, isConciseView }) => + isConciseView && + css` + padding: ${SPACING_1} 0; + color: ${theme.colourPalette.secondary}; + `} +`; + +const MatchProgress = ({ data, isConciseView }) => { + const { home, away, periodLabel, status, multiLeg } = data; + + const shouldDisplayAggScore = + multiLeg && multiLeg.leg > 1 && home.runningScores?.aggregate && away.runningScores?.aggregate; + + const fallbackPeriod = + periodLabel && + getFallbackFootballPeriodLabel( + periodLabel, + status, + home.runningScores, + away.runningScores, + home.fullName, + away.fullName + ); + + const shouldDisplayPeriod = periodLabel && fallbackPeriod && shouldShowScores(status); + + if (!shouldDisplayAggScore && !shouldDisplayPeriod) { + return null; + } + + return ( + + {shouldDisplayAggScore && ( + <> + + {`Aggregate score ${home.fullName} ${home.runningScores.aggregate} , ${away.fullName} ${away.runningScores.aggregate}`} + + + + )} + {shouldDisplayPeriod && ( + <> + + {`${fallbackPeriod.accessible}${ + isInProgressStatus(status) && periodLabel.value !== 'PENS' ? ' , in progress' : '' + }`} + + + + )} + + ); +}; + +export default MatchProgress; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/penalty-scores.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/penalty-scores.jsx new file mode 100644 index 00000000000..b12dc7fb42f --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/penalty-scores.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import styled, { css } from '@bbc/web-styled'; +import { + GROUP_3, + SPACING_1, + SPACING_2, + fontEmphasised, + fontScaleBody, + fontScaleDescription +} from '@bbc/web-gel-foundations'; +import VisuallyHidden from '@bbc/web-components/visually-hidden/index.js'; + +const PenaltyScoresContainer = styled.div` + ${fontScaleBody} + text-align: center; + padding: ${SPACING_1} 0; + + @media (min-width: ${GROUP_3}) { + padding-bottom: ${SPACING_2}; + } + + ${({ isConciseView }) => + isConciseView && + css` + ${fontScaleDescription} + `} +`; + +const WinningTeamName = styled.span` + ${fontEmphasised} + color: ${({ theme, isConciseView }) => (isConciseView ? theme.colourPalette.primary : theme.colourPalette.accent)}; +`; + +const PenaltiesText = styled.div` + color: ${({ theme }) => theme.colourPalette.secondary}; +`; + +const PenaltyScores = ({ data, isConciseView }) => { + const { winner, seriesWinner, multiLeg, status } = data; + + const isPostEvent = status?.toLowerCase() === 'postevent'; + const hasWinner = winner !== undefined; + const isDrawWithNoSeriesWinner = winner === 'draw' && !seriesWinner; + const isMultiLegWithNoSeriesWinner = multiLeg?.leg > 1 && !seriesWinner; + + if (!isPostEvent || !hasWinner || isDrawWithNoSeriesWinner || isMultiLegWithNoSeriesWinner) { + return null; + } + + const winnerOnPenalties = seriesWinner ?? winner; + const loserOnPenalties = winnerOnPenalties.toLowerCase() === 'home' ? 'away' : 'home'; + const winnerOnPenaltiesName = data[winnerOnPenalties].fullName; + const winnerOnPenaltiesScore = data[winnerOnPenalties].runningScores.penaltyShootout; + const loserOnPenaltiesScore = data[loserOnPenalties].runningScores.penaltyShootout; + + return ( + + + {`${winnerOnPenaltiesName} win ${winnerOnPenaltiesScore} - ${loserOnPenaltiesScore} on penalties`} + + + + ); +}; + +export default PenaltyScores; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/period.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/period.jsx new file mode 100644 index 00000000000..e0522a8e0bd --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/period.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import styled, { css } from '@bbc/web-styled'; +import { SPACING_1, fontScaleBody, fontScaleDescription } from '@bbc/web-gel-foundations'; +import { getFallbackFootballPeriodLabel } from '@bbc/web-sport-utils'; +import { getStyledMatchProgress } from '../helpers/colour-styles.js'; + +const StyledPeriod = styled.div` + display: flex; + justify-content: center; + color: ${({ status, theme, isConciseView }) => getStyledMatchProgress({ status, theme, isConciseView })}; + ${fontScaleBody} + + ${({ isConciseView }) => + isConciseView && + css` + padding-top: ${SPACING_1}; + ${fontScaleDescription} + `} +`; + +const Period = ({ labels, status, homeRunningScores, awayRunningScores, isConciseView }) => { + const period = getFallbackFootballPeriodLabel(labels, status, homeRunningScores, awayRunningScores); + return ( + + ); +}; + +export default Period; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/score-details.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/score-details.jsx new file mode 100644 index 00000000000..1e0bc2ae8a5 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/score-details.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import styled from '@bbc/web-styled'; +import VisuallyHidden from '@bbc/web-components/visually-hidden/index.js'; +import { fontScaleDescription, SPACING_1, GROUP_3, SPACING_2, SPACING_3 } from '@bbc/web-gel-foundations'; +import { GRID_AREAS } from './action-grid.jsx'; + +const ScoreDetailsWrapper = styled.div` + grid-area: ${GRID_AREAS.centreText}; + display: flex; + row-gap: ${SPACING_2}; + ${fontScaleDescription} + text-align: center; + color: ${({ theme }) => theme.colourPalette.primary}; + padding: ${SPACING_1} 0 ${SPACING_3}; + + flex-direction: row; + justify-content: center; + @media (min-width: ${GROUP_3}) { + padding: ${SPACING_1} 0 ${SPACING_2}; + flex-direction: column; + justify-content: flex-start; + } +`; + +const Score = styled.div` + color: ${({ theme }) => theme.colourPalette.secondary}; +`; + +const Comma = styled.span` + color: ${({ theme }) => theme.colourPalette.secondary}; + + padding-right: ${SPACING_1}; + @media (min-width: ${GROUP_3}) { + display: none; + } +`; + +const ScoreDetails = ({ homeName, awayName, homeRunningScores, awayRunningScores }) => { + const shouldDisplayHT = Boolean(homeRunningScores?.halftime && awayRunningScores?.halftime); + const shouldDisplayFT = Boolean( + homeRunningScores?.fulltime && + awayRunningScores?.fulltime && + homeRunningScores?.extratime && + awayRunningScores?.extratime + ); + + if (!shouldDisplayFT && !shouldDisplayHT) { + return null; + } + + return ( + + {shouldDisplayFT && ( + <> + {`Full Time ${homeName} ${homeRunningScores.fulltime} , ${awayName} ${awayRunningScores.fulltime}`} + + + + , + + )} + {shouldDisplayHT && ( + <> + {`Half Time ${homeName} ${homeRunningScores.halftime} , ${awayName} ${awayRunningScores.halftime}`}{' '} + + + )} + + ); +}; + +export default ScoreDetails; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/score.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/score.jsx new file mode 100644 index 00000000000..0deeb81c988 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/score.jsx @@ -0,0 +1,115 @@ +import React from 'react'; +import styled, { css } from '@bbc/web-styled'; +import { + createSize, + fontEmphasised, + GROUP_3, + SPACING_1, + SPACING_2, + SPACING_3, + SPACING_4, + SPACING_6, + // eslint-disable-next-line no-restricted-imports + fontWeights +} from '@bbc/web-gel-foundations'; +import { getScoreColourStyle, getStyledLineColour } from '../helpers/colour-styles.js'; +import { fixedHeightConciseView } from '../helpers/concise-styles.jsx'; + +const HOME_SCORE = 'home_score'; +const VERTICAL_LINE = 'vertical_line'; +const AWAY_SCORE = 'away_score'; + +const MATCH_STATUS_LETTERS = { + Postponed: 'P', + Cancelled: 'C' +}; + +const StyledScore = styled.div` + display: grid; + grid-template-columns: 1fr auto 1fr; + grid-template-areas: '${HOME_SCORE} ${VERTICAL_LINE} ${AWAY_SCORE}'; + font-weight: ${fontWeights.medium}; + align-items: center; + color: ${({ theme, status, isConciseView }) => getScoreColourStyle({ theme, status, isConciseView })}; + + font-size: ${createSize(40)}; + line-height: 1.1; + padding-left: ${SPACING_1}; + padding-right: ${SPACING_1}; + @media (min-width: ${GROUP_3}) { + font-size: ${createSize(50)}; + line-height: 1.08; + padding-left: ${SPACING_6}; + padding-right: ${SPACING_6}; + } + + ${({ isConciseView, theme }) => + isConciseView && + css` + ${fontEmphasised({ theme })} + + ${fixedHeightConciseView} + font-size: ${createSize(20)}; + line-height: 1.2; + padding-left: ${SPACING_3}; + padding-right: ${SPACING_3}; + + @media (min-width: ${GROUP_3}) { + font-size: ${createSize(20)}; + line-height: 1.2; + padding-left: ${SPACING_4}; + padding-right: ${SPACING_4}; + } + `} +`; + +const HomeScore = styled.div` + grid-area: ${HOME_SCORE}; + text-align: right; +`; + +const AwayScore = styled.div` + grid-area: ${AWAY_SCORE}; + text-align: left; +`; + +export const VerticalLine = styled.div` + ${({ theme, status, isConciseView }) => css` + border-left: ${createSize(2)} solid ${getStyledLineColour({ theme, status, isConciseView })}; + `}; + display: inline-block; + margin: 0 ${SPACING_4}; + grid-area: ${VERTICAL_LINE}; + + height: ${createSize(38)}; + @media (min-width: ${GROUP_3}) { + height: ${createSize(44)}; + } + + ${({ isConciseView }) => + isConciseView && + css` + height: ${createSize(24)}; + margin: 0 ${SPACING_2}; + @media (min-width: ${GROUP_3}) { + height: ${createSize(28)}; + margin: 0 ${SPACING_3}; + } + `} +`; + +const Score = ({ status, home, homeScoreUnconfirmed, away, awayScoreUnconfirmed, isConciseView }) => { + const matchStatusLetter = MATCH_STATUS_LETTERS[status]; + const homeScore = homeScoreUnconfirmed || home; + const awayScore = awayScoreUnconfirmed || away; + + return ( + + ); +}; + +export default Score; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/team-name.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/team-name.jsx new file mode 100644 index 00000000000..ae5052a45b6 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/team-name.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { + createSize, + fontScaleBody, + fontScaleDescription, + fontScaleIndexHeadlineMedium, + GROUP_3, + GROUP_4, + SPACING_2 +} from '@bbc/web-gel-foundations'; +import styled, { css } from '@bbc/web-styled'; +import VisuallyHidden from '@bbc/web-components/visually-hidden/index.js'; +import { fixedHeightConciseView } from '../helpers/concise-styles.jsx'; + +const TeamNameWrapper = styled.div` + display: flex; + gap: ${SPACING_2}; + align-items: center; + ${({ shouldHideBadges }) => (shouldHideBadges ? fontScaleIndexHeadlineMedium : fontScaleBody)} + padding: ${({ shouldHideBadges }) => (shouldHideBadges ? `0 ${SPACING_2}` : `0 0 ${SPACING_2}`)}; + + @media (min-width: ${GROUP_3}) { + padding: 0; + ${fontScaleIndexHeadlineMedium} + } + + ${({ isConciseView }) => + isConciseView && + css` + ${fixedHeightConciseView} + ${fontScaleDescription} + padding: 0; + + @media (min-width: ${GROUP_3}) { + font-size: ${createSize(16)}; + line-height: 1.375; + } + `} +`; + +const MobileValue = styled.span` + @media (min-width: ${GROUP_4}) { + display: none; + } +`; + +const DesktopValue = styled.span` + display: none; + @media (min-width: ${GROUP_4}) { + display: inline; + } +`; + +const TeamName = ({ fullName, shortName, isConciseView, shouldHideBadges }) => ( + + + + {fullName === 'TBC' ? 'Team to be confirmed' : fullName} + +); + +export default TeamName; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/team.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/team.jsx new file mode 100644 index 00000000000..47220129280 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/components/team.jsx @@ -0,0 +1,102 @@ +import React from 'react'; +import styled, { css } from '@bbc/web-styled'; +import { GROUP_3, fontScaleBody, createSize, SPACING_3, SPACING_2, SPACING_5 } from '@bbc/web-gel-foundations'; +import SportBadge from '@bbc/web-components/sport-badge/index.js'; +import TeamName from './team-name.jsx'; + +const StyledTeam = styled.div` + display: flex; + gap: ${SPACING_2}; + align-items: center; + justify-content: flex-start; + flex-grow: 2; + ${fontScaleBody} + + flex-direction: ${({ isConciseView, shouldHideBadges }) => (isConciseView || shouldHideBadges ? 'row' : 'column')}; + + @media (min-width: ${GROUP_3}) { + gap: ${SPACING_5}; + flex-direction: row; + } + + ${({ isConciseView }) => + isConciseView && + css` + @media (min-width: ${GROUP_3}) { + gap: ${SPACING_3}; + } + `} +`; + +const HomeTeam = styled(StyledTeam)` + justify-content: flex-end; + text-align: right; + ${({ isConciseView, shouldHideBadges }) => + !isConciseView && + !shouldHideBadges && + css` + @media (max-width: calc(${GROUP_3} - ${createSize(1)})) { + justify-content: flex-end; + flex-direction: column-reverse; + text-align: center; + } + `} +`; + +const AwayTeam = styled(StyledTeam)` + justify-content: flex-start; + text-align: left; + + ${({ isConciseView, shouldHideBadges }) => + !isConciseView && + !shouldHideBadges && + css` + @media (max-width: calc(${GROUP_3} - ${createSize(1)})) { + text-align: center; + } + `} +`; + +const Team = ({ alignment, name, shortName, urn, isConciseView, shouldHideBadges, badgePlaceholderFallbackType }) => { + const size = isConciseView ? { small: 20, medium: 24, large: 24 } : { small: 40, medium: 44, large: 44 }; + if (alignment === 'home') { + return ( + + + {!shouldHideBadges && ( + + )} + + ); + } + return ( + + {!shouldHideBadges && ( + + )} + + + ); +}; + +export default Team; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-mid-events.stories.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-mid-events.stories.jsx new file mode 100644 index 00000000000..237337114c7 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-mid-events.stories.jsx @@ -0,0 +1,399 @@ +import { BREAKPOINT_VIEWPORTS } from '@bbc/web-gel-foundations'; +import { INITIAL_VIEWPORTS } from 'storybook/viewport'; +import { + firstHalf90Data, + secondHalf90Data, + firstHalfAggData, + etFirstHalfData, + inPensAetData, + beforePensData, + beforePensAetData, + beforeEtData, + inPens90Data, + secondLegETData, + secondLegAETInPensData +} from '@bbc/web-sport-utils/tests/static-data/football/event/transformed/mid-event/index.js'; + +import mdx from './head-to-head-v2.mdx'; +import metadata from './metadata.json'; +import { HeadToHeadV2 } from './head-to-head-v2.jsx'; +import { shortNamesMap } from './storybook/helpers/short-name-map.js'; +import { HeadToHeadV2Component, HeadToHeadV2ConciseComponent } from './storybook/helpers/base-component.jsx'; + +import venuesData from './static-data/premier-league-venues.json'; + +const { venues } = venuesData; + +const getBaseDataWithEuropaLeagueTournament = baseData => ({ + ...baseData, + tournamentDescriptionLabel: 'UEFA Europa Conference League' +}); + +export default { + title: 'Components/Presentation/Head To Head V2/Mid Event', + component: HeadToHeadV2, + tags: ['autodocs'], + parameters: { + metadata, + docs: { + description: { + component: 'The `Head to Head V2` component is used to render event data.' + }, + page: mdx + }, + chromatic: { + viewports: [375, ...BREAKPOINT_VIEWPORTS] + }, + viewport: { + viewports: INITIAL_VIEWPORTS + } + }, + globals: { + corePalette: 'lightAlternative', + servicePalette: 'sportLight', + fontPalette: 'sansSimple' + }, + argTypes: { + home: { + options: Object.keys(shortNamesMap()), + control: { type: 'select' } + }, + away: { + options: Object.keys(shortNamesMap()), + control: { type: 'select' } + }, + venue: { + options: venues, + control: { type: 'select' } + }, + status: { + table: { disable: true } + }, + date: { control: 'date' } + } +}; + +export const FirstHalfOf90Mins = HeadToHeadV2Component.bind({}); +export const FirstHalfOf90MinsWithHomeScoreUnconfirmed = HeadToHeadV2Component.bind({}); +export const SecondHalfOf90Mins = HeadToHeadV2Component.bind({}); +export const InPenaltiesAfter90Mins = HeadToHeadV2Component.bind({}); +export const ExtraTime = HeadToHeadV2Component.bind({}); +export const InPenaltiesAfterExtraTime = HeadToHeadV2Component.bind({}); +export const BeforePensAfterExtraTime = HeadToHeadV2Component.bind({}); +export const BeforeEt = HeadToHeadV2Component.bind({}); +export const FirstHalfOf90MinsSecondLeg = HeadToHeadV2Component.bind({}); +export const ExtraTimeSecondLeg = HeadToHeadV2Component.bind({}); +export const InPenaltiesAfterExtraTimeSecondLeg = HeadToHeadV2Component.bind({}); +export const BeforePens = HeadToHeadV2Component.bind({}); +export const FirstHalfOf90MinsConcise = HeadToHeadV2ConciseComponent.bind({}); +export const SecondHalfOf90MinsConcise = HeadToHeadV2ConciseComponent.bind({}); +export const InPenaltiesAfter90MinsConcise = HeadToHeadV2ConciseComponent.bind({}); +export const ExtraTimeConcise = HeadToHeadV2ConciseComponent.bind({}); +export const InPenaltiesAfterExtraTimeConcise = HeadToHeadV2ConciseComponent.bind({}); +export const BeforePensConcise = HeadToHeadV2ConciseComponent.bind({}); +export const BeforePensAfterExtraTimeConcise = HeadToHeadV2ConciseComponent.bind({}); +export const BeforeEtConcise = HeadToHeadV2ConciseComponent.bind({}); +export const FirstHalfOf90MinsSecondLegConcise = HeadToHeadV2ConciseComponent.bind({}); +export const ExtraTimeSecondLegConcise = HeadToHeadV2ConciseComponent.bind({}); +export const InPenaltiesAfterExtraTimeSecondLegConcise = HeadToHeadV2ConciseComponent.bind({}); + +FirstHalfOf90Mins.args = { + home: 'Arsenal', + homeScore: '0', + away: 'Aston Villa', + awayScore: '0', + baseData: getBaseDataWithEuropaLeagueTournament(firstHalf90Data), + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' }, + homeActions: firstHalf90Data.home.actions, + awayActions: firstHalf90Data.away.actions +}; + +FirstHalfOf90MinsWithHomeScoreUnconfirmed.args = { + home: 'Arsenal', + homeScore: '3', + homeScoreUnconfirmed: '4', + away: 'Aston Villa', + awayScore: '0', + awayScoreUnconfirmed: '0', + baseData: getBaseDataWithEuropaLeagueTournament(firstHalf90Data), + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' }, + homeActions: firstHalf90Data.home.actions, + awayActions: firstHalf90Data.away.actions +}; + +SecondHalfOf90Mins.args = { + home: 'Arsenal', + homeScore: '0', + away: 'Aston Villa', + awayScore: '0', + baseData: getBaseDataWithEuropaLeagueTournament(secondHalf90Data), + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' }, + homeActions: secondHalf90Data.home.actions, + awayActions: secondHalf90Data.away.actions +}; + +InPenaltiesAfter90Mins.args = { + home: 'Arsenal', + homeScore: '3', + away: 'Aston Villa', + awayScore: '3', + baseData: getBaseDataWithEuropaLeagueTournament(inPens90Data), + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'League Cup', urn: 'urn:bbc:sportsdata:football:tournament:league-cup' }, + homeActions: inPens90Data.home.actions, + awayActions: inPens90Data.away.actions +}; + +BeforePens.args = { + home: 'Arsenal', + homeScore: '3', + away: 'Aston Villa', + awayScore: '3', + baseData: beforePensData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'League Cup', urn: 'urn:bbc:sportsdata:football:tournament:league-cup' }, + homeActions: beforePensData.home.actions, + awayActions: beforePensData.away.actions +}; + +BeforeEt.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: beforeEtData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'FA Cup - 3rd Round', urn: 'urn:bbc:sportsdata:football:tournament:fa-cup' }, + homeActions: beforeEtData.home.actions, + awayActions: beforeEtData.away.actions +}; + +ExtraTime.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: etFirstHalfData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'FA Cup - 3rd Round', urn: 'urn:bbc:sportsdata:football:tournament:fa-cup' }, + homeActions: etFirstHalfData.home.actions, + awayActions: etFirstHalfData.away.actions +}; + +InPenaltiesAfterExtraTime.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: getBaseDataWithEuropaLeagueTournament(inPensAetData), + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: inPensAetData.home.actions, + awayActions: inPensAetData.away.actions +}; + +BeforePensAfterExtraTime.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: beforePensAetData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: beforePensAetData.home.actions, + awayActions: beforePensAetData.away.actions +}; + +FirstHalfOf90MinsSecondLeg.args = { + home: 'Arsenal', + homeScore: '0', + away: 'Aston Villa', + awayScore: '0', + baseData: getBaseDataWithEuropaLeagueTournament(firstHalfAggData), + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Champions League', urn: 'urn:bbc:sportsdata:football:tournament:champions-league' }, + homeActions: firstHalfAggData.home.actions, + awayActions: firstHalfAggData.away.actions +}; + +ExtraTimeSecondLeg.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: getBaseDataWithEuropaLeagueTournament(secondLegETData), + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: secondLegETData.home.actions, + awayActions: secondLegETData.away.actions +}; + +InPenaltiesAfterExtraTimeSecondLeg.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: getBaseDataWithEuropaLeagueTournament(secondLegAETInPensData), + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: secondLegAETInPensData.home.actions, + awayActions: secondLegAETInPensData.away.actions +}; + +FirstHalfOf90MinsConcise.args = { + home: 'Arsenal', + homeScore: '0', + away: 'Aston Villa', + awayScore: '0', + baseData: firstHalf90Data, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' }, + homeActions: firstHalf90Data.home.actions, + awayActions: firstHalf90Data.away.actions +}; + +SecondHalfOf90MinsConcise.args = { + home: 'Arsenal', + homeScore: '0', + away: 'Aston Villa', + awayScore: '0', + baseData: secondHalf90Data, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' }, + homeActions: secondHalf90Data.home.actions, + awayActions: secondHalf90Data.away.actions +}; + +InPenaltiesAfter90MinsConcise.args = { + home: 'Arsenal', + homeScore: '3', + away: 'Aston Villa', + awayScore: '3', + baseData: inPens90Data, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'League Cup', urn: 'urn:bbc:sportsdata:football:tournament:league-cup' }, + homeActions: inPens90Data.home.actions, + awayActions: inPens90Data.away.actions +}; + +ExtraTimeConcise.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: etFirstHalfData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'FA Cup - 3rd Round', urn: 'urn:bbc:sportsdata:football:tournament:fa-cup' }, + homeActions: etFirstHalfData.home.actions, + awayActions: etFirstHalfData.away.actions +}; + +BeforeEtConcise.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: beforeEtData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'FA Cup - 3rd Round', urn: 'urn:bbc:sportsdata:football:tournament:fa-cup' }, + homeActions: beforeEtData.home.actions, + awayActions: beforeEtData.away.actions +}; + +BeforePensConcise.args = { + home: 'Arsenal', + homeScore: '3', + away: 'Aston Villa', + awayScore: '3', + baseData: beforePensData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'League Cup', urn: 'urn:bbc:sportsdata:football:tournament:league-cup' }, + homeActions: beforePensData.home.actions, + awayActions: beforePensData.away.actions +}; + +InPenaltiesAfterExtraTimeConcise.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: inPensAetData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: inPensAetData.home.actions, + awayActions: inPensAetData.away.actions +}; + +BeforePensAfterExtraTimeConcise.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: beforePensAetData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: beforePensAetData.home.actions, + awayActions: beforePensAetData.away.actions +}; + +FirstHalfOf90MinsSecondLegConcise.args = { + home: 'Arsenal', + homeScore: '0', + away: 'Aston Villa', + awayScore: '0', + baseData: firstHalfAggData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Champions League', urn: 'urn:bbc:sportsdata:football:tournament:champions-league' }, + homeActions: firstHalfAggData.home.actions, + awayActions: firstHalfAggData.away.actions +}; + +ExtraTimeSecondLegConcise.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: secondLegETData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: secondLegETData.home.actions, + awayActions: secondLegETData.away.actions +}; + +InPenaltiesAfterExtraTimeSecondLegConcise.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: secondLegAETInPensData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: secondLegAETInPensData.home.actions, + awayActions: secondLegAETInPensData.away.actions +}; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-post-events.stories.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-post-events.stories.jsx new file mode 100644 index 00000000000..a5854f30d4a --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-post-events.stories.jsx @@ -0,0 +1,265 @@ +import { BREAKPOINT_VIEWPORTS } from '@bbc/web-gel-foundations'; +import { INITIAL_VIEWPORTS } from 'storybook/viewport'; +import { + postEventData, + postEventAETData, + postEventAgg90Data, + postEventPens90Data, + postEventPensAetData, + postEventPensAetAggData, + finishedAetAggData +} from '@bbc/web-sport-utils/tests/static-data/football/event/transformed/post-event/index.js'; + +import mdx from './head-to-head-v2.mdx'; +import metadata from './metadata.json'; +import { HeadToHeadV2 } from './head-to-head-v2.jsx'; +import { shortNamesMap } from './storybook/helpers/short-name-map.js'; +import { HeadToHeadV2Component, HeadToHeadV2ConciseComponent } from './storybook/helpers/base-component.jsx'; + +import venuesData from './static-data/premier-league-venues.json'; + +const { venues } = venuesData; + +export default { + title: 'Components/Presentation/Head To Head V2/Post Events', + component: HeadToHeadV2, + tags: ['autodocs'], + parameters: { + metadata, + docs: { + description: { + component: 'The `Head to Head V2` component is used to render event data.' + }, + page: mdx + }, + chromatic: { + viewports: [375, ...BREAKPOINT_VIEWPORTS] + }, + viewport: { + viewports: INITIAL_VIEWPORTS + } + }, + globals: { + corePalette: 'lightAlternative', + servicePalette: 'sportLight', + fontPalette: 'sansSimple' + }, + argTypes: { + home: { + options: Object.keys(shortNamesMap()), + control: { type: 'select' } + }, + away: { + options: Object.keys(shortNamesMap()), + control: { type: 'select' } + }, + venue: { + options: venues, + control: { type: 'select' } + }, + status: { + table: { disable: true } + }, + date: { control: 'date' } + }, + args: { + isView: false + } +}; + +export const FullTimeAfter90Mins = HeadToHeadV2Component.bind({}); +export const AfterExtraTime = HeadToHeadV2Component.bind({}); +export const FullTimeAfter90MinsSecondLeg = HeadToHeadV2Component.bind({}); +export const PenaltiesAfter90Mins = HeadToHeadV2Component.bind({}); +export const PenaltiesAfterExtraTime = HeadToHeadV2Component.bind({}); +export const PenaltiesAfterExtraTimeSecondLeg = HeadToHeadV2Component.bind({}); +export const AfterExtraTimeSecondLeg = HeadToHeadV2Component.bind({}); +export const FullTimeAfter90MinsConcise = HeadToHeadV2ConciseComponent.bind({}); +export const AfterExtraTimeConcise = HeadToHeadV2ConciseComponent.bind({}); +export const FullTimeAfter90MinsSecondLegConcise = HeadToHeadV2ConciseComponent.bind({}); +export const PenaltiesAfter90MinsConcise = HeadToHeadV2ConciseComponent.bind({}); +export const PenaltiesAfterExtraTimeConcise = HeadToHeadV2ConciseComponent.bind({}); +export const PenaltiesAfterExtraTimeSecondLegConcise = HeadToHeadV2ConciseComponent.bind({}); +export const AfterExtraTimeSecondLegConcise = HeadToHeadV2ConciseComponent.bind({}); + +FullTimeAfter90Mins.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: postEventData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' }, + homeActions: postEventData.home.actions, + awayActions: postEventData.away.actions +}; + +AfterExtraTime.args = { + home: 'Arsenal', + homeScore: '1', + away: 'Aston Villa', + awayScore: '2', + baseData: postEventAETData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'FA Cup', urn: 'urn:bbc:sportsdata:football:tournament:fa-cup' }, + homeActions: postEventAETData.home.actions, + awayActions: postEventAETData.away.actions +}; + +FullTimeAfter90MinsSecondLeg.args = { + home: 'Arsenal', + homeScore: '1', + away: 'Aston Villa', + awayScore: '1', + baseData: postEventAgg90Data, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: postEventAgg90Data.home.actions, + awayActions: postEventAgg90Data.away.actions +}; + +PenaltiesAfter90Mins.args = { + home: 'Arsenal', + homeScore: '1', + away: 'Aston Villa', + awayScore: '1', + baseData: postEventPens90Data, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'League Cup', urn: 'urn:bbc:sportsdata:football:tournament:league-cup' }, + homeActions: postEventPens90Data.home.actions, + awayActions: postEventPens90Data.away.actions +}; + +PenaltiesAfterExtraTime.args = { + home: 'Arsenal', + homeScore: '1', + away: 'Aston Villa', + awayScore: '1', + baseData: postEventPensAetData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'FA Cup', urn: 'urn:bbc:sportsdata:football:tournament:fa-cup' }, + homeActions: postEventPensAetData.home.actions, + awayActions: postEventPensAetData.away.actions +}; + +PenaltiesAfterExtraTimeSecondLeg.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: postEventPensAetAggData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: postEventPensAetAggData.home.actions, + awayActions: postEventPensAetAggData.away.actions +}; + +AfterExtraTimeSecondLeg.args = { + home: 'Arsenal', + homeScore: '4', + away: 'Aston Villa', + awayScore: '2', + baseData: finishedAetAggData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: finishedAetAggData.home.actions, + awayActions: finishedAetAggData.away.actions +}; + +FullTimeAfter90MinsConcise.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: postEventData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' }, + homeActions: postEventData.home.actions, + awayActions: postEventData.away.actions +}; + +AfterExtraTimeConcise.args = { + home: 'Arsenal', + homeScore: '1', + away: 'Aston Villa', + awayScore: '2', + baseData: postEventAETData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'FA Cup', urn: 'urn:bbc:sportsdata:football:tournament:fa-cup' }, + homeActions: postEventAETData.home.actions, + awayActions: postEventAETData.away.actions +}; + +FullTimeAfter90MinsSecondLegConcise.args = { + home: 'Arsenal', + homeScore: '1', + away: 'Aston Villa', + awayScore: '1', + baseData: postEventAgg90Data, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: postEventAgg90Data.home.actions, + awayActions: postEventAgg90Data.away.actions +}; + +PenaltiesAfter90MinsConcise.args = { + home: 'Arsenal', + homeScore: '1', + away: 'Aston Villa', + awayScore: '1', + baseData: postEventPens90Data, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'League Cup', urn: 'urn:bbc:sportsdata:football:tournament:league-cup' }, + homeActions: postEventPens90Data.home.actions, + awayActions: postEventPens90Data.away.actions +}; + +PenaltiesAfterExtraTimeConcise.args = { + home: 'Arsenal', + homeScore: '1', + away: 'Aston Villa', + awayScore: '1', + baseData: postEventPensAetData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'FA Cup', urn: 'urn:bbc:sportsdata:football:tournament:fa-cup' }, + homeActions: postEventPensAetData.home.actions, + awayActions: postEventPensAetData.away.actions +}; + +PenaltiesAfterExtraTimeSecondLegConcise.args = { + home: 'Arsenal', + homeScore: '2', + away: 'Aston Villa', + awayScore: '2', + baseData: postEventPensAetAggData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: postEventPensAetAggData.home.actions, + awayActions: postEventPensAetAggData.away.actions +}; + +AfterExtraTimeSecondLegConcise.args = { + home: 'Arsenal', + homeScore: '4', + away: 'Aston Villa', + awayScore: '2', + baseData: finishedAetAggData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'UEFA Europa League', urn: 'urn:bbc:sportsdata:football:tournament:europa-league' }, + homeActions: finishedAetAggData.home.actions, + awayActions: finishedAetAggData.away.actions +}; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-pre-events.stories.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-pre-events.stories.jsx new file mode 100644 index 00000000000..039d210f7f7 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-pre-events.stories.jsx @@ -0,0 +1,120 @@ +import { BREAKPOINT_VIEWPORTS } from '@bbc/web-gel-foundations'; +import { INITIAL_VIEWPORTS } from 'storybook/viewport'; +import { preEventData } from '@bbc/web-sport-utils/tests/static-data/football/event/transformed/pre-event/index.js'; + +import mdx from './head-to-head-v2.mdx'; +import metadata from './metadata.json'; +import { HeadToHeadV2 } from './head-to-head-v2.jsx'; +import { shortNamesMap } from './storybook/helpers/short-name-map.js'; + +import { HeadToHeadV2Component, HeadToHeadV2ConciseComponent } from './storybook/helpers/base-component.jsx'; + +import venuesData from './static-data/premier-league-venues.json'; + +const { venues } = venuesData; + +export default { + title: 'Components/Presentation/Head To Head V2/Pre Events', + component: HeadToHeadV2, + tags: ['autodocs'], + parameters: { + metadata, + docs: { + description: { + component: 'The `Head to Head V2` container is used to render event data.' + }, + page: mdx + }, + chromatic: { + viewports: [375, ...BREAKPOINT_VIEWPORTS] + }, + viewport: { + viewports: INITIAL_VIEWPORTS + } + }, + globals: { + corePalette: 'lightAlternative', + servicePalette: 'sportLight', + fontPalette: 'sansSimple' + }, + argTypes: { + home: { + options: Object.keys(shortNamesMap()), + control: { type: 'select' } + }, + away: { + options: Object.keys(shortNamesMap()), + control: { type: 'select' } + }, + venue: { + options: venues, + control: { type: 'select' } + }, + status: { + table: { disable: true } + }, + date: { control: 'date' } + } +}; + +export const PreEventConcise = HeadToHeadV2ConciseComponent.bind({}); +export const PreEventConciseOneTeam = HeadToHeadV2ConciseComponent.bind({}); +export const PreEventConciseNoTeams = HeadToHeadV2ConciseComponent.bind({}); + +PreEventConcise.args = { + home: 'Arsenal', + away: 'Aston Villa', + baseData: preEventData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' } +}; + +PreEventConciseOneTeam.args = { + home: 'TBC', + away: 'Aston Villa', + baseData: preEventData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'To be confirmed', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' } +}; + +PreEventConciseNoTeams.args = { + home: 'TBC', + away: 'TBC', + baseData: preEventData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'To be confirmed', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' } +}; + +export const PreEvent = HeadToHeadV2Component.bind({}); +export const PreEventOneTeam = HeadToHeadV2Component.bind({}); +export const PreEventNoTeams = HeadToHeadV2Component.bind({}); + +PreEvent.args = { + home: 'Arsenal', + away: 'Aston Villa', + baseData: preEventData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'Emirates Stadium', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' } +}; + +PreEventOneTeam.args = { + home: 'TBC', + away: 'Aston Villa', + baseData: preEventData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'To be confirmed', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' } +}; + +PreEventNoTeams.args = { + home: 'TBC', + away: 'TBC', + baseData: preEventData, + date: new Date('2023-01-01T13:00:00Z'), + venue: 'To be confirmed', + tournament: { name: 'Premier League', urn: 'urn:bbc:sportsdata:football:tournament:premier-league' } +}; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-rugby-events.stories.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-rugby-events.stories.jsx new file mode 100644 index 00000000000..8e155b96d9a --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2-rugby-events.stories.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { HeadToHeadV2 } from './head-to-head-v2.jsx'; +import { rugbyUnionPostEvent } from './static-data/transformed/rugby-event/index.js'; + +export default { + title: 'Components/Presentation/Head To Head V2/Rugby Events', + component: HeadToHeadV2, + parameters: { + chromatic: { + disableSnapshot: true + } + }, + globals: { + corePalette: 'lightAlternative', + servicePalette: 'sportLight', + fontPalette: 'sansSimple' + }, + args: { + data: rugbyUnionPostEvent + } +}; + +export const RugbyUnion = args => ; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2.d.ts b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2.d.ts new file mode 100644 index 00000000000..21bb54c4d00 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2.d.ts @@ -0,0 +1,162 @@ +import type { JSX } from 'react'; + +declare enum EventStatus { + PreEvent = 'PreEvent', + MidEvent = 'MidEvent', + PostEvent = 'PostEvent', + Abandoned = 'Abandoned', + Cancelled = 'Cancelled', + Suspended = 'Suspended', + Postponed = 'Postponed' +} + +type Action = { + type: string; + typeLabel: { + value: string; + accessible: string; + }; + timeLabel: { + value: string; + accessible: string; + }; +}; + +type Actions = { + playerId: string; + playerName: string; + actionType: string; + actions: Action[]; +}; + +type GroupedActions = { + /** + * The name of the grouped action e.g. Penalties, Tries. + */ + groupName: { fullName: string; shortName: string }; + /** + * The home team action details. + */ + homeTeamActions: string[]; + /** + * The away team action details. + */ + awayTeamActions: string[]; +}; + +type Team = { + /** + * The Team's unique id. + */ + id: string; + /** + * Full name of the team. + */ + fullName: string; + /** + * Abbreviated name of the team. + */ + shortName: string; + /** + * The urn for the team. This is used to lookup the team badge. + * If no urn is provided or a badge does not exist, a placeholder badge is used. + */ + urn?: string; + /** + * The fulltime and halftime running scores for the team. + */ + runningScore: { halftime?: string; fulltime?: string }; + /** + * The current team score. + */ + score?: string; + /** + * This attribute is an early indication of a goal. It collects a goal event, significantly shortening the time you receive score updates. + * Fallback to score total value (when there are no unconfirmed goals) + */ + scoreUnconfirmed?: string; + /** + * The team actions displayed as a summary. Any actions that are available will be rendered. + * Actions are not rendered in concise view. + */ + actions?: Actions[]; +}; + +export type HeadToHeadV2Data = { + /** + * The status of the event. + */ + status: EventStatus; + /** + * The date of the event in 'EEE d MMM yyyy' format. E.g. 'Sat 28 Oct 2023'. + */ + date: string; + /** + * The tournament details. + */ + tournament: { + id: string; + name: string; + urn: string; + }; + /** + * The name/description of the tournament. + */ + tournamentDescriptionLabel: string; + /** + * The current period of the event e.g FT, HT. + */ + periodLabel?: { value: string; accessible: string }; + /** + * Actions for the home and away teams grouped by group name. + * Any grouped actions that are available will be rendered. + * Actions are not rendered in concise view. + */ + groupedActions?: GroupedActions[]; + /** + * Details and scores for home team. + */ + home: Team; + /** + * Details and scores for home team. + */ + away: Team; + /** + * Time of the event in 'HH:mm' format. + */ + time: { displayTimeUK: string; accessibleTime: string }; + /** + * Name of the venue. + * Venue is not rendered in concise view. + * @default 'To be confirmed' + */ + venue?: { name: string }; + /** + * Summary of event to be used with assistive technology. + */ + accessibleEventSummary: string; +}; + +export declare const HeadToHeadV2: (props: { + data: HeadToHeadV2Data; + isConciseView: boolean; + shouldHideBadges: boolean; + shouldShowActions: boolean; + /** + * The maximum number of digits (i.e. characters) in any score in a stack of H2Hv2 components. This ensures that + * the badges/teams line up horizontally the whole way down the stack, without adding extra padding when not required. + * + * By default, the central section of H2Hv2 will expand as little as possible to fit the given score. + */ + maximumContainerScoreDigits?: string; + /** + * Optional setting for the sport badge's placeholder fallback type when a mapping doesn't exist for a team. + * + * Used to e.g. fall back to a grey rectangle instead of a badge icon when the page predominantly shows flags. + * + * @default 'badge' + */ + teamBadgePlaceholderFallbackType?: 'badge' | 'flag'; +}) => JSX.Element; + +export default HeadToHeadV2; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2.jsx b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2.jsx new file mode 100644 index 00000000000..85edfded22a --- /dev/null +++ b/ws-nextjs-app/pages/[service]/live/[id]/SportDataHeader/head-to-head-v2/head-to-head-v2.jsx @@ -0,0 +1,88 @@ +// import React from 'react'; +// import styled from '@bbc/web-styled'; +import styled from '@emotion/styled'; +// import { fontStandard, GROUP_3, SPACING_2 } from '@bbc/web-gel-foundations'; +// import { shouldShowTeamBadges } from '@bbc/web-sport-utils'; +import shouldShowTeamBadges from './helpers/badges/should-show-team-badges'; + +import Footer from './components/footer'; +import HeadToHeadHeader from './components/head-to-head-header.jsx'; +import { getStatusBorderStyles } from './helpers/colour-styles.js'; +import { HeadToHeadBanner } from './components/head-to-head-banner.jsx'; +import { ConditionalOnwardJourneyLink } from './components/conditional-onward-journey-link.jsx'; +import { Actions } from './components/actions.jsx'; + +const StyledHeadToHeadWrapper = styled.div` + background: ${({ isConciseView }) => (isConciseView ? '#202020' : '#181818')}; + border-left: ${({ status, isConciseView }) => + getStatusBorderStyles({ status, isConciseView })}; +`; + +const StyledHeadToHead = styled.div` + font-family: 'ReithSans, Helvetica, Arial, freesans, sans-serif'; + font-weight: 400; + font-feature-settings: 'ss01' off; + color: '#F8F8F8'; + padding: ${({ isConciseView }) => (isConciseView ? `8px` : `0`)}; + + @media (max-width: 600px) { + padding-top: ${({ isConciseView }) => (isConciseView ? `8px` : `0`)}; + } +`; + +/** + * @type {typeof import('./head-to-head-v2.d.ts').HeadToHeadV2} + */ +export const HeadToHeadV2 = ({ + data, + isConciseView, + shouldShowActions, + maximumContainerScoreDigits, + teamBadgePlaceholderFallbackType = 'badge', +}) => { + const hasActions = + data?.home?.actions?.length > 0 || data?.away?.actions?.length > 0; + const shouldHideBadges = !shouldShowTeamBadges(data.tournament?.urn); + + return ( + + + + {!isConciseView && ( + + )} + + {hasActions && shouldShowActions && } + {!isConciseView && } + {!isConciseView && ( +