Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
const { isLite } = use(RequestContext);

const clickTrackerHandler = useClickTrackerHandler(eventTrackingData);

console.log('recommendation item: ', recommendation);

Check warning on line 23 in src/app/components/Recommendations/RecommendationsItem/index.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected console statement
if (!recommendation) return null;

const { title, image, href } = recommendation;
Expand Down
263 changes: 263 additions & 0 deletions src/app/components/Recommendations/helpers/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import pathOr from 'ramda/src/pathOr';
import pathEq from 'ramda/src/pathEq';
import tail from 'ramda/src/tail';
import slice from 'ramda/src/slice';
import last from 'ramda/src/last';
import filter from 'ramda/src/filter';
import pipe from 'ramda/src/pipe';
import { OptimoBlock } from '#app/models/types/optimo';
import { Recommendation } from '#app/models/types/onwardJourney';

type Features = {
id: string;
locators: {
canonicalUrl?: string;
};
headlines: {
promoHeadline?: { blocks: { model: { text: string } }[] };
seoHeadline?: string;
};
images?: {
defaultPromoImage?: {
blocks?: any[];

Check warning on line 22 in src/app/components/Recommendations/helpers/index.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
};
};
};

type TopStoryItem = {
id: string;
headlines?: {
overtyped?: string;
headline?: string;
promoHeadline?: {
blocks?: Array<{
model?: {
blocks?: Array<{
model?: {
text?: string;
};
}>;
};
}>;
};
};
name?: string;
locators?: {
assetUri?: string;
canonicalUrl?: string;
};
uri?: string;
images?: {
defaultPromoImage?: {
blocks?: any[];

Check warning on line 52 in src/app/components/Recommendations/helpers/index.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
};
};
};

// --- Shared utilities for extracting image data from defaultPromoImage ---

const getAltTextFromDefaultPromoImage = (defaultPromoImage?: {
blocks?: any[];

Check warning on line 60 in src/app/components/Recommendations/helpers/index.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
}) => {
const altTextBlock = defaultPromoImage?.blocks?.find(
block => block.type === 'altText',
);
return (
altTextBlock?.model?.blocks?.[0]?.model?.blocks?.[0]?.model?.blocks?.[0]
?.model?.text || ''
);
};

const getRawImageBlock = (defaultPromoImage?: { blocks?: any[] }) =>

Check warning on line 71 in src/app/components/Recommendations/helpers/index.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
defaultPromoImage?.blocks?.find(block => block.type === 'rawImage')?.model ??
{};

// --- Related Content (Optimo) ---

export const getRelatedContentData = (blocks: OptimoBlock[]) => {
const BLOCKS_TO_IGNORE = ['wsoj', 'mpu', 'continueReading'];
const removeCustomBlocks = pipe(
filter((block: OptimoBlock) => !BLOCKS_TO_IGNORE.includes(block.type)),
last,
);
const relatedContentBlock = removeCustomBlocks(blocks);
if (
!relatedContentBlock ||
!pathEq('relatedContent', ['type'], relatedContentBlock)
) {
return [];
}
const items = pathOr([], ['model', 'blocks'], relatedContentBlock);
const hasCustomTitle =
pathEq('title', [0, 'type'], items) &&
pathOr(
'',
[0, 'model', 'blocks', 0, 'model', 'blocks', 0, 'model', 'text'],
items,
);
const storyPromoItems = hasCustomTitle ? tail(items) : items;
return slice(0, 4, storyPromoItems);
};

export const getHeadlineFromOptimoBlock = (block: any) => {

Check warning on line 102 in src/app/components/Recommendations/helpers/index.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
const headlineFirst = pathOr<string>(
'',
['model', 'blocks', 0, 'model', 'blocks', 0, 'model', 'text'],
block,
);
const headlineSecond = pathOr<string>(
'',
['model', 'blocks', 1, 'model', 'blocks', 0, 'model', 'text'],
block,
);
return headlineFirst || headlineSecond;
};

