Skip to content

Commit

Permalink
feat: add subject label to item descriptions
Browse files Browse the repository at this point in the history
This change required a markdown parser to be able to inject the label
below any paragraphs that contain only media (images, videos, audio)
and above the first paragraph that contains textual content.

Also noticed that the `Banners` component was incorrectly placed in a
scrollable container, making banner alerts (such as validation errors)
not appear to users who have scrolled down on an item that has a long
description.
  • Loading branch information
farmerpaul committed Sep 5, 2024
1 parent 25ecf9e commit bfc5b43
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 17 deletions.
3 changes: 2 additions & 1 deletion src/features/PassSurvey/ui/SurveyLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ const SurveyLayout = (props: Props) => {
title={props.title}
/>

<Banners />

<Box
id="assessment-content-container"
display="flex"
flex={1}
flexDirection="column"
overflow="scroll"
>
<Banners />
<Box display="flex" flex={1} justifyContent="center">
{props.children}
</Box>
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@
},
"charactersCount": "{{numCharacters}}/{{maxCharacters}} characters",
"targetSubjectLabel": "About {{name}}",
"targetSubjectBanner": "Please ensure all your responses are about <strong>{{name}}</strong>"
"targetSubjectBanner": "Please ensure all your responses are about <strong>{{name}}</strong>",
"loading": "Loading…"
}
}
3 changes: 2 additions & 1 deletion src/i18n/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@
},
"charactersCount": "{{numCharacters}}/{{maxCharacters}} caractères",
"targetSubjectLabel": "À propos de {{name}}",
"targetSubjectBanner": "Assurez-vous que toutes vos réponses concernent <strong>{{name}}</strong>"
"targetSubjectBanner": "Assurez-vous que toutes vos réponses concernent <strong>{{name}}</strong>",
"loading": "Chargement en cours..."
}
}
22 changes: 22 additions & 0 deletions src/shared/ui/CardItem/TargetSubjectLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { SubjectDTO } from '~/shared/api/types/subject';
import { Box } from '~/shared/ui';
import { TargetSubjectLabel } from '~/widgets/TargetSubjectLabel';

type Props = {
subject: SubjectDTO | null;
};

export const TargetSubjectLine = ({ subject }: Props) => {
if (!subject) return null;

return (
<Box
sx={{
'&:not(:first-child)': { mt: 4.8 },
mb: 1.2,
}}
>
<TargetSubjectLabel subject={subject} />
</Box>
);
};
32 changes: 25 additions & 7 deletions src/shared/ui/CardItem/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { PropsWithChildren } from 'react';
import { PropsWithChildren, useContext, useMemo } from 'react';

import { Theme } from '../../constants';
import { useCustomMediaQuery, useCustomTranslation } from '../../utils';
import { TargetSubjectLine } from './TargetSubjectLine';

import { Markdown } from '~/shared/ui';
import Box from '~/shared/ui/Box';
import Text from '~/shared/ui/Text';
import { SurveyContext } from '~/features/PassSurvey';
import { Theme } from '~/shared/constants';
import { Box, Markdown, Text } from '~/shared/ui';
import { insertAfterMedia, useCustomMediaQuery, useCustomTranslation } from '~/shared/utils';

interface CardItemProps extends PropsWithChildren {
watermark?: string;
Expand All @@ -20,6 +20,14 @@ export const CardItem = ({ children, markdown, isOptional, testId }: CardItemPro

const { t } = useCustomTranslation();

const context = useContext(SurveyContext);

const processedMarkdown = useMemo(() => {
if (!context.targetSubject) return markdown;

return insertAfterMedia(markdown, '<div id="target-subject"></div>');
}, [markdown, context.targetSubject]);

return (
<Box
data-testid={testId || 'active-item'}
Expand All @@ -31,7 +39,17 @@ export const CardItem = ({ children, markdown, isOptional, testId }: CardItemPro
sx={{ fontFamily: 'Atkinson', fontWeight: '400', fontSize: '18px', lineHeight: '28px' }}
>
<Box>
<Markdown markdown={markdown} />
<Markdown
markdown={processedMarkdown}
components={{
div: (props) =>
props.id === 'target-subject' ? (
<TargetSubjectLine subject={context.targetSubject} />
) : (
<div {...props} />
),
}}
/>
{isOptional && (
<Text
variant="body1"
Expand Down
17 changes: 11 additions & 6 deletions src/shared/ui/Markdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import ReactMarkdown from 'react-markdown';
import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';

import { useMarkdownExtender } from './lib/useMarkdownExtender';

import './style.css';
import { useCustomTranslation } from '~/shared/utils';

interface MarkdownProps {
type MarkdownProps = Omit<ReactMarkdownOptions, 'children'> & {
markdown: string;
}
};

export const Markdown = (props: MarkdownProps) => {
const { isLoading, markdown } = useMarkdownExtender(props.markdown);
export const Markdown = ({ markdown: markdownProp, ...rest }: MarkdownProps) => {
const { t } = useCustomTranslation();
const { isLoading, markdown } = useMarkdownExtender(markdownProp);

if (isLoading) {
return <div>Loading...</div>;
return <div>{t('loading')}</div>;
}

return (
<div id="markdown-wrapper" data-testid="markdown">
<ReactMarkdown rehypePlugins={[rehypeRaw, remarkGfm]}>{markdown}</ReactMarkdown>
<ReactMarkdown rehypePlugins={[rehypeRaw, remarkGfm]} {...rest}>
{markdown}
</ReactMarkdown>
</div>
);
};
1 change: 1 addition & 0 deletions src/shared/utils/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './getInitials';
export * from './delay';
export * from './cutString';
export * from './getSubjectName';
export * from './insertAfterMedia';
31 changes: 31 additions & 0 deletions src/shared/utils/helpers/insertAfterMedia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Inserts the given string into the given markdown after the first line containing content
* that does not contain solely media.
*
* @param markdown Markdown content to parse
* @param inserted Inserted string
* @returns Processed markdown with string inserted
*/
export const insertAfterMedia = (markdown: string, inserted: string) => {
const lines = markdown.split('\n');
// Stop at first line containing content that is not:
// - solely a media element (<img>/<video>/<audio>/![...](...))
// - a media element wrapped in an alignment block (::: hljs-* ... :::)
let i: number;
for (i = 0; i < lines.length; i++) {
const lineWithoutMedia = lines[i]
.replaceAll(/^:::\s+hljs-\S+|:::$|<(img|video|audio)[^>]+>|!\[[^\]]*\]\([^)]*\)/gi, '')
.trim();
if (lineWithoutMedia) break;
}

if (i === lines.length) {
// Append string to bottom if no lines containing non-media content were found
lines.push(inserted);
} else {
// Else insert string before line containing non-media content
lines.splice(i, 0, inserted);
}

return lines.join('\n');
};
2 changes: 1 addition & 1 deletion src/widgets/TargetSubjectLabel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const TargetSubjectLabel = ({ subject }: Props) => {
<Box
data-testid="subject-label"
sx={{
display: 'flex',
display: 'inline-flex',
alignItems: 'center',
padding: '4px 8px',
borderRadius: '8px',
Expand Down

0 comments on commit bfc5b43

Please sign in to comment.