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

{ const count = format.design === ArticleDesign.Comment ? 8 : 4; - switch (format.theme) { - case Pillar.Sport: - return ; - default: - return ( - - ); + if ( + format.theme === Pillar.Sport && + format.design !== ArticleDesign.Picture + ) { + return ; } + + return ( + + ); }; diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index e912f480019..5a31ce8480a 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -75,6 +75,13 @@ const chooseWrapper = (format: ArticleFormat) => { 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 b6dd7f4a27c..091adfd22be 100644 --- a/dotcom-rendering/src/layouts/DecideLayout.tsx +++ b/dotcom-rendering/src/layouts/DecideLayout.tsx @@ -14,7 +14,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 { @@ -91,15 +90,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/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 8d24f6d3b8f..6125d6f2e05 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -20,6 +20,7 @@ import { ArticleMetaApps } from '../components/ArticleMeta.apps'; import { ArticleMeta } from '../components/ArticleMeta.web'; import { ArticleTitle } from '../components/ArticleTitle'; import { Carousel } from '../components/Carousel.island'; +import { ContributorAvatar } from '../components/ContributorAvatar'; import { CricketMatchHeaderWrapper } from '../components/CricketMatchHeaderWrapper.island'; import { DecideLines } from '../components/DecideLines'; import { DirectoryPageNavIsland } from '../components/DirectoryPageNavIsland'; @@ -52,6 +53,7 @@ import { type ArticleFormat, ArticleSpecial, } from '../lib/articleFormat'; +import { getSoleContributor } from '../lib/byline'; import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideStoryPackageTrails } from '../lib/decideTrail'; @@ -81,6 +83,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; @@ -168,6 +218,7 @@ export const StandardLayout = (props: WebProps | AppProps) => { const isVideo = format.design === ArticleDesign.Video; const isShowcase = format.display === ArticleDisplay.Showcase; + const isPicture = format.design === ArticleDesign.Picture; const showComments = article.isCommentable && !isPaidContent; @@ -183,9 +234,18 @@ export const StandardLayout = (props: WebProps | AppProps) => { const layoutType: LayoutType = isMedia ? 'media' - : isShowcase - ? 'showcase' - : 'standard'; + : isPicture + ? 'picture' + : isShowcase + ? 'showcase' + : 'standard'; + + const avatarUrl = getSoleContributor( + article.tags, + article.byline, + )?.bylineLargeImageUrl; + + const displayAvatarUrl = avatarUrl ? true : false; return ( <> @@ -282,7 +342,17 @@ export const StandardLayout = (props: WebProps | AppProps) => { `, ]} > - + { area="title" layoutType={layoutType} element="aside" + css={css` + display: flex; + flex-direction: column; + justify-content: space-between; + `} > { guardianBaseURL={article.guardianBaseURL} isMatch={!!footballMatchUrl} /> + {displayAvatarUrl && ( + + + + )} - - - + {displayAvatarUrl ? ( + +
+ + +
+ {!!avatarUrl && ( +
+ +
+ )} + +
+
+
+ ) : ( + + + + )} { format.theme === ArticleSpecial.Labs && format.design !== ArticleDesign.Video ? ( - ) : ( + ) : !displayAvatarUrl ? ( - )} + ) : null}

{isApps ? ( <> @@ -595,30 +721,32 @@ export const StandardLayout = (props: WebProps | AppProps) => { } `} > - - - - - + {!isPicture && ( + + + + + + )} diff --git a/dotcom-rendering/src/layouts/lib/articleArrangements.ts b/dotcom-rendering/src/layouts/lib/articleArrangements.ts index 2cf5fb6843f..00e4ec032cc 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,48 @@ const mediaCss: LayoutCssMap = { }, }; +const pictureCss: LayoutCssMap = { + title: { + mobile: 'grid-row: 1;', + tablet: 'grid-row: 1;', + leftCol: `grid-row: 1; ${grid.column.left}`, + }, + headline: { + mobile: 'grid-row: 2;', + tablet: 'grid-row: 2;', + desktop: `grid-row: 2; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: 'grid-row: 1;', + }, + standfirst: { + mobile: 'grid-row: 4;', + tablet: 'grid-row: 4;', + desktop: 'grid-row: 4;', + leftCol: 'grid-row: 2;', + }, + media: { + mobile: 'grid-row: 5;', + tablet: 'grid-row: 5;', + desktop: `grid-row: 5; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 3; ${grid.between('centre-column-start', 'right-column-end')};`, + }, + meta: { + mobile: 'grid-row: 3;', + tablet: 'grid-row: 3;', + desktop: `grid-row: 3; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 3 / span 2; ${grid.column.left};`, + }, + body: { + tablet: 'grid-row: 6;', + desktop: `grid-row: 6; ${grid.between('centre-column-start', 'right-column-end')};`, + leftCol: `grid-row: 5; ${grid.between('centre-column-start', 'right-column-end')};`, + }, +}; + const layoutCssMaps: Record = { standard: standardCss, showcase: showcaseCss, media: mediaCss, + picture: pictureCss, }; /**