src/app/legacy/containers/StoryPromo/index.jsx

Summary

Maintainability
C
1 day
Test Coverage
C
74%
import React, { useContext } from 'react';
import styled from '@emotion/styled';
import StoryPromo, {
  Headline,
  Summary,
  Link,
} from '#psammead/psammead-story-promo/src';
import { GEL_GROUP_4_SCREEN_WIDTH_MIN } from '#psammead/gel-foundations/src/breakpoints';
import pathOr from 'ramda/src/pathOr';
import LiveLabel from '#app/components/LiveLabel';
import ImagePlaceholder from '#psammead/psammead-image-placeholder/src';
import { RequestContext } from '#contexts/RequestContext';
import { createSrcsets } from '#lib/utilities/srcSet';
import buildIChefURL from '#lib/utilities/ichefURL';
import getOriginCode from '#lib/utilities/imageSrcHelpers/originCode';
import getLocator from '#lib/utilities/imageSrcHelpers/locator';
import {
  getAssetTypeCode,
  getHeadline,
  getUrl,
  getIsLive,
} from '#lib/utilities/getStoryPromoInfo';
import loggerNode from '#lib/logger.node';
import { MEDIA_MISSING } from '#lib/logger.const';
import { MEDIA_ASSET_PAGE, STORY_PAGE } from '#app/routes/utils/pageTypes';
import PromoTimestamp from '#components/Promo/timestamp';
import { ServiceContext } from '../../../contexts/ServiceContext';
import LinkContents from './LinkContents';
import MediaIndicatorContainer from './MediaIndicator';
import IndexAlsosContainer from './IndexAlsos';
import { getHeadingTagOverride, buildUniquePromoId } from './utilities';
import Image from '../../../components/Image';
import useCombinedClickTrackerHandler from './useCombinedClickTrackerHandler';

const logger = loggerNode(__filename);

const SingleColumnStoryPromo = styled(StoryPromo)`
  @media (min-width: ${GEL_GROUP_4_SCREEN_WIDTH_MIN}) {
    display: grid;
  }
`;

// eslint-disable-next-line consistent-return
const extractAltText = blocks => {
  // eslint-disable-next-line no-restricted-syntax
  for (const block of blocks) {
    if (block.type === 'paragraph') {
      return block.model.text;
    }
    if (block.model && block.model.blocks) {
      return extractAltText(block.model.blocks);
    }
    return '';
  }
};
const getBlockByType = (blocks, blockType) => {
  let blockData;
  blocks.forEach(block => {
    if (!blockData && block.type === blockType) {
      blockData = block;
    }
  });
  return blockData;
};

const StoryPromoImage = ({
  isAmp = false,
  useLargeImages,
  imageValues = {
    path: '',
    altText: '',
    height: '',
    width: '',
  },
  lazyLoad = false,
  pageType = '',
}) => {
  if (!imageValues) {
    const landscapeRatio = (9 / 16) * 100;
    return <ImagePlaceholder ratio={landscapeRatio} />;
  }

  // eslint-disable-next-line prefer-const
  let { height, width, path, altText, copyrightHolder } = imageValues;
  let originCode = getOriginCode(path);
  let locator = getLocator(path);
  if (imageValues.defaultPromoImage) {
    const { blocks } = imageValues.defaultPromoImage;
    const rawImageBlock = getBlockByType(blocks, 'rawImage').model;
    const altTextBlocks = getBlockByType(blocks, 'altText').model.blocks;
    altText = extractAltText(altTextBlocks);
    height = rawImageBlock.height;
    width = rawImageBlock.width;
    locator = rawImageBlock.locator;
    originCode = rawImageBlock.originCode;
    copyrightHolder = rawImageBlock.copyrightHolder;
  }
  const imageResolutions = [70, 95, 144, 183, 240, 320, 660];
  const { primarySrcset, primaryMimeType, fallbackSrcset, fallbackMimeType } =
    createSrcsets({
      originCode,
      locator,
      originalImageWidth: width,
      imageResolutions,
    });
  let sizes = useLargeImages
    ? '(min-width: 1100px) 496px, (min-width: 600px) 45.83vw, 94.29vw'
    : '(min-width: 1020px) 232px, calc(31.86vw - 7px)';
  if (pageType === STORY_PAGE) {
    sizes = '(min-width: 1080px) 315px, 29.74vw';
  }
  const DEFAULT_IMAGE_RES = 660;
  const src = buildIChefURL({
    originCode,
    locator,
    resolution: DEFAULT_IMAGE_RES,
  });

  return (
    <Image
      isAmp={isAmp}
      src={src}
      alt={altText}
      srcSet={primarySrcset}
      mediaType={primaryMimeType}
      fallbackSrcSet={fallbackSrcset}
      fallbackMediaType={fallbackMimeType}
      sizes={sizes}
      width={width}
      height={height}
      lazyLoad={lazyLoad}
      attribution={copyrightHolder}
    />
  );
};

