refactor: simplify curriculum ordering (#55657)

This commit is contained in:
Oliver Eyton-Williams
2024-07-25 17:37:24 +02:00
committed by GitHub
parent cb861ca971
commit 0eebe3ee2e
4 changed files with 82 additions and 192 deletions

View File

@@ -4,15 +4,19 @@ import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import {
SuperBlockStages,
SuperBlocks,
superBlockOrder
type SuperBlocks,
SuperBlockStage,
getStageOrder,
superBlockStages
} from '../../../../shared/config/curriculum';
import { SuperBlockIcon } from '../../assets/icons/superblock-icon';
import LinkButton from '../../assets/icons/link-button';
import { Spacer, ButtonLink } from '../helpers';
import { getSuperBlockTitleForMap } from '../../utils/superblock-map-titles';
import { showUpcomingChanges } from '../../../config/env.json';
import {
showUpcomingChanges,
showNewCurriculum
} from '../../../config/env.json';
import './map.css';
@@ -49,11 +53,15 @@ const linkSpacingStyle = {
gap: '15px'
};
const coreCurriculum = [
...superBlockOrder[SuperBlockStages.FrontEnd],
...superBlockOrder[SuperBlockStages.Backend],
...superBlockOrder[SuperBlockStages.Python]
];
const superBlockHeadings: { [key in SuperBlockStage]: string } = {
[SuperBlockStage.Core]: 'landing.core-certs-heading',
[SuperBlockStage.English]: 'landing.learn-english-heading',
[SuperBlockStage.Professional]: 'landing.professional-certs-heading',
[SuperBlockStage.Extra]: 'landing.interview-prep-heading',
[SuperBlockStage.Legacy]: 'landing.legacy-curriculum-heading',
[SuperBlockStage.New]: '', // TODO: add translation
[SuperBlockStage.Upcoming]: 'landing.upcoming-heading'
};
const mapStateToProps = createSelector(
isSignedInSelector,
@@ -173,107 +181,28 @@ function Map({
return (
<div className='map-ui' data-test-label='curriculum-map'>
<h2 className={forLanding ? 'big-heading' : ''}>
{t('landing.core-certs-heading')}
</h2>
<ul>
{coreCurriculum.map((superBlock, i) => (
<MapLi
key={i}
superBlock={superBlock}
landing={forLanding}
index={i}
claimed={isClaimed(superBlock)}
showProgressionLines={true}
showNumbers={true}
completed={allSuperblockChallengesCompleted(superBlock)}
/>
))}
</ul>
<Spacer size='medium' />
<h2 className={forLanding ? 'big-heading' : ''}>
{t('landing.learn-english-heading')}
</h2>
<ul>
{superBlockOrder[SuperBlockStages.English].map((superBlock, i) => (
<MapLi
key={i}
superBlock={superBlock}
landing={forLanding}
completed={allSuperblockChallengesCompleted(superBlock)}
claimed={isClaimed(superBlock)}
index={i}
/>
))}
</ul>
<Spacer size='medium' />
<h2 className={forLanding ? 'big-heading' : ''}>
{t('landing.professional-certs-heading')}
</h2>
<ul>
{superBlockOrder[SuperBlockStages.Professional].map((superBlock, i) => (
<MapLi
key={i}
superBlock={superBlock}
landing={forLanding}
completed={allSuperblockChallengesCompleted(superBlock)}
claimed={isClaimed(superBlock)}
index={i}
/>
))}
</ul>
<Spacer size='medium' />
<h2 className={forLanding ? 'big-heading' : ''}>
{t('landing.interview-prep-heading')}
</h2>
<ul>
{superBlockOrder[SuperBlockStages.Extra].map((superBlock, i) => (
<MapLi
key={i}
superBlock={superBlock}
landing={forLanding}
completed={allSuperblockChallengesCompleted(superBlock)}
claimed={isClaimed(superBlock)}
index={i}
/>
))}
</ul>
<Spacer size='medium' />
<h2 className={forLanding ? 'big-heading' : ''}>
{t('landing.legacy-curriculum-heading')}
</h2>
<ul>
{superBlockOrder[SuperBlockStages.Legacy].map((superBlock, i) => (
<MapLi
key={i}
superBlock={superBlock}
landing={forLanding}
completed={allSuperblockChallengesCompleted(superBlock)}
claimed={isClaimed(superBlock)}
index={i}
/>
))}
</ul>
{showUpcomingChanges && (
{getStageOrder({ showNewCurriculum, showUpcomingChanges }).map(stage => (
<>
<Spacer size='medium' />
<h2 className={forLanding ? 'big-heading' : ''}>
{t('landing.upcoming-heading')}
{t(superBlockHeadings[stage])}
</h2>
<ul>
{superBlockOrder[SuperBlockStages.Upcoming].map((superBlock, i) => (
<ul key={stage}>
{superBlockStages[stage].map((superblock, i) => (
<MapLi
key={i}
superBlock={superBlock}
key={superblock}
superBlock={superblock}
landing={forLanding}
completed={allSuperblockChallengesCompleted(superBlock)}
index={i}
claimed={isClaimed(superBlock)}
claimed={isClaimed(superblock)}
showProgressionLines={stage === SuperBlockStage.Core}
showNumbers={stage === SuperBlockStage.Core}
completed={allSuperblockChallengesCompleted(superblock)}
/>
))}
</ul>
<Spacer size='medium' />
</>
)}
))}
</div>
);
}

View File

@@ -2,20 +2,18 @@ import { test, expect } from '@playwright/test';
import translations from '../client/i18n/locales/english/translations.json';
import intro from '../client/i18n/locales/english/intro.json';
import { SuperBlockStages, superBlockOrder } from '../shared/config/curriculum';
import { SuperBlockStage, superBlockStages } from '../shared/config/curriculum';
test.beforeEach(async ({ page }) => {
await page.goto('/learn');
});
const superBlocksWithLinks = [
...superBlockOrder[SuperBlockStages.FrontEnd],
...superBlockOrder[SuperBlockStages.Backend],
...superBlockOrder[SuperBlockStages.Python],
...superBlockOrder[SuperBlockStages.English],
...superBlockOrder[SuperBlockStages.Professional],
...superBlockOrder[SuperBlockStages.Extra],
...superBlockOrder[SuperBlockStages.Legacy]
...superBlockStages[SuperBlockStage.Core],
...superBlockStages[SuperBlockStage.English],
...superBlockStages[SuperBlockStage.Professional],
...superBlockStages[SuperBlockStage.Extra],
...superBlockStages[SuperBlockStage.Legacy]
];
const superBlockTitleOverride: Record<string, string> = {

View File

@@ -1,10 +1,9 @@
import { Languages } from './i18n';
import {
SuperBlocks,
SuperBlockStages,
superBlockOrder,
SuperBlockStage,
superBlockStages,
notAuditedSuperBlocks,
createSuperBlockMap,
createFlatSuperBlockMap,
getAuditedSuperBlocks
} from './curriculum';
@@ -12,7 +11,7 @@ import {
describe('superBlockOrder', () => {
it('should contain all SuperBlocks', () => {
const allSuperBlocks = Object.values(SuperBlocks);
const superBlockOrderValues = Object.values(superBlockOrder).flat();
const superBlockOrderValues = Object.values(superBlockStages).flat();
expect(superBlockOrderValues).toHaveLength(allSuperBlocks.length);
expect(superBlockOrderValues).toEqual(
expect.arrayContaining(allSuperBlocks)
@@ -20,37 +19,13 @@ describe('superBlockOrder', () => {
});
});
describe('createSuperBlockMap', () => {
it('should return an object with New and Upcoming when { showNewCurriculum: true, showUpcomingChanges: true }', () => {
const result = createSuperBlockMap({
showNewCurriculum: true,
showUpcomingChanges: true
});
expect(result[SuperBlockStages.New]).toHaveLength(
superBlockOrder[SuperBlockStages.New].length
);
expect(result[SuperBlockStages.Upcoming]).toHaveLength(
superBlockOrder[SuperBlockStages.Upcoming].length
);
});
it('should return an object without New and Upcoming when { showNewCurriculum: false, showUpcomingChanges: false }', () => {
const result = createSuperBlockMap({
showNewCurriculum: false,
showUpcomingChanges: false
});
expect(result[SuperBlockStages.New]).toHaveLength(0);
expect(result[SuperBlockStages.Upcoming]).toHaveLength(0);
});
});
describe('createFlatSuperBlockMap', () => {
it('should return an array of SuperBlocks object with New and Upcoming when { showNewCurriculum: true, showUpcomingChanges: true }', () => {
const result = createFlatSuperBlockMap({
showNewCurriculum: true,
showUpcomingChanges: true
});
expect(result).toHaveLength(Object.values(superBlockOrder).flat().length);
expect(result).toHaveLength(Object.values(superBlockStages).flat().length);
});
it('should return an array of SuperBlocks without New and Upcoming when { showNewCurriculum: false, showUpcomingChanges: false }', () => {
@@ -58,9 +33,9 @@ describe('createFlatSuperBlockMap', () => {
showNewCurriculum: false,
showUpcomingChanges: false
});
const tempSuperBlockMap = { ...superBlockOrder };
tempSuperBlockMap[SuperBlockStages.New] = [];
tempSuperBlockMap[SuperBlockStages.Upcoming] = [];
const tempSuperBlockMap = { ...superBlockStages };
tempSuperBlockMap[SuperBlockStage.New] = [];
tempSuperBlockMap[SuperBlockStage.Upcoming] = [];
expect(result).toHaveLength(Object.values(tempSuperBlockMap).flat().length);
});
});
@@ -68,7 +43,7 @@ describe('createFlatSuperBlockMap', () => {
describe('Immutability of superBlockOrder, notAuditedSuperBlocks, and flatSuperBlockMap', () => {
it('should not allow modification of superBlockOrder', () => {
expect(() => {
superBlockOrder[SuperBlockStages.FrontEnd] = [];
superBlockStages[SuperBlockStage.Core] = [];
}).toThrowError(TypeError);
});

View File

@@ -37,10 +37,8 @@ export enum SuperBlocks {
* SuperBlockStages.Upcoming = SHOW_UPCOMING_CHANGES === 'true'
* 'Upcoming' is for development -> not shown on stag or prod anywhere
*/
export enum SuperBlockStages {
FrontEnd,
Backend,
Python,
export enum SuperBlockStage {
Core,
English,
Professional,
Extra,
@@ -49,53 +47,67 @@ export enum SuperBlockStages {
Upcoming
}
export type SuperBlockOrder = {
[key in SuperBlockStages]: SuperBlocks[];
const defaultStageOrder = [
SuperBlockStage.Core,
SuperBlockStage.English,
SuperBlockStage.Professional,
SuperBlockStage.Extra,
SuperBlockStage.Legacy
];
export function getStageOrder({
showNewCurriculum,
showUpcomingChanges
}: Config): SuperBlockStage[] {
const stageOrder = [...defaultStageOrder];
if (showNewCurriculum) stageOrder.push(SuperBlockStage.New);
if (showUpcomingChanges) stageOrder.push(SuperBlockStage.Upcoming);
return stageOrder;
}
export type StageMap = {
[key in SuperBlockStage]: SuperBlocks[];
};
// order of buttons on map, this should include all superblocks
// new and upcoming superblocks are removed below
export const superBlockOrder: SuperBlockOrder = {
[SuperBlockStages.FrontEnd]: [
// Groups of superblocks in learn map. This should include all superblocks.
export const superBlockStages: StageMap = {
[SuperBlockStage.Core]: [
SuperBlocks.RespWebDesignNew,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.FrontEndDevLibs,
SuperBlocks.DataVis
],
[SuperBlockStages.Backend]: [
SuperBlocks.DataVis,
SuperBlocks.RelationalDb,
SuperBlocks.BackEndDevApis,
SuperBlocks.QualityAssurance
],
[SuperBlockStages.Python]: [
SuperBlocks.QualityAssurance,
SuperBlocks.SciCompPy,
SuperBlocks.DataAnalysisPy,
SuperBlocks.InfoSec,
SuperBlocks.MachineLearningPy,
SuperBlocks.CollegeAlgebraPy
],
[SuperBlockStages.English]: [SuperBlocks.A2English],
[SuperBlockStages.Professional]: [SuperBlocks.FoundationalCSharp],
[SuperBlockStages.Extra]: [
[SuperBlockStage.English]: [SuperBlocks.A2English],
[SuperBlockStage.Professional]: [SuperBlocks.FoundationalCSharp],
[SuperBlockStage.Extra]: [
SuperBlocks.TheOdinProject,
SuperBlocks.CodingInterviewPrep,
SuperBlocks.ProjectEuler,
SuperBlocks.RosettaCode
],
[SuperBlockStages.Legacy]: [
[SuperBlockStage.Legacy]: [
SuperBlocks.RespWebDesign,
SuperBlocks.JsAlgoDataStruct,
SuperBlocks.PythonForEverybody
],
[SuperBlockStages.New]: [],
[SuperBlockStages.Upcoming]: [
[SuperBlockStage.New]: [],
[SuperBlockStage.Upcoming]: [
SuperBlocks.B1English,
SuperBlocks.FrontEndDevelopment,
SuperBlocks.UpcomingPython
]
};
Object.freeze(superBlockOrder);
Object.freeze(superBlockStages);
type NotAuditedSuperBlocks = {
[key in Languages]: SuperBlocks[];
@@ -277,34 +289,10 @@ type LanguagesConfig = Config & {
language: string;
};
// removes new and upcoming from superBlockOrder
// not used yet, will be used when adding progress indicators to map
export function createSuperBlockMap({
showNewCurriculum,
showUpcomingChanges
}: Config): SuperBlockOrder {
const superBlockMap = { ...superBlockOrder };
if (!showNewCurriculum) {
superBlockMap[SuperBlockStages.New] = [];
}
if (!showUpcomingChanges) {
superBlockMap[SuperBlockStages.Upcoming] = [];
}
return superBlockMap;
}
export function createFlatSuperBlockMap({
showNewCurriculum,
showUpcomingChanges
}: Config): SuperBlocks[] {
const superBlockMap = { ...superBlockOrder };
if (!showNewCurriculum) {
superBlockMap[SuperBlockStages.New] = [];
}
if (!showUpcomingChanges) {
superBlockMap[SuperBlockStages.Upcoming] = [];
}
return Object.values(superBlockMap).flat();
export function createFlatSuperBlockMap(config: Config): SuperBlocks[] {
return getStageOrder(config)
.map(stage => superBlockStages[stage])
.flat();
}
export function getAuditedSuperBlocks({