|
| 1 | +import { css } from '@emotion/react'; |
| 2 | +import { |
| 3 | + from, |
| 4 | + headlineBold24, |
| 5 | + space, |
| 6 | + until, |
| 7 | +} from '@guardian/source/foundations'; |
| 8 | +import { StraightLines } from '@guardian/source-development-kitchen/react-components'; |
| 9 | +import { grid } from '../grid'; |
| 10 | +import { decideFormat } from '../lib/articleFormat'; |
| 11 | +import { useApi } from '../lib/useApi'; |
| 12 | +import { palette } from '../palette'; |
| 13 | +import type { DCRFrontCard } from '../types/front'; |
| 14 | +import type { FETrailType } from '../types/trails'; |
| 15 | +import { Card } from './Card/Card'; |
| 16 | +import type { Props as CardProps } from './Card/Card'; |
| 17 | +import { LeftColumn } from './LeftColumn'; |
| 18 | +import { Placeholder } from './Placeholder'; |
| 19 | + |
| 20 | +type OnwardsResponse = { |
| 21 | + trails: FETrailType[]; |
| 22 | + heading: string; |
| 23 | + displayname: string; |
| 24 | + description: string; |
| 25 | +}; |
| 26 | + |
| 27 | +const standardCardListStyles = css` |
| 28 | + width: 100%; |
| 29 | + display: grid; |
| 30 | + gap: 20px; |
| 31 | +
|
| 32 | + ${from.tablet} { |
| 33 | + grid-template-columns: repeat(4, 1fr); |
| 34 | + padding-top: ${space[2]}px; |
| 35 | + } |
| 36 | +
|
| 37 | + ${from.leftCol} { |
| 38 | + &::before { |
| 39 | + content: ''; |
| 40 | + position: absolute; |
| 41 | + left: -11px; |
| 42 | + bottom: 0; |
| 43 | + top: 8px; |
| 44 | + width: 1px; |
| 45 | + background-color: ${palette('--onward-content-border')}; |
| 46 | + } |
| 47 | + } |
| 48 | +`; |
| 49 | + |
| 50 | +const cardsContainerStyles = css` |
| 51 | + ${grid.column.centre} |
| 52 | + position: relative; |
| 53 | +
|
| 54 | + ${from.desktop} { |
| 55 | + ${grid.between('centre-column-start', 'right-column-end')} |
| 56 | + } |
| 57 | +`; |
| 58 | + |
| 59 | +const splashCardStyles = css` |
| 60 | + margin-bottom: ${space[4]}px; |
| 61 | + ${from.leftCol} { |
| 62 | + margin-top: ${space[2]}px; |
| 63 | +
|
| 64 | + &::before { |
| 65 | + content: ''; |
| 66 | + position: absolute; |
| 67 | + left: -11px; |
| 68 | + top: 8px; |
| 69 | + bottom: 0; |
| 70 | + width: 1px; |
| 71 | + background-color: ${palette('--onward-content-border')}; |
| 72 | + } |
| 73 | + } |
| 74 | +`; |
| 75 | + |
| 76 | +const standardCardStyles = css` |
| 77 | + position: relative; |
| 78 | + display: flex; |
| 79 | +
|
| 80 | + ${from.tablet} { |
| 81 | + :not(:first-child)::before { |
| 82 | + content: ''; |
| 83 | + position: absolute; |
| 84 | + top: 0; |
| 85 | + bottom: 0; |
| 86 | + left: -11px; |
| 87 | + width: 1px; |
| 88 | + background: ${palette('--onward-content-border')}; |
| 89 | + } |
| 90 | + } |
| 91 | +`; |
| 92 | + |
| 93 | +const containerStyles = css` |
| 94 | + display: flex; |
| 95 | + flex-direction: column; |
| 96 | + padding-bottom: ${space[6]}px; |
| 97 | +
|
| 98 | + ${from.leftCol} { |
| 99 | + flex-direction: row; |
| 100 | + padding-right: 80px; |
| 101 | + } |
| 102 | +`; |
| 103 | + |
| 104 | +const headerStyles = css` |
| 105 | + ${headlineBold24}; |
| 106 | + color: ${palette('--carousel-text')}; |
| 107 | + padding-bottom: ${space[2]}px; |
| 108 | + padding-top: ${space[1]}px; |
| 109 | + margin-left: 0; |
| 110 | +`; |
| 111 | + |
| 112 | +const mobileHeaderStyles = css` |
| 113 | + ${headerStyles}; |
| 114 | + ${from.tablet} { |
| 115 | + padding-left: 4px; |
| 116 | + } |
| 117 | + ${from.leftCol} { |
| 118 | + display: none; |
| 119 | + } |
| 120 | +`; |
| 121 | + |
| 122 | +const convertFETrailToDcrTrail = ( |
| 123 | + trails: FETrailType[], |
| 124 | + discussionApiUrl: string, |
| 125 | +): DCRFrontCard[] => |
| 126 | + trails.map((trail) => ({ |
| 127 | + dataLinkName: 'onwards-content-card', |
| 128 | + discussionId: trail.discussion?.discussionId, |
| 129 | + discussionApiUrl, |
| 130 | + format: decideFormat(trail.format), |
| 131 | + headline: trail.headline, |
| 132 | + image: { |
| 133 | + src: trail.masterImage ?? '', |
| 134 | + altText: trail.linkText ?? '', |
| 135 | + }, |
| 136 | + isExternalLink: false, |
| 137 | + onwardsSource: 'related-content', |
| 138 | + showLivePlayable: false, |
| 139 | + showQuotedHeadline: false, |
| 140 | + url: trail.url, |
| 141 | + webPublicationDate: trail.webPublicationDate, |
| 142 | + isImmersive: false, |
| 143 | + })); |
| 144 | + |
| 145 | +const getDefaultCardProps = ( |
| 146 | + trail: DCRFrontCard, |
| 147 | + isInOnwardsAbTestVariantStandardCard: boolean, |
| 148 | +) => { |
| 149 | + const defaultProps: CardProps = { |
| 150 | + aspectRatio: '5:4', |
| 151 | + avatarUrl: trail.avatarUrl, |
| 152 | + branding: trail.branding, |
| 153 | + byline: trail.byline, |
| 154 | + dataLinkName: `onwards-content-gallery-style ${trail.dataLinkName}`, |
| 155 | + discussionApiUrl: trail.discussionApiUrl, |
| 156 | + discussionId: trail.discussionId, |
| 157 | + format: trail.format, |
| 158 | + headlineText: trail.headline, |
| 159 | + image: trail.image, |
| 160 | + imageLoading: 'lazy', |
| 161 | + isCrossword: trail.isCrossword, |
| 162 | + isExternalLink: false, |
| 163 | + isInOnwardsAbTestVariantStandardCard, |
| 164 | + isOnwardContent: true, |
| 165 | + kickerText: trail.kickerText, |
| 166 | + linkTo: trail.url, |
| 167 | + mainMedia: trail.mainMedia, |
| 168 | + onwardsSource: 'related-content', |
| 169 | + showAge: false, |
| 170 | + showByline: trail.showByline, |
| 171 | + showClock: false, |
| 172 | + showPulsingDot: false, |
| 173 | + showQuotedHeadline: trail.showQuotedHeadline, |
| 174 | + showTopBarDesktop: false, |
| 175 | + showTopBarMobile: false, |
| 176 | + snapData: trail.snapData, |
| 177 | + starRating: trail.starRating, |
| 178 | + trailText: trail.trailText, |
| 179 | + webPublicationDate: trail.webPublicationDate, |
| 180 | + }; |
| 181 | + |
| 182 | + return defaultProps; |
| 183 | +}; |
| 184 | + |
| 185 | +type Props = { |
| 186 | + url: string; |
| 187 | + discussionApiUrl: string; |
| 188 | + isInOnwardsAbTestVariant?: boolean; |
| 189 | +}; |
| 190 | + |
| 191 | +/** |
| 192 | + * We are running an AB test to use the More Galleries style of onwards content on all articles. |
| 193 | + * If the test is successful, this component will likely be removed the MoreGalleries.tsx will |
| 194 | + * be generalised so that is can be used for onwards content across all articles. |
| 195 | + */ |
| 196 | +export const MoreGalleriesStyleOnwardsContent = ({ |
| 197 | + url, |
| 198 | + discussionApiUrl, |
| 199 | + isInOnwardsAbTestVariant, |
| 200 | +}: Props) => { |
| 201 | + const { data, error } = useApi<OnwardsResponse>(url); |
| 202 | + |
| 203 | + if (error) { |
| 204 | + // Send the error to Sentry and then prevent the element from rendering |
| 205 | + window.guardian.modules.sentry.reportError(error, 'onwards-lower'); |
| 206 | + return null; |
| 207 | + } |
| 208 | + |
| 209 | + if (!data?.trails) { |
| 210 | + return ( |
| 211 | + <Placeholder |
| 212 | + heights={ |
| 213 | + new Map([ |
| 214 | + ['mobile', 900], |
| 215 | + ['tablet', 600], |
| 216 | + ['desktop', 900], |
| 217 | + ]) |
| 218 | + } |
| 219 | + shouldShimmer={false} |
| 220 | + backgroundColor={palette('--article-background')} |
| 221 | + /> |
| 222 | + ); |
| 223 | + } |
| 224 | + |
| 225 | + const trails: DCRFrontCard[] = convertFETrailToDcrTrail( |
| 226 | + data.trails, |
| 227 | + discussionApiUrl, |
| 228 | + ); |
| 229 | + |
| 230 | + const [firstTrail, ...standardTrails] = trails; |
| 231 | + if (!firstTrail) return null; |
| 232 | + |
| 233 | + const heading = data.heading || data.displayname; |
| 234 | + |
| 235 | + return ( |
| 236 | + <div |
| 237 | + data-component="onwards-content-gallery-style" |
| 238 | + css={containerStyles} |
| 239 | + > |
| 240 | + <LeftColumn> |
| 241 | + <h2 css={headerStyles}> |
| 242 | + <span>{heading}</span> |
| 243 | + </h2> |
| 244 | + </LeftColumn> |
| 245 | + <h2 css={mobileHeaderStyles}> |
| 246 | + <span>{heading}</span> |
| 247 | + </h2> |
| 248 | + <div> |
| 249 | + <div css={[cardsContainerStyles, splashCardStyles]}> |
| 250 | + <Card |
| 251 | + {...getDefaultCardProps(firstTrail, false)} |
| 252 | + mediaPositionOnDesktop="right" |
| 253 | + mediaPositionOnMobile="top" |
| 254 | + mediaSize="medium" |
| 255 | + headlineSizes={{ |
| 256 | + desktop: 'small', |
| 257 | + tablet: 'small', |
| 258 | + mobile: 'small', |
| 259 | + }} |
| 260 | + /> |
| 261 | + </div> |
| 262 | + <StraightLines |
| 263 | + cssOverrides={[ |
| 264 | + cardsContainerStyles, |
| 265 | + css` |
| 266 | + ${until.tablet} { |
| 267 | + display: none; |
| 268 | + } |
| 269 | + `, |
| 270 | + ]} |
| 271 | + count={1} |
| 272 | + color={palette('--onward-content-border')} |
| 273 | + /> |
| 274 | + <ul css={[cardsContainerStyles, standardCardListStyles]}> |
| 275 | + {standardTrails.slice(0, 4).map((trail) => ( |
| 276 | + <li key={trail.url} css={standardCardStyles}> |
| 277 | + <Card |
| 278 | + {...getDefaultCardProps( |
| 279 | + trail, |
| 280 | + !!isInOnwardsAbTestVariant, |
| 281 | + )} |
| 282 | + mediaPositionOnDesktop="bottom" |
| 283 | + mediaPositionOnMobile="left" |
| 284 | + mediaSize="small" |
| 285 | + /> |
| 286 | + </li> |
| 287 | + ))} |
| 288 | + </ul> |
| 289 | + </div> |
| 290 | + </div> |
| 291 | + ); |
| 292 | +}; |
0 commit comments