export const getHrefFromOptimoBlock = (block: any) => {

Check warning on line 116 in src/app/components/Recommendations/helpers/index.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
const assetUriFirst = pathOr<string>(
'',
[
'model',
'blocks',
0,
'model',
'blocks',
0,
'model',
'blocks',
0,
'model',
'locator',
],
block,
);
const assetUriSecond = pathOr<string>(
'',
[
'model',
'blocks',
1,
'model',
'blocks',
0,
'model',
'blocks',
0,
'model',
'locator',
],
block,
);
return assetUriFirst || assetUriSecond;
};

export const getAltTextFromOptimoBlock = (block: any) =>

Check warning on line 154 in src/app/components/Recommendations/helpers/index.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
pathOr<string>(
'',
[
'model',
'blocks',
0,
'model',
'blocks',
0,
'model',
'blocks',
0,
'model',
'blocks',
0,
'model',
'text',
],
block,
);

export const getImageFromOptimoBlock = (block: any) => {

Check warning on line 176 in src/app/components/Recommendations/helpers/index.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
const imageBlock = block?.model?.blocks?.find((b: any) => b.type === 'image');

Check warning on line 177 in src/app/components/Recommendations/helpers/index.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
const rawImageBlock = imageBlock?.model?.blocks?.find(
(b: any) => b.type === 'rawImage',
);
return {
locator: rawImageBlock?.model?.locator ?? '',
altText: getAltTextFromOptimoBlock(block),
width: rawImageBlock?.model?.width ?? 0,
height: rawImageBlock?.model?.height ?? 0,
copyrightHolder: rawImageBlock?.model?.copyrightHolder ?? '',
originCode: rawImageBlock?.model?.originCode ?? '',
};
};

export const mapOptimoBlockToRecommendation = (block: any): Recommendation => ({
id: block.id,
title: getHeadlineFromOptimoBlock(block),
href: getHrefFromOptimoBlock(block),
image: getImageFromOptimoBlock(block),
});

// --- Features ---

export const mapFeaturesToRecommendation = (featuresContent: Features) => {
const promoHeadlineBlocks = featuresContent.headlines?.promoHeadline;
const promoHeadlineText =
Array.isArray(promoHeadlineBlocks) &&
promoHeadlineBlocks[0]?.blocks &&
Array.isArray(promoHeadlineBlocks[0].blocks)
? promoHeadlineBlocks[0].blocks[0]?.model?.text
: '';
const title =
promoHeadlineText || featuresContent.headlines?.seoHeadline || '';

const defaultPromoImage = featuresContent.images?.defaultPromoImage;
const rawImage = getRawImageBlock(defaultPromoImage);

const image = {
locator: rawImage.locator ?? '',
altText: getAltTextFromDefaultPromoImage(defaultPromoImage) || title,
width: rawImage.width ?? 0,
height: rawImage.height ?? 0,
copyrightHolder: rawImage.copyrightHolder ?? '',
originCode: rawImage.originCode ?? '',
};

return {
id: featuresContent.id,
title,
href: featuresContent.locators?.canonicalUrl ?? '',
image,
};
};

// --- Top Stories ---

const getTopStoryHeadline = (item: TopStoryItem) => {
const overtypedHeadline = item?.headlines?.overtyped ?? '';
const mainHeadline = item?.headlines?.headline ?? '';
const promoHeadlineText =
item?.headlines?.promoHeadline?.blocks?.[0]?.model?.blocks?.[0]?.model
?.text ?? '';
const name = item?.name ?? '';
return overtypedHeadline || mainHeadline || promoHeadlineText || name;
};

export const mapTopStoryToRecommendation = (item: TopStoryItem) => {
const title = getTopStoryHeadline(item);
const defaultPromoImage = item.images?.defaultPromoImage;
const rawImage = getRawImageBlock(defaultPromoImage);

const image = {
locator: rawImage.locator ?? '',
altText: getAltTextFromDefaultPromoImage(defaultPromoImage) || title,
width: rawImage.width ?? 0,
height: rawImage.height ?? 0,
copyrightHolder: rawImage.copyrightHolder ?? '',
originCode: rawImage.originCode ?? '',
};

return {
id: item.id,
title,
href: item.locators?.canonicalUrl ?? item.uri ?? '',
image,
};
};
73 changes: 60 additions & 13 deletions src/app/components/Recommendations/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
/** @jsx jsx */
import { use } from 'react';
import { jsx, useTheme } from '@emotion/react';

