Skip to content

Commit 601b9da

Browse files
feat: add advertise-here CTA on ad surfaces (#5855)
Co-authored-by: Chris Bongers <chrisbongers@gmail.com>
1 parent 45a782b commit 601b9da

11 files changed

Lines changed: 250 additions & 80 deletions

File tree

packages/shared/src/components/FeedItemComponent.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,6 @@ function FeedItemComponent({
261261
onCommentClick,
262262
onReadArticleClick,
263263
virtualizedNumCards,
264-
disableAdRefresh,
265264
}: FeedItemComponentProps): ReactElement | null {
266265
const { logEvent } = useLogContext();
267266
const inViewRef = useLogImpression(
@@ -459,11 +458,6 @@ function FeedItemComponent({
459458
index={item.index}
460459
feedIndex={index}
461460
onLinkClick={(ad: Ad) => onAdAction(AdActions.Click, ad)}
462-
onRefresh={
463-
disableAdRefresh
464-
? undefined
465-
: (ad: Ad) => onAdAction(AdActions.Refresh, ad)
466-
}
467461
/>
468462
);
469463
}

packages/shared/src/components/cards/ad/AdCard.spec.tsx

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,35 @@ import React from 'react';
22
import type { RenderResult } from '@testing-library/react';
33
import { render, screen, waitFor, within } from '@testing-library/react';
44
import { QueryClient } from '@tanstack/react-query';
5+
import { GrowthBook } from '@growthbook/growthbook-react';
56
import ad from '../../../../__tests__/fixture/ad';
67
import { AdGrid } from './AdGrid';
78
import { AdList } from './AdList';
89
import { SignalAdList } from './SignalAdList';
910
import type { AdCardProps } from './common/common';
1011
import { TestBootProvider } from '../../../../__tests__/helpers/boot';
1112
import { ActiveFeedContext } from '../../../contexts';
13+
import { businessWebsiteUrl } from '../../../lib/constants';
1214

1315
const defaultProps: AdCardProps = {
1416
ad,
17+
index: 0,
18+
feedIndex: 0,
1519
onLinkClick: jest.fn(),
1620
};
1721

1822
beforeEach(() => {
1923
jest.clearAllMocks();
2024
});
2125

22-
const renderComponent = (props: Partial<AdCardProps> = {}): RenderResult => {
23-
const client = new QueryClient();
24-
return render(
25-
<TestBootProvider client={client}>
26-
<ActiveFeedContext.Provider value={{ queryKey: 'test' }}>
27-
<AdGrid {...defaultProps} {...props} />
28-
</ActiveFeedContext.Provider>
29-
</TestBootProvider>,
30-
);
31-
};
32-
3326
const renderListComponent = (
3427
props: Partial<AdCardProps> = {},
28+
gb = new GrowthBook(),
3529
): RenderResult => {
3630
const client = new QueryClient();
3731
return render(
38-
<TestBootProvider client={client}>
39-
<ActiveFeedContext.Provider value={{ queryKey: 'test' }}>
32+
<TestBootProvider client={client} gb={gb}>
33+
<ActiveFeedContext.Provider value={{ items: [], queryKey: ['test'] }}>
4034
<AdList {...defaultProps} {...props} />
4135
</ActiveFeedContext.Provider>
4236
</TestBootProvider>,
@@ -49,26 +43,53 @@ const renderSignalListComponent = (
4943
const client = new QueryClient();
5044
return render(
5145
<TestBootProvider client={client}>
52-
<ActiveFeedContext.Provider value={{ queryKey: 'test' }}>
46+
<ActiveFeedContext.Provider value={{ items: [], queryKey: ['test'] }}>
5347
<SignalAdList {...defaultProps} {...props} />
5448
</ActiveFeedContext.Provider>
5549
</TestBootProvider>,
5650
);
5751
};
5852

53+
const getGrowthBook = (isAdReferralCtaEnabled = false): GrowthBook => {
54+
const gb = new GrowthBook();
55+
56+
gb.setFeatures({
57+
ad_referral_cta: {
58+
defaultValue: isAdReferralCtaEnabled,
59+
},
60+
});
61+
62+
return gb;
63+
};
64+
5965
const getNormalizedText = (element?: Element | null): string =>
6066
element?.textContent?.replace(/\u200B/g, '').trim() ?? '';
6167

68+
const renderGridComponent = (
69+
props: Partial<AdCardProps> = {},
70+
gb = new GrowthBook(),
71+
): RenderResult => {
72+
const client = new QueryClient();
73+
74+
return render(
75+
<TestBootProvider client={client} gb={gb}>
76+
<ActiveFeedContext.Provider value={{ items: [], queryKey: ['test'] }}>
77+
<AdGrid {...defaultProps} {...props} />
78+
</ActiveFeedContext.Provider>
79+
</TestBootProvider>,
80+
);
81+
};
82+
6283
it('should call on click on component left click', async () => {
63-
renderComponent();
84+
renderGridComponent();
6485
const el = await screen.findByTestId('adItem');
6586
const links = await within(el).findAllByRole('link');
6687
links[0].click();
6788
await waitFor(() => expect(defaultProps.onLinkClick).toBeCalledWith(ad));
6889
});
6990

7091
it('should call on click on component middle mouse up', async () => {
71-
renderComponent();
92+
renderGridComponent();
7293
const el = await screen.findByTestId('adItem');
7394
const links = await within(el).findAllByRole('link');
7495
links[0].dispatchEvent(
@@ -78,29 +99,46 @@ it('should call on click on component middle mouse up', async () => {
7899
});
79100

80101
it('should show a single image by default', async () => {
81-
renderComponent();
102+
renderGridComponent();
82103
const img = await screen.findByAltText('Ad image');
83104
const background = screen.queryByAltText('Ad image background');
84105
expect(img).toBeInTheDocument();
85106
expect(background).not.toBeInTheDocument();
86107
});
87108

88109
it('should show blurred image for carbon', async () => {
89-
renderComponent({ ad: { ...ad, source: 'Carbon' } });
110+
renderGridComponent({ ad: { ...ad, source: 'Carbon' } });
90111
const img = await screen.findByAltText('Ad image');
91112
const background = screen.queryByAltText('Ad image background');
92113
expect(img).toHaveClass('absolute');
93114
expect(background).toBeInTheDocument();
94115
});
95116

96117
it('should show pixel images', async () => {
97-
renderComponent({
118+
renderGridComponent({
98119
ad: { ...ad, pixel: ['https://daily.dev/pixel'] },
99120
});
100121
const el = await screen.findByTestId('pixel');
101122
expect(el).toHaveAttribute('src', 'https://daily.dev/pixel');
102123
});
103124

125+
it('should not render advertise link by default', () => {
126+
renderGridComponent();
127+
128+
expect(
129+
screen.queryByRole('link', { name: 'Advertise here' }),
130+
).not.toBeInTheDocument();
131+
});
132+
133+
it('should render advertise link on grid ad when feature is enabled', () => {
134+
renderGridComponent({}, getGrowthBook(true));
135+
136+
expect(screen.getByRole('link', { name: 'Advertise here' })).toHaveAttribute(
137+
'href',
138+
businessWebsiteUrl,
139+
);
140+
});
141+
104142
it('should render promoted attribution outside of list title clamp', async () => {
105143
const promotedMatcher = (_: string, element?: Element | null): boolean => {
106144
const text = getNormalizedText(element);
@@ -114,6 +152,15 @@ it('should render promoted attribution outside of list title clamp', async () =>
114152
expect(await screen.findByText(promotedMatcher)).toBeInTheDocument();
115153
});
116154

155+
it('should render advertise link on list ad when feature is enabled', () => {
156+
renderListComponent({}, getGrowthBook(true));
157+
158+
expect(screen.getByRole('link', { name: 'Advertise here' })).toHaveAttribute(
159+
'href',
160+
businessWebsiteUrl,
161+
);
162+
});
163+
117164
it('should render company logo and company name in signal ad header', async () => {
118165
const companyLogo = 'https://daily.dev/company-logo.png';
119166
const companyName = 'daily.dev';

packages/shared/src/components/cards/ad/AdGrid.tsx

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ReactElement } from 'react';
2-
import React, { forwardRef, useCallback } from 'react';
2+
import React, { forwardRef } from 'react';
33

44
import {
55
Card,
@@ -18,31 +18,28 @@ import { RemoveAd } from './common/RemoveAd';
1818
import { usePlusSubscription } from '../../../hooks/usePlusSubscription';
1919
import type { InViewRef } from '../../../hooks/feed/useAutoRotatingAds';
2020
import { useAutoRotatingAds } from '../../../hooks/feed/useAutoRotatingAds';
21-
import { AdRefresh } from './common/AdRefresh';
2221
import { Button } from '../../buttons/Button';
2322
import { ButtonSize, ButtonVariant } from '../../buttons/common';
2423
import { AdFavicon } from './common/AdFavicon';
2524
import PostTags from '../common/PostTags';
2625
import { useFeature } from '../../GrowthBookProvider';
2726
import { adImprovementsV3Feature } from '../../../lib/featureManagement';
27+
import { TargetId } from '../../../lib/log';
28+
import { AdvertiseLink } from './common/AdvertiseLink';
2829

29-
export const AdGrid = forwardRef(function AdGrid(
30-
{ ad, onLinkClick, onRefresh, domProps, index, feedIndex }: AdCardProps,
31-
inViewRef: InViewRef,
30+
export const AdGrid = forwardRef<HTMLElement, AdCardProps>(function AdGrid(
31+
{ ad, onLinkClick, domProps, index, feedIndex },
32+
forwardedRef,
3233
): ReactElement {
3334
const { isPlus } = usePlusSubscription();
3435
const adImprovementsV3 = useFeature(adImprovementsV3Feature);
35-
const { ref, refetch, isRefetching } = useAutoRotatingAds(
36+
const { ref } = useAutoRotatingAds(
3637
ad,
3738
index,
3839
feedIndex,
39-
inViewRef,
40+
forwardedRef as InViewRef,
4041
);
41-
42-
const onRefreshClick = useCallback(async () => {
43-
onRefresh?.(ad);
44-
await refetch();
45-
}, [ad, onRefresh, refetch]);
42+
const matchingTags = ad?.matchingTags ?? [];
4643

4744
return (
4845
<Card {...domProps} data-testid="adItem" ref={ref}>
@@ -51,9 +48,9 @@ export const AdGrid = forwardRef(function AdGrid(
5148
<CardTextContainer className="flex-1">
5249
<CardTitle className="typo-title3">{ad.description}</CardTitle>
5350
<CardSpace />
54-
{adImprovementsV3 && ad?.matchingTags?.length > 0 ? (
51+
{adImprovementsV3 && matchingTags.length > 0 ? (
5552
<PostTags
56-
post={{ tags: ad.matchingTags.slice(0, 6) }}
53+
post={{ tags: matchingTags.slice(0, 6) }}
5754
className="!items-end"
5855
/>
5956
) : null}
@@ -76,19 +73,17 @@ export const AdGrid = forwardRef(function AdGrid(
7673
{ad.callToAction}
7774
</Button>
7875
)}
76+
<AdvertiseLink
77+
targetId={TargetId.AdCard}
78+
buttonStyle
79+
size={ButtonSize.Small}
80+
/>
7981
<div className="ml-auto flex items-center gap-2">
80-
{!!onRefresh && (
81-
<AdRefresh
82-
variant={ButtonVariant.Tertiary}
83-
size={ButtonSize.Small}
84-
onClick={onRefreshClick}
85-
loading={isRefetching}
86-
/>
87-
)}
8882
{!isPlus && (
8983
<RemoveAd
9084
variant={ButtonVariant.Tertiary}
9185
size={ButtonSize.Small}
86+
className="!font-normal typo-footnote"
9287
/>
9388
)}
9489
</div>

packages/shared/src/components/cards/ad/AdList.tsx

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { AnchorHTMLAttributes, ReactElement } from 'react';
2-
import React, { forwardRef, useCallback } from 'react';
2+
import React, { forwardRef } from 'react';
33
import {
44
CardContent,
55
CardImage,
@@ -17,14 +17,15 @@ import { RemoveAd } from './common/RemoveAd';
1717
import { usePlusSubscription } from '../../../hooks/usePlusSubscription';
1818
import type { InViewRef } from '../../../hooks/feed/useAutoRotatingAds';
1919
import { useAutoRotatingAds } from '../../../hooks/feed/useAutoRotatingAds';
20-
import { AdRefresh } from './common/AdRefresh';
2120
import { Button } from '../../buttons/Button';
2221
import { ButtonSize, ButtonVariant } from '../../buttons/common';
2322
import AdAttribution from './common/AdAttribution';
2423
import { AdFavicon } from './common/AdFavicon';
2524
import PostTags from '../common/PostTags';
2625
import { useFeature } from '../../GrowthBookProvider';
2726
import { adImprovementsV3Feature } from '../../../lib/featureManagement';
27+
import { TargetId } from '../../../lib/log';
28+
import { AdvertiseLink } from './common/AdvertiseLink';
2829

2930
const getLinkProps = ({
3031
ad,
@@ -42,39 +43,38 @@ const getLinkProps = ({
4243
};
4344
};
4445

45-
export const AdList = forwardRef(function AdCard(
46-
{ ad, onLinkClick, onRefresh, domProps, index, feedIndex }: AdCardProps,
47-
inViewRef: InViewRef,
46+
export const AdList = forwardRef<HTMLElement, AdCardProps>(function AdCard(
47+
{ ad, onLinkClick, domProps, index, feedIndex },
48+
forwardedRef,
4849
): ReactElement {
4950
const { isPlus } = usePlusSubscription();
5051
const adImprovementsV3 = useFeature(adImprovementsV3Feature);
51-
const { ref, refetch, isRefetching } = useAutoRotatingAds(
52+
const { ref } = useAutoRotatingAds(
5253
ad,
5354
index,
5455
feedIndex,
55-
inViewRef,
56+
forwardedRef as InViewRef,
5657
);
57-
58-
const onRefreshClick = useCallback(async () => {
59-
onRefresh?.(ad);
60-
await refetch();
61-
}, [ad, onRefresh, refetch]);
58+
const matchingTags = ad?.matchingTags ?? [];
6259

6360
return (
6461
<FeedItemContainer
65-
domProps={domProps}
62+
domProps={domProps ?? {}}
6663
ref={ref}
6764
data-testid="adItem"
68-
linkProps={getLinkProps({ ad, onLinkClick })}
65+
linkProps={getLinkProps({
66+
ad,
67+
onLinkClick: onLinkClick ?? (() => undefined),
68+
})}
6969
>
7070
<CardContent>
7171
<CardTextContainer className="mr-4 flex-1">
7272
<CardTitle className="!mt-0 typo-title3">
7373
<AdFavicon ad={ad} className="mx-0 !mt-0 mb-2" />
7474
{ad.description}
7575
</CardTitle>
76-
{adImprovementsV3 && ad?.matchingTags?.length > 0 ? (
77-
<PostTags post={{ tags: ad.matchingTags.slice(0, 6) }} />
76+
{adImprovementsV3 && matchingTags.length > 0 ? (
77+
<PostTags post={{ tags: matchingTags.slice(0, 6) }} />
7878
) : null}
7979
<AdAttribution
8080
ad={ad}
@@ -98,11 +98,18 @@ export const AdList = forwardRef(function AdCard(
9898
{ad.callToAction}
9999
</Button>
100100
)}
101-
<div className="ml-auto flex items-center gap-2">
102-
{!!onRefresh && (
103-
<AdRefresh onClick={onRefreshClick} loading={isRefetching} />
101+
<AdvertiseLink
102+
targetId={TargetId.AdCard}
103+
buttonStyle
104+
size={ButtonSize.Small}
105+
/>
106+
<div className="ml-auto">
107+
{!isPlus && (
108+
<RemoveAd
109+
size={ButtonSize.Small}
110+
className="!font-normal typo-footnote"
111+
/>
104112
)}
105-
{!isPlus && <RemoveAd />}
106113
</div>
107114
</div>
108115
<AdPixel pixel={ad.pixel} />

0 commit comments

Comments
 (0)