diff --git a/dotcom-rendering/src/components/ArticleHeadline.tsx b/dotcom-rendering/src/components/ArticleHeadline.tsx index 3b0c141fc86..3dde08ed9b2 100644 --- a/dotcom-rendering/src/components/ArticleHeadline.tsx +++ b/dotcom-rendering/src/components/ArticleHeadline.tsx @@ -874,10 +874,13 @@ export const ArticleHeadline = ({ case ArticleDesign.Picture: return (

{ switch (format.theme) { case Pillar.Sport: - return ; + if (format.design !== ArticleDesign.Picture) { + return ; + } + return ( + + ); default: return ( { return noGutters; } } + case ArticleDisplay.Showcase: + switch (format.design) { + case ArticleDesign.Picture: + return ''; + default: + return noGutters; + } default: return noGutters; } diff --git a/dotcom-rendering/src/layouts/DecideLayout.tsx b/dotcom-rendering/src/layouts/DecideLayout.tsx index 77803138313..3dd8aae0ba2 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -13,7 +13,6 @@ import { ImmersiveLayout } from './ImmersiveLayout'; import { InteractiveLayout } from './InteractiveLayout'; import { LiveLayout } from './LiveLayout'; import { NewsletterSignupLayout } from './NewsletterSignupLayout'; -import { PictureLayout } from './PictureLayout'; import { StandardLayout } from './StandardLayout'; interface BaseProps { @@ -89,15 +88,6 @@ const DecideLayoutApps = ({ article, renderingTarget }: AppProps) => { serverTime={serverTime} /> ); - case ArticleDesign.Picture: - return ( - - ); default: return ( { serverTime={serverTime} /> ); - case ArticleDesign.Picture: - return ( - - ); default: return ( ( -
- {children} -
-); - -const maxWidth = css` - ${from.desktop} { - max-width: 620px; - } -`; - -const stretchLines = css` - ${until.phablet} { - margin-left: -20px; - margin-right: -20px; - } - ${until.mobileLandscape} { - margin-left: -10px; - margin-right: -10px; - } -`; - -const mainMediaWrapper = (displayAvatarUrl: boolean) => css` - position: relative; - ${until.phablet} { - margin-left: 20px; - margin-right: 20px; - } - ${until.mobileLandscape} { - margin-left: 10px; - margin-right: 10px; - } - ${displayAvatarUrl - ? css` - margin-top: 8px; - ` - : ``} -`; - -const avatarHeadlineWrapper = css` - display: flex; - flex-direction: column; - justify-content: space-between; -`; - -// This styling taken from the similar approach in CommentLayout.tsx -// If in mobile increase the margin top and margin right deficit -const avatarPositionStyles = css` - display: flex; - justify-content: flex-end; - position: relative; - margin-bottom: -29px; - pointer-events: none; - ${from.desktop} { - margin-top: -50px; - } - ${until.tablet} { - overflow: hidden; - } - - /* Why target img element? - - Because only in this context, where we have overflow: hidden - and the margin-bottom and margin-top of avatarPositionStyles - do we also want to apply our margin-right. These styles - are tightly coupled in this context, and so it does not - make sense to move them to the avatar component. - - It's imperfect from the perspective of DCR, the alternative is to bust - the combined elements into a separate component (with the - relevant stories) and couple them that way, which might be what - you want to do if you find yourself adding more styles - to this section. For now, this works without making me 🤢. - */ - - ${from.mobile} { - img { - margin-right: -1.85rem; - } - } - ${from.mobileLandscape} { - img { - margin-right: -1.25rem; - } - } -`; - -const LeftColLines = (displayAvatarUrl: boolean) => css` - margin-bottom: 4px; - ${displayAvatarUrl - ? css` - margin-top: -29px; - ` - : ''} -`; - -interface CommonProps { - article: ArticleDeprecated; - format: ArticleFormat; - renderingTarget: RenderingTarget; - serverTime?: number; -} - -interface WebProps extends CommonProps { - NAV: NavType; - renderingTarget: 'Web'; -} - -interface AppsProps extends CommonProps { - renderingTarget: 'Apps'; -} - -export const PictureLayout = (props: WebProps | AppsProps) => { - const { article, format, renderingTarget, serverTime } = props; - - const { - config: { isPaidContent, host, hasSurveyAd }, - } = article; - - const isWeb = renderingTarget === 'Web'; - const isApps = renderingTarget === 'Apps'; - - // TODO: - // 1) Read 'forceEpic' value from URL parameter and use it to force the slot to render - // 2) Otherwise, ensure slot only renders if `article.config.shouldHideReaderRevenue` equals false. - - const showComments = article.isCommentable && !isPaidContent; - - const { branding } = article.commercialProperties[article.editionId]; - - const contributionsServiceUrl = getContributionsServiceUrl(article); - - const renderAds = canRenderAds(article); - - const isWorldCup2026 = article.tags.some((tag) => tag.id === worldCupTagId); - - const avatarUrl = getSoleContributor( - article.tags, - article.byline, - )?.bylineLargeImageUrl; - - const displayAvatarUrl = avatarUrl ? true : false; - - return ( - <> - {isWeb && ( -
- {renderAds && ( - -
- -
-
- )} - tag.id)} - sectionId={article.config.section} - contentType={article.contentType} - /> -
- )} - - {isWeb && renderAds && hasSurveyAd && ( - - )} - -
- {isApps && renderAds && ( - - - - )} - -
- - - - - - - - - {displayAvatarUrl ? ( - -
-
- -
- -
- {!!avatarUrl && ( -
- -
- )} - -
-
-
- ) : ( - -
- -
-
- )} - - - - -
- -
-
- -
- -
-
- {isApps ? ( - <> - - - - - - - - ) : ( - - )} -
-
- - - - - - -
-
- - {isWeb && renderAds && ( -
- -
- )} - - {article.storyPackage && ( -
- - - -
- )} - - {isWeb && ( - - - - )} - {showComments && ( -
- -
- )} - - {!isPaidContent && ( -
- - - - - -
- )} - - {isWeb && renderAds && ( -
- -
- )} -
- - {isWeb && props.NAV.subNavSections && ( -
- - - -
- )} - - {isWeb && ( - <> -
-
-
- - - - - - - {renderAds && ( - - )} - - )} - {isApps && ( -
- - - -
- )} - - ); -}; diff --git a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx index 84fb7928e6d..b8bd6fee064 100644 --- a/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx +++ b/dotcom-rendering/src/layouts/StandardLayoutArticleGrid.tsx @@ -11,6 +11,7 @@ import { ArticleHeadline } from '../components/ArticleHeadline'; import { ArticleMetaApps } from '../components/ArticleMeta.apps'; import { ArticleMeta } from '../components/ArticleMeta.web'; import { ArticleTitle } from '../components/ArticleTitle'; +import { ContributorAvatar } from '../components/ContributorAvatar'; import { DecideLines } from '../components/DecideLines'; import { FootballMatchInfoWrapper } from '../components/FootballMatchInfoWrapper.island'; import { GuardianLabsLines } from '../components/GuardianLabsLines'; @@ -28,6 +29,7 @@ import { type ArticleFormat, ArticleSpecial, } from '../lib/articleFormat'; +import { getSoleContributor } from '../lib/byline'; import { getContributionsServiceUrl } from '../lib/contributions'; import { safeParseURL } from '../lib/parse'; import { parse } from '../lib/slot-machine-flags'; @@ -51,6 +53,54 @@ const stretchLines = css` } `; +const avatarHeadlineWrapper = css` + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +// This styling taken from the similar approach in CommentLayout.tsx +// If in mobile increase the margin top and margin right deficit +const avatarPositionStyles = css` + display: flex; + justify-content: flex-end; + position: relative; + margin-bottom: -29px; + pointer-events: none; + ${from.desktop} { + margin-top: -50px; + } + ${until.tablet} { + overflow: hidden; + } + + /* Why target img element? + + Because only in this context, where we have overflow: hidden + and the margin-bottom and margin-top of avatarPositionStyles + do we also want to apply our margin-right. These styles + are tightly coupled in this context, and so it does not + make sense to move them to the avatar component. + + It's imperfect from the perspective of DCR, the alternative is to bust + the combined elements into a separate component (with the + relevant stories) and couple them that way, which might be what + you want to do if you find yourself adding more styles + to this section. For now, this works without making me 🤢. + */ + + ${from.mobile} { + img { + margin-right: -1.85rem; + } + } + ${from.mobileLandscape} { + img { + margin-right: -1.25rem; + } + } +`; + interface GridItemProps { area: Area; layoutType: LayoutType; @@ -109,7 +159,7 @@ export const StandardLayoutArticleGrid = ({ const isMedia = format.design === ArticleDesign.Video || format.design === ArticleDesign.Audio; - const isShowcase = format.display === ArticleDisplay.Showcase; + const isPicture = format.design === ArticleDesign.Picture; const footballMatchUrl = article.matchType === 'FootballMatchType' @@ -119,11 +169,31 @@ export const StandardLayoutArticleGrid = ({ const isFootballMatchReport = format.design === ArticleDesign.MatchReport && !!footballMatchUrl; - const layoutType: LayoutType = isMedia - ? 'media' - : isShowcase - ? 'showcase' - : 'standard'; + const getLayoutType = (articleFormat: ArticleFormat): LayoutType => { + switch (articleFormat.display) { + case ArticleDisplay.Showcase: + return 'showcase'; + default: + switch (articleFormat.design) { + case ArticleDesign.Video: + case ArticleDesign.Audio: + return 'media'; + case ArticleDesign.Picture: + return 'picture'; + default: + return 'standard'; + } + } + }; + + const layoutType: LayoutType = getLayoutType(format); + + const avatarUrl = getSoleContributor( + article.tags, + article.byline, + )?.bylineLargeImageUrl; + + const displayAvatarUrl = avatarUrl ? true : false; return (
- + - + - + {displayAvatarUrl && isPicture ? ( +
+ + + {!!avatarUrl && isPicture && ( + <> +
+ +
+ + + )} +
+ ) : ( + + )}
@@ -401,30 +524,29 @@ export const StandardLayoutArticleGrid = ({ } `} > - - - - - + {!isPicture && ( + + + + + + )}
); diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 2cf5fb6843f..7cdc106e726 100644 --- a/dotcom-rendering/src/layouts/lib/articleArrangements.ts +++ b/dotcom-rendering/src/layouts/lib/articleArrangements.ts @@ -2,7 +2,7 @@ import { css, type SerializedStyles } from '@emotion/react'; import { from, until } from '@guardian/source/foundations'; import { grid } from '../../grid'; -export type LayoutType = 'standard' | 'showcase' | 'media'; +export type LayoutType = 'standard' | 'showcase' | 'media' | 'picture'; export type Area = | 'title' @@ -149,10 +149,47 @@ const mediaCss: LayoutCssMap = { }, }; +const pictureCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + leftCol: grid.column.left, + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: grid.between('centre-column-start', 'right-column-end'), + leftCol: 'grid-row: 1;', + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + leftCol: 'grid-row: 2;', + }, + media: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: grid.between('centre-column-start', 'right-column-end'), + leftCol: `grid-row: 3;`, + }, + meta: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: grid.between('centre-column-start', 'right-column-end'), + leftCol: `grid-row: 3 / span 2; ${grid.column.left};`, + }, + body: { + tablet: 'grid-row: 6;', + desktop: grid.between('centre-column-start', 'right-column-end'), + leftCol: `grid-row: 5;`, + }, +}; + const layoutCssMaps: Record = { standard: standardCss, showcase: showcaseCss, media: mediaCss, + picture: pictureCss, }; /**