refactor(client): use different challenges component in different blocks (#59479)

This commit is contained in:
Oliver Eyton-Williams 2025-10-16 18:25:53 +02:00 committed by GitHub
parent 8965cf3180
commit 5add4262bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 160 additions and 78 deletions

View File

@ -25,7 +25,11 @@ import {
BlockTypes
} from '../../../../../shared-dist/config/blocks';
import CheckMark from './check-mark';
import Challenges from './challenges';
import {
GridMapChallenges,
ChallengesList,
ChallengesWithDialogues
} from './challenges';
import BlockLabel from './block-label';
import BlockIntros from './block-intros';
import BlockHeader from './block-header';
@ -220,12 +224,7 @@ export class Block extends Component<BlockProps> {
</span>
</div>
</button>
{isExpanded && (
<Challenges
challenges={extendedChallenges}
isProjectBlock={isProjectBlock}
/>
)}
{isExpanded && <ChallengesList challenges={extendedChallenges} />}
</div>
</ScrollableAnchor>
);
@ -257,10 +256,7 @@ export class Block extends Component<BlockProps> {
)}
</div>
<BlockIntros intros={blockIntroArr} />
<Challenges
challenges={extendedChallenges}
isProjectBlock={isProjectBlock}
/>
<ChallengesList challenges={extendedChallenges} />
</div>
</ScrollableAnchor>
);
@ -304,10 +300,55 @@ export class Block extends Component<BlockProps> {
<div id={`${block}-panel`}>
<BlockIntros intros={blockIntroArr} />
<Challenges
<GridMapChallenges
challenges={extendedChallenges}
isProjectBlock={isProjectBlock}
isGridMap={true}
blockTitle={blockTitle}
/>
</div>
</>
)}
</div>
</ScrollableAnchor>
);
/**
* TaskGridBlock displays tasks in a grid and dialogues separately.
* This layout is used for task-based blocks.
* Example: https://www.freecodecamp.org/learn/a2-english-for-developers/#learn-greetings-in-your-first-day-at-the-office
*/
const TaskGridBlock = (
<ScrollableAnchor id={block}>
<div className={`block block-grid ${isExpanded ? 'open' : ''}`}>
<BlockHeader
blockDashed={block}
blockTitle={blockTitle}
blockType={blockType}
completedCount={completedCount}
courseCompletionStatus={courseCompletionStatus()}
handleClick={this.handleBlockClick}
isCompleted={isBlockCompleted}
isExpanded={isExpanded}
percentageCompleted={percentageCompleted}
/>
{isExpanded && (
<>
{!isAudited && (
<div className='tags-wrapper'>
<Link
className='cert-tag'
to={t('links:help-translate-link-url')}
>
{t('misc.translation-pending')}
</Link>
</div>
)}
<div id={`${block}-panel`}>
<BlockIntros intros={blockIntroArr} />
<ChallengesWithDialogues
challenges={extendedChallenges}
blockTitle={blockTitle}
/>
</div>
@ -412,12 +453,15 @@ export class Block extends Component<BlockProps> {
id={`${block}-panel`}
className={isGridBlock ? 'challenge-grid-block-panel' : ''}
>
<Challenges
challenges={extendedChallenges}
isProjectBlock={false}
isGridMap={isGridBlock}
blockTitle={blockTitle}
/>
{isGridBlock ? (
<GridMapChallenges
challenges={extendedChallenges}
blockTitle={blockTitle}
isProjectBlock={isProjectBlock}
/>
) : (
<ChallengesList challenges={extendedChallenges} />
)}
</div>
</div>
)}
@ -433,7 +477,7 @@ export class Block extends Component<BlockProps> {
[BlockLayouts.LegacyLink]: LegacyLinkBlock,
[BlockLayouts.LegacyChallengeList]: LegacyChallengeListBlock,
[BlockLayouts.LegacyChallengeGrid]: LegacyChallengeGridBlock,
[BlockLayouts.DialogueGrid]: LegacyChallengeGridBlock
[BlockLayouts.DialogueGrid]: TaskGridBlock
};
return (

View File

@ -1,5 +1,5 @@
import React from 'react';
import { withTranslation, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
@ -18,11 +18,16 @@ interface ChallengeInfo {
challengeType: number;
}
interface Challenges {
interface ChallengesProps {
challenges: ChallengeInfo[];
}
interface BlockTitleProps {
blockTitle: string;
}
interface IsProjectBlockProps {
isProjectBlock: boolean;
isGridMap?: boolean;
blockTitle?: string | null;
}
const CheckMark = ({ isCompleted }: { isCompleted: boolean }) =>
@ -46,21 +51,40 @@ const CertChallenge = ({ challenge }: { challenge: ChallengeInfo }) => (
</Link>
);
export function ChallengesList({ challenges }: ChallengesProps): JSX.Element {
return (
<ul className={`map-challenges-ul`}>
{challenges.map(challenge => (
<li
className={'map-challenge-title map-challenge-wrap'}
id={challenge.dashedName}
key={'map-challenge' + challenge.fields.slug}
>
<ListChallenge challenge={challenge} />
</li>
))}
</ul>
);
}
// Step or Task challenge
const GridChallenge = ({ challenge }: { challenge: ChallengeInfo }) => {
const GridChallenge = ({
challenge,
isTask = false
}: {
challenge: ChallengeInfo;
isTask?: boolean;
}) => {
const { t } = useTranslation();
return (
<Link
to={challenge.fields.slug}
className={`map-grid-item ${
+challenge.isCompleted ? 'challenge-completed' : ''
challenge.isCompleted ? 'challenge-completed' : ''
}`}
>
<span className='sr-only'>
{challenge.superBlock === SuperBlocks.A2English
? t('aria.task')
: t('aria.step')}
{isTask ? t('aria.task') : t('aria.step')}
</span>
<span>{challenge.stepNumber}</span>
<span className='sr-only'>
@ -70,12 +94,10 @@ const GridChallenge = ({ challenge }: { challenge: ChallengeInfo }) => {
);
};
function Challenges({
const LinkToFirstIncompleteChallenge = ({
challenges,
isProjectBlock,
isGridMap = false,
blockTitle
}: Challenges): JSX.Element {
}: ChallengesProps & BlockTitleProps) => {
const { t } = useTranslation();
const firstIncompleteChallenge = challenges.find(
@ -85,28 +107,32 @@ function Challenges({
const isChallengeStarted = !!challenges.find(
challenge => challenge.isCompleted
);
return firstIncompleteChallenge ? (
<div className='challenge-jump-link'>
<ButtonLink size='small' href={firstIncompleteChallenge.fields.slug}>
{!isChallengeStarted
? t('buttons.start-project')
: t('buttons.resume-project')}{' '}
<span className='sr-only'>{blockTitle}</span>
</ButtonLink>
</div>
) : null;
};
return isGridMap ? (
export const GridMapChallenges = ({
challenges,
blockTitle,
isProjectBlock
}: ChallengesProps & BlockTitleProps & IsProjectBlockProps) => {
const { t } = useTranslation();
return (
<>
{firstIncompleteChallenge && (
<div className='challenge-jump-link'>
<ButtonLink size='small' href={firstIncompleteChallenge.fields.slug}>
{!isChallengeStarted
? t('buttons.start-project')
: t('buttons.resume-project')}{' '}
{blockTitle && <span className='sr-only'>{blockTitle}</span>}
</ButtonLink>
</div>
)}
<nav
aria-label={
blockTitle
? challenges[0].superBlock === SuperBlocks.A2English
? t('aria.dialogues-and-tasks-for', { blockTitle })
: t('aria.steps-for', { blockTitle })
: t('aria.steps')
}
>
<LinkToFirstIncompleteChallenge
challenges={challenges}
blockTitle={blockTitle}
/>
<nav aria-label={t('aria.steps-for', { blockTitle })}>
<ul className={`map-challenges-ul map-challenges-grid`}>
{challenges.map(challenge => (
<li
@ -120,11 +146,8 @@ function Challenges({
id={challenge.dashedName}
key={`map-challenge ${challenge.fields.slug}`}
>
{!isProjectBlock &&
challenge.challengeType !== challengeTypes.dialogue ? (
{!isProjectBlock ? (
<GridChallenge challenge={challenge} />
) : challenge.challengeType === challengeTypes.dialogue ? (
<ListChallenge challenge={challenge} />
) : (
<CertChallenge challenge={challenge} />
)}
@ -133,27 +156,42 @@ function Challenges({
</ul>
</nav>
</>
) : (
<ul className={`map-challenges-ul`}>
{challenges.map(challenge => (
<li
className={`map-challenge-title ${
isProjectBlock ? 'map-project-wrap' : 'map-challenge-wrap'
}`}
id={challenge.dashedName}
key={'map-challenge' + challenge.fields.slug}
>
{!isProjectBlock ? (
<ListChallenge challenge={challenge} />
) : (
<CertChallenge challenge={challenge} />
)}
</li>
))}
</ul>
);
}
};
Challenges.displayName = 'Challenges';
export const ChallengesWithDialogues = ({
challenges,
blockTitle
}: ChallengesProps & BlockTitleProps) => {
const { t } = useTranslation();
export default withTranslation()(Challenges);
return (
<>
<LinkToFirstIncompleteChallenge
challenges={challenges}
blockTitle={blockTitle}
/>
<nav aria-label={t('aria.dialogues-and-tasks-for', { blockTitle })}>
<ul className={`map-challenges-ul map-challenges-grid`}>
{challenges.map(challenge => (
<li
className={`map-challenge-title map-challenge-title-grid ${
challenge.challengeType === challengeTypes.dialogue
? 'map-dialogue-wrap'
: 'map-challenge-wrap'
}`}
id={challenge.dashedName}
key={`map-challenge ${challenge.fields.slug}`}
>
{challenge.challengeType === challengeTypes.dialogue ? (
<ListChallenge challenge={challenge} />
) : (
<GridChallenge challenge={challenge} isTask />
)}
</li>
))}
</ul>
</nav>
</>
);
};