mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-06-13 21:02:08 +08:00
refactor(client): use different challenges component in different blocks (#59479)
This commit is contained in:
parent
8965cf3180
commit
5add4262bd
@ -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 (
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user