diff --git a/dotcom-rendering/fixtures/manual/onwardsTrails.ts b/dotcom-rendering/fixtures/manual/onwardsTrails.ts new file mode 100644 index 00000000000..83647b85ca2 --- /dev/null +++ b/dotcom-rendering/fixtures/manual/onwardsTrails.ts @@ -0,0 +1,192 @@ +import { + ArticleDesign, + ArticleDisplay, + Pillar, +} from '../../src/lib/articleFormat'; +import { getDataLinkNameCard } from '../../src/lib/getDataLinkName'; +import { type TrailType } from '../../src/types/trails'; + +export const galleryOnwardsTrails: TrailType[] = [ + { + url: 'https://www.theguardian.com/environment/gallery/2025/aug/22/week-in-wildlife-a-clumsy-fox-swinging-orangutang-and-rescued-jaguarundi-cub', + linkText: + 'Week in wildlife: a clumsy fox, a swinging orangutan and a rescued jaguarundi cub', + showByline: false, + byline: 'Pejman Faratin', + image: { + src: 'https://media.guim.co.uk/a81e974ffee6c8c88fa280c2d02eaf5dc2af863e/151_292_1020_816/master/1020.jpg', + altText: '', + }, + format: { + theme: Pillar.News, + design: ArticleDesign.Gallery, + display: ArticleDisplay.Standard, + }, + webPublicationDate: '2022-01-01T06:00:25.000Z', + headline: + 'Week in wildlife: a clumsy fox, a swinging orangutan and a rescued jaguarundi cub', + shortUrl: 'https://www.theguardian.com/p/x32n89', + discussion: { + isCommentable: false, + isClosedForComments: true, + discussionId: '/p/x32n89', + }, + discussionId: 'zHoBy6HNKsk', + dataLinkName: getDataLinkNameCard( + { + theme: Pillar.News, + design: ArticleDesign.Gallery, + display: ArticleDisplay.Standard, + }, + '0', + 0, + ), + trailText: + 'Guinness World Records is looking back at the extraordinary feats achieved since its inception - as well as unveiling 70 whacky and unclaimed records ', + kickerText: 'Politics', // Get data for this + mainMedia: { type: 'Gallery', count: '6' }, + }, + { + url: 'https://www.theguardian.com/money/gallery/2025/aug/22/characterful-cottages-for-sale-in-england-in-pictures', + linkText: 'Characterful cottages for sale in England – in pictures', + showByline: false, + byline: 'Anna White', + image: { + src: 'https://media.guim.co.uk/58cd9356e6d68e8efa6028162bb959f9798307d5/515_0_5000_4000/master/5000.jpg', + altText: '', + }, + format: { + design: ArticleDesign.Gallery, + theme: Pillar.Lifestyle, + display: ArticleDisplay.Standard, + }, + webPublicationDate: '2022-01-01T06:00:24.000Z', + headline: 'Characterful cottages for sale in England – in pictures', + shortUrl: 'https://www.theguardian.com/p/x32gqj', + discussion: { + isCommentable: false, + isClosedForComments: true, + discussionId: '/p/x32gqj', + }, + dataLinkName: getDataLinkNameCard( + { + design: ArticleDesign.Gallery, + theme: Pillar.Lifestyle, + display: ArticleDisplay.Standard, + }, + '0', + 1, + ), + trailText: + 'Picked from a record 60,636 entries, the first images from the Natural History Museum’s wildlife photographer of the year competition have been released. The photographs, which range from a lion facing down a cobra to magnified mould spores, show the diversity, beauty and complexity of the natural world and humanity’s relationship with it', + mainMedia: { type: 'Gallery', count: '6' }, + }, + { + url: 'https://www.theguardian.com/news/gallery/2025/aug/22/sunsets-aid-parachutes-and-giant-pandas-photos-of-the-day-friday', + linkText: + 'Sunsets, aid parachutes and giant pandas: photos of the day – Friday ', + showByline: false, + byline: 'Eithne Staunton', + image: { + src: 'https://media.guim.co.uk/4ce0b080206fe9b65b976c1acf219d81072cc814/0_0_2113_1690/master/2113.png', + altText: '', + }, + format: { + design: ArticleDesign.Gallery, + theme: Pillar.News, + display: ArticleDisplay.Standard, + }, + webPublicationDate: '2022-01-01T08:49:42.000Z', + headline: + 'Sunsets, aid parachutes and giant pandas: photos of the day – Friday ', + shortUrl: 'https://www.theguardian.com/p/x3359z', + discussion: { + isCommentable: false, + isClosedForComments: true, + discussionId: '/p/x3359z', + }, + dataLinkName: getDataLinkNameCard( + { + design: ArticleDesign.Gallery, + theme: Pillar.News, + display: ArticleDisplay.Standard, + }, + '0', + 2, + ), + trailText: + 'From the mock-Tudor fad of the 1920s to drivers refuelling on a roundabout, each era produces its own distinctive petrol stations – as photographer Philip Butler discovered', + mainMedia: { type: 'Gallery', count: '6' }, + }, + { + url: 'https://www.theguardian.com/fashion/gallery/2025/aug/22/what-to-wear-to-notting-hill-carnival', + linkText: 'On parade: what to wear to Notting Hill carnival', + showByline: false, + byline: 'Melanie Wilkinson', + image: { + src: 'https://media.guim.co.uk/49a9656cd10c4f64f8bdd54380afb915c7a3648b/207_0_1500_1200/master/1500.jpg', + altText: '', + }, + format: { + design: ArticleDesign.Gallery, + theme: Pillar.Lifestyle, + display: ArticleDisplay.Standard, + }, + webPublicationDate: '2022-08-22T05:00:23.000Z', + headline: 'On parade: what to wear to Notting Hill carnival', + shortUrl: 'https://www.theguardian.com/p/x32mte', + discussion: { + isCommentable: false, + isClosedForComments: true, + discussionId: '/p/x32mte', + }, + dataLinkName: getDataLinkNameCard( + { + design: ArticleDesign.Gallery, + theme: Pillar.Lifestyle, + display: ArticleDisplay.Standard, + }, + '0', + 1, + ), + trailText: + 'The Guardian’s picture editors select photographs from around the world', + mainMedia: { type: 'Gallery', count: '6' }, + }, + { + url: 'https://www.theguardian.com/artanddesign/gallery/2025/aug/21/psychedelic-rock-glass-mountain-michael-lundgren', + linkText: + 'Psychedelic rock! Formations that mess with your mind – in pictures ', + showByline: false, + image: { + src: 'https://media.guim.co.uk/2810af61b2d2d2d5f71ec01e56e6555e0a6d4635/55_0_2813_2250/master/2813.jpg', + altText: '', + }, + format: { + design: ArticleDesign.Gallery, + theme: Pillar.Culture, + display: ArticleDisplay.Standard, + }, + webPublicationDate: '2025-08-21T06:01:01.000Z', + headline: + 'Psychedelic rock! Formations that mess with your mind – in pictures ', + shortUrl: 'https://www.theguardian.com/p/x2p663', + discussion: { + isCommentable: false, + isClosedForComments: true, + discussionId: '/p/x2p663', + }, + dataLinkName: getDataLinkNameCard( + { + design: ArticleDesign.Gallery, + theme: Pillar.Culture, + display: ArticleDisplay.Standard, + }, + '0', + 1, + ), + trailText: + 'Politicians and their partners put on their best show at this year’s Midwinter Ball, an annual dinner hosted by the Federal Parliamentary Press Gallery in Canberra', + mainMedia: { type: 'Gallery', count: '6' }, + }, +]; diff --git a/dotcom-rendering/src/components/Card/Card.tsx b/dotcom-rendering/src/components/Card/Card.tsx index 87a9dd8608b..2b496d005e7 100644 --- a/dotcom-rendering/src/components/Card/Card.tsx +++ b/dotcom-rendering/src/components/Card/Card.tsx @@ -79,6 +79,8 @@ export type Position = 'inner' | 'outer' | 'none'; export type Props = { linkTo: string; format: ArticleFormat; + /** The format of the article holding the card */ + contextFormat?: ArticleFormat; serverTime?: number; headlineText: string; headlineSizes?: ResponsiveFontSize; @@ -350,6 +352,7 @@ const liveBulletStyles = css` export const Card = ({ linkTo, format, + contextFormat, headlineText, headlineSizes, showQuotedHeadline, @@ -551,7 +554,14 @@ export const Card = ({ - */ const isMediaCardOrNewsletter = isMediaCard(format) || isNewsletter; - const showPill = isMediaCardOrNewsletter; + // This is due to a re-design for onwards content. + // Currently this re-design is only applied for galleries secondary onwards content. + // We plan to apply this to all onwards content in the future. + const isGallerySecondaryOnward = + contextFormat?.design === ArticleDesign.Gallery && + onwardsSource !== 'more-galleries'; + + const showPill = isMediaCardOrNewsletter && !isGallerySecondaryOnward; const media = getMedia({ imageUrl: image?.src, @@ -594,7 +604,8 @@ export const Card = ({ containerType === 'flexible/special' || containerType === 'flexible/general'; - const isSmallCard = containerType === 'scrollable/small'; + const isSmallCard = + containerType === 'scrollable/small' || isGallerySecondaryOnward; const hideTrailTextUntil = () => { if (isFlexibleContainer) { @@ -625,6 +636,13 @@ export const Card = ({ * Order matters here as the logic is based on the card properties */ const getGapSizes = (): GapSizes => { + if (isGallerySecondaryOnward) { + return { + row: 'medium', + column: 'medium', + }; + } + if (isOnwardContent) { return { row: 'none', @@ -739,7 +757,9 @@ export const Card = ({ onwardContent: boolean, ): 'large' | 'small' | undefined => { if (mediaCard && betaContainer) return 'large'; - if (mediaCard || onwardContent) return 'small'; + if ((mediaCard || onwardContent) && !isGallerySecondaryOnward) { + return 'small'; + } return undefined; }; @@ -826,6 +846,7 @@ export const Card = ({ showTopBarMobile={showTopBarMobile} isOnwardContent={isOnwardContent} containerPalette={containerPalette} + contextFormat={contextFormat} > @@ -1079,7 +1103,10 @@ export const Card = ({ loading={imageLoading} roundedCorners={ isOnwardContent && - !isMoreGalleriesOnwardContent + !( + isMoreGalleriesOnwardContent || + isGallerySecondaryOnward + ) } aspectRatio={aspectRatio} /> @@ -1124,7 +1151,10 @@ export const Card = ({ loading={imageLoading} roundedCorners={ isOnwardContent && - !isMoreGalleriesOnwardContent + !( + isMoreGalleriesOnwardContent || + isGallerySecondaryOnward + ) } aspectRatio="1:1" /> @@ -1155,7 +1185,7 @@ export const Card = ({ padContent={determinePadContent( isMediaCardOrNewsletter, isBetaContainer, - isOnwardContent && !isMoreGalleriesOnwardContent, + isOnwardContent, )} > {/* This div is needed to keep the headline and trail text justified at the start */} diff --git a/dotcom-rendering/src/components/Card/components/CardWrapper.tsx b/dotcom-rendering/src/components/Card/components/CardWrapper.tsx index 78476ce250e..2192252103a 100644 --- a/dotcom-rendering/src/components/Card/components/CardWrapper.tsx +++ b/dotcom-rendering/src/components/Card/components/CardWrapper.tsx @@ -13,6 +13,7 @@ type Props = { showTopBarMobile: boolean; isOnwardContent: boolean; containerPalette?: DCRContainerPalette; + contextFormat?: ArticleFormat; }; const baseCardStyles = css` @@ -72,9 +73,12 @@ const hoverStyles = css` } `; -const topBarStyles = css` +const topBarStyles = (isOnwardContent: boolean) => css` ::before { - border-top: 1px solid ${palette('--card-border-top')}; + border-top: 1px solid + ${isOnwardContent + ? palette('--onward-content-border') + : palette('--card-border-top')}; content: ''; z-index: 2; width: 100%; @@ -82,14 +86,14 @@ const topBarStyles = css` background-color: unset; } `; -const mobileTopBarStyles = css` +const mobileTopBarStyles = (isOnwardContent: boolean) => css` ${until.tablet} { - ${topBarStyles} + ${topBarStyles(isOnwardContent)} } `; -const desktopTopBarStyles = css` +const desktopTopBarStyles = (isOnwardContent: boolean) => css` ${from.tablet} { - ${topBarStyles} + ${topBarStyles(isOnwardContent)} } `; @@ -109,7 +113,9 @@ export const CardWrapper = ({ showTopBarMobile, isOnwardContent, containerPalette, + contextFormat, }: Props) => { + const isGalleryContext = contextFormat?.design === ArticleDesign.Gallery; return ( @@ -117,10 +123,12 @@ export const CardWrapper = ({ css={[ baseCardStyles, hoverStyles, - showTopBarDesktop && desktopTopBarStyles, - showTopBarMobile && mobileTopBarStyles, + showTopBarDesktop && + desktopTopBarStyles(isGalleryContext), + showTopBarMobile && + mobileTopBarStyles(isGalleryContext), isOnwardContent && - format.design !== ArticleDesign.Gallery && + !isGalleryContext && onwardContentStyles, ]} > diff --git a/dotcom-rendering/src/components/FetchMoreGalleriesData.importable.tsx b/dotcom-rendering/src/components/FetchMoreGalleriesData.importable.tsx index a09a696067d..9fc89811701 100644 --- a/dotcom-rendering/src/components/FetchMoreGalleriesData.importable.tsx +++ b/dotcom-rendering/src/components/FetchMoreGalleriesData.importable.tsx @@ -1,6 +1,7 @@ import { isNonNullable, isObject } from '@guardian/libs'; import { useEffect, useState } from 'react'; import { decideFormat } from '../lib/articleFormat'; +import type { ArticleFormat } from '../lib/articleFormat'; import { getDataLinkNameCard } from '../lib/getDataLinkName'; import { addDiscussionIds } from '../lib/useCommentCount'; import { palette } from '../palette'; @@ -16,6 +17,7 @@ type Props = { isAdFreeUser: boolean; ajaxUrl: string; guardianBaseUrl: string; + format: ArticleFormat; }; type MoreGalleriesResponse = { @@ -90,6 +92,7 @@ export const FetchMoreGalleriesData = ({ isAdFreeUser, ajaxUrl, guardianBaseUrl, + format, }: Props) => { const [data, setData] = useState( undefined, @@ -151,6 +154,7 @@ export const FetchMoreGalleriesData = ({ trails={buildTrails(data.trails, 5, isAdFreeUser)} discussionApiUrl={discussionApiUrl} guardianBaseUrl={guardianBaseUrl} + format={format} /> ); }; diff --git a/dotcom-rendering/src/components/FetchOnwardsData.importable.tsx b/dotcom-rendering/src/components/FetchOnwardsData.importable.tsx index d6bd4e9c721..c92fe12f574 100644 --- a/dotcom-rendering/src/components/FetchOnwardsData.importable.tsx +++ b/dotcom-rendering/src/components/FetchOnwardsData.importable.tsx @@ -4,7 +4,11 @@ import type { ComponentEvent } from '@guardian/ophan-tracker-js'; import { useEffect, useState } from 'react'; import { submitComponentEvent } from '../client/ophan/ophan'; import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; -import { decideTrail, dedupeTrail } from '../lib/decideTrail'; +import { + decideTrail, + decideTrailWithMasterImage, + dedupeTrail, +} from '../lib/decideTrail'; import { useApi } from '../lib/useApi'; import { addDiscussionIds } from '../lib/useCommentCount'; import { useIsInView } from '../lib/useIsInView'; @@ -14,6 +18,7 @@ import type { RenderingTarget } from '../types/renderingTarget'; import type { FETrailType, TrailType } from '../types/trails'; import { Carousel } from './Carousel.importable'; import { Placeholder } from './Placeholder'; +import { ScrollableSmallOnwards } from './ScrollableSmallOnwards'; type Props = { url: string; @@ -47,12 +52,13 @@ const buildTrails = ( trailLimit: number, isAdFreeUser: boolean, webURL: string, + withMasterImage: boolean, ): TrailType[] => { return trails .filter((trailType) => !(isTrailPaidContent(trailType) && isAdFreeUser)) .filter((trailType) => dedupeTrail(trailType, webURL)) .slice(0, trailLimit) - .map(decideTrail); + .map(withMasterImage ? decideTrailWithMasterImage : decideTrail); }; export const FetchOnwardsData = ({ @@ -117,26 +123,38 @@ export const FetchOnwardsData = ({ .filter(isNonNullable), ); - const trails = buildTrails(data.trails, limit, isAdFreeUser, webURL); + const trails = ({ withMasterImage }: { withMasterImage: boolean }) => + buildTrails(data.trails, limit, isAdFreeUser, webURL, withMasterImage); return ( - + {format.design === ArticleDesign.Gallery ? ( + + ) : ( + + )} ); }; diff --git a/dotcom-rendering/src/components/MoreGalleries.stories.tsx b/dotcom-rendering/src/components/MoreGalleries.stories.tsx index 37d5a6fd95f..7dd15afb3e7 100644 --- a/dotcom-rendering/src/components/MoreGalleries.stories.tsx +++ b/dotcom-rendering/src/components/MoreGalleries.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { galleryOnwardsTrails } from '../../fixtures/manual/onwardsTrails'; import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; -import { getDataLinkNameCard } from '../lib/getDataLinkName'; import { MoreGalleries as MoreGalleriesComponent } from './MoreGalleries'; const meta = { @@ -16,190 +16,11 @@ export const MoreGalleries = { args: { discussionApiUrl: 'https://discussion.theguardian.com/discussion-api', guardianBaseUrl: 'https://www.theguardian.com', - trails: [ - { - url: 'https://www.theguardian.com/environment/gallery/2025/aug/22/week-in-wildlife-a-clumsy-fox-swinging-orangutang-and-rescued-jaguarundi-cub', - linkText: - 'Week in wildlife: a clumsy fox, a swinging orangutan and a rescued jaguarundi cub', - showByline: false, - byline: 'Pejman Faratin', - image: { - src: 'https://media.guim.co.uk/a81e974ffee6c8c88fa280c2d02eaf5dc2af863e/151_292_1020_816/master/1020.jpg', - altText: '', - }, - format: { - theme: Pillar.News, - design: ArticleDesign.Gallery, - display: ArticleDisplay.Standard, - }, - webPublicationDate: '2025-08-22T06:00:25.000Z', - headline: - 'Week in wildlife: a clumsy fox, a swinging orangutan and a rescued jaguarundi cub', - shortUrl: 'https://www.theguardian.com/p/x32n89', - discussion: { - isCommentable: false, - isClosedForComments: true, - discussionId: '/p/x32n89', - }, - dataLinkName: getDataLinkNameCard( - { - theme: Pillar.News, - design: ArticleDesign.Gallery, - display: ArticleDisplay.Standard, - }, - '0', - 0, - ), - trailText: - 'Guinness World Records is looking back at the extraordinary feats achieved since its inception - as well as unveiling 70 whacky and unclaimed records ', - kickerText: 'Politics', - mainMedia: { type: 'Gallery', count: '6' }, - }, - { - url: 'https://www.theguardian.com/money/gallery/2025/aug/22/characterful-cottages-for-sale-in-england-in-pictures', - linkText: - 'Characterful cottages for sale in England – in pictures', - showByline: false, - byline: 'Anna White', - image: { - src: 'https://media.guim.co.uk/58cd9356e6d68e8efa6028162bb959f9798307d5/515_0_5000_4000/master/5000.jpg', - altText: '', - }, - format: { - design: ArticleDesign.Gallery, - theme: Pillar.Lifestyle, - display: ArticleDisplay.Standard, - }, - webPublicationDate: '2025-08-22T06:00:24.000Z', - headline: - 'Characterful cottages for sale in England – in pictures', - shortUrl: 'https://www.theguardian.com/p/x32gqj', - discussion: { - isCommentable: false, - isClosedForComments: true, - discussionId: '/p/x32gqj', - }, - dataLinkName: getDataLinkNameCard( - { - design: ArticleDesign.Gallery, - theme: Pillar.Lifestyle, - display: ArticleDisplay.Standard, - }, - '0', - 1, - ), - trailText: - 'Picked from a record 60,636 entries, the first images from the Natural History Museum’s wildlife photographer of the year competition have been released. The photographs, which range from a lion facing down a cobra to magnified mould spores, show the diversity, beauty and complexity of the natural world and humanity’s relationship with it', - mainMedia: { type: 'Gallery', count: '6' }, - }, - { - url: 'https://www.theguardian.com/news/gallery/2025/aug/22/sunsets-aid-parachutes-and-giant-pandas-photos-of-the-day-friday', - linkText: - 'Sunsets, aid parachutes and giant pandas: photos of the day – Friday ', - showByline: false, - byline: 'Eithne Staunton', - image: { - src: 'https://media.guim.co.uk/4ce0b080206fe9b65b976c1acf219d81072cc814/0_0_2113_1690/master/2113.png', - altText: '', - }, - format: { - design: ArticleDesign.Gallery, - theme: Pillar.News, - display: ArticleDisplay.Standard, - }, - webPublicationDate: '2025-08-22T12:49:42.000Z', - headline: - 'Sunsets, aid parachutes and giant pandas: photos of the day – Friday ', - shortUrl: 'https://www.theguardian.com/p/x3359z', - discussion: { - isCommentable: false, - isClosedForComments: true, - discussionId: '/p/x3359z', - }, - dataLinkName: getDataLinkNameCard( - { - design: ArticleDesign.Gallery, - theme: Pillar.News, - display: ArticleDisplay.Standard, - }, - '0', - 2, - ), - trailText: - 'From the mock-Tudor fad of the 1920s to drivers refuelling on a roundabout, each era produces its own distinctive petrol stations – as photographer Philip Butler discovered', - mainMedia: { type: 'Gallery', count: '6' }, - }, - { - url: 'https://www.theguardian.com/fashion/gallery/2025/aug/22/what-to-wear-to-notting-hill-carnival', - linkText: 'On parade: what to wear to Notting Hill carnival', - showByline: false, - byline: 'Melanie Wilkinson', - image: { - src: 'https://media.guim.co.uk/49a9656cd10c4f64f8bdd54380afb915c7a3648b/207_0_1500_1200/master/1500.jpg', - altText: '', - }, - format: { - design: ArticleDesign.Gallery, - theme: Pillar.Lifestyle, - display: ArticleDisplay.Standard, - }, - webPublicationDate: '2025-08-22T05:00:23.000Z', - headline: 'On parade: what to wear to Notting Hill carnival', - shortUrl: 'https://www.theguardian.com/p/x32mte', - discussion: { - isCommentable: false, - isClosedForComments: true, - discussionId: '/p/x32mte', - }, - dataLinkName: getDataLinkNameCard( - { - design: ArticleDesign.Gallery, - theme: Pillar.Lifestyle, - display: ArticleDisplay.Standard, - }, - '0', - 1, - ), - trailText: - 'The Guardian’s picture editors select photographs from around the world', - mainMedia: { type: 'Gallery', count: '6' }, - }, - { - url: 'https://www.theguardian.com/artanddesign/gallery/2025/aug/21/psychedelic-rock-glass-mountain-michael-lundgren', - linkText: - 'Psychedelic rock! Formations that mess with your mind – in pictures ', - showByline: false, - image: { - src: 'https://media.guim.co.uk/2810af61b2d2d2d5f71ec01e56e6555e0a6d4635/55_0_2813_2250/master/2813.jpg', - altText: '', - }, - format: { - design: ArticleDesign.Gallery, - theme: Pillar.Culture, - display: ArticleDisplay.Standard, - }, - webPublicationDate: '2025-08-21T06:01:01.000Z', - headline: - 'Psychedelic rock! Formations that mess with your mind – in pictures ', - shortUrl: 'https://www.theguardian.com/p/x2p663', - discussion: { - isCommentable: false, - isClosedForComments: true, - discussionId: '/p/x2p663', - }, - dataLinkName: getDataLinkNameCard( - { - design: ArticleDesign.Gallery, - theme: Pillar.Culture, - display: ArticleDisplay.Standard, - }, - '0', - 1, - ), - trailText: - 'Politicians and their partners put on their best show at this year’s Midwinter Ball, an annual dinner hosted by the Federal Parliamentary Press Gallery in Canberra', - mainMedia: { type: 'Gallery', count: '6' }, - }, - ], + trails: galleryOnwardsTrails, + format: { + design: ArticleDesign.Gallery, + display: ArticleDisplay.Standard, + theme: Pillar.Culture, + }, }, } satisfies Story; diff --git a/dotcom-rendering/src/components/MoreGalleries.tsx b/dotcom-rendering/src/components/MoreGalleries.tsx index 475fcf32a09..778f23e853b 100644 --- a/dotcom-rendering/src/components/MoreGalleries.tsx +++ b/dotcom-rendering/src/components/MoreGalleries.tsx @@ -8,6 +8,7 @@ import { } from '@guardian/source/foundations'; import { StraightLines } from '@guardian/source-development-kitchen/react-components'; import { grid } from '../grid'; +import type { ArticleFormat } from '../lib/articleFormat'; import { palette } from '../palette'; import { type TrailType } from '../types/trails'; import { Card } from './Card/Card'; @@ -18,6 +19,7 @@ type Props = { trails: TrailType[]; discussionApiUrl: string; guardianBaseUrl: string; + format: ArticleFormat; }; const standardCardStyles = css` @@ -81,11 +83,13 @@ const cardsContainerStyles = css` const getDefaultCardProps = ( trail: TrailType, discussionApiUrl: string, + format: ArticleFormat, serverTime?: number, ) => { const defaultProps: CardProps = { linkTo: trail.url, format: trail.format, + contextFormat: format, headlineText: trail.headline, byline: trail.byline, showByline: trail.showByline, @@ -100,7 +104,9 @@ const getDefaultCardProps = ( dataLinkName: trail.dataLinkName, snapData: trail.snapData, discussionApiUrl, - discussionId: trail.discussion?.discussionId, + discussionId: trail.discussion?.isCommentable + ? trail.discussion.discussionId + : undefined, avatarUrl: trail.avatarUrl, mainMedia: trail.mainMedia, isExternalLink: false, @@ -151,6 +157,7 @@ export const MoreGalleries = (props: Props) => { defaultProps={getDefaultCardProps( firstTrail, props.discussionApiUrl, + props.format, props.serverTime, )} /> @@ -173,6 +180,7 @@ export const MoreGalleries = (props: Props) => { {...getDefaultCardProps( trail, props.discussionApiUrl, + props.format, props.serverTime, )} mediaSize="medium" diff --git a/dotcom-rendering/src/components/OnwardsUpper.importable.tsx b/dotcom-rendering/src/components/OnwardsUpper.importable.tsx index a0f3da797ed..2bc7a4a105b 100644 --- a/dotcom-rendering/src/components/OnwardsUpper.importable.tsx +++ b/dotcom-rendering/src/components/OnwardsUpper.importable.tsx @@ -321,6 +321,15 @@ export const OnwardsUpper = ({ css` content: ''; position: absolute; top: 0; bottom: 0; left: -10px; width: 1px; - background-color: ${palette('--card-border-top')}; + background-color: ${isOnwardContent + ? palette('--onward-content-border') + : palette('--card-border-top')}; transform: translateX(-50%); `; -const singleRowLeftBorderStyles = css` +const singleRowLeftBorderStyles = (isOnwardContent?: boolean) => css` :not(:first-child)::before { - ${leftBorderStyles} + ${leftBorderStyles(isOnwardContent)} } `; -const stackedRowLeftBorderStyles = css` +const stackedRowLeftBorderStyles = (isOnwardContent?: boolean) => css` ${from.tablet} { :not(:first-child)::before { - ${leftBorderStyles} + ${leftBorderStyles(isOnwardContent)} } } ${until.tablet} { :not(:first-child):not(:nth-child(2))::before { - ${leftBorderStyles} + ${leftBorderStyles(isOnwardContent)} } } `; @@ -410,17 +412,20 @@ export const ScrollableCarousel = ({ ScrollableCarousel.Item = ({ isStackingCarousel = false, + isOnwardContent = false, children, }: { isStackingCarousel?: boolean; + /** The colour of borders can be overriden */ + isOnwardContent?: boolean; children: React.ReactNode; }) => ( {children} diff --git a/dotcom-rendering/src/components/ScrollableSmallOnwards.stories.tsx b/dotcom-rendering/src/components/ScrollableSmallOnwards.stories.tsx new file mode 100644 index 00000000000..6c46175f14d --- /dev/null +++ b/dotcom-rendering/src/components/ScrollableSmallOnwards.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { galleryOnwardsTrails } from '../../fixtures/manual/onwardsTrails'; +import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; +import { ScrollableSmallOnwards } from './ScrollableSmallOnwards'; + +const meta = { + title: 'Components/ScrollableSmallOnwards', + component: ScrollableSmallOnwards, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const ScrollableSmallOnwardsStory = { + args: { + serverTime: new Date('2022-09-01T20:00:25.000Z').getTime(), + discussionApiUrl: + 'https://discussion.code.dev-theguardian.com/discussion-api', + heading: 'More on this story', + headingUrl: 'http://localhost:9000/more-galleries', + onwardsSource: 'related-content', + format: { + design: ArticleDesign.Gallery, + theme: Pillar.News, + display: ArticleDisplay.Standard, + }, + trails: galleryOnwardsTrails, + }, +} satisfies Story; diff --git a/dotcom-rendering/src/components/ScrollableSmallOnwards.tsx b/dotcom-rendering/src/components/ScrollableSmallOnwards.tsx new file mode 100644 index 00000000000..c5f47f4ebf2 --- /dev/null +++ b/dotcom-rendering/src/components/ScrollableSmallOnwards.tsx @@ -0,0 +1,240 @@ +import { css } from '@emotion/react'; +import { + between, + from, + space, + textSansBold17, + textSansBold20, + until, +} from '@guardian/source/foundations'; +import { StraightLines } from '@guardian/source-development-kitchen/react-components'; +import { grid } from '../grid'; +import type { ArticleFormat } from '../lib/articleFormat'; +import { palette } from '../palette'; +import { type OnwardsSource } from '../types/onwards'; +import { type TrailType } from '../types/trails'; +import { Card } from './Card/Card'; +import type { Props as CardProps } from './Card/Card'; +import { ScrollableCarousel } from './ScrollableCarousel'; + +type Props = { + serverTime?: number; + trails: TrailType[]; + discussionApiUrl: string; + heading: string; + onwardsSource: OnwardsSource; + format: ArticleFormat; + headingUrl?: string; +}; + +const cardsContainerStyles = css` + ${grid.column.centre} + position: relative; + + ${from.desktop} { + ${grid.between('centre-column-start', 'right-column-end')} + } + + ${from.leftCol} { + ${grid.between('centre-column-start', 'right-column-end')} + } + + ${from.leftCol} { + &::before { + content: ''; + position: absolute; + left: -11px; + top: 0; + bottom: 0; + width: 1px; + background-color: ${palette('--onward-content-border')}; + } + + ol { + padding-left: 0; + } + } +`; + +export const ScrollableSmallOnwards = (props: Props) => { + const trails = props.trails.slice(0, 4); // Limit to 4 cards + if (trails.length !== 4) return null; + + const mobileBottomCards = [1, 3]; + const desktopBottomCards = [2, 3]; + + return ( + + + + + + {trails.map((trail, index) => { + return ( + + + + ); + })} + + + + ); +}; + +const Title = ({ + title, + headingUrl, +}: { + title: string; + headingUrl?: string; +}) => + headingUrl ? ( + + {title} + + ) : ( + {title} + ); + +const headerGridStyles = css` + ${grid.column.centre} + color: ${palette('--caption-text')}; + text-decoration: none; + align-self: start; + ${between.tablet.and.leftCol} { + margin-left: 10px; + } + ${from.leftCol} { + ${grid.column.left} + } +`; + +const headerStyles = css` + color: ${palette('--carousel-text')}; + ${textSansBold17}; + padding-bottom: ${space[3]}px; + padding-top: ${space[1]}px; + + :hover { + text-decoration: underline; + } + + ${from.tablet} { + ${textSansBold20}; + } +`; + +const getDefaultCardProps = ( + trail: TrailType, + discussionApiUrl: string, + onwardsSource: OnwardsSource, + format: ArticleFormat, + serverTime?: number, +) => { + const defaultProps: CardProps = { + linkTo: trail.url, + format: trail.format, + contextFormat: format, + headlineText: trail.headline, + byline: trail.byline, + showByline: trail.showByline, + showQuotedHeadline: trail.showQuotedHeadline, + webPublicationDate: trail.webPublicationDate, + kickerText: trail.kickerText, + showPulsingDot: false, + showClock: false, + image: trail.image, + isCrossword: trail.isCrossword, + starRating: trail.starRating, + dataLinkName: trail.dataLinkName, + snapData: trail.snapData, + discussionApiUrl, + discussionId: trail.discussion?.isCommentable + ? trail.discussion.discussionId + : undefined, + avatarUrl: trail.avatarUrl, + mainMedia: trail.mainMedia, + isExternalLink: false, + branding: trail.branding, + serverTime, + imageLoading: 'lazy', + showAge: true, + aspectRatio: '5:4', + mediaSize: 'scrollable-small', + mediaPositionOnDesktop: 'left', + mediaPositionOnMobile: 'left', + headlineSizes: { + desktop: 'xxsmall', + mobile: 'xxxsmall', + }, + trailText: undefined, + supportingContent: undefined, + canPlayInline: false, + onwardsSource, + isOnwardContent: true, + }; + return defaultProps; +}; diff --git a/dotcom-rendering/src/layouts/GalleryLayout.tsx b/dotcom-rendering/src/layouts/GalleryLayout.tsx index ded5ecef66f..a2b6480b472 100644 --- a/dotcom-rendering/src/layouts/GalleryLayout.tsx +++ b/dotcom-rendering/src/layouts/GalleryLayout.tsx @@ -16,7 +16,6 @@ import { ArticleMetaApps } from '../components/ArticleMeta.apps'; import { ArticleMeta } from '../components/ArticleMeta.web'; import { ArticleTitle } from '../components/ArticleTitle'; import { Caption } from '../components/Caption'; -import { Carousel } from '../components/Carousel.importable'; import { DiscussionLayout } from '../components/DiscussionLayout'; import { FetchMoreGalleriesData } from '../components/FetchMoreGalleriesData.importable'; import { Footer } from '../components/Footer'; @@ -30,6 +29,7 @@ import { Masthead } from '../components/Masthead/Masthead'; import { MostViewedFooterData } from '../components/MostViewedFooterData.importable'; import { MostViewedFooterLayout } from '../components/MostViewedFooterLayout'; import { OnwardsUpper } from '../components/OnwardsUpper.importable'; +import { ScrollableSmallOnwards } from '../components/ScrollableSmallOnwards'; import { Section } from '../components/Section'; import { Standfirst } from '../components/Standfirst'; import { StickyBottomBanner } from '../components/StickyBottomBanner.importable'; @@ -203,6 +203,7 @@ export const GalleryLayout = (props: WebProps | AppProps) => { discussionApiUrl={discussionApiUrl} serverTime={serverTime} isAdFreeUser={frontendData.isAdFreeUser} + format={format} /> @@ -212,13 +213,13 @@ export const GalleryLayout = (props: WebProps | AppProps) => { show={showMerchandisingHigh} display={format.display} /> - ) : null; - -const StoryPackage = ({ - storyPackage, - format, - discussionApiUrl, - serverTime, - renderingTarget, - topBorder, -}: { - storyPackage: Gallery['storyPackage']; - format: ArticleFormat; - discussionApiUrl: string; - serverTime?: number; - renderingTarget: RenderingTarget; - topBorder: boolean; -}) => - storyPackage === undefined ? null : ( - - - - - - ); diff --git a/dotcom-rendering/src/lib/decideTrail.ts b/dotcom-rendering/src/lib/decideTrail.ts index f9d313e6eb9..b5264dc63eb 100644 --- a/dotcom-rendering/src/lib/decideTrail.ts +++ b/dotcom-rendering/src/lib/decideTrail.ts @@ -32,3 +32,25 @@ export const decideTrail = (trail: FETrailType, index = 0): TrailType => { dataLinkName: getDataLinkNameCard(format, '0', index), }; }; + +// This is used for the new ScrollableSmallOnwards component which relies on the masterImage +// In future work, we will need to fully migrate to this function and deprecate decideTrail +export const decideTrailWithMasterImage = ( + trail: FETrailType, + index = 0, +): TrailType => { + const format: ArticleFormat = decideFormat(trail.format); + const image: DCRFrontImage | undefined = trail.masterImage + ? { + src: trail.masterImage, + altText: '', + } + : undefined; + + return { + ...trail, + image, + format, + dataLinkName: getDataLinkNameCard(format, '0', index), + }; +}; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 6a4752c5e1c..618c3de9c1c 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -2520,8 +2520,14 @@ const cardMetaTextDark: PaletteFunction = () => sourcePalette.neutral[60]; const cardBackgroundLight: PaletteFunction = () => 'transparent'; const cardBackgroundDark: PaletteFunction = () => 'transparent'; -const cardMediaBackgroundLight: PaletteFunction = () => - sourcePalette.neutral[97]; +const cardMediaBackgroundLight: PaletteFunction = (format) => { + switch (format.design) { + case ArticleDesign.Gallery: + return sourcePalette.neutral[100]; + default: + return sourcePalette.neutral[97]; + } +}; const cardMediaBackgroundDark: PaletteFunction = () => sourcePalette.neutral[20]; diff --git a/dotcom-rendering/src/storyPackage.ts b/dotcom-rendering/src/storyPackage.ts index 2a647c129c9..30f860db226 100644 --- a/dotcom-rendering/src/storyPackage.ts +++ b/dotcom-rendering/src/storyPackage.ts @@ -1,5 +1,5 @@ import type { FEStoryPackage } from './frontend/feArticle'; -import { decideTrail } from './lib/decideTrail'; +import { decideTrail, decideTrailWithMasterImage } from './lib/decideTrail'; import type { TrailType } from './types/trails'; export type StoryPackage = { @@ -9,6 +9,7 @@ export type StoryPackage = { export const parse = ( feStoryPackage: FEStoryPackage | undefined, + withMasterImage: boolean = false, ): StoryPackage | undefined => { if (feStoryPackage === undefined) { return undefined; @@ -16,6 +17,8 @@ export const parse = ( return { heading: feStoryPackage.heading, - trails: feStoryPackage.trails.map(decideTrail), + trails: feStoryPackage.trails.map( + withMasterImage ? decideTrailWithMasterImage : decideTrail, + ), }; }; diff --git a/dotcom-rendering/src/types/article.ts b/dotcom-rendering/src/types/article.ts index 27d01788f4e..47a0b002fed 100644 --- a/dotcom-rendering/src/types/article.ts +++ b/dotcom-rendering/src/types/article.ts @@ -117,7 +117,10 @@ export const enhanceArticleType = ( data.main, )(data.mainMediaElements); - const storyPackage = parseStoryPackage(data.storyPackage); + const storyPackage = parseStoryPackage( + data.storyPackage, + format.design === ArticleDesign.Gallery, + ); if (format.design === ArticleDesign.Gallery) { const design = ArticleDesign.Gallery;