diff --git a/dotcom-rendering/src/components/ExpandableMarketingCard.tsx b/dotcom-rendering/src/components/ExpandableMarketingCard.tsx index 2fe5e4eb31a..01546958e18 100644 --- a/dotcom-rendering/src/components/ExpandableMarketingCard.tsx +++ b/dotcom-rendering/src/components/ExpandableMarketingCard.tsx @@ -16,7 +16,6 @@ import { SvgCross, } from '@guardian/source/react-components'; import type { Dispatch, SetStateAction } from 'react'; -import { getZIndex } from '../lib/getZIndex'; import { palette } from '../palette'; import { useConfig } from './ConfigContext'; @@ -43,17 +42,6 @@ const fillBarStyles = css` } `; -const containerStyles = css` - ${getZIndex('expandableMarketingCardOverlay')} - position: sticky; - top: 0; - - ${from.leftCol} { - padding-bottom: ${space[5]}px; - margin-right: -1px; /* To align with rich link - if we move this feature to production, we should remove this and make rich link align with everything instead */ - } -`; - const contentStyles = css` position: relative; display: flex; @@ -121,8 +109,16 @@ const detailsStyles = css` flex-direction: column; gap: ${space[4]}px; margin-bottom: ${space[2]}px; +`; - h2 { +const sectionStyles = css` + display: flex; + flex-direction: column; + gap: ${space[3]}px; + border-top: 1px solid ${neutral[100]}; + padding-top: ${space[2]}px; + + h3 { ${headlineBold17}; } @@ -133,14 +129,6 @@ const detailsStyles = css` } `; -const sectionStyles = css` - display: flex; - flex-direction: column; - gap: ${space[3]}px; - border-top: 1px solid ${neutral[100]}; - padding-top: ${space[2]}px; -`; - const imageTopStyles = css` position: absolute; top: 0; @@ -154,6 +142,15 @@ const imageBottomStyles = css` const buttonStyles = css` z-index: 1; + background-color: ${palette( + '--expandable-marketing-card-button-background', + )}; + width: fit-content; + + ${textSansBold12}; + ${from.wide} { + ${textSansBold14}; + } `; interface Props { @@ -175,90 +172,98 @@ export const ExpandableMarketingCard = ({ setIsClosed, }: Props) => { return ( -
+
{!isExpanded ? ( - - ) : ( <> - - - )} -
-
- {heading} - -
-
{kicker}
-
- {isExpanded && ( -
-
-

We're independent

-

- With no billionaire owner or shareholders, our - journalism is funded by readers -

-
-
-

We're open

-

- With misinformation threatening democracy, we - keep our fact-based news paywall-free -

-
-
-

We're global

-

- With 200 years of history and staff across - America and the world, we offer an outsider - perspective on US news -

-
-
+
+

{heading}

+
+ +
+
+
{kicker}
+ + + ) : ( + <> + + +
+
+

{heading}

+ +
+
{kicker}
+
+
+
+

We’re independent

+

+ With no billionaire owner or shareholders, + our journalism is funded by readers +

+
+
+

We’re open

+

+ With misinformation threatening democracy, + we keep our fact-based news paywall-free +

+
+
+

We’re global

+

+ With 200 years of history and staff across + America and the world, we offer an outsider + perspective on US news +

+
View newsletters
-
+ )}
diff --git a/dotcom-rendering/src/components/ExpandableMarketingCardWrapper.importable.tsx b/dotcom-rendering/src/components/ExpandableMarketingCardWrapper.importable.tsx new file mode 100644 index 00000000000..cfcb42ddf6f --- /dev/null +++ b/dotcom-rendering/src/components/ExpandableMarketingCardWrapper.importable.tsx @@ -0,0 +1,82 @@ +import { getCookie } from '@guardian/libs'; +import { useEffect, useState } from 'react'; +import type { DailyArticle } from '../lib/dailyArticleCount'; +import { getDailyArticleCount } from '../lib/dailyArticleCount'; +import { getLocaleCode } from '../lib/getCountryCode'; +import { useAB } from '../lib/useAB'; +import { ExpandableMarketingCard } from './ExpandableMarketingCard'; + +interface Props { + guardianBaseURL: string; +} + +const isFirstArticle = () => { + const [dailyCount = {} as DailyArticle] = getDailyArticleCount() ?? []; + return Object.keys(dailyCount).length === 0 || dailyCount.count <= 1; +}; + +const isNewUSUser = async () => { + const isUserInUS = (await getLocaleCode()) === 'US'; + if (!isUserInUS) { + return false; + } + + // Exclude users who have selected a non-US edition. + const editionCookie = getCookie({ name: 'GU_EDITION' }); + const hasUserSelectedNonUSEdition = + !!editionCookie && editionCookie !== 'US'; + + // This check must happen AFTER we've ensured that the user is in the US. + const isNewUser = isFirstArticle(); + + return !hasUserSelectedNonUSEdition && !isNewUser; +}; + +// todo - semantic html accordion-details? +export const ExpandableMarketingCardWrapper = ({ guardianBaseURL }: Props) => { + const [isExpanded, setIsExpanded] = useState(false); + const [isClosed, setIsClosed] = useState(false); + const [isApplicableUser, setIsApplicableUser] = useState(false); + + const abTestAPI = useAB()?.api; + const isInVariantFree = !!abTestAPI?.isUserInVariant( + 'UsaExpandableMarketingCard', + 'variant-free', + ); + const isInVariantBubble = !!abTestAPI?.isUserInVariant( + 'UsaExpandableMarketingCard', + 'variant-bubble', + ); + const isInEitherVariant = isInVariantFree || isInVariantBubble; + + useEffect(() => { + void isNewUSUser().then((show) => { + if (show) { + setIsApplicableUser(true); + } + }); + }, []); + + if (!isInEitherVariant || !isApplicableUser || isClosed) { + return null; + } + + const heading = isInVariantBubble + ? 'Pop your US news bubble' + : 'Yes, this story is free'; + + const kicker = isInVariantBubble + ? 'How the Guardian is different' + : 'Why the Guardian has no paywall'; + + return ( + + ); +}; diff --git a/dotcom-rendering/src/components/GridItem.tsx b/dotcom-rendering/src/components/GridItem.tsx index ac72369de68..4249a69cc3a 100644 --- a/dotcom-rendering/src/components/GridItem.tsx +++ b/dotcom-rendering/src/components/GridItem.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/react'; +import { from, space } from '@guardian/source/foundations'; import { getZIndex } from '../lib/getZIndex'; type Props = { @@ -28,6 +29,26 @@ const bodyStyles = css` ${getZIndex('bodyArea')} `; +const usCardStyles = css` + align-self: start; + position: sticky; + top: 0; + ${getZIndex('expandableMarketingCardOverlay')} + + ${from.leftCol} { + margin-top: ${space[6]}px; + margin-bottom: ${space[9]}px; + + /* To align with rich links - if we move this feature to production, we should remove this and make rich link align with everything instead */ + margin-left: 1px; + margin-right: -1px; + } + + ${from.wide} { + margin-left: 0; + } +`; + const gridArea = css` grid-area: var(--grid-area); `; @@ -41,6 +62,7 @@ export const GridItem = ({ css={[ area === 'body' && bodyStyles, area === 'right-column' && rightColumnStyles, + area === 'uscard' && usCardStyles, gridArea, ]} style={{ diff --git a/dotcom-rendering/src/components/SignInGate/displayRule.ts b/dotcom-rendering/src/components/SignInGate/displayRule.ts index a979e2728c7..5ccc743b0c4 100644 --- a/dotcom-rendering/src/components/SignInGate/displayRule.ts +++ b/dotcom-rendering/src/components/SignInGate/displayRule.ts @@ -1,7 +1,6 @@ // use the dailyArticleCount from the local storage to see how many articles the user has viewed in a day import { onConsent } from '@guardian/libs'; -import type { ConsentState } from '@guardian/libs'; -import type { CountryCode } from '@guardian/libs'; +import type { ConsentState, CountryCode } from '@guardian/libs'; import type { DailyArticle } from '../../lib/dailyArticleCount'; import { getDailyArticleCount } from '../../lib/dailyArticleCount'; import type { TagType } from '../../types/tag'; diff --git a/dotcom-rendering/src/experiments/ab-tests.ts b/dotcom-rendering/src/experiments/ab-tests.ts index 7a9613eb586..89624262880 100644 --- a/dotcom-rendering/src/experiments/ab-tests.ts +++ b/dotcom-rendering/src/experiments/ab-tests.ts @@ -7,6 +7,7 @@ import { mpuWhenNoEpic } from './tests/mpu-when-no-epic'; import { optimiseSpacefinderInline } from './tests/optimise-spacefinder-inline'; import { signInGateMainControl } from './tests/sign-in-gate-main-control'; import { signInGateMainVariant } from './tests/sign-in-gate-main-variant'; +import { UsaExpandableMarketingCard } from './tests/usa-expandable-marketing-card'; // keep in sync with ab-tests in frontend // https://github.com/guardian/frontend/tree/main/static/src/javascripts/projects/common/modules/experiments/ab-tests.ts @@ -19,4 +20,5 @@ export const tests: ABTest[] = [ mpuWhenNoEpic, adBlockAsk, optimiseSpacefinderInline, + UsaExpandableMarketingCard, ]; diff --git a/dotcom-rendering/src/experiments/tests/usa-expandable-marketing-card.ts b/dotcom-rendering/src/experiments/tests/usa-expandable-marketing-card.ts new file mode 100644 index 00000000000..b6194b3a99a --- /dev/null +++ b/dotcom-rendering/src/experiments/tests/usa-expandable-marketing-card.ts @@ -0,0 +1,35 @@ +import type { ABTest } from '@guardian/ab-core'; + +export const UsaExpandableMarketingCard: ABTest = { + id: 'UsaExpandableMarketingCard', + start: '2024-10-02', + expiry: '2024-12-18', + author: 'dotcom.platform@guardian.co.uk', + description: + 'Test the impact of showing the user a component that highlights the Guardians journalism.', + audience: 0 / 100, + audienceOffset: 0 / 100, + audienceCriteria: 'US-based users that see the US edition.', + successMeasure: 'Users are more likely to engage with the site.', + canRun: () => true, + variants: [ + { + id: 'control', + test: (): void => { + /* no-op */ + }, + }, + { + id: 'variant-free', + test: (): void => { + /* no-op */ + }, + }, + { + id: 'variant-bubble', + test: (): void => { + /* no-op */ + }, + }, + ], +}; diff --git a/dotcom-rendering/src/layouts/CommentLayout.tsx b/dotcom-rendering/src/layouts/CommentLayout.tsx index 11830384a8e..0404c9a0b64 100644 --- a/dotcom-rendering/src/layouts/CommentLayout.tsx +++ b/dotcom-rendering/src/layouts/CommentLayout.tsx @@ -20,6 +20,7 @@ import { Border } from '../components/Border'; import { Carousel } from '../components/Carousel.importable'; import { ContributorAvatar } from '../components/ContributorAvatar'; import { DiscussionLayout } from '../components/DiscussionLayout'; +import { ExpandableMarketingCardWrapper } from '../components/ExpandableMarketingCardWrapper.importable'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; @@ -100,18 +101,20 @@ const StandardGrid = ({ 'title border headline headline headline' 'lines border headline headline headline' 'meta border standfirst standfirst standfirst' - 'meta border media media media' - '. border body . right-column' - '. border . . right-column'; + 'uscard border standfirst standfirst standfirst' + 'uscard border media media media' + 'uscard border body . right-column' + 'uscard border . . right-column'; ` : css` grid-template-areas: 'title border headline . right-column' 'lines border headline . right-column' 'meta border standfirst . right-column' - 'meta border media . right-column' - '. border body . right-column' - '. border . . right-column'; + 'uscard border standfirst . right-column' + 'uscard border media . right-column' + 'uscard border body . right-column' + 'uscard border . . right-column'; `} } @@ -129,21 +132,23 @@ const StandardGrid = ({ ${display === ArticleDisplay.Showcase ? css` grid-template-areas: - 'title border headline headline' - 'lines border headline headline' - 'meta border standfirst standfirst' - 'meta border media media' - '. border body right-column' - '. border . right-column'; + 'title border headline headline' + 'lines border headline headline' + 'meta border standfirst standfirst' + 'uscard border standfirst standfirst' + 'uscard border media media' + 'uscard border body right-column' + 'uscard border . right-column'; ` : css` grid-template-areas: - 'title border headline right-column' - 'lines border headline right-column' - 'meta border standfirst right-column' - 'meta border media right-column' - '. border body right-column' - '. border . right-column'; + 'title border headline right-column' + 'lines border headline right-column' + 'meta border standfirst right-column' + 'uscard border standfirst right-column' + 'uscard border media right-column' + 'uscard border body right-column' + 'uscard border . right-column'; `} } @@ -551,6 +556,22 @@ export const CommentLayout = (props: WebProps | AppsProps) => { )}
+ {isWeb && ( + + + + + + + + )}
diff --git a/dotcom-rendering/src/layouts/ImmersiveLayout.tsx b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx index c86f6070132..f294b24fa1a 100644 --- a/dotcom-rendering/src/layouts/ImmersiveLayout.tsx +++ b/dotcom-rendering/src/layouts/ImmersiveLayout.tsx @@ -23,6 +23,7 @@ import { Caption } from '../components/Caption'; import { Carousel } from '../components/Carousel.importable'; import { DecideLines } from '../components/DecideLines'; import { DiscussionLayout } from '../components/DiscussionLayout'; +import { ExpandableMarketingCardWrapper } from '../components/ExpandableMarketingCardWrapper.importable'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; import { GuardianLabsLines } from '../components/GuardianLabsLines'; @@ -88,6 +89,8 @@ const ImmersiveGrid = ({ children }: { children: React.ReactNode }) => ( Vertical grey border Main content Right Column + + Duplicate lines are required to ensure the left column does not have extra vertical space. */ ${from.wide} { grid-column-gap: 10px; @@ -99,9 +102,10 @@ const ImmersiveGrid = ({ children }: { children: React.ReactNode }) => ( '. border byline . right-column' 'lines border body . right-column' 'meta border body . right-column' - 'meta border body . right-column' - '. border body . right-column' - '. border . . right-column'; + 'uscard border body . right-column' + 'uscard border . . right-column' + 'uscard border . . right-column' + 'uscard border . . right-column'; } /* @@ -111,6 +115,8 @@ const ImmersiveGrid = ({ children }: { children: React.ReactNode }) => ( Vertical grey border Main content Right Column + + Duplicate lines are required to ensure the left column does not have extra vertical space. */ ${until.wide} { grid-column-gap: 10px; @@ -122,9 +128,10 @@ const ImmersiveGrid = ({ children }: { children: React.ReactNode }) => ( '. border byline right-column' 'lines border body right-column' 'meta border body right-column' - 'meta border body right-column' - '. border body right-column' - '. border . right-column'; + 'uscard border body right-column' + 'uscard border . right-column' + 'uscard border . right-column' + 'uscard border . right-column'; } /* @@ -645,6 +652,22 @@ export const ImmersiveLayout = (props: WebProps | AppProps) => { )}
+ {isWeb && ( + + + + + + + + )} { standfirst={article.standfirst} /> -
diff --git a/dotcom-rendering/src/layouts/PictureLayout.tsx b/dotcom-rendering/src/layouts/PictureLayout.tsx index 5aa392f5dfb..fabd0410a60 100644 --- a/dotcom-rendering/src/layouts/PictureLayout.tsx +++ b/dotcom-rendering/src/layouts/PictureLayout.tsx @@ -21,6 +21,7 @@ import { Carousel } from '../components/Carousel.importable'; import { ContributorAvatar } from '../components/ContributorAvatar'; import { DecideLines } from '../components/DecideLines'; import { DiscussionLayout } from '../components/DiscussionLayout'; +import { ExpandableMarketingCardWrapper } from '../components/ExpandableMarketingCardWrapper.importable'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; @@ -67,7 +68,6 @@ const PictureGrid = ({ children }: { children: React.ReactNode }) => ( display: grid; width: 100%; margin-left: 0; - grid-column-gap: 10px; /* @@ -78,6 +78,7 @@ const PictureGrid = ({ children }: { children: React.ReactNode }) => ( Main content Right Column + Duplicate lines are required to ensure the left column does not have extra vertical space. */ ${from.wide} { grid-template-columns: 219px 1px 1020px; @@ -86,7 +87,10 @@ const PictureGrid = ({ children }: { children: React.ReactNode }) => ( '. border standfirst' 'lines border media' 'meta border media' - 'meta border submeta'; + 'uscard border media' + 'uscard border submeta' + 'uscard border .' + 'uscard border .'; } ${until.wide} { @@ -96,7 +100,10 @@ const PictureGrid = ({ children }: { children: React.ReactNode }) => ( '. border standfirst standfirst standfirst' 'lines border media media media' 'meta border media media media' - 'meta border submeta submeta submeta'; + 'uscard border media media media' + 'uscard border submeta submeta submeta' + 'uscard border . . .' + 'uscard border . . .'; } /* @@ -566,6 +573,22 @@ export const PictureLayout = (props: WebProps | AppsProps) => { /> + {isWeb && ( + + + + + + + + )} diff --git a/dotcom-rendering/src/layouts/ShowcaseLayout.tsx b/dotcom-rendering/src/layouts/ShowcaseLayout.tsx index 4d096827451..fd01a9c7468 100644 --- a/dotcom-rendering/src/layouts/ShowcaseLayout.tsx +++ b/dotcom-rendering/src/layouts/ShowcaseLayout.tsx @@ -22,6 +22,7 @@ import { Border } from '../components/Border'; import { Carousel } from '../components/Carousel.importable'; import { DecideLines } from '../components/DecideLines'; import { DiscussionLayout } from '../components/DiscussionLayout'; +import { ExpandableMarketingCardWrapper } from '../components/ExpandableMarketingCardWrapper.importable'; import { Footer } from '../components/Footer'; import { GridItem } from '../components/GridItem'; import { HeaderAdSlot } from '../components/HeaderAdSlot'; @@ -94,9 +95,10 @@ const ShowcaseGrid = ({ children }: { children: React.ReactNode }) => ( 'title border headline headline headline' 'lines border media media media' 'meta border media media media' - 'meta border standfirst . right-column' - '. border body . right-column' - '. border . . right-column'; + 'uscard border media media media' + 'uscard border standfirst . right-column' + 'uscard border body . right-column' + 'uscard border . . right-column'; } ${until.wide} { @@ -105,9 +107,10 @@ const ShowcaseGrid = ({ children }: { children: React.ReactNode }) => ( 'title border headline headline' 'lines border media media' 'meta border media media' - 'meta border standfirst right-column' - '. border body right-column' - '. border . right-column'; + 'uscard border media media' + 'uscard border standfirst right-column' + 'uscard border body right-column' + 'uscard border . right-column'; } /* @@ -540,6 +543,22 @@ export const ShowcaseLayout = (props: WebProps | AppsProps) => { )}
+ {isWeb && ( + + + + + + + + )} {
)}
+ {isWeb && ( + + + + + + + + )} const expandableMarketingCardSvgFill: PaletteFunction = () => sourcePalette.neutral[0]; +const expandableMarketingCardButtonBackground: PaletteFunction = () => + sourcePalette.neutral[100]; + const expandableMarketingCardSvgBackground: PaletteFunction = () => sourcePalette.neutral[100]; @@ -6284,6 +6287,10 @@ const paletteColours = { light: expandableMarketingCardBackground, dark: expandableMarketingCardBackground, }, + '--expandable-marketing-card-button-background': { + light: expandableMarketingCardButtonBackground, + dark: expandableMarketingCardButtonBackground, + }, '--expandable-marketing-card-fill-background': { light: expandableMarketingCardFillBackgroundLight, dark: expandableMarketingCardFillBackgroundDark,