client/components/helpCentre/HelpCentreArticle.tsx (334 lines of code) (raw):
import { css } from '@emotion/react';
import {
from,
headlineBold20,
palette,
space,
textSans15,
textSansBold17,
} from '@guardian/source/foundations';
import { Button } from '@guardian/source/react-components';
import { captureException, captureMessage } from '@sentry/browser';
import { useEffect, useState } from 'react';
import * as React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { trackEvent } from '../../utilities/analytics';
import { useHelpArticleSeo } from '../../utilities/hooks/useHelpArticleSeo';
import { setPageTitle } from '../../utilities/pageTitle';
import { ThumbsUpIcon } from '../mma/shared/assets/ThumbsUpIcon';
import { CallCentreEmailAndNumbers } from '../shared/CallCenterEmailAndNumbers';
import { SelectedTopicObjectContext } from '../shared/SectionContent';
import { Spinner } from '../shared/Spinner';
import { WithStandardTopMargin } from '../shared/WithStandardTopMargin';
import { BackToHelpCentreLink } from './BackToHelpCentreLink';
import { HelpCentreContactOptions } from './HelpCentreContactOptions';
import { h2Css } from './HelpCentreStyles';
import type {
Article,
BaseNode,
ElementNode,
LinkNode,
TextNode,
} from './HelpCentreTypes';
import { isArticleLiveChatFeatureEnabled } from './liveChat/liveChatFeatureSwitch';
export const HelpCentreArticle = () => {
const [article, setArticle] = useState<Article | undefined>(undefined);
const { articleCode } = useParams();
const navigate = useNavigate();
useEffect(() => {
setArticle(undefined);
fetch(`/api/help-centre/article/${articleCode}`)
.then((response) => {
if (response.ok) {
return response.json();
} else {
captureMessage(
`Fetching article ${articleCode} returned ${response.status}.`,
);
navigate('/help-centre');
}
})
.then((articleData) => setArticle(articleData as Article))
.catch((error) =>
captureException(
`Failed to fetch article ${articleCode}. Error: ${error}`,
),
);
}, [articleCode, navigate]);
const setSelectedTopicId = React.useContext(SelectedTopicObjectContext);
useEffect(() => {
setSelectedTopicId(article?.topics[0].path);
}, [article, setSelectedTopicId]);
const articleContainerCss = css`
max-width: 620px;
color: ${palette.neutral['7']};
`;
setPageTitle(article?.title);
useHelpArticleSeo(article);
return (
<>
<div css={articleContainerCss}>
<h2 css={h2Css}>{article?.title}</h2>
{article ? (
<>
<ArticleBody
article={article}
articleCode={articleCode ?? ''}
/>
<ArticleFeedbackWidget
articleCode={articleCode ?? ''}
/>
{isArticleLiveChatFeatureEnabled() ? (
<HelpCentreContactOptions
compactLayout={true}
hideContactOptions={true}
/>
) : (
<>
<h2 css={h2Css}>
Still can’t find what you’re looking for?
</h2>
<CallCentreEmailAndNumbers />
<p>
Or use our contact form to get in touch and
we’ll get back to you as soon as possible.
</p>
<Button
priority="secondary"
onClick={() => {
navigate('/help-centre/contact-us');
}}
>
Contact us
</Button>
</>
)}
</>
) : (
<Loading />
)}
<BackToHelpCentreLink />
</div>
</>
);
};
const Loading = () => (
<WithStandardTopMargin>
<Spinner loadingMessage={'Fetching article...'} />
</WithStandardTopMargin>
);
interface ArticleBodyProps {
article: Article;
articleCode: string;
}
const ArticleBody = (props: ArticleBodyProps) => {
const aCss = css`
color: ${palette.brand[500]};
text-decoration: underline;
`;
const ulCss = css`
padding-left: 0;
`;
const liCss = css`
list-style: none;
padding-left: ${space[3] + space[2]}px;
position: relative;
:before {
content: '';
position: absolute;
top: 8px;
left: 0;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #c4c4c4;
}
`;
const articleBodyH2Css = css`
${headlineBold20};
margin: ${space[6]}px 0 ${space[2]}px;
b {
font-weight: 700;
}
`;
const articleBodyPCss = css`
margin: 0 0 ${space[4]}px;
font-size: 17px;
`;
// This is to appease React's "Lists need a unique key" error
let keyCounter = 0;
const getKey = () => `${props.articleCode}${keyCounter++}`;
const parseBody = (body: BaseNode[] | BaseNode): React.ReactNode => {
if (Array.isArray(body)) {
return body.map(parseBody);
} else {
const key = getKey();
switch (body.element) {
case 'text': {
return (body as TextNode).content;
}
case 'h2': {
return (
<h2 key={key} css={articleBodyH2Css}>
{parseBody((body as ElementNode).content)}
</h2>
);
}
case 'p': {
return (
<p key={key} css={articleBodyPCss}>
{parseBody((body as ElementNode).content)}
</p>
);
}
case 'ol': {
return (
<ol key={key}>
{parseBody((body as ElementNode).content)}
</ol>
);
}
case 'ul': {
return (
<ul key={key} css={ulCss}>
{parseBody((body as ElementNode).content)}
</ul>
);
}
case 'li': {
return (
<li key={key} css={liCss}>
{parseBody((body as ElementNode).content)}
</li>
);
}
case 'b': {
return (
<b key={key}>
{parseBody((body as ElementNode).content)}
</b>
);
}
case 'i': {
return (
<i key={key}>
{parseBody((body as ElementNode).content)}
</i>
);
}
case 'a': {
const node = body as LinkNode;
return (
<a key={key} href={node.href} css={aCss}>
{parseBody(node.content)}
</a>
);
}
default: {
captureMessage(
`Found unexpected element (${body.element}).`,
);
return null;
}
}
}
};
return <div>{parseBody(props.article.body)}</div>;
};
const articleFeedbackWidgetCss = css`
display: flex;
flex-direction: column;
border: 1px solid ${palette.neutral[86]};
padding: ${space[4]}px ${space[3]}px;
margin: 36px 0 48px;
${from.desktop} {
margin: 54px 0 66px;
}
${from.mobileLandscape} {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
& p {
margin: 0;
${textSansBold17};
}
& .buttonDiv {
min-height: 36px;
display: flex;
align-items: center;
margin-top: ${space[4]}px;
${from.mobileLandscape} {
margin-top: 0;
}
& > * {
margin-right: ${space[2]}px;
${from.mobileLandscape} {
margin-right: ${space[3]}px;
}
}
& p {
${textSans15};
}
}
`;
interface ArticleFeedbackWidgetProps {
articleCode: string;
}
export const ArticleFeedbackWidget = (props: ArticleFeedbackWidgetProps) => {
const [feedBackButtonClicked, setFeedBackButtonClicked] = useState(false);
return (
<div css={articleFeedbackWidgetCss}>
<p>Did you find the information you need?</p>
<div className="buttonDiv">
{feedBackButtonClicked ? (
<p>Thank you!</p>
) : (
<>
<Button
icon={<ThumbsUpIcon />}
hideLabel={true}
size="small"
cssOverrides={css`
svg {
width: initial;
}
`}
onClick={() => {
setFeedBackButtonClicked(true);
trackEvent({
eventCategory: 'help-centre',
eventAction: 'article-feedback',
eventLabel: props.articleCode,
eventValue: 1,
});
}}
>
Yes
</Button>
<Button
icon={<ThumbsUpIcon invertIcon={true} />}
hideLabel={true}
size="small"
cssOverrides={css`
svg {
width: initial;
}
`}
onClick={() => {
setFeedBackButtonClicked(true);
trackEvent({
eventCategory: 'help-centre',
eventAction: 'article-feedback',
eventLabel: props.articleCode,
eventValue: 0,
});
}}
>
No
</Button>
</>
)}
</div>
</div>
);
};