mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-09-24 23:01:18 +08:00
refactor(client): data sourcing for super-block-intro (#55751)
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
BilibiliIds
|
||||
} from '../../../redux/prop-types';
|
||||
import { isAuditedSuperBlock } from '../../../../../shared/utils/is-audited';
|
||||
import { BlockTypes } from '../../../../../shared/config/blocks';
|
||||
import Block from './block';
|
||||
|
||||
jest.mock('../../../../../shared/utils/is-audited', () => ({
|
||||
@@ -20,65 +21,64 @@ jest.mock('../../../../../shared/utils/is-audited', () => ({
|
||||
}));
|
||||
|
||||
const defaultProps = {
|
||||
blockDashedName: 'test-block',
|
||||
block: 'test-block',
|
||||
challenges: [
|
||||
{
|
||||
challenge: {
|
||||
block: 'testblock',
|
||||
certification: 'mockCertification',
|
||||
challengeOrder: 1,
|
||||
challengeType: 0,
|
||||
dashedName: 'mock-dashed-name',
|
||||
description: 'mockDescription',
|
||||
challengeFiles: {} as ChallengeFiles,
|
||||
fields: {} as Fields,
|
||||
forumTopicId: 12345,
|
||||
guideUrl: 'https://mockurl.com',
|
||||
head: ['mockHead'],
|
||||
hasEditableBoundaries: false,
|
||||
helpCategory: 'mockHelpCategory',
|
||||
id: 'mockId',
|
||||
instructions: 'mockInstructions',
|
||||
isComingSoon: false,
|
||||
internal: {
|
||||
content: 'mockContent',
|
||||
contentDigest: 'mockContentDigest',
|
||||
description: 'mockInternalDescription',
|
||||
fieldOwners: ['mockOwner'],
|
||||
ignoreType: null,
|
||||
mediaType: 'mockMediaType',
|
||||
owner: 'mockOwner',
|
||||
type: 'mockType'
|
||||
},
|
||||
notes: 'mockNotes',
|
||||
prerequisites: [] as PrerequisiteChallenge[],
|
||||
isLocked: false,
|
||||
isPrivate: false,
|
||||
order: 1,
|
||||
questions: [] as Question[],
|
||||
assignments: ['mockAssignment'],
|
||||
required: [],
|
||||
solutions: {
|
||||
['indexhtml']: {} as FileKeyChallenge,
|
||||
['scriptjs']: {} as FileKeyChallenge,
|
||||
['stylescss']: {} as FileKeyChallenge,
|
||||
['indexjsx']: {} as FileKeyChallenge
|
||||
},
|
||||
sourceInstanceName: 'mockSourceInstanceName',
|
||||
superOrder: 1,
|
||||
superBlock: SuperBlocks.UpcomingPython,
|
||||
tail: ['mockTail'],
|
||||
template: 'mockTemplate',
|
||||
tests: [] as Test[],
|
||||
title: 'mockTitle',
|
||||
translationPending: false,
|
||||
url: 'https://mockurl.com',
|
||||
usesMultifileEditor: false,
|
||||
videoId: 'mockVideoId',
|
||||
videoLocaleIds: {},
|
||||
bilibiliIds: {} as BilibiliIds,
|
||||
videoUrl: 'https://mockvideourl.com'
|
||||
}
|
||||
block: 'testblock',
|
||||
blockType: BlockTypes.lab,
|
||||
certification: 'mockCertification',
|
||||
challengeOrder: 1,
|
||||
challengeType: 0,
|
||||
dashedName: 'mock-dashed-name',
|
||||
description: 'mockDescription',
|
||||
challengeFiles: {} as ChallengeFiles,
|
||||
fields: {} as Fields,
|
||||
forumTopicId: 12345,
|
||||
guideUrl: 'https://mockurl.com',
|
||||
head: ['mockHead'],
|
||||
hasEditableBoundaries: false,
|
||||
helpCategory: 'mockHelpCategory',
|
||||
id: 'mockId',
|
||||
instructions: 'mockInstructions',
|
||||
isComingSoon: false,
|
||||
internal: {
|
||||
content: 'mockContent',
|
||||
contentDigest: 'mockContentDigest',
|
||||
description: 'mockInternalDescription',
|
||||
fieldOwners: ['mockOwner'],
|
||||
ignoreType: null,
|
||||
mediaType: 'mockMediaType',
|
||||
owner: 'mockOwner',
|
||||
type: 'mockType'
|
||||
},
|
||||
notes: 'mockNotes',
|
||||
prerequisites: [] as PrerequisiteChallenge[],
|
||||
isLocked: false,
|
||||
isPrivate: false,
|
||||
order: 1,
|
||||
questions: [] as Question[],
|
||||
assignments: ['mockAssignment'],
|
||||
required: [],
|
||||
solutions: {
|
||||
['indexhtml']: {} as FileKeyChallenge,
|
||||
['scriptjs']: {} as FileKeyChallenge,
|
||||
['stylescss']: {} as FileKeyChallenge,
|
||||
['indexjsx']: {} as FileKeyChallenge
|
||||
},
|
||||
sourceInstanceName: 'mockSourceInstanceName',
|
||||
superOrder: 1,
|
||||
superBlock: SuperBlocks.UpcomingPython,
|
||||
tail: ['mockTail'],
|
||||
template: 'mockTemplate',
|
||||
tests: [] as Test[],
|
||||
title: 'mockTitle',
|
||||
translationPending: false,
|
||||
url: 'https://mockurl.com',
|
||||
usesMultifileEditor: false,
|
||||
videoId: 'mockVideoId',
|
||||
videoLocaleIds: {},
|
||||
bilibiliIds: {} as BilibiliIds,
|
||||
videoUrl: 'https://mockvideourl.com'
|
||||
}
|
||||
],
|
||||
completedChallengeIds: ['testchallengeIds'],
|
||||
|
||||
@@ -26,11 +26,10 @@ import '../intro.css';
|
||||
|
||||
const { curriculumLocale, showUpcomingChanges, showNewCurriculum } = envData;
|
||||
|
||||
const mapStateToProps = (
|
||||
state: unknown,
|
||||
ownProps: { blockDashedName: string }
|
||||
) => {
|
||||
const expandedSelector = makeExpandedBlockSelector(ownProps.blockDashedName);
|
||||
type Challenge = ChallengeNode['challenge'];
|
||||
|
||||
const mapStateToProps = (state: unknown, ownProps: { block: string }) => {
|
||||
const expandedSelector = makeExpandedBlockSelector(ownProps.block);
|
||||
|
||||
return createSelector(
|
||||
expandedSelector,
|
||||
@@ -46,8 +45,8 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
bindActionCreators({ toggleBlock }, dispatch);
|
||||
|
||||
interface BlockProps {
|
||||
blockDashedName: string;
|
||||
challenges: ChallengeNode[];
|
||||
block: string;
|
||||
challenges: Challenge[];
|
||||
completedChallengeIds: string[];
|
||||
isExpanded: boolean;
|
||||
superBlock: SuperBlocks;
|
||||
@@ -82,14 +81,14 @@ class Block extends Component<BlockProps> {
|
||||
}
|
||||
|
||||
handleBlockClick(): void {
|
||||
const { blockDashedName, toggleBlock } = this.props;
|
||||
const { block, toggleBlock } = this.props;
|
||||
void playTone('block-toggle');
|
||||
toggleBlock(blockDashedName);
|
||||
toggleBlock(block);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const {
|
||||
blockDashedName,
|
||||
block,
|
||||
completedChallengeIds,
|
||||
challenges,
|
||||
isExpanded,
|
||||
@@ -99,7 +98,7 @@ class Block extends Component<BlockProps> {
|
||||
|
||||
let completedCount = 0;
|
||||
|
||||
const challengesWithCompleted = challenges.map(({ challenge }) => {
|
||||
const challengesWithCompleted = challenges.map(challenge => {
|
||||
const { id } = challenge;
|
||||
const isCompleted = completedChallengeIds.some(
|
||||
(completedChallengeId: string) => completedChallengeId === id
|
||||
@@ -110,11 +109,11 @@ class Block extends Component<BlockProps> {
|
||||
return { ...challenge, isCompleted };
|
||||
});
|
||||
|
||||
const isProjectBlock = challenges.some(({ challenge }) => {
|
||||
return isProjectBased(challenge.challengeType, blockDashedName);
|
||||
const isProjectBlock = challenges.some(challenge => {
|
||||
return isProjectBased(challenge.challengeType, block);
|
||||
});
|
||||
|
||||
const isGridBlock = challenges.some(({ challenge }) => {
|
||||
const isGridBlock = challenges.some(challenge => {
|
||||
return isGridBased(superBlock, challenge.challengeType);
|
||||
});
|
||||
|
||||
@@ -123,12 +122,12 @@ class Block extends Component<BlockProps> {
|
||||
showUpcomingChanges
|
||||
});
|
||||
|
||||
const blockTitle = t(`intro:${superBlock}.blocks.${blockDashedName}.title`);
|
||||
const blockTitle = t(`intro:${superBlock}.blocks.${block}.title`);
|
||||
// the real type of TFunction is the type below, because intro can be an array of strings
|
||||
// type RealTypeOFTFunction = TFunction & ((key: string) => string[]);
|
||||
// But changing the type will require refactoring that isn't worth it for a wrong type.
|
||||
const blockIntroArr = t<string, DefaultTFuncReturn & string[]>(
|
||||
`intro:${superBlock}.blocks.${blockDashedName}.intro`
|
||||
`intro:${superBlock}.blocks.${block}.intro`
|
||||
);
|
||||
const expandText = t('intro:misc-text.expand');
|
||||
const collapseText = t('intro:misc-text.collapse');
|
||||
@@ -151,7 +150,7 @@ class Block extends Component<BlockProps> {
|
||||
const Block = (
|
||||
<>
|
||||
{' '}
|
||||
<ScrollableAnchor id={blockDashedName}>
|
||||
<ScrollableAnchor id={block}>
|
||||
<div className={`block ${isExpanded ? 'open' : ''}`}>
|
||||
<div className='block-header'>
|
||||
<h3 className='big-block-title'>{blockTitle}</h3>
|
||||
@@ -207,7 +206,7 @@ class Block extends Component<BlockProps> {
|
||||
|
||||
const ProjectBlock = (
|
||||
<>
|
||||
<ScrollableAnchor id={blockDashedName}>
|
||||
<ScrollableAnchor id={block}>
|
||||
<div className='block'>
|
||||
<div className='block-header'>
|
||||
<h3 className='big-block-title'>{blockTitle}</h3>
|
||||
@@ -245,12 +244,12 @@ class Block extends Component<BlockProps> {
|
||||
const GridBlock = (
|
||||
<>
|
||||
{' '}
|
||||
<ScrollableAnchor id={blockDashedName}>
|
||||
<ScrollableAnchor id={block}>
|
||||
<div className={`block block-grid ${isExpanded ? 'open' : ''}`}>
|
||||
<h3 className='block-grid-title'>
|
||||
<button
|
||||
aria-expanded={isExpanded ? 'true' : 'false'}
|
||||
aria-controls={`${blockDashedName}-panel`}
|
||||
aria-controls={`${block}-panel`}
|
||||
className='block-header'
|
||||
onClick={() => {
|
||||
this.handleBlockClick();
|
||||
@@ -283,7 +282,7 @@ class Block extends Component<BlockProps> {
|
||||
</div>
|
||||
)}
|
||||
{isExpanded && (
|
||||
<div id={`${blockDashedName}-panel`}>
|
||||
<div id={`${block}-panel`}>
|
||||
<BlockIntros intros={blockIntroArr} />
|
||||
<Challenges
|
||||
challengesWithCompleted={challengesWithCompleted}
|
||||
@@ -299,7 +298,7 @@ class Block extends Component<BlockProps> {
|
||||
);
|
||||
|
||||
const GridProjectBlock = (
|
||||
<ScrollableAnchor id={blockDashedName}>
|
||||
<ScrollableAnchor id={block}>
|
||||
<div className='block block-grid grid-project-block'>
|
||||
<div className='tags-wrapper'>
|
||||
<span className='cert-tag' aria-hidden='true'>
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
userFetchStateSelector,
|
||||
signInLoadingSelector
|
||||
} from '../../redux/selectors';
|
||||
import type { AllChallengeNode, User } from '../../redux/prop-types';
|
||||
import type { ChallengeNode, User } from '../../redux/prop-types';
|
||||
import { CertTitle } from '../../../config/cert-and-project-map';
|
||||
import Block from './components/block';
|
||||
import CertChallenge from './components/cert-challenge';
|
||||
@@ -45,7 +45,7 @@ type FetchState = {
|
||||
type SuperBlockProp = {
|
||||
currentChallengeId: string;
|
||||
data: {
|
||||
allChallengeNode: AllChallengeNode;
|
||||
allChallengeNode: { nodes: ChallengeNode[] };
|
||||
};
|
||||
expandedState: {
|
||||
[key: string]: boolean;
|
||||
@@ -119,7 +119,7 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
|
||||
const getChosenBlock = (): string => {
|
||||
const {
|
||||
data: {
|
||||
allChallengeNode: { edges }
|
||||
allChallengeNode: { nodes }
|
||||
},
|
||||
isSignedIn,
|
||||
currentChallengeId,
|
||||
@@ -144,20 +144,18 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
|
||||
return dashedBlock;
|
||||
}
|
||||
|
||||
const edge = edges[0];
|
||||
const firstChallenge = nodes[0]?.challenge;
|
||||
|
||||
if (isSignedIn) {
|
||||
// see if currentChallenge is in this superBlock
|
||||
const currentChallengeEdge = edges.find(
|
||||
edge => edge.node.challenge.id === currentChallengeId
|
||||
);
|
||||
const currentChallenge = nodes.find(
|
||||
node => node.challenge.id === currentChallengeId
|
||||
)?.challenge;
|
||||
|
||||
return currentChallengeEdge
|
||||
? currentChallengeEdge.node.challenge.block
|
||||
: edge.node.challenge.block;
|
||||
return currentChallenge ? currentChallenge.block : firstChallenge?.block;
|
||||
}
|
||||
|
||||
return edge.node.challenge.block;
|
||||
return firstChallenge?.block;
|
||||
};
|
||||
|
||||
const initializeExpandedState = () => {
|
||||
@@ -169,7 +167,7 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
|
||||
|
||||
const {
|
||||
data: {
|
||||
allChallengeNode: { edges }
|
||||
allChallengeNode: { nodes }
|
||||
},
|
||||
isSignedIn,
|
||||
signInLoading,
|
||||
@@ -177,16 +175,11 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
|
||||
pageContext: { superBlock, title, certification }
|
||||
} = props;
|
||||
|
||||
const allChallenges = edges.map(({ node }) => node.challenge);
|
||||
const nodesForSuperBlock = edges
|
||||
.filter(edge => edge.node.challenge.superBlock === superBlock)
|
||||
.map(({ node }) => node);
|
||||
const blockDashedNames = uniq(
|
||||
nodesForSuperBlock.map(({ challenge: { block } }) => block)
|
||||
);
|
||||
const allChallenges = nodes.map(({ challenge }) => challenge);
|
||||
const challenges = allChallenges.filter(c => c.superBlock === superBlock);
|
||||
const blocks = uniq(challenges.map(({ block }) => block));
|
||||
|
||||
const i18nTitle = getSuperBlockTitleForMap(superBlock);
|
||||
const defaultCurriculumNames = blockDashedNames;
|
||||
|
||||
const superblockWithoutCert = [
|
||||
SuperBlocks.RespWebDesign,
|
||||
@@ -230,13 +223,11 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
|
||||
</h2>
|
||||
<Spacer size='medium' />
|
||||
<div className='block-ui'>
|
||||
{defaultCurriculumNames.map(blockDashedName => (
|
||||
{blocks.map(block => (
|
||||
<Block
|
||||
key={blockDashedName}
|
||||
blockDashedName={blockDashedName}
|
||||
challenges={nodesForSuperBlock.filter(
|
||||
node => node.challenge.block === blockDashedName
|
||||
)}
|
||||
key={block}
|
||||
block={block}
|
||||
challenges={challenges.filter(c => c.block === block)}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
))}
|
||||
@@ -292,21 +283,19 @@ export const query = graphql`
|
||||
]
|
||||
}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
challenge {
|
||||
fields {
|
||||
slug
|
||||
blockName
|
||||
}
|
||||
id
|
||||
block
|
||||
challengeType
|
||||
title
|
||||
order
|
||||
superBlock
|
||||
dashedName
|
||||
nodes {
|
||||
challenge {
|
||||
fields {
|
||||
slug
|
||||
blockName
|
||||
}
|
||||
id
|
||||
block
|
||||
challengeType
|
||||
title
|
||||
order
|
||||
superBlock
|
||||
dashedName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user