const StoryPromoContainer = ({
  item,
  index = 0,
  promoType = 'regular',
  lazyLoadImage = true,
  dir = 'ltr',
  displayImage = true,
  displaySummary = true,
  isSingleColumnLayout = false,
  serviceDatetimeLocale = null,
  eventTrackingData = null,
  labelId = '',
  sectionType = '',
}) => {
  const { script, service } = useContext(ServiceContext);
  const { isAmp, isLite, pageType } = useContext(RequestContext);
  const handleClickTracking = useCombinedClickTrackerHandler(eventTrackingData);

  const linkId = buildUniquePromoId({
    sectionType,
    promoGroupId: labelId,
    promoItem: item,
    promoIndex: index,
  });

  const isAssetTypeCode = getAssetTypeCode(item);
  const isStoryPromoPodcast =
    isAssetTypeCode === 'PRO' &&
    pathOr(null, ['contentType'], item) === 'Podcast';
  const isContentTypeGuide =
    isAssetTypeCode === 'PRO' &&
    pathOr(null, ['contentType'], item) === 'Guide';
  const headline = getHeadline(item);
  const url = getUrl(item);
  const isLive = getIsLive(item);

  const overtypedSummary = pathOr(null, ['overtypedSummary'], item);
  const hasWhiteSpaces = overtypedSummary && !overtypedSummary.trim().length;

  let promoSummary;
  if (overtypedSummary && !hasWhiteSpaces) {
    promoSummary = overtypedSummary;
  } else {
    const summary = pathOr(null, ['summary'], item);
    promoSummary = summary;
  }

  const timestamp = pathOr(null, ['timestamp'], item);
  const relatedItems = pathOr(null, ['relatedItems'], item);
  const cpsType = pathOr(null, ['cpsType'], item);
  // If mediaStatusCode is visible, there is an error in rendering the block
  const mediaStatuscode = pathOr(null, ['media', 'statusCode'], item);

  const displayTimestamp =
    timestamp && !isStoryPromoPodcast && !isContentTypeGuide && !isLive;

  if (cpsType === MEDIA_ASSET_PAGE && mediaStatuscode) {
    logger.warn(MEDIA_MISSING, {
      url: pathOr(null, ['section', 'uri'], item),
      mediaStatuscode,
      mediaBlock: item.media,
    });
  }

  const linkcontents = (
    <LinkContents
      item={item}
      isInline={!displayImage}
      // ID is a temporary fix for the a11y nested span's bug experienced in TalkBack, refer to the following issue: https://github.com/bbc/simorgh/issues/9652
      id={!isLive ? linkId : null}
    />
  );

  if (!headline || !url) {
    return null;
  }

  const isTopOrLeadingPromo = promoType === 'top' || promoType === 'leading';

  const isFirstPromo = index === 0 && isTopOrLeadingPromo;

  const headingTagOverride =
    item.headingTag ||
    getHeadingTagOverride({
      pageType,
      isContentTypeGuide,
    });

  const StyledLink = styled(Link)`
    overflow-wrap: anywhere;
  `;

  const Info = (
    <>
      <Headline
        script={script}
        service={service}
        promoType={promoType}
        promoHasImage={displayImage}
        as={headingTagOverride}
      >
        <StyledLink
          href={url}
          onClick={eventTrackingData ? handleClickTracking : null}
          // Aria-labelledby a temporary fix for the a11y nested span's bug experienced in TalkBack, refer to the following issue: https://github.com/bbc/simorgh/issues/9652
          aria-labelledby={linkId}
          className="focusIndicatorDisplayInlineBlock"
        >
          {isLive ? (
            <LiveLabel
              id={linkId}
              {...(isFirstPromo && {
                className: 'first-promo',
              })}
            >
              {linkcontents}
            </LiveLabel>
          ) : (
            linkcontents
          )}
        </StyledLink>
      </Headline>
      {promoSummary && displaySummary && (
        <Summary
          script={script}
          service={service}
          promoType={promoType}
          promoHasImage={displayImage}
        >
          {promoSummary}
        </Summary>
      )}
      {displayTimestamp && (
        <PromoTimestamp serviceDatetimeLocale={serviceDatetimeLocale}>
          {timestamp}
        </PromoTimestamp>
      )}
      {promoType === 'top' && relatedItems && (
        <IndexAlsosContainer
          alsoItems={relatedItems}
          script={script}
          service={service}
          dir={dir}
        />
      )}
    </>
  );
  const imageValues =
    pathOr(null, ['indexImage'], item) || pathOr(null, ['images'], item);

  const MediaIndicator = (
    <MediaIndicatorContainer
      item={item}
      script={script}
      service={service}
      dir={dir}
      isInline={!displayImage}
    />
  );

  const StoryPromoComponent = isSingleColumnLayout
    ? SingleColumnStoryPromo
    : StoryPromo;
  return (
    <StoryPromoComponent
      data-e2e="story-promo"
      image={
        <StoryPromoImage
          isAmp={isAmp}
          useLargeImages={isTopOrLeadingPromo}
          lazyLoad={lazyLoadImage}
          imageValues={imageValues}
          pageType={pageType}
        />
      }
      info={Info}
      mediaIndicator={MediaIndicator}
      promoType={promoType}
      dir={dir}
      displayImage={displayImage && !isLite}
    />
  );
};

export default StoryPromoContainer;