import { pathOr } from 'ramda';
import useToggle from '#hooks/useToggle';
import SectionLabel from '#psammead/psammead-section-label/src';
import SkipLinkWrapper from '#components/SkipLinkWrapper';

import { ServiceContext } from '#contexts/ServiceContext';
import useViewTracker from '#app/hooks/useViewTracker';
import { Recommendation } from '#app/models/types/onwardJourney';
import RecommendationsItem from './RecommendationsItem';
import { OptimoBlock } from '#app/models/types/optimo';
import styles from './index.styles';
import RecommendationsItem from './RecommendationsItem';
import {
getRelatedContentData,
mapOptimoBlockToRecommendation,
mapFeaturesToRecommendation,
mapTopStoryToRecommendation,
} from './helpers';

const eventTrackingData = {
componentName: 'midarticle-mostread',
};

const Recommendations = ({ data }: { data: Recommendation[] }) => {
const { recommendations, mostRead, script, service, dir } =
interface RecommendationsProps {
data: Recommendation[];
blocks?: OptimoBlock[];
topStoriesContent?: unknown;
featuresContent?: unknown;
referrerExperimentVariant?: string;
}

const Recommendations = ({
data, // control
blocks, // search
topStoriesContent, // direct
featuresContent, // social
referrerExperimentVariant, // experiment variant for referrer
}: RecommendationsProps) => {
const { recommendations, script, service, dir, translations } =
use(ServiceContext);

const viewTracker = useViewTracker(eventTrackingData);
Expand All @@ -28,9 +49,39 @@ const Recommendations = ({ data }: { data: Recommendation[] }) => {

const { enabled } = useToggle('midArticleOnwardJourney');

const { hasMostRead } = mostRead || {};
let displayData: Recommendation[] = [];
const { skipLink, header } = recommendations || {};

if (!enabled || !hasMostRead || !data?.length) return null;
let title = header ?? 'Most read';
// most read was there originally, so is there for control and when the user is not in an experiment
if (
!referrerExperimentVariant ||
referrerExperimentVariant === 'off' ||
referrerExperimentVariant.includes('control')
) {
displayData = data ?? [];
} else if (referrerExperimentVariant === 'adaptive_search') {
displayData = getRelatedContentData(blocks ?? []).map(
mapOptimoBlockToRecommendation,
);
title = pathOr('Related Content', ['relatedContent'], translations);
} else if (referrerExperimentVariant === 'adaptive_direct') {
displayData = Array.isArray(topStoriesContent)
? topStoriesContent.map(mapTopStoryToRecommendation)
: [];
title = translations?.topStoriesTitle ?? 'Top Stories';
} else if (referrerExperimentVariant === 'adaptive_social') {
displayData = Array.isArray(featuresContent)
? featuresContent.slice(0, 4).map(mapFeaturesToRecommendation)
: [];
title = pathOr(
'Features & Analysis',
['featuresAnalysisTitle'],
translations,
);
}

if (!enabled || !displayData.length) return null;

const labelId = 'recommendations-heading';

Expand All @@ -39,18 +90,14 @@ const Recommendations = ({ data }: { data: Recommendation[] }) => {
'aria-labelledby': labelId,
};

const { skipLink, header } = recommendations || {};

const { text, endTextVisuallyHidden } = skipLink || {
text: 'Skip %title% and continue reading',
endTextVisuallyHidden: 'End of %title%',
};

const title = header ?? 'Most read';

const terms = { '%title%': title };

const isSinglePromo = data?.length === 1;
const isSinglePromo = displayData.length === 1;

const endTextId = `end-of-recommendations`;

Expand Down Expand Up @@ -85,10 +132,10 @@ const Recommendations = ({ data }: { data: Recommendation[] }) => {
</SectionLabel>
) : null}
{isSinglePromo ? (
<RecommendationsItem recommendation={data?.[0]} />
<RecommendationsItem recommendation={displayData?.[0]} />
) : (
<ul css={styles.recommendationsList} role="list" {...viewTracker}>
{data?.map(recommendation => (
{displayData?.map(recommendation => (
<li key={recommendation.id} role="listitem">
<RecommendationsItem recommendation={recommendation} />
</li>
Expand Down
Loading
